SuperSign

超级签名

证书类型

整数类型 使用场景
开发(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,只要更新 .mobileprovision 描述文件既可以

  • 如果证书过期,.p12 和 .mobileprovision 文件需要重新生成,如果 revoke后生成新的证书,旧的证书和证书对应的 ipa 不能再安装在设备上

  • 描述文件保存在 macOS 路径:

    1
    ~/Library/MobileDevice/Provisioning\ Profiles/
  • 查看安装的证书使用命令:

    1
    security find-identity -p codesigning -v

获取 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>
  • 需要填写回调数据的 URLPayloadUUID。该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 文件下载显示已签名

INSTALL_PROFILE.PNG

  • 查看签名信息

OBTAIN_UDID.PNG

  • 安装成功后,系统会自动的访问 mobileconfig 中的地址,返回数据

RESIGN.PNG

使用 fastlaneipa 进行重签名

安装 fastlane

  • 安装 ruby
    由于系统自带 ruby,因此安装任何包需要 root 权限,为了防止更新系统导致兼容性等问题,故全新安装 ruby,将 localsystemruby 隔离
1
brew install ruby

并且根据提示将 ruby 加入到 path 中

1
export PATH="/usr/local/opt/ruby/bin:$PATH"
  • 安装 fastlane 以及依赖
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") #文本文件里录入的udid和设备名用tab分隔
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

  • 安装 openssl
1
brew install openssl
  • 生成 csr 文件
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'
  • 将生成的文件上传到开发者网站创建 Provisioning Profile

  • 下载生成好的文件(证书)生成 PEM 文件

1
openssl x509 -inform der -in ios_distribution.cer -outform PEM -out ios_distribution.pem
  • 下载根证书并且转为 PEM 文件
1
2
wget http://developer.apple.com/certificationauthority/AppleWWDRCA.cer
openssl x509 -in AppleWWDRCA.cer -inform DER -out AppleWWDRCA.pem -outform PEM
  • 导出P12
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
exit
end

# args.each do |key, value|
# case key
# when "--apple_id" then
#
# end
# # puts key
# # puts value
# end

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 # 让用户可以选择 team
# Spaceship.client.team_id = "这里输入TeamId"
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'

访问重签名页面调用脚本即可

作者

shouyi.www

发布于

2020-04-17

更新于

2025-01-30

许可协议

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×