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

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-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类似,但是范围更广,即访问整个钥匙链都不需要输入密码。

  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是renew
renew的作用是从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中。
  1. 检测development和adhoc
    :development:adhoc用来指定profile的类型,profile的类型总共有四种,分别是Development、AppStore、AdHoc、InHouse
  1. 检测force
    如果:force是true,则不会删除不可用的profile,因为后面会强制创建新的profile,不会使用当前这些profile,也就无所谓可用还是不可用了。
  1. 过滤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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容