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类型的证书对象
- 下载此证书文件到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
。