超级签名 证书类型
整数类型
使用场景
开发(Development)证书和描述文件
用于开发测试,在Xcode中打包后,可在真机环境调试、安装
发布(Distribution)证书和描述文件
* 发布(Distribution)证书和描述文件:用于提交Appstore,在Xcode中打包后,可使用Xcode、Application Loader提交到Appstore审核发布
证书作用
证书
文件后缀
文件类型
作用
Provisioning Profile
.mobileprovision
描述文件
绑定设备UDID,所以在申请开发描述文件之前,先添加调试的设备。
Signing Certificate
.cer/.p12
证书文件
有开发和发布的证书,可以在钥匙串查看安装的可用的证书,过期时间等。p12是一个加密的文件,只要知道其密码,就可以供给所有的 Mac 设备使用,是这个应用的唯一标识证书和开发者,用于对应 bundleID 的应用开发和打包测试
如果是团队开发,一般会生成 p12 给组员使用,方便管理证书
证书名字
类型
证书用途
adhocXXX.mobileprovision
描述文件
用于生成 adhoc 包时,描述可以安装ipa包的设备UDID和证书关系。(包含推送、apple pay等权限声明内容)
devXXX.mobileprovision
描述文件
用于生成 dev 包时,描述可以安装ipa包的设备UDID和证书关系。(包含推送、apple pay等权限声明内容)
devXXXPushXXX.p12
推送证书
用于 dev 包推送时,认证和关联 应用bundleID 的证书关系
devXXX.p12
开发证书
用于打包App时,生成 dev 的 ipa 包需要的开发者信息。
disXXX.mobileprovision
描述文件
用于生成 dis 包时,描述应用bundleID与证书的关系。(包含推送、apple pay等权限声明内容)。
disXXXPushXXX.p12
推送证书
用于 dis(或adhoc) 包推送时,认证和关联 应用bundleID 的证书关系。
disXXX.p12
发布证书
用于打包App时,生成 dis (或adhoc) 的 ipa 包需要的开发者信息。
证书类型的说明
用途
dev
adhoc
dis
企业证书
用于送审
F
T
T
F
未越狱,未在证书中的设备能否安装
F
F
F
T
未越狱,在证书中的设备能否安装
T
T
F
T
越狱能否安装
T
T
T
T
能否用于送审 TestFlight
F
F
T
F
不越狱能否打开应用的 Document 等
T
F
F
F
获取 UDID 使用配置文件获取 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd" > <plist version ="1.0" > <dict > <key > PayloadContent</key > <dict > <key > URL</key > <string > https://your.hostname/receive.php</string > <key > DeviceAttributes</key > <array > <string > UDID</string > <string > IMEI</string > <string > ICCID</string > <string > VERSION</string > <string > PRODUCT</string > </array > </dict > <key > PayloadOrganization</key > <string > your orignation name</string > <key > PayloadDisplayName</key > <string > Obtain UDID</string > <key > PayloadVersion</key > <integer > 1</integer > <key > PayloadUUID</key > <string > 3C4DC7D2-E475-3375-489C-0BB8D737A653</string > <key > PayloadIdentifier</key > <string > your identifier</string > <key > PayloadDescription</key > <string > Obtaining UDID</string > <key > PayloadType</key > <string > Profile Service</string > </dict > </plist >
需要填写回调数据的 URL 和 PayloadUUID。该PayloadUUID仅仅是随机生成的唯一字符串,类似bundleid,一般是域名倒置,用来标识唯一。
mobileconfig中的URL要用https地址。否则 https://your.hostname/receive.php 会报ATS错误。
mobileconfig 文件签名 1 security cms -S -N "Apple Development: XXX" -i udid.mobileconfig -o udid.signed.mobileconfig
这个地方使用自己的开发者账号就行,具体的内容打开keychain,我的账号是绑定在公司的 team 里面,就用它签名就行
receive.php 安装成功后,系统会自动回调 mobileconfig 中的地址
服务器端支持 ssl 这块在本地我用的 mamp pro 来搞的,比较傻瓜化
index.php
为了测试方便,就放了个按钮,指向旁边的 mobileconfig 文件1 2 3 <div class ="title-box" > <a class ="buttons" href ="udid.signed.mobileconfig" target ="_blank" > 获取UDID</a >
receive.php
主要做的事情是解析上传的内容,并且跳转到新的页面
跳转是必须的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 <?php $data = file_get_contents ('php://input' );$plistBegin = '<?xml version="1.0"' ;$plistEnd = '</plist>' ;$pos1 = strpos ($data , $plistBegin );$pos2 = strpos ($data , $plistEnd );$data2 = substr ($data ,$pos1 ,$pos2 -$pos1 );$xml = xml_parser_create ();xml_parse_into_struct ($xml , $data2 , $vs );xml_parser_free ($xml );$UDID = "" ;$CHALLENGE = "" ;$DEVICE_NAME = "" ;$DEVICE_PRODUCT = "" ;$DEVICE_VERSION = "" ;$iterator = 0 ;$arrayCleaned = array ();foreach ($vs as $v ){ if ($v ['level' ] == 3 && $v ['type' ] == 'complete' ){ $arrayCleaned []= $v ; } $iterator ++; }$data = "" ;$iterator = 0 ;foreach ($arrayCleaned as $elem ){ $data .= "\n==" .$elem ['tag' ]." -> " .$elem ['value' ]."<br/>" ; switch ($elem ['value' ]) { case "CHALLENGE" : $CHALLENGE = $arrayCleaned [$iterator +1 ]['value' ]; break ; case "DEVICE_NAME" : $DEVICE_NAME = $arrayCleaned [$iterator +1 ]['value' ]; break ; case "PRODUCT" : $DEVICE_PRODUCT = $arrayCleaned [$iterator +1 ]['value' ]; break ; case "UDID" : $UDID = $arrayCleaned [$iterator +1 ]['value' ]; break ; case "VERSION" : $DEVICE_VERSION = $arrayCleaned [$iterator +1 ]['value' ]; break ; } $iterator ++; }$params = "UDID=" .$UDID ."&CHALLENGE=" .$CHALLENGE ."&DEVICE_NAME=" .$DEVICE_NAME ."&DEVICE_PR ODUCT=" .$DEVICE_PRODUCT ."&DEVICE_VERSION=" .$DEVICE_VERSION ;header ('HTTP/1.1 301 Moved Permanently' );header ("Location: https://your.hostname/receive.php?" .$params );?>
前期工作准备好之后 如下是在手机端的截图,能看到 mobileconfig 是签名过的,并且还有回调地址。
安装成功后,系统会自动的访问 mobileconfig 中的地址,返回数据
使用 fastlane 对 ipa 进行重签名 安装 fastlane
安装 ruby 由于系统自带 ruby,因此安装任何包需要 root 权限,为了防止更新系统导致兼容性等问题,故全新安装 ruby,将 local 和 system 的 ruby 隔离
并且根据提示将 ruby 加入到 path 中
1 export PATH="/usr/local/opt/ruby/bin:$PATH "
1 2 gem install pry gem install fastlane
测试安装是否正确 尝试拉取一下
1 2 3 4 5 6 7 8 9 10 11 require "spaceship" Spaceship .login('your_developerapple_id' ,'your_pass_word' )Spaceship .certificate.all.each do |cert | cert_type = Spaceship : :Portal : :Certificate : :CERTIFICATE_TYPE_IDS [cert.type_display_id].to_s.split("::" )[-1 ] puts "Cert id: #{cert.id} , name: #{cert.name} , expires: #{cert.expires.strftime("%Y-%m-%d" )} , type: #{cert_type} " end all_devices = Spaceship : :Portal .device.all.each do |device | puts "device id: #{device.id} , name: #{device.name} , udid: #{device.udid} " end
结果如下,只截取关键部分
1 2 3 4 5 Cert id: XXXXXX, name: Development, expires: 2021-01-15, type: AppleDevelopment ..... // 另外还有 device 列表 device id: XXXX, name: bbbb, udid: XXXXXXXX
将获取到的 UDID 加入到设备列表中 需要将获取到的 UDID 加入到 team 的设备列表里面,才能够将重签名的 ipa 下发给需要的设备安装使用。
如下的代码从文件中读取上传来的 UDID 和设备名称,加入到所有的 ad_hoc 的描述文件中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 file = File .open("devices-udid-name-ios.txt" ) file.each do |line | arr = line.strip.split(" " ) device = Spaceship .device.create!(name: arr[1 ], udid: arr[0 ]) puts "add device: #{device.name} #{device.udid} #{device.model} " end devices = Spaceship .device.all profiles = Array .new profiles += Spaceship .provisioning_profile.development.all profiles += Spaceship .provisioning_profile.ad_hoc.all profiles.each do |p | puts "Updating #{p.name} " p.devices = devices p.update! end
运行结果能看到
1 2 Updating Test01 Updating adHocResign
已经将获取到的 UDID 加到 ad_hoc 中
provision 位置 如果需要查看系统中已经保存的描述文件:
1 ~/Library/MobileDevice/Provisioning\ Profiles/
签名 1 fastlane sigh resign your.ipa --signing_identity 'iPhone Distribution: XXXX' -p your.mobileprovision
使用 Linux 对 ipa 进行签名 zsign GitHub - zhlynn/zsign: Maybe is the most quickly codesign alternative for iOS12+ in the world, cross-platform ( Linux & macOS ), more features. sign!
从介绍看只需要提供 p12 文件和 provisioning profile 即可完成签名
生成 p12
1 2 openssl genrsa -out ios_distribution.key 2048 openssl req -new -key ios_distribution.key -out ios_distribution.csr -subj '/emailAddress=me@example.com, CN=Example, C=US'
1 openssl x509 -inform der -in ios_distribution.cer -outform PEM -out ios_distribution.pem
1 2 wget http://developer.apple.com/certificationauthority/AppleWWDRCA.cer openssl x509 -in AppleWWDRCA.cer -inform DER -out AppleWWDRCA.pem -outform PEM
1 openssl pkcs12 -export -out ios_distribution.p12 -inkey ios_distribution.key -in ios_distribution.pem -certfile AppleWWDRCA.pem
安装 zsign 1 g++ *.cpp common/*.cpp -lcrypto -I/usr/local/Cellar/openssl@1.1/1.1.1f/include -L/usr/local/Cellar/openssl@1.1/1.1.1f/lib -O3 -o zsign
注意指定头文件和库文件
使用 zsign 签名 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Usage: zsign [-options] [-k privkey.pem] [-m dev.prov] [-o output.ipa] file|folder options: -k, --pkey Path to private key or p12 file. (PEM or DER format) -m, --prov Path to mobile provisioning profile. -c, --cert Path to certificate file. (PEM or DER format) -d, --debug Generate debug output files. (.zsign_debug folder) -f, --force Force sign without cache when signing folder. -o, --output Path to output ipa file. -p, --password Password for private key or p12 file. -b, --bundleid New bundle id to change. -n, --bundlename New bundle name to change. -e, --entitlements New entitlements to change. -z, --ziplevel Compressed level when output the ipa file. (0-9) -l, --dylib Path to inject dylib file. -w, --weak Inject dylib as LC_LOAD_WEAK_DYLIB. -i, --install Install ipa file using ideviceinstaller command for test . -q, --quiet Quiet operation. -v, --version Show version. -h, --help Show help .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 /usr/local/bin/zsign -f -k ios_distribution.p12 -p 123456 -m adHocResign.mobileprovision -b 'com.your.package.BundleId' -n 'your.app.name' -o your.ipa -z 9 your.signed.ipa >>> Unzip: your..ipa (21.26 MB) -> /tmp/zsign_folder_1586264064122616 ... >>> Unzip OK! (0.318s, 317925us) >>> BundleId: your.bundle.id -> your.new.bundle.id >>> BundleName: -> ToTok >>> Signing: /tmp/zsign_folder_1586264064122616/Payload/your.app ... >>> AppName: your.app.name >>> BundleId: com.your.package.BundleId >>> TeamId: XXXXXXX >>> SubjectCN: Apple Distribution: XXXXXXX >>> ReadCache: NO >>> SignFile: libswiftRemoteMirror.dylib >>> SignFile: Frameworks/libswiftCoreImage.dylib >>> SignFile: Frameworks/libswiftObjectiveC.dylib >>> SignFile: Frameworks/libswiftCore.dylib >>> SignFile: Frameworks/libswiftCoreGraphics.dylib >>> SignFile: Frameworks/libswiftUIKit.dylib >>> SignFile: Frameworks/libswiftMetal.dylib >>> SignFile: Frameworks/libswiftDispatch.dylib >>> SignFile: Frameworks/libswiftos.dylib >>> SignFile: Frameworks/libswiftCoreFoundation.dylib >>> SignFile: Frameworks/libswiftDarwin.dylib >>> SignFile: Frameworks/libswiftQuartzCore.dylib >>> SignFile: Frameworks/libswiftFoundation.dylib >>> SignFile: Frameworks/libswiftSwiftOnoneSupport.dylib >>> SignFolder: your.singed.app, (your.app.name) >>> Signed OK! (0.746s, 745663us) >>> Archiving: your.singed.appipa ... >>> Archive OK! (21.23 MB) (2.342s, 2341848us) >>> Done. (3.422s, 3421883us)
安装成功,并且可以正常打开
超级签名脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 require 'docopt' require 'spaceship' require 'pathname' doc = <<DOCOPT Usage: #{__FILE__ } --apple_id=<apple_id> --password=<password> --bundle_id=<bundle_id> --input_ipa=<input_ipa> #{__FILE__ } --help | -h #{__FILE__ } --input_file=<file_name> DOCOPT begin args = Docopt : :docopt (doc)rescue Docopt : :Exit => e puts e.message exitend class DevelopPortalHandle def initialize (userName, passwd, bundleID ) puts(" " ) puts ("====== initialize ======" ) @userName = userName @bundleID = bundleID @passwd = passwd list = bundleID.split("." ) appidLastName = list.last @appName = appidLastName @provisionName = appidLastName end def login () puts(" " ) puts ("====== login ======" ) Spaceship : :Portal .login(@userName ,@passwd ) Spaceship : :Portal .select_team end def allApp () puts(" " ) puts ("====== allApp ======" ) Spaceship : :Portal .app.all.collect do |app | puts "app.name: #{app.name} , app id #{app.app_id} , bundle id :#{app.bundle_id} " end end def createApp () puts(" " ) puts ("====== createApp ======" ) puts "createApp find: bundle id = #{@bundleID } appName = #{@appName } " app = Spaceship : :Portal .app.find(@bundleID ) if !app then puts("can't find #{@bundleID } create a new one" ) app = Spaceship : :Portal .app.create!(bundle_id: @bundleID , name: @appName ) puts "createApp #{app} " end end def createDistributionProvision (provisioningClass ) puts(" " ) puts ("====== createDistributionProvision ======" ) cert = Spaceship : :Portal .certificate.production.all.last provisionNameDis = @provisionName + '_dis' profile = provisioningClass.create!(bundle_id: @bundleID ,certificate: cert,name: @provisionName ) return profile end def downloadDistributionProvision (provisioningClass ) puts(" " ) puts ("====== downloadDistributionProvision ======" ) filtered_profiles = provisioningClass.find_by_bundle_id(bundle_id: @bundleID ) profile = nil if 0 < filtered_profiles.length then profile = filtered_profiles[0 ] elsif 0 == filtered_profiles.length then profile = createProvision(provisioningClass) end provisionNameDis = @provisionName + '_dis' provisionFileName = provisionNameDis + '.mobileprovision' File .write(provisionFileName, profile.download) return provisionFileName end def downloadAdHocProvision () puts(" " ) puts ("====== downloadAdHocProvision ======" ) filtered_profiles = Spaceship : :Portal .provisioning_profile.ad_hoc.find_by_bundle_id(bundle_id: @bundleID ) profile = nil if 0 < filtered_profiles.length then puts ("#{@bundleID } 's provisioning profile exist" ) profile = filtered_profiles[0 ] elsif 0 == filtered_profiles.length then puts ("createProvision" ) profile = createProvision(provisioningClass) end provisionNameAdHoc = @provisionName + '_adhoc' provisionFileName = provisionNameAdHoc + '.mobileprovision' absolutePath = Pathname .new(File .dirname(__FILE__ )).realpath.to_s << "/" << provisionFileName puts("absolutePath = #{absolutePath} ;" ) File .write(absolutePath, profile.download) return absolutePath end def createDevelopProvision () puts(" " ) puts ("====== createDevelopProvision ======" ) dev_certs = Spaceship : :Portal .certificate.development.all all_devices = Spaceship : :Portal .device.all provisionNameDev = @provisionName + '_dev' profile = Spaceship : :Portal .provisioning_profile.development.create!(bundle_id: @bundleID ,certificate: dev_certs,name: provisionNameDev,devices: all_devices) return profile end def downloadDevelopProvision () puts(" " ) puts ("====== downloadDevelopProvision ======" ) filtered_profiles = Spaceship : :Portal .provisioning_profile.development.find_by_bundle_id(bundle_id: @bundleID ) profile = nil if 0 < filtered_profiles.length then profile = filtered_profiles[0 ] elsif 0 == filtered_profiles.length then profile = createDevelopProvision() end provisionNameDev = @provisionName + '_dev' provisionFileName = provisionNameDev + '.mobileprovision' File .write(provisionFileName, profile.download) return provisionFileName end def addServices (appServiceObj ) puts(" " ) puts ("====== addServices ======" ) puts(" add #{appServiceObj} " ) app = Spaceship : :Portal .app.find(@bundleID ) app.update_service(appServiceObj) end def addDevices (fileName ) puts(" " ) puts ("====== addDevices ======" ) file = File .open(fileName) file.each do |line | arr = line.strip.split(" " ) device = Spaceship .device.create!(name: arr[1 ], udid: arr[0 ]) puts "add device: name = #{device.name} ; udid = #{device.udid} ; model = #{device.model} " end puts ("====== addDevices-updateAdHoc ======" ) profiles = Array .new profiles += Spaceship .provisioning_profile.ad_hoc.all devices = Spaceship .device.all profiles.each do |p | puts "Updating #{p.name} " p.devices = devices p.update! end end def resign (provisionFile,inputIpa ) puts(" " ) puts ("====== resign ======" ) resignCmd = "/usr/local/bin/zsign -f -k adHoc_resign.p12 -p 123456" resignCmd << " -m " << provisionFile resignCmd << " -b " << "'#{@bundleID } '" resignCmd << " -n 'ToTok'" resignCmd << " -o ToTok_AD_HOC_signed.ipa" resignCmd << " -z 9" resignCmd << " " << inputIpa puts ("resign cmd = #{resignCmd} ;" ) exec(resignCmd) end end handle = DevelopPortalHandle .new(args['--apple_id' ], args['--password' ], args['--bundle_id' ]) handle.login() handle.createApp() handle.addDevices("udid.txt" ) handle.addServices(Spaceship : :Portal .app_service.push_notification.on) provisionPath = handle.downloadAdHocProvision() handle.resign(provisionPath, args['--input_ipa' ])
脚本运行 1 ruby resignIPAWithUDID.rb --apple_id=your.apple.id --password=your.password --bundle_id=your.bundle.id --input_ipa=your.signed.ipa
Trouble shooting ruby 脚本不能执行 因为在服务器端执行的 ruby 脚本的用户和我测试用的用户不是同一个用户,因此 ruby 的环境不一样,尤其是 gem
解决办法: 将 ruby 脚本封装成 shell 脚本,并且在执行 shell 脚本的时候重新制定 ruby 环境
1 2 3 export GEM_HOME='/home/www/.rvm/gems/ruby-2.3.0' ;export GEM_PATH='/home/www/.rvm/gems/ruby-2.3.0:/home/www/.rvm/gems/ruby-2.3.0@global' ;export MY_RUBY_HOME='/home/www/.rvm/rubies/ruby-2.3.0' ;
换了用户会弹出两步验证 参考文档:
Continuous Integration - fastlane docs
生成 fastlane session
1 fastlane spaceauth -u user@email.com
将生成的内容放到脚本里
1 export FASTLANE_SESSION='XXXXXX\n'
访问重签名页面调用脚本即可