使用 Developer ID 为 Mac app 签名

为何要签名

对 app 进行代码签名可让用户确信它来自已知来源,且自最后一次签名之后未被修改。在您的 Mac app 或 iOS app 可以使用商店服务,安装到 iOS 设备上进行开发或测试,或者提交到 App Store 之前,必须先使用 Apple 颁布的证书对其进行签名。

在 OS X 的安全性与隐私的设置里有一项设置是允许从以下位置下载的应用程序,默认的设置是Mac App Store 和被认可的开发者

也就是说在用户不手动修改设置的情况下,用户无法打开未签名的程序。

打开未签名程序时的警告

下面就来介绍如何使用 Developer ID 为 app 签名。

注意:本文不适用于发布到 Mac App Store 的签名。

如何签名

将你的 Apple ID 添加到 Xcode

  1. 打开 Xcode 的 Preferences。
  2. 选择 Accounts
  3. 点击左下角的加号按钮。
  4. 填入你的 Apple ID。

加入一个 Program

在添加 Apple ID 的界面,你可以选择 Join a Program... 来加入一个 program。

你也可以让 program 的管理员来将你添加到你们公司的 program。

在 Xcode 中设置 signing identity

  1. 选择你项目的 target。
  2. 选择 General
  3. Signing 中选择 Developer ID
  4. Team 中选择你加入的 program。

创建 Developer ID certificate

方法一:

首先在自己的电脑上使用钥匙串访问来创建一个certificate signing request (CSR)

  1. 打开钥匙串访问,在菜单栏中选择钥匙串访问 > 证书助理 > 从证书颁发机构请求证书
  2. 填入你的邮件地址和常用名,选择下方的存储到磁盘,点击继续选择文件位置保存即可。
  3. 管理员在 Member Center 里面的Certificates, Identifiers & Profiles页面中,选择Certificates下方的All
  4. 选择右上角的加号按钮。
  5. 选择Production下方的Developer ID
  6. 选择证书类型Developer ID Application,点击Continue
  7. 选择前面生成的 CSR(扩展名是 .certSigningRequest) 文件,点击Continue
  8. 选择Generate并且下载证书。
  9. 将证书导入到 Keychain 中。

方法二:

  1. 打开 Xcode 的偏好设置,选择Accounts选项卡,选择加入的 program,点击View Details
  2. 此时 Xcode 没有找到任何证书,会提示是否要帮你申请证书,此时只要选择以The Developer ID Application开头的选项即可。(也可以点击左边的加号来添加一个Developer ID Distribution
  3. 等待 program 的管理员来审批。

验证:

方法一:

在 Xcode 的 Preference > Accounts 中选择加入的 program,选择 View Details...,应该能看到一项 Developer ID Application

方法二:

在钥匙串访问中能找到一个名为 Developer ID Application: Your Company Name 的证书。

签名

方法一(使用 Xcode 签名):

  1. 选择菜单中的 Product > Archive
  2. (可选)选择 Validate... 来验证打包。
  3. 点击 Export...
  4. 按照提示选择 Export a Developer ID-signed Application
  5. 选择一个 team。
  6. 输入文件名,保存即可。

但是使用 Xcode 签名有可能会失败,下面的签名验证部分会提到。

方法二(使用命令行):

1
codesign --force --verbose=4 --sign "Developer ID Application: Your Company Name" Foo.app

codesign 要求项目中包含的所有框架、库都已经被签名,并且 codesign 不会自动帮你完成,这需要我们单独为每一个库进行签名后再为整个 app 签名。

签名或验证一个 framework 时使用的路径是MyCustomFramework/Versions/A,比如

1
codesign -s "Developer ID Application: Your Company Name" ../MyCustomFramework/Versions/A

签名时不要使用--deep参数。

如果 framework 签名失败,可能是由于

  • Info.plist 文件中的 CFBundleExecutable 值与可执行文件的文件名不一致。
  • 目录结构不对(比如 QT),framework 的目录结构必须与下面完全一样。
1
2
3
4
5
6
7
8
9
10
11
MyFramework.framework/
MyFramework -> Versions/Current/MyFramework
Resources -> Versions/Current/Resources
Versions/
A/
MyFramework
Resources/
English.lproj/
InfoPlist.strings
Info.plist
Current -> A

签名验证

输入下面的命令:

1
spctl -a -v Foo.app

如果验证通过,则会提示:

1
2
3
Foo.app: accepted  
source=Developer ID
origin=Developer ID Application: Your Company Name

出现下面的提示:

1
2
Foo.app: rejected  
source=obsolete resource envelope

此时如果用户通过浏览器或者邮箱接收到安装包再打开 app 时就会提示文件已损坏。

解决方法:

使用下面的命令

1
codesign --verify --deep --verbose=3 /path/to/signed/app

找出验证不通过的重新签名,再重新签名整个 app 即可。

脚本

下面是我用来签名的脚本。

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
#!/usr/bin/python

import os
import commands

APP_PATH = "/path/to/Foo.app"
DEVELOPER_ID = "Developer ID Application: Your Awesome Company"

def signWithPath(path):
signCommand = "codesign --force --sign \"%s\" \"%s\"" % (DEVELOPER_ID, path)
retCode, result = commands.getstatusoutput(signCommand)
if retCode != 0:
print result
print "code sign failed"
return retCode

def validateWithPath(path):
signCommand = "codesign --verify --deep --verbose=3 \"%s\"" % path
retCode, result = commands.getstatusoutput(signCommand)
if retCode == 0:
print "Accepted!"
return 0
else:
print result
print "Rejected!"
return -2

def sign():
print "> signing frameworks & dylibs..."

if not os.path.exists(APP_PATH):
print "where's your app?!"
return

frameworkDir = os.path.join(APP_PATH, "Contents/")

# sign dylibs
for root, dirs, files in os.walk(frameworkDir):
for f in files:
if f.endswith(".dylib"):
print "signing", f
dylibPath = os.path.join(root, f)
signWithPath(dylibPath)

# sign frameworks
for root, dirs, files in os.walk(frameworkDir):
for d in dirs:
if d.endswith(".framework"):
print "signing", d
frameworkPath = os.path.join(root, d, "Versions/A")
signWithPath(frameworkPath)


print "> singing app..."
print "singing", APP_PATH
signWithPath(APP_PATH)

print "> validate code sign..."
if validateWithPath(APP_PATH) == 0:
print "Code sign completed!"
else:
print "Dohhh!"


if __name__ == '__main__':
sign()

参考

https://developer.apple.com/library/mac/documentation/IDEs/Conceptual/AppDistributionGuide/MaintainingCertificates/MaintainingCertificates.html#//apple_ref/doc/uid/TP40012582-CH31-SW30

http://furbo.org/2013/10/17/code-signing-and-mavericks/

https://github.com/sunuslee/sunus-cookbook/blob/master/Cocoa/codesign.md

给鸡排饭加个蛋