本文介绍Android系统build阶段的签名机制。
一、系统build阶段签名机制
- 系统中有4组key用于build阶段对apk进行签名:
- Media
- Platform
- Shared
- Testkey
default key是放在Android源码的/build/target/product/security目录下:
- media.pk8与media.x509.pem;
- platform.pk8与platform.x509.pem;
- shared.pk8与shared.x509.pem;
- testkey.pk8与testkey.x509.pem;
其中,*.pk8文件为私钥,*.x509.pem文件为公钥,这需要去了解非对称加密方式。
- 在apk的android.mk文件中会指定LOCAL_CERTIFICATE 变量:
LOCAL_CERTIFICATE可设置的值如下:
1 | LOCAL_CERTIFICATE := testkey # 普通APK,默认情况下使用 |
对应的,除了在Android.mk指定上述的值,还需要在APK源码的AndroidManifest.xml文件的manifest节点里面申明权限:
1 | android:sharedUserId="android.uid.system" |
- Build规则是Build/core/prebuilt.mk。
- 在build/core/config.mk中,DEFAULT_SYSTEM_DEV_CERTIFICATE可以通过PRODUCT_DEFAULT_DEV_CERTIFICATE去指定各家厂商的key path。
我们可以看到,默认为build/target/product/security/testkey。
1 | # The default key if not set as LOCAL_CERTIFICATE |
二、自定义系统签名的key
上面介绍了系统有默认四组key,那么如果我们要制作自己的key,需要怎么做呢?
在build/target/product/security/目录下有一个README,里面有说明怎么制作这些key并且使用。
- 进入Development/tools/ 目录
- 使用make_key工具生成签名文件:其中:
1
sh make_key releasekey '/C=CN/ST=Guangdong/L=Shenzhen/O=Mediatek/OU=MTK/CN=fzll/emailAddress=maoao530@foxmail.com'
C : Country Name (2 letter code)
ST : State or Province Name (full name)
L : Locality Name (eg, city)
O : Organization Name (eg, company)
OU : Organizational Unit Name (eg, section)
CN : Common Name (eg, your name or your server’s hostname)
emailAddress : Contact email address
3. 用ls命令发现目录下多了两个文件:releasekey.x509.pem 和 releasekey.pk8
同样的步骤生成platform / shared / media
用自定义的key替换build/target/product/security/目录下面的key。
三、对APK进行系统签名
为了使apk有system权限,通常我们需要对其进行系统签名:
1、在应用程序的AndroidManifest.xml中的manifest节点中加入
1 | android:sharedUserId="android.uid.system"这个属性。 |
2、修改它的Android.mk文件,加入
1 | LOCAL_CERTIFICATE := platform |
三. Android签名机制之—签名过程详解
- 两种签名方式
- jarsigner
- signapk
- 签名流程机制
源码位置:com/android/signapk/sign.java
通过上面的签名时我们可以看到,Android签名apk之后,会有一个META-INF文件夹,这里有三个文件:
MANIFEST.MF
CERT.RSA
CERT.SF
下面来看看这三个文件到底是干啥的?
- MANIFEST.MF
开下源码:总结:生成三个文件 MANIFEST.MF,CERT.SF,CERT.RSA1
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【main】
public static void main(String[] args) {
if (args.length < 4) usage();
sBouncyCastleProvider = new BouncyCastleProvider();
Security.addProvider(sBouncyCastleProvider);
boolean signWholeFile = false;
int argstart = 0;
if (args[0].equals("-w")) {
signWholeFile = true;
argstart = 1;
}
if ((args.length - argstart) % 2 == 1) usage();
int numKeys = ((args.length - argstart) / 2) - 1;
if (signWholeFile && numKeys > 1) {
System.err.println("Only one key may be used with -w.");
System.exit(2);
}
String inputFilename = args[args.length-2];
String outputFilename = args[args.length-1];
JarFile inputJar = null;
FileOutputStream outputFile = null;
int hashes = 0;
try {
File firstPublicKeyFile = new File(args[argstart+0]);
X509Certificate[] publicKey = new X509Certificate[numKeys];
try {
for (int i = 0; i < numKeys; ++i) {
int argNum = argstart + i*2;
publicKey[i] = readPublicKey(new File(args[argNum]));
hashes |= getAlgorithm(publicKey[i]);
}
} catch (IllegalArgumentException e) {
System.err.println(e);
System.exit(1);
}
// Set the ZIP file timestamp to the starting valid time
// of the 0th certificate plus one hour (to match what
// we've historically done).
long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
PrivateKey[] privateKey = new PrivateKey[numKeys];
for (int i = 0; i < numKeys; ++i) {
int argNum = argstart + i*2 + 1;
privateKey[i] = readPrivateKey(new File(args[argNum]));
}
inputJar = new JarFile(new File(inputFilename), false); // Don't verify.
outputFile = new FileOutputStream(outputFilename);
if (signWholeFile) {
SignApk.signWholeFile(inputJar, firstPublicKeyFile,
publicKey[0], privateKey[0], outputFile);
} else {
JarOutputStream outputJar = new JarOutputStream(outputFile);
// For signing .apks, use the maximum compression to make
// them as small as possible (since they live forever on
// the system partition). For OTA packages, use the
// default compression level, which is much much faster
// and produces output that is only a tiny bit larger
// (~0.1% on full OTA packages I tested).
outputJar.setLevel(9);
Manifest manifest = addDigestsToManifest(inputJar, hashes);
copyFiles(manifest, inputJar, outputJar, timestamp);
signFile(manifest, inputJar, publicKey, privateKey, outputJar);
outputJar.close();
}
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
} finally {
try {
if (inputJar != null) inputJar.close();
if (outputFile != null) outputFile.close();
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
}
【signWholeFile中调用 signFile】
private static void signFile(Manifest manifest, JarFile inputJar,
X509Certificate[] publicKey, PrivateKey[] privateKey,
JarOutputStream outputJar)
throws Exception {
// Assume the certificate is valid for at least an hour.
long timestamp = publicKey[0].getNotBefore().getTime() + 3600L * 1000;
// MANIFEST.MF
JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
manifest.write(outputJar);
int numKeys = publicKey.length;
for (int k = 0; k < numKeys; ++k) {
// CERT.SF / CERT#.SF
je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
(String.format(CERT_SF_MULTI_NAME, k)));
je.setTime(timestamp);
outputJar.putNextEntry(je);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
writeSignatureFile(manifest, baos, getAlgorithm(publicKey[k]));
byte[] signedData = baos.toByteArray();
outputJar.write(signedData);
// CERT.RSA / CERT#.RSA
je = new JarEntry(numKeys == 1 ? CERT_RSA_NAME :
(String.format(CERT_RSA_MULTI_NAME, k)));
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureBlock(new CMSProcessableByteArray(signedData),
publicKey[k], privateKey[k], outputJar);
}
}
【main函数中 addDigestsToManifest】
/**
* Add the hash(es) of every file to the manifest, creating it if
* necessary.
*/
private static Manifest addDigestsToManifest(JarFile jar, int hashes)
throws IOException, GeneralSecurityException {
Manifest input = jar.getManifest();
Manifest output = new Manifest();
Attributes main = output.getMainAttributes();
if (input != null) {
main.putAll(input.getMainAttributes());
} else {
main.putValue("Manifest-Version", "1.0");
main.putValue("Created-By", "1.0 (Android SignApk)");
}
MessageDigest md_sha1 = null;
MessageDigest md_sha256 = null;
if ((hashes & USE_SHA1) != 0) {
md_sha1 = MessageDigest.getInstance("SHA1");
}
if ((hashes & USE_SHA256) != 0) {
md_sha256 = MessageDigest.getInstance("SHA256");
}
byte[] buffer = new byte[4096];
int num;
// We sort the input entries by name, and add them to the
// output manifest in sorted order. We expect that the output
// map will be deterministic.
TreeMap<String, JarEntry> byName = new TreeMap<String, JarEntry>();
for (Enumeration<JarEntry> e = jar.entries(); e.hasMoreElements(); ) {
JarEntry entry = e.nextElement();
byName.put(entry.getName(), entry);
}
for (JarEntry entry: byName.values()) {
String name = entry.getName();
if (!entry.isDirectory() &&
(stripPattern == null || !stripPattern.matcher(name).matches())) {
InputStream data = jar.getInputStream(entry);
while ((num = data.read(buffer)) > 0) {
if (md_sha1 != null) md_sha1.update(buffer, 0, num);
if (md_sha256 != null) md_sha256.update(buffer, 0, num);
}
Attributes attr = null;
if (input != null) attr = input.getAttributes(name);
attr = attr != null ? new Attributes(attr) : new Attributes();
if (md_sha1 != null) {
attr.putValue("SHA1-Digest",
new String(Base64.encode(md_sha1.digest()), "ASCII"));
}
if (md_sha256 != null) {
attr.putValue("SHA-256-Digest",
new String(Base64.encode(md_sha256.digest()), "ASCII"));
}
output.getEntries().put(name, attr);
}
}
return output;
}
// MANIFEST.MF
JarEntry je = new JarEntry(JarFile.MANIFEST_NAME);
je.setTime(timestamp);
outputJar.putNextEntry(je);
manifest.write(outputJar);
除了三个文件(MANIFEST.MF,CERT.RSA,CERT.SF),其他的文件都会对文件内容做一次SHA1算法,就是计算出文件的摘要信息,然后用Base64进行编码即可,保存到MANIFEST.MF// CERT.SF / CERT#.SF
je = new JarEntry(numKeys == 1 ? CERT_SF_NAME :
(String.format(CERT_SF_MULTI_NAME, k)));
je.setTime(timestamp);
outputJar.putNextEntry(je);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
writeSignatureFile(manifest, baos, getAlgorithm(publicKey[k]));
byte[] signedData = baos.toByteArray();
outputJar.write(signedData);
1》计算这个MANIFEST.MF文件的整体SHA1值,再经过BASE64编码后,记录在CERT.SF主属性块(在文件头上)的“SHA1-Digest-Manifest”属性值值下
2》逐条计算MANIFEST.MF文件中每一个块的SHA1,并经过BASE64编码后,记录在CERT.SF中的同名块中,属性的名字是“SHA1-Digest“
- // CERT.RSA / CERT#.RSA
je = new JarEntry(numKeys == 1 ? CERT_RSA_NAME :
(String.format(CERT_RSA_MULTI_NAME, k)));
je.setTime(timestamp);
outputJar.putNextEntry(je);
writeSignatureBlock(new CMSProcessableByteArray(signedData),
publicKey[k], privateKey[k], outputJar);
我们看到,这里会把之前生成的 CERT.SF文件, 用私钥计算出签名, 然后将签名以及包含公钥信息的数字证书一同写入 CERT.RSA 中保存。CERT.RSA是一个满足PKCS7格式的文件。