1. 前言
cert、sigh和match
是Fastlane中的三個Tool,他們都是與證書相關的工具。cert
的作用是獲取簽名證書或刪除過期的證書;sigh
的作用是管理配置檔案(provisioning profile),比如建立新的、修復過期的、刪除本地的等;match
的主要作用是使用cert
和sigh
建立新的證書和配置檔案,並它們放置在git上,然後重複使用。
2. cert
cert
這個Tool下定義了兩個Command,分別是create
和revoke_expired
,其中create
是預設Command。
可以通過在終端中執行下列命令呼叫
#呼叫create
fastlane cert
fastlane cert create
#呼叫revoke_expired
fastlane cert revoke_expired
複製程式碼
除了在終端使用,cert
還可以在lane中被當做action來呼叫,這也是使用最頻繁的呼叫方式。
當cert
被當做action被呼叫時,其效果和在終端呼叫fastlane cert [create]
的效果是一樣的。
cert
中的create
的作用是獲取簽名證書和其私鑰,然後將簽名證書和其私鑰(p12)匯入到鑰匙鏈中。
為了獲取證書,首先它會去檢測本地是否存在它想要的證書,如果沒有則它會去你的AppleID賬號中嘗試建立一個新的。
本文只討論create
這個Command,下文中如果沒有特殊說明,指的都是這種情況。
當在終端執行fastlane cert
時,其執行邏輯如下
-
建立
:output_path
指向的目錄 -
獲取AppleID 可通過
:username
、環境變數CERT_USERNAME、DELIVER_USER、DELIVER_USERNAME
或Appfile
三種途徑獲取;如果沒有,則在終端請求使用者輸入AppleID。 -
獲取AppleID對應密碼 可通過環境變數
FASTLANE_PASSWORD
和DELIVER_PASSWORD
設定;如果沒有,則在終端使用security find-internet-password -g -s deliver.#{AppleID}
檢視鑰匙鏈中是否儲存了對應密碼,其中AppleID是[步驟2]中獲取的;如果沒有,則在終端請求使用者輸入,並且會將使用者輸入的密碼儲存在鑰匙鏈中。 -
登入到蘋果開發網站 如果有兩步驗證,則還需要輸入對應手機的驗證碼
-
獲取TeamID 如果這個AppleID賬號加入了多個Team,可以通過設定TeamID或TeamName來指定一個Team,具體來說可以通過環境變數
FASTLANE_TEAM_ID
、CERT_TEAM_ID
或:team_id
指定TeamID,通過環境變數FASTLANE_TEAM_NAME
,CERT_TEAM_NAME
或:team_name
指定TeamName,否則,需要使用者手動來選擇。如果你的AppleID賬號只加入了一個Team,則直接使用此Team的TeamID。 -
檢測force 6.1. 當
:force
為true
時,強制建立證書,執行[步驟8] 6.2. 當:force
為false
時,執行[步驟7] -
檢測本地證書 遍歷AppleID賬號中的已建立證書,檢測此證書是否存在於鑰匙鏈中,或者
:output_path
目錄下是否存在此證書對應的金鑰(p12),其具體的檢測流程會在下文中講到。 7.1. 本地有可用證書,執行[步驟9] 7.2. 本地無可用證書,執行[步驟8] -
建立新證書 首先生成CSR檔案和RSA金鑰對
def create_certificate_signing_request
key = OpenSSL::PKey::RSA.new(2048)
csr = OpenSSL::X509::Request.new
csr.version = 0
csr.subject = OpenSSL::X509::Name.new([
['CN', 'PEM', OpenSSL::ASN1::UTF8STRING]
])
csr.public_key = key.public_key
csr.sign(key, OpenSSL::Digest::SHA1.new)
return [csr, key]
end
複製程式碼
然後生成請求
r = request(:post, "account/#{platform_slug(mac)}/certificate/submitCertificateRequest.action", {
teamId: team_id,
type: type,
csrContent: csr,
appIdId: app_id # optional
})
複製程式碼
若建立成功,則在output_path
目錄下儲存此新建立的CSR檔案、簽名證書和簽名證書對應的私鑰。
AppleID賬戶下,相同型別的證書只能建立兩個,如果已經建立了兩個之後,再去嘗試建立證書,則會報錯。
- 匯入此證書和它的私鑰到鑰匙鏈中
在終端中使用
security
命令來匯入
security import certificate_path -k keychain_path -P certificate_password -T /usr/bin/codesign -T /usr/bin/security
複製程式碼
其中certificate_path
表示要匯入證書的路徑;
keychain_path
表示鑰匙鏈的路徑,一般是~/Library/Keychains/login.keychain-db
;
certificate_password
表示證書的密碼,預設是空字串,通過cert
建立的證書的密碼為空;
-T usr/bin/codesign
表示使用usr/bin/codesign
訪問這個證書的時候不需要授權,也就是不需要輸入鑰匙鏈的密碼,這個在CI中會很有用。
最後需要注意的是,如果證書本來就是在鑰匙鏈中,則不會執行這個步驟,也不會執行這條命令,所以在CI中使用時,最好在構建指令碼中加上security unlock-keychain -p certificate_password ~/Library/Keychains/login.keychain-db
,這條命令的作用和上面的-T
類似,但是範圍更廣,即訪問整個鑰匙鏈都不需要輸入密碼。
- 設定全域性變數
設定
CER_CERTIFICATE_ID
和CER_FILE_PATH
這兩個環境變數,分別表示證書的id和證書的路徑,證書的路徑就是:output_path
目錄下的證書檔案的路徑。 如果是在lane中呼叫cert
,則還會設定環境變數SIGH_CERTIFICATE_ID
,這樣設定之後,如果接下來sigh
需要建立一個配置檔案,就會使用環境變數SIGH_CERTIFICATE_ID
指向的簽名證書來建立。(環境變數SIGH_CERTIFICATE_ID
僅僅只是在建立新的配置檔案的時候才會使用)
2.1. 檢測本地證書
- 獲取AppleID中已建立的證書列表
根據
:development
指定證書的型別,true表示除錯證書,false表示生產證書,預設是false,本步驟只獲取指定型別的證書。證書列表中的物件的型別都是Spaceship::Portal::Certificate
或其子類。 類Spaceship::Portal::Certificate
中的例項變數
module Spaceship
module Portal
class Certificate < PortalBase
# @return (String) The ID given from the developer portal. You'll probably not need it.
attr_accessor :id
# @return (String) The name of the certificate
attr_accessor :name
# @return (String) Status of the certificate
attr_accessor :status
# @return (Date) The date and time when the certificate was created
attr_accessor :created
# @return (Date) The date and time when the certificate will expire
attr_accessor :expires
# @return (String) The owner type that defines if it's a push profile or a code signing identity
# @example Code Signing Identity
# "team"
# @example Push Certificate
# "bundle"
attr_accessor :owner_type
# @return (String) The name of the owner
# @example Code Signing Identity (usually the company name)
# "SunApps Gmbh"
# @example Push Certificate (the bundle identifier)
# "tools.fastlane.app"
attr_accessor :owner_name
# @return (String) The ID of the owner, that can be used to fetch more information
attr_accessor :owner_id
# Indicates the type of this certificate
attr_accessor :type_display_id
# @return (Bool) Whether or not the certificate can be downloaded
attr_accessor :can_download
end
end
end
複製程式碼
- 獲取證書列表中的下一個證書 遍歷[步驟1]獲取的證書列表 如果下一個證書不存在,則執行[步驟7],表明本地沒有可用證書 如果下一個證書存在,則執行[步驟3] 一個InHouse型別的證書物件
<Spaceship::Portal::Certificate::InHouse
id="GF0ZY66W6D",
name="iOS Distribution",
status="Issued",
created=2017-12-19 02:52:11 UTC,
expires=2020-12-18 02:42:11 UTC,
owner_type="team",
owner_name="Communications Corporation Limited",
owner_id="12GF5VQGBX",
type_display_id="9RQEK7MSXA",
can_download=true>
複製程式碼
- 下載此證書檔案到output_path 根據[步驟2]中獲取的證書物件,從AppleID中下載證書檔案
r = request(:get, "account/#{platform_slug(mac)}/certificate/downloadCertificateContent.action", {
teamId: team_id,
certificateId: certificate_id,
type: type
})
複製程式碼
將下載的證書檔案儲存在:output_path
指向的目錄中,指定檔名為#{certificate.id}.cer
,certificate.id
表示上述證書物件的id。
- 檢測本地鑰匙鏈
這一步的目的就是檢測本地鑰匙鏈中是否存在[步驟2]中獲取的證書,由於無法從鑰匙鏈中獲取證書的唯一識別符號,所以這裡是通過對比證書檔案的
SHA1
摘要來判斷其是否存在。 使用security find-identity -v -p codesigning
獲取鑰匙鏈中可用的簽名證書列表,下列每一條資料都包含了證書的SHA1
摘要和其名稱
wang:temp mac$ security find-identity -v -p codesigning
1) 9C3C5AE7820F33F6D919595E971C9B458519ACE5 "iPhone Developer"
2) 57F720F51EA851BA8E2D6EC4D4D752F9EF43D2F7 "iPhone Distribution"
2 valid identities found
複製程式碼
然後獲取[步驟3]中證書檔案的SHA1
摘要,如果這個摘要存在於上述輸出中,則表示這個證書已經在鑰匙鏈中了,執行[步驟8]
如果沒有包含,則執行[步驟5]
-
output_path中檢測私鑰 檢測
:output_path
目錄中是否存在#{certificate.id}.p12
,certificate.id
表示[步驟2]中獲取的證書物件的id,這裡是僅僅只是通過檔名來判斷其是否存在。 若存在,說明本地存在可用證書,則執行[步驟8] 若不存在,說明本地不存在可用證書,則執行[步驟6] -
從output_path中刪除此證書 刪除[步驟3]中下載的證書檔案
-
本地沒有可用證書
-
本地有可用證書
3. sigh
sigh
是用於管理配置檔案profile,在 sigh
這個Tool中,其內部整合了多個Command,分別是renew、download_all、repair、resign、manage
,其中預設Command是renew
。
renew
的作用是從AppleID賬號中獲取一個可用的配置檔案profile,如果沒有,則建立一個新的profile,然後將它按照到xcode中。
這裡只討論renew
,如果沒有特殊說明,指的都是這種情況。
當在終端執行fastlane sigh [renew]
時,其執行邏輯如下
前幾步與cert
類似,只是有一些用來傳值的環境變數有些不同。
-
獲取AppleID 可通過
:username
、環境變數SIGH_USERNAME、DELIVER_USER、DELIVER_USERNAME
或Appfile
三種途徑獲取;如果沒有,則在終端請求使用者輸入AppleID。 -
獲取AppleID對應密碼
-
登入到蘋果開發網站
-
獲取TeamID 通過環境變數
FASTLANE_TEAM_ID
、環境變數SIGH_TEAM_ID
或:team_id
指定TeamID,通過環境變數FASTLANE_TEAM_NAME
,環境變數SIGH_TEAM_NAME
或:team_name
指定TeamName -
獲取profile列表 首先從AppleID賬號中,獲取所有已建立的provisioning profiles的列表(也包含xcode管理的),然後經過一步步的過濾,最終得到所有可用的profile。 5.1 獲取的profile列表有值,則執行[步驟6] 5.2 獲取的profile列表有值,則執行[步驟16]
-
獲取第一個profile
-
檢測force
:force
指定是否強制建立新的provisioning profile 7.1:force
等於true,執行[步驟8] 7.2:force
等於false,執行[步驟10] -
在AppleID中刪除此profile 在AppleID賬號中,刪除[步驟6]中獲取的profile
-
在AppleID中建立新的profile 如果是[步驟16]跳轉過來的,還需要保證AppleID賬號中存在此
:app_identifier
-
返回profile 如果
:force
等於true,則返回[步驟9]中建立的profile; 如果:force
等於false,則返回[步驟6]中獲取的profile. -
下載profile檔案 之前步驟中提到profile是provisioning profile的概要描述,這裡下載的profile檔案,則是在專案中使用的配置檔案。下載完成後,將檔案儲存在臨時目錄中。
-
output_path目錄下儲存profile檔案 將[步驟11]下載的檔案移動到
:output_path
目錄下,如果指定了:filename
,則檔名為#{filename}.mobileprovision
;否則,檔名為#{type}_#{app_identifier}.mobileprovision
,其中type表示prifile的型別,可能是AppStore、AdHoc、InHouse和Development。 -
檢測skip_install
:skip_install
指定是否安裝profile到鑰匙鏈中 如果:skip_install
等於true,則執行[步驟15] 如果:skip_install
等於false,則執行[步驟14] -
安裝profile到鑰匙鏈中 將[步驟12]中的profile檔案複製到
~/Library/MobileDevice/Provisioning Profiles/
目錄下,檔名為#{uuid}.mobileprovision
,其中uuid
指的是profile的uuid -
返回output_path路徑 返回
:output_path
指定的目錄路徑,然後退出程式 -
檢測readonly
:readonly
指定是否在AppleID賬號中建立新的profile 如果:readonly
等於false,則執行[步驟9] 如果:readonly
等於true,異常退出
3.1 獲取profile列表
獲取所有已建立的provisioning profiles的列表,然後經過一步步的過濾,最終得到所有可用的profile。
-
下載所有的profile 所有的pofile是指AppleID賬號中看得到的所有provisioning profile(即使是invalid)和通過xcode建立的,通過xcode建立的profile不會顯示在AppleID中。
-
檢測development和adhoc
:development
和:adhoc
用來指定profile的型別,profile的型別總共有四種,分別是Development、AppStore、AdHoc、InHouse
-
檢測force 如果
:force
是true,則不會刪除不可用的profile,因為後面會強制建立新的profile,不會使用當前這些profile,也就無所謂可用還是不可用了。 -
過濾adhoc或appstore 下面是
sigh
的原始碼,個人猜測,下載profile時,返回的json資料中有一個叫做distributionMethod
的key,這個key的取值範圍是['inhouse', 'store', 'limited', 'direct']。adhoc
和appstore
型別的profile返回的distributionMethod
的值都是store。在本步驟之前都沒有區分adhoc
和appstore
,在這一步驟中,會根據profile中是否帶有device來區分這兩種型別。
klass = case attrs['distributionMethod']
when 'limited'
Development
when 'store'
AppStore
when 'inhouse'
InHouse
when 'direct'
Direct # Mac-only
else
raise "Can't find class '#{attrs['distributionMethod']}'"
end
複製程式碼
- 刪除不可用證書的profile 每一個profile都會關聯一個簽名證書的陣列(開發環境的profile的證書陣列裡可以包含多個簽名證書,生產環境的profile只能包含一個簽名證書),檢測與profile相關聯的證書是否在本地鑰匙鏈中,如果不在,則刪除此profile。
3.2. 在AppleID中建立新的profile
下面是建立profile時,請求的引數
params = {
teamId: team_id,
provisioningProfileName: name,
appIdId: app_id,
distributionType: distribution_method,
certificateIds: certificate_ids,
deviceIds: device_ids
}
params[:subPlatform] = sub_platform if sub_platform
# if `template_name` is nil, Default entitlements will be used
params[:template] = template_name if template_name
複製程式碼
想要在AppleID賬號中建立新的profile,首先需要獲取上述程式碼中的各個引數,主要是簽名證書列表、包含的裝置、釋出型別和名稱等
下圖中,步驟1到步驟9都是在篩選可用的簽名證書列表
-
下載當前平臺和釋出模式的證書列表 比如當前使用的AppleID賬號是一個企業開發者賬號,且
:platform
等於ios
,:development
和:adhoc
都等於false
,則在本步驟中會下載ios
平臺下所有的In-House
簽名證書。 -
檢測cert_id和cert_owner_name
:cert_id
是簽名證書的唯一識別符號,:cert_owner_name
是簽名證書所屬的team的name。 -
刪除不匹配的證書 當
:cert_id
有值,且證書的cert_id和它不相等,則從證書列表中刪除此證書;:cert_owner_name
有值,且證書的cert_owner_name和它不相等,則從證書列表中刪除此證書; -
檢測skip_certificate_verification
-
刪除不在鑰匙鏈中的證書 檢測證書是否在本地鑰匙鏈,其具體步驟可檢視2.1節的步驟4
-
檢測剩餘證書的數目 剩餘的證書資料為0,異常退出
-
檢測development
-
返回所有剩餘證書 開發環境下的profile可以包含多個簽名證書,所有返回所有的剩餘證書
-
返回剩餘證書中的第一個 生產環境下的profile只能包含一個簽名證書,所有返回剩餘證書中的第一個。如果想使用特定的簽名證書,最好使用
:cert_id
指定。 -
獲取profile的name 首先,如果有設定
:provisioning_name
,則使用設定的值作為profile的name;否則,使用#{bundle_id} #{profile_type}
這種格式,比如com.fastlane.demo InHouse
然後,如果skip_fetch_profiles
的值是fasle,則會去檢測這個名字是否已經被使用了,如果被使用了,就在這個名字後面加上一個空格和一個當前的時間戳。 -
獲取註冊裝置的ids 如果當前的釋出模式是
AppStore、InHouse、Direct
,即development=false and adhoc=false
,ids等於空陣列; 否則,ids等於當前平臺的所有註冊裝置的id集合; -
獲取其他引數 其他引數還包含
:team_id、:app_identifier、:template_name
等,:app_identifier
指定的bundle_id必須在AppleID賬號中有建立對應的App ID,否則會異常退出。 -
生成併發出建立profile的請求 到了這一步,建立profile請求的引數都已經獲取到了,接下來就是發出這個請求。
下面再來看看建立profile時的請求引數
params = {
teamId: team_id,
provisioningProfileName: name,
appIdId: app_id,
distributionType: distribution_method,
certificateIds: certificate_ids,
deviceIds: device_ids
}
params[:subPlatform] = sub_platform if sub_platform
# if `template_name` is nil, Default entitlements will be used
params[:template] = template_name if template_name
複製程式碼
建立profile的前提就是要構建好上述程式碼中的引數,而這些引數又依賴於執行fastlane sigh
時傳入的外部引數。
下面列出了一些請求引數與外部引數的對照關係
請求引數 | 外部引數 |
---|---|
teamId | :team_id |
provisioningProfileName | :provisioning_name |
appIdId | :app_identifier |
distributionType | :adhoc、:development |
certificateIds | :cert_id、:cert_owner_name |
deviceIds | :platform、:development、:adhoc |
subPlatform | :platform |
template | template_name |
通過:platform
,可以指定建立profile時的平臺。它有三種取值,分別是mac、ios、tvos
。
當:platform
等於mac
或ios
時,請求引數subPlatform等於nil;否則subPlatform等於tvos
。