Fastlane证书管理(一):cert、sigh

4,987 阅读13分钟

1. 前言

cert、sigh和match是Fastlane中的三个Tool,他们都是与证书相关的工具。cert的作用是获取签名证书或删除过期的证书;sigh的作用是管理配置文件(provisioning profile),比如创建新的、修复过期的、删除本地的等;match的主要作用是使用certsigh创建新的证书和配置文件,并它们放置在git上,然后重复使用。

2. cert

cert这个Tool下定义了两个Command,分别是createrevoke_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时,其执行逻辑如下

  1. 创建:output_path指向的目录

  2. 获取AppleID 可通过:username、环境变量CERT_USERNAME、DELIVER_USER、DELIVER_USERNAMEAppfile三种途径获取;如果没有,则在终端请求用户输入AppleID。

  3. 获取AppleID对应密码 可通过环境变量FASTLANE_PASSWORDDELIVER_PASSWORD设置;如果没有,则在终端使用security find-internet-password -g -s deliver.#{AppleID}查看钥匙链中是否存储了对应密码,其中AppleID是[步骤2]中获取的;如果没有,则在终端请求用户输入,并且会将用户输入的密码存储在钥匙链中。

  4. 登录到苹果开发网站 如果有两步验证,则还需要输入对应手机的验证码

  5. 获取TeamID 如果这个AppleID账号加入了多个Team,可以通过设置TeamID或TeamName来指定一个Team,具体来说可以通过环境变量FASTLANE_TEAM_IDCERT_TEAM_ID:team_id指定TeamID,通过环境变量FASTLANE_TEAM_NAME,CERT_TEAM_NAME:team_name指定TeamName,否则,需要用户手动来选择。如果你的AppleID账号只加入了一个Team,则直接使用此Team的TeamID。

  6. 检测force 6.1. 当:forcetrue时,强制创建证书,执行[步骤8] 6.2. 当:forcefalse时,执行[步骤7]

  7. 检测本地证书 遍历AppleID账号中的已创建证书,检测此证书是否存在于钥匙链中,或者:output_path目录下是否存在此证书对应的密钥(p12),其具体的检测流程会在下文中讲到。 7.1. 本地有可用证书,执行[步骤9] 7.2. 本地无可用证书,执行[步骤8]

  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账户下,相同类型的证书只能创建两个,如果已经创建了两个之后,再去尝试创建证书,则会报错。

  1. 导入此证书和它的私钥到钥匙链中 在终端中使用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-dbcertificate_password表示证书的密码,默认是空字符串,通过cert创建的证书的密码为空; -T usr/bin/codesign表示使用usr/bin/codesign访问这个证书的时候不需要授权,也就是不需要输入钥匙链的密码,这个在CI中会很有用。 最后需要注意的是,如果证书本来就是在钥匙链中,则不会执行这个步骤,也不会执行这条命令,所以在CI中使用时,最好在构建脚本中加上security unlock-keychain -p certificate_password ~/Library/Keychains/login.keychain-db,这条命令的作用和上面的-T类似,但是范围更广,即访问整个钥匙链都不需要输入密码。

  1. 设置全局变量 设置CER_CERTIFICATE_IDCER_FILE_PATH这两个环境变量,分别表示证书的id和证书的路径,证书的路径就是:output_path目录下的证书文件的路径。 如果是在lane中调用cert,则还会设置环境变量SIGH_CERTIFICATE_ID,这样设置之后,如果接下来sigh需要创建一个配置文件,就会使用环境变量SIGH_CERTIFICATE_ID指向的签名证书来创建。(环境变量SIGH_CERTIFICATE_ID仅仅只是在创建新的配置文件的时候才会使用)

2.1. 检测本地证书

  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. 获取证书列表中的下一个证书 遍历[步骤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>

  1. 下载此证书文件到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}.cercertificate.id表示上述证书对象的id。

  1. 检测本地钥匙链 这一步的目的就是检测本地钥匙链中是否存在[步骤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]

  1. output_path中检测私钥 检测:output_path目录中是否存在#{certificate.id}.p12certificate.id表示[步骤2]中获取的证书对象的id,这里是仅仅只是通过文件名来判断其是否存在。 若存在,说明本地存在可用证书,则执行[步骤8] 若不存在,说明本地不存在可用证书,则执行[步骤6]

  2. 从output_path中删除此证书 删除[步骤3]中下载的证书文件

  3. 本地没有可用证书

  4. 本地有可用证书

3. sigh

sigh是用于管理配置文件profile,在 sigh这个Tool中,其内部集成了多个Command,分别是renew、download_all、repair、resign、manage,其中默认Command是renewrenew的作用是从AppleID账号中获取一个可用的配置文件profile,如果没有,则创建一个新的profile,然后将它按照到xcode中。

这里只讨论renew,如果没有特殊说明,指的都是这种情况。 当在终端执行fastlane sigh [renew]时,其执行逻辑如下

前几步与cert类似,只是有一些用来传值的环境变量有些不同。

  1. 获取AppleID 可通过:username、环境变量SIGH_USERNAME、DELIVER_USER、DELIVER_USERNAMEAppfile三种途径获取;如果没有,则在终端请求用户输入AppleID。

  2. 获取AppleID对应密码

  3. 登录到苹果开发网站

  4. 获取TeamID 通过环境变量FASTLANE_TEAM_ID、环境变量SIGH_TEAM_ID:team_id指定TeamID,通过环境变量FASTLANE_TEAM_NAME,环境变量SIGH_TEAM_NAME:team_name指定TeamName

  5. 获取profile列表 首先从AppleID账号中,获取所有已创建的provisioning profiles的列表(也包含xcode管理的),然后经过一步步的过滤,最终得到所有可用的profile。 5.1 获取的profile列表有值,则执行[步骤6] 5.2 获取的profile列表有值,则执行[步骤16]

  6. 获取第一个profile

  7. 检测force :force指定是否强制创建新的provisioning profile 7.1 :force等于true,执行[步骤8] 7.2 :force等于false,执行[步骤10]

  8. 在AppleID中删除此profile 在AppleID账号中,删除[步骤6]中获取的profile

  9. 在AppleID中创建新的profile 如果是[步骤16]跳转过来的,还需要保证AppleID账号中存在此:app_identifier

  10. 返回profile 如果:force等于true,则返回[步骤9]中创建的profile; 如果:force等于false,则返回[步骤6]中获取的profile.

  11. 下载profile文件 之前步骤中提到profile是provisioning profile的概要描述,这里下载的profile文件,则是在项目中使用的配置文件。下载完成后,将文件存储在临时目录中。

  12. output_path目录下存储profile文件 将[步骤11]下载的文件移动到:output_path目录下,如果指定了:filename,则文件名为#{filename}.mobileprovision;否则,文件名为#{type}_#{app_identifier}.mobileprovision,其中type表示prifile的类型,可能是AppStore、AdHoc、InHouse和Development。

  13. 检测skip_install :skip_install指定是否安装profile到钥匙链中 如果:skip_install等于true,则执行[步骤15] 如果:skip_install等于false,则执行[步骤14]

  14. 安装profile到钥匙链中 将[步骤12]中的profile文件复制到~/Library/MobileDevice/Provisioning Profiles/目录下,文件名为#{uuid}.mobileprovision,其中uuid指的是profile的uuid

  15. 返回output_path路径 返回:output_path指定的目录路径,然后退出程序

  16. 检测readonly :readonly指定是否在AppleID账号中创建新的profile 如果:readonly等于false,则执行[步骤9] 如果:readonly等于true,异常退出

3.1 获取profile列表

获取所有已创建的provisioning profiles的列表,然后经过一步步的过滤,最终得到所有可用的profile。

  1. 下载所有的profile 所有的pofile是指AppleID账号中看得到的所有provisioning profile(即使是invalid)和通过xcode创建的,通过xcode创建的profile不会显示在AppleID中。

  2. 检测development和adhoc :development:adhoc用来指定profile的类型,profile的类型总共有四种,分别是Development、AppStore、AdHoc、InHouse

  3. 检测force 如果:force是true,则不会删除不可用的profile,因为后面会强制创建新的profile,不会使用当前这些profile,也就无所谓可用还是不可用了。

  4. 过滤adhoc或appstore 下面是sigh的源码,个人猜测,下载profile时,返回的json数据中有一个叫做distributionMethod的key,这个key的取值范围是['inhouse', 'store', 'limited', 'direct']。adhocappstore类型的profile返回的distributionMethod的值都是store。在本步骤之前都没有区分adhocappstore,在这一步骤中,会根据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
  1. 删除不可用证书的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都是在筛选可用的签名证书列表

  1. 下载当前平台和发布模式的证书列表 比如当前使用的AppleID账号是一个企业开发者账号,且:platform等于ios:development:adhoc都等于false,则在本步骤中会下载ios平台下所有的In-House签名证书。

  2. 检测cert_id和cert_owner_name :cert_id是签名证书的唯一标识符,:cert_owner_name是签名证书所属的team的name。

  3. 删除不匹配的证书 当:cert_id有值,且证书的cert_id和它不相等,则从证书列表中删除此证书; :cert_owner_name有值,且证书的cert_owner_name和它不相等,则从证书列表中删除此证书;

  4. 检测skip_certificate_verification

  5. 删除不在钥匙链中的证书 检测证书是否在本地钥匙链,其具体步骤可查看2.1节的步骤4

  6. 检测剩余证书的数目 剩余的证书数据为0,异常退出

  7. 检测development

  8. 返回所有剩余证书 开发环境下的profile可以包含多个签名证书,所有返回所有的剩余证书

  9. 返回剩余证书中的第一个 生产环境下的profile只能包含一个签名证书,所有返回剩余证书中的第一个。如果想使用特定的签名证书,最好使用:cert_id指定。

  10. 获取profile的name 首先,如果有设置:provisioning_name,则使用设置的值作为profile的name;否则,使用#{bundle_id} #{profile_type}这种格式,比如com.fastlane.demo InHouse 然后,如果skip_fetch_profiles的值是fasle,则会去检测这个名字是否已经被使用了,如果被使用了,就在这个名字后面加上一个空格和一个当前的时间戳。

  11. 获取注册设备的ids 如果当前的发布模式是AppStore、InHouse、Direct,即development=false and adhoc=false,ids等于空数组; 否则,ids等于当前平台的所有注册设备的id集合;

  12. 获取其他参数 其他参数还包含:team_id、:app_identifier、:template_name等,:app_identifier指定的bundle_id必须在AppleID账号中有创建对应的App ID,否则会异常退出。

  13. 生成并发出创建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等于macios时,请求参数subPlatform等于nil;否则subPlatform等于tvos