APK签名
签名基础
加密算法
对称加密
对称加密算法是较传统的加密体制,即通信双方在加/解密过程中使用他们共享的单一密钥,鉴于其算法简单和加密速度快的优点,目前仍然在使用,但是安全性方面就差一点可能。最常用的对称密码算法是 DES 算法,而 DES 密钥长度较短,已经不适合当今分布式开放网络对数据加密安全性的要求。一种新的基于 Rijndael 算法的对称高级数据加密标准 AES 取代了数据加密标准 DES,弥补了 DES 的缺陷,目前使用比较多一点
非对称加密
非对称加密由于加/解密钥不同(公钥加密,私钥解密),密钥管理简单,得到了很广泛的应用。RSA 是非对称加密系统最著名的公钥密码算法。但是由于 RSA 算法进行的都是大数计算,使得 RSA 最快的情况也比 AES 慢上倍,这是 RSA 最大的缺陷。但是其安全性较高,这也是大家比较喜欢的地方吧。
非对称加密应用:
- 加密数据:公钥加密,私钥解密,如 HTTPS 过程中 Client 拿 Server 的公钥加密数据发给 Server
- 验证:私钥加密,公钥解密,如数字签名
消息摘要
什么是消息摘要?
将任意长度的消息通过 hash 算法转换成固定长度的短消息,这就是消息摘要;经过 hash 算法生成的密文也被称为数字指纹
常见摘要算法?
常见的摘要算法都有 MD5、SHA-1 和 SHA-256
特点:
- 固定长度长度固定,与内容长度无关:MD5 是 128 位、SHA-1 是 160 位、SHA-256 是 256 位
- 唯一性在不考虑碰撞的情况下,不同的数据计算出的摘要是不同的
- 不可逆性正向计算的摘要不可能逆向推导出原始数据
数字签名
消息摘要:原数据经过 hash 后的数据
数字签名:原数据 + 通过 RSA 私钥加密后的消息摘要
数字签名容易伪造,造成中间人攻击
数字签名是可以被伪造的,不能辨别数字签名的发送方的真实身份
数字证书
前提:
接收方必须要知道发送方的公钥和所使用的算法。如果数字签名和公钥一起被篡改,接收方无法得知,还是会校验通过。如何保证公钥的可靠性呢?
数字证书是由权威的 CA 机构颁发的无法被伪造的证书,用于校验发送方实体身份的认证。文件中包含了证书颁发机构,颁发机构的签名,颁发机构的加密算法(非对称加密),算法的公钥等。
数字证书:数字证书中包含的明文内容 + 数字签名 +CA 公钥
为什么 CA 制作的证书是无法被伪造的?
CA 制作的数字证书内包含 CA 对证书的数字签名,接收方可以使用 CA 公开的公钥解密数字证书,并使用相同的摘要算法验证当前数字证书是否合法。制作证书需要使用对应 CA 机构的私钥,只要 CA 的私钥不被泄露,CA 颁发的证书是无法被非法伪造的。
数字证书解决的问题:主要是用来解决公钥的安全发放问题
数字证书的格式普遍采用的是 X.509V3 国际标准,一个标准的 X.509 数字证书包含以下一些内容:
1、证书的版本信息; 2、证书的序列号,每个证书都有一个唯一的证书序列号; 3、证书所使用的签名算法; 4、证书的发行机构名称,命名规则一般采用 X.500 格式; 5、证书的有效期,通用的证书一般采用 UTC 时间格式; 6、证书所有人的名称,命名规则一般采用 X.500 格式; 7、证书所有人的公开密钥; 8、证书发行者对证书的签名。
签名过程和校验过程(没有数字证书)
签名过程:
- 计算摘要 通过 Hash 算法计算出原生数据的摘要
- 计算签名 通过私钥的非对称加密算法对摘要进行加密,加密后的数据就是签名信息
- 写入签名 将签名信息写入原始数据的签名区块内
校验过程:
- 计算摘要 接收方接收到数据后,首先用同样的 Hash 算法从接收到的数据中计算出摘要
- 解密签名 使用发送方的公钥对数字签名进行解密,解密出原始摘要;
- 比较摘要 如果解密后的数据和步骤 1 计算出的摘要一致,则校验通过;如果数据被第三方篡改过,解密后的数据和摘要不一致,校验不通过。
签名和校验过程(带数字证书,完整的)
签名
.jks 和 .keystore 的区别
- keystore 是 Eclipse 打包生成的签名
- jks 是 Android studio 生成的签名
标准 keystore (standard jdk keystore types)
包括 JCEKS
,JKS
,PKCS12
这几种格式。
- JCEKS : 存储对称密钥(分组密钥、私密密钥)
- JKS : 只能存储非对称密钥对(私钥 + x509 公钥证书)
- PKCS12 : 通用格式(rsa 公司标准)。微软和 java 都支持。
生成签名
Android Studio 生成 keystore
Android studio 本身也可以生成 keystore:Build--》Generate Signed apk-->create new keystore
然后一步步 next 就可以了;
Eclipse keystore
利用 JDK 下的 keytool 工具生成
keytool -genkey -alias alias_hacket-keypass 998866 -keyalg RSA -keysize 2048 -validity 36500 -keystore hacket.jks -storepass 998866
- -keystore hacket.jks (hacket.jks 签名文件名字)
- -keyalg RSA (密钥算法名称 为 RSA)
- -keysize 2048 (密钥位大小 为 2048)
- -validity 36500 (有效期为 36500 天)
- -alias alias_hacket (别名为 alias_hacket )
- -keypass 后面 证书密码
- -storepass 998866 自定义密码
Android 默认 debug.keystore
- keystore 名字:debug.keystore
- alias:androiddebugkey
- keystore 密码:android
- alias 别名密码:android
keystore 和证书格式
Apk 签名时并没有直接指定私钥、公钥和数字证书,而是使用 keystore 文件,这些信息都包含在了 keystore 文件中。
根据编码不同,keystore 文件分为很多种,Android 使用的是 Java 标准 keystore 格式JKS(Java Key Storage),所以通过 Android Studio 导出的 keystore 文件是以.jks 结尾的。
keystore 使用的证书标准是 X.509,X.509 标准也有多种编码格式,常用的有两种:pem(Privacy Enhanced Mail)和der(Distinguished Encoding Rules)。jks 使用的是 der 格式,Android 也支持直接使用 pem 格式的证书进行签名
- DER(Distinguished Encoding Rules)二进制格式,所有类型的证书和私钥都可以存储为 der 格式。
- PEM(Privacy Enhanced Mail)base64 编码,内容以
-----BEGIN xxx-----
开头,以-----END xxx-----
结尾
—–BEGIN RSA PRIVATE KEY—– MIIEpAIBAAKCAQEAlmXFRXEZomRKhNRp2XRoXH+2hm17RfrfecQlT49fktoDLkF6r99uiNnuUdPi6UQuXOnzEbe1nZkfuqfB10aBLrDqBUSZ+3 —–END RSA PRIVATE KEY—– —–BEGIN CERTIFICATE—– MIICvTCCAaWgAwIBAgIEcWTElDANBgkqhkiG9w0BAQsFADAPMQ0wCwYDVQQDEwRyPQDLnVKeEIh81OwD3KIrQOUwsxyptOVVea1D8CzIAnGs —–END CERTIFICATE—–
签名工具 jarsigner 和 apksigner
Android 提供了两种对 Apk 的签名方式,一种是基于 JAR 的签名方式,另一种是基于 Apk 的签名方式,它们的主要区别在于使用的签名文件不一样:jarsigner 使用 keystore 文件进行签名;apksigner 除了支持使用 keystore 文件进行签名外,还支持直接指定 pem 证书文件和私钥进行签名。
jarsigner
JDK 自带的签名工具,可以对 jar 进行签名;使用 keystore 文件进行签名,生成的签名文件默认使用 keystore 的别名命名
apksigner
Android SDK 提供的专门用于 Android 应用的签名工具,除了支持 keystore,也可以使用 pk8、x509.pem 文件进行签名,其中 pk8 是私钥文件,x509.pem 是含有公钥的文件,生成的签名文件统一使用 CERT
命名
APK 签名的作用
- 对开发者的身份认证
由于开发者可能通过使用相同的 package name 来混淆替换已经安装的程序,以此保证签名不同的包不被替换;
- 保证 APK 的安全
签名在 APK 中根据文件写入指纹,APK 中有任何修改,指纹就会失效,Android 系统在安装 APK 进行签名校验时就会不通过,从而保证了安全,防止 APK 被伪造
- 防止交易中的抵赖发生,market 对软件的要求。
获取应用签名 (MD5/SHA1/SHA256)
keytool
1
keytool -list -v -keystore hacket.keystore
高版本只有 SHA1 和 SHA256,没有 MD5
高版本 java 移除了 这些 Disable MD5 or MD2 signed jars
| 2017-04-18 | 8u131 b11
, 7u141 b11
, 6u151 b10
, R28.3.14 | MD5 | JAR files signed with MD5 algorithms are treated as unsigned JARs. | Disabling MD5 signed jars | 2017-04-18 Released2016-12-08 Target date changed from 2017-01-17 to 2017-04-182016-10-24 Testing instructions added2016-09-30 Announced | | ———- | ——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————– | — | —————————————————————— | ————————————————————————————- | —————————————————————————————————————————————- |
通过 APK 中的 CERT.RSA 文件查询 MD5 签名
- 解压构建的 Apk 得到 RSA 文件:APK 以 zip 文件方式打开,在\META-INF\目录中存在一个.RSA 后缀的文件,一般名为 CERT.RSA。
- 使用 keytool 命令获取 MD5 签名
keytool -printcert -file CERT.RSA
jadx-gui 查看
jadx-gui xxx.apk
通过 Gradle task signingReport
1
gradle signingReport
通过代码
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
/**
* 获取签名工具类
*/
public class AppSigning {
public final static String MD5 = "MD5";
public final static String SHA1 = "SHA1";
public final static String SHA256 = "SHA256";
private static HashMap<String, ArrayList<String>> mSignMap = new HashMap<>();
/**
* 返回一个签名的对应类型的字符串
*
* @param context
* @param type
* @return 因为一个安装包可以被多个签名文件签名,所以返回一个签名信息的list
*/
public static ArrayList<String> getSignInfo(Context context, String type) {
if (context == null || type == null) {
return null;
}
String packageName = context.getPackageName();
if (packageName == null) {
return null;
}
if (mSignMap.get(type) != null) {
return mSignMap.get(type);
}
ArrayList<String> mList = new ArrayList<String>();
try {
Signature[] signs = getSignatures(context, packageName);
for (Signature sig : signs) {
String tmp = "error!";
if (MD5.equals(type)) {
tmp = getSignatureByteString(sig, MD5);
} else if (SHA1.equals(type)) {
tmp = getSignatureByteString(sig, SHA1);
} else if (SHA256.equals(type)) {
tmp = getSignatureByteString(sig, SHA256);
}
mList.add(tmp);
}
} catch (Exception e) {
LogUtil.e(e.toString());
}
mSignMap.put(type, mList);
return mList;
}
/**
* 获取签名sha1值
*
* @param context
* @return
*/
public static String getSha1(Context context) {
String res = "";
ArrayList<String> mlist = getSignInfo(context, SHA1);
if (mlist != null && mlist.size() != 0) {
res = mlist.get(0);
}
return res;
}
/**
* 获取签名MD5值
*
* @param context
* @return
*/
public static String getMD5(Context context) {
String res = "";
ArrayList<String> mlist = getSignInfo(context, MD5);
if (mlist != null && mlist.size() != 0) {
res = mlist.get(0);
}
return res;
}
/**
* 获取签名SHA256值
*
* @param context
* @return
*/
public static String getSHA256(Context context) {
String res = "";
ArrayList<String> mlist = getSignInfo(context, SHA256);
if (mlist != null && mlist.size() != 0) {
res = mlist.get(0);
}
return res;
}
/**
* 返回对应包的签名信息
*
* @param context
* @param packageName
* @return
*/
private static Signature[] getSignatures(Context context, String packageName) {
PackageInfo packageInfo = null;
try {
packageInfo = context.getPackageManager().getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
return packageInfo.signatures;
} catch (Exception e) {
LogUtil.e(e.toString());
}
return null;
}
/**
* 获取相应的类型的字符串(把签名的byte[]信息转换成16进制)
*
* @param sig
* @param type
* @return
*/
private static String getSignatureString(Signature sig, String type) {
byte[] hexBytes = sig.toByteArray();
String fingerprint = "error!";
try {
MessageDigest digest = MessageDigest.getInstance(type);
if (digest != null) {
byte[] digestBytes = digest.digest(hexBytes);
StringBuilder sb = new StringBuilder();
for (byte digestByte : digestBytes) {
sb.append((Integer.toHexString((digestByte & 0xFF) | 0x100)).substring(1, 3));
}
fingerprint = sb.toString();
}
} catch (Exception e) {
LogUtil.e(e.toString());
}
return fingerprint;
}
/**
* 获取相应的类型的字符串(把签名的byte[]信息转换成 95:F4:D4:FG 这样的字符串形式)
*
* @param sig
* @param type
* @return
*/
private static String getSignatureByteString(Signature sig, String type) {
byte[] hexBytes = sig.toByteArray();
String fingerprint = "error!";
try {
MessageDigest digest = MessageDigest.getInstance(type);
if (digest != null) {
byte[] digestBytes = digest.digest(hexBytes);
StringBuilder sb = new StringBuilder();
for (byte digestByte : digestBytes) {
sb.append(((Integer.toHexString((digestByte & 0xFF) | 0x100)).substring(1, 3)).toUpperCase());
sb.append(":");
}
fingerprint = sb.substring(0, sb.length() - 1).toString();
}
} catch (Exception e) {
LogUtil.e(e.toString());
}
return fingerprint;
}
}
工具
zipalign
https://developer.android.com/studio/command-line/zipalign
zipalign 是一种 zip 归档文件对齐工具。它可确保归档中的所有未压缩文件相对于文件开头都是对齐的。这样一来,您便可直接通过 mmap
访问这些文件,而无需在 RAM 中复制相关数据并减少了应用的内存用量。
在将 APK 文件分发给最终用户之前,应该先使用 zipalign 进行优化。如果您使用 Android Studio 进行构建,则此步骤会自动完成。自定义构建系统的维护者需要注意
- 如果您使用的是 apksigner,只能在为 APK 文件签名之前执行 zipalign。如果您在使用 apksigner 为 APK 签名之后对 APK 做出了进一步更改,签名便会失效。
- 如果您使用的是 jarsigner,只能在为 APK 文件签名之后执行 zipalign。
用法
如果您的 APK 包含共享库(.so 文件),则应使用 -p 来确保它们与适合 mmap(2) 的 4KiB 页面边界对齐。对于其他文件(其对齐方式由 zipalign 的必选对齐参数确定),Studio 将在 32 位和 64 位系统中对齐到 4 个字节。
- 如需对齐 infile.apk 并将其保存为 outfile.apk,请运行以下命令:
1
zipalign -p -f -v 4 infile.apk outfile.apk
- 如需确认 existing.apk 的对齐方式,请运行以下命令:
1
zipalign -c -v 4 existing.apk
您可以使用 zipalign -h
来查看支持的完整标志集。
一个未 zipalign 的 apk:
APK 签名机制
APK 格式(zip 文件结构)
Apk 文件本质上就是一个 zip 文件, zip 文件整体是由三个部分组成,分别是 Contents of ZIP entries
(数据区)、Central Directory Header
(中央目录区)以及 End of Central Directory Record
(中央目录结尾记录)。
- Contents of ZIP entries 此区块包含了 zip 中所有文件的记录,是一个列表,每条记录包含:文件名、压缩前后 size、压缩后的数据等
- Central Directory 存放目录信息,也是一个列表,每条记录包含:文件吗、压缩前后 size、本地文件头的起始偏移量等。通过本地文件头的起始偏移量即可找到压缩后的数据
- End of Central Directory 标识中央目录结尾,包含:中央目录条目数、size、起始偏移量、zip 文件注释内容等存储 zip 文件的整体信息
通过中央目录起始偏移量和 size 即可定位到中央目录,再遍历中央目录条目,根据本地文件头的起始偏移量即可在数据区中找到相应的压缩数据
v1 签名、v2 签名、v3 签名、v4 签名
V1 签名:JAR 签名
APK 最初的签名,JAR 签名
JAR 签名过程
对一个 APK 文件签名之后,APK 文件根目录下会增加 META-INF 目录,该目录下增加三个文件,分别是:MANIFEST.MF
、CERT.SF
和 CERT.RSA
,Android 系统就是根据这三个文件的内容对 APK 文件进行签名检验的。
MAINFEST.MF
对 APK 中的每个文件 (除了/META-INF 文件夹)的SHA1+Base64编码后的值保存到MAINFEST.MF
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Manifest-Version: 1.0
Built-By: Generated-by-ADT
Created-By: Android Gradle 3.5.0
Name: AndroidManifest.xml
SHA1-Digest: R7+PmGTdYXnFfiDdNMwRZoe6b5I=
Name: META-INF/androidx.activity_activity.version
SHA1-Digest: xTi2bHEQyjoCjM/kItDx+iAKmTU=
Name: META-INF/androidx.appcompat_appcompat-resources.version
SHA1-Digest: BeF7ZGqBckDCBhhvlPj0xwl01dw=
Name: META-INF/androidx.appcompat_appcompat.version
SHA1-Digest: BeF7ZGqBckDCBhhvlPj0xwl01dw=
Name: META-INF/androidx.arch.core_core-runtime.version
SHA1-Digest: H7e+Eu+qFgjcY+eE4zCCrgrHkZs=
CERT.SF
- 计算这个 MANIFEST.MF 文件的整体 SHA1 值,再经过 BASE64 编码后,记录在 CERT.SF 主属性块(在文件头上)的 “SHA1-Digest-Manifest” 属性值值下
- 逐条计算 MANIFEST.MF 文件中每一个块的 SHA1,并经过 BASE64 编码后,记录在 CERT.SF 中的同名块中,属性的名字是 “SHA1-Digest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Signature-Version: 1.0
Created-By: 1.0 (Android)
SHA1-Digest-Manifest: 2+h4dULSBbymj0FVSLjqAW9znkI=
X-Android-APK-Signed: 2
Name: AndroidManifest.xml
SHA1-Digest: 9/BgaLnfamzOiddg+fhuZx5LGug=
Name: META-INF/androidx.activity_activity.version
SHA1-Digest: RkNW8YDqxBjvnU/8M+42MoBO998=
Name: META-INF/androidx.appcompat_appcompat-resources.version
SHA1-Digest: L1WSnCxLg4cpL9uEb+hKu7Q2iL0=
Name: META-INF/androidx.appcompat_appcompat.version
SHA1-Digest: Sibj0VVmL7B67oBCzlyitRpAkSE=
Name: META-INF/androidx.arch.core_core-runtime.version
SHA1-Digest: HpjKtBXDZV16FYTUu9XKKNWOX6k=
CERT.RSA
CERT.RSA 中的是二进制内容,里面保存了签名者的证书信息,以及对 cert.sf 文件的签名
JAR 签名校验
首先校验 cert.sf 文件的签名
计算 cert.sf 文件的摘要,与通过签名者公钥解密签名得到的摘要进行对比,如果一致则进入下一步;
校验 manifest.mf 文件的完整性
计算 manifest.mf 文件的摘要,与 cert.sf 主属性中记录的摘要进行对比,如一致则逐一校验 mf 文件各个条目的完整性;
校验 apk 中每个文件的完整性
逐一计算 apk 中每个文件(META-INF 目录除外)的摘要,与 mf 中的记录进行对比,如全部一致,刚校验通过;
校验签名的一致性
如果是升级安装,还需校验证书签名是否与已安装 app 一致。
APK 签名过程为什么能保证 apk 没有被篡改?
我们来看看篡改了 apk 内容会发生什么?
- 篡改 apk 内容,没有改 manifest.mf 内容
如果你改变了 apk 中的任何文件,那么在 apk 安装校验时,改变后的文件摘要信息与 MANIFEST.MF 的校验信息不同,于是验证失败
- 篡改 apk 内容,同时篡改 manifest.mf 文件 item 相应的摘要信息,但没有改 cert.sf 内容
如果你改变了 apk 的文件,并更改了 MANIFEST.MF 文件里对应的属性值,那么 mf 计算出的摘要值必定与 CERT.SF 文件中算出的摘要值不一样,验证失败
- 篡改 apk 内容,同时篡改 manifest.mf 文件相应的摘要,以及 cert.sf 文件的内容
最后,如果你改变的 apk 的文件,更改了 MANIFEST.MF 文件的值,并计算出 MANIFEST.MF 的摘要值,相应的更改了 CERT.SF 里面的值,那么数字签名值必定与 CERT.RSA 文件中记录的不一样,还是验证失败;由于不能伪造数字证书,没有对应的私钥,就改变不了 cert.rsa 中的内容。
- 把 apk 内容和签名信息一同全部篡改
这相当于对 apk 进行了重新签名,在此 apk 没有安装到系统中的情况下,是可以正常安装的,这相当于是一个新的 app;但如果进行覆盖安装,则证书不一证,安装失败
JAR 签名机制缺点
- 签名校验速度慢 校验过程中需要对 apk 中所有的文件进行摘要计算,在 apk 资源很多、性能较差的机器上签名校验会花费较长时间,导致安装速度慢
- **完整性保障不够 **META-INF 目录用来存放签名,自然此目录本身是不计入签名校验过程的,可以随意在这个目录中添加文件,之前一些旧的打渠道包方案就是在这里添加渠道文件的
V2 签名:APK Signing Block
Android7 引入,v2 签名模式在原先 APK 块中增加了一个 APK 签名分块 APK Signing Block
。
JAR 签名在 APK 中添加 META-INF 目录,需要修改数据区、中央目录,因为添加文件后会导致中央目录大小和偏移量发生变化,还需要修改中央目录结尾记录;V2 签名为加强数据完整性保证,不在数据区和中央目录中插入数据,新增一个 APK 签名分块,从而保证了 APK 数据的完整性
V2 签名块负责保护第 1、3、4 部分的完整性,以及第 2 部分包含的 APK Signing Block 中的 signed data 分块的完整性;V2 签名后,任何对 1、3、4 部分的修改都逃不过 v2 签名方案的检查,APK Signing Block 块替代了 V1 中 META-INF 的作用。
V2 签名块格式
- size of block 8 字节
- 带 uint64 长度前缀的
ID-VALUE
对序列 变长 - size of block 8 字节
- magic value APK 签名分块 42 (16 个字节)
APK 签名分块包含了 4 部分:分块长度、ID-VALUE 序列、分块长度、固定 magic 值。其中 APK 签名方案 v2 分块存放在 ID 为 0x7109871a
的键值对中,包含的内容如下:
- 带长度前缀的 signer1:
- 带长度前缀的 signed data,包含 digests 序列,X.509 certificates 序列, additional attributes 序列
- 带长度前缀的 signatures(带长度前缀)序列
- 带长度前缀的 public key(SubjectPublicKeyInfo,ASN.1 DER 形式)
- signer2,因为 Android 允许多个签名。
V2 签名过程
- 拆分 chunk
将 ZIP 条目的内容 _、_ZIP 中央目录、_ZIP 中央目录结尾 _ 拆分成多个大小为 1MB 大小的 chunk,最后一个 chunk 可能小于 1M。之所以分块,是为了可以通过并行计算摘要以加快计算速度;
- 计算 chunk 摘要
计算每个小块的数据摘要,数据内容是:0xa5 + 块字节长度 + 块的内容
- 计算整体摘要
数据内容是:0x5a + 数据块的数量(chunk数量) + 每个数据块的摘要内容
总之,就是把 APK 按照 1M 大小分割,分别计算这些分段的摘要,最后把这些分段的摘要在进行计算得到最终的摘要也就是 APK 的摘要。然后将 APK 的摘要 + 数字证书 + 其他属性生成签名数据写入到 APK Signing Block 区块。
V2 签名校验
v2 签名机制是在 Android 7.0 以及以上版本才支持的。因此对于 Android 7.0 以及以上版本,在安装过程中,如果发现有 v2 签名块,则必须走 v2 签名机制,不能绕过。否则降级走 v1 签名机制。v1 和 v2 签名机制是可以同时存在的,其中对于 v1 和 v2 版本同时存在的时候,v1 版本的 META_INF 的 .SF 文件属性当中有一个 X-Android-APK-Signed: 2
属性。
V3 签名:密钥转轮
Android9.0 引入,支持密钥轮换,使得应用能够在 APK 更新过程中更改其签名密钥。为了支持密钥轮换,在 V2 的基础上增加两个数据块来存储 APK 密钥轮替所需要的一些信息。
V3 在签名部分可以添加新的证书(Attr 块)。在这个新块中,会记录我们之前的签名信息以及新的签名信息,以密钥转轮的方案,来做签名的替换和升级。这意味着,只要旧签名证书在手,我们就可以通过它在新的 APK 文件中,更改签名。
v3 签名新增的新块(attr)存储了所有的签名信息,由更小的 Level 块,以链表的形式存储。
其中每个节点都包含用于为之前版本的应用签名的签名证书,最旧的签名证书对应根节点,系统会让每个节点中的证书为列表中下一个证书签名,从而为每个新密钥提供证据来证明它应该像旧密钥一样可信。
V3 签名校验过程
Android 的签名方案,无论怎么升级,都是要确保向下兼容。因此,在引入 v3 方案后,Android 9.0 及更高版本中,可以根据 APK 签名方案,v3 -> v2 -> v1 依次尝试验证 APK。而较旧的平台会忽略 v3 签名并尝试 v2 签名,最后才去验证 v1 签名。
需要注意的是,对于覆盖安装的情况,签名校验只支持升级,而不支持降级。也就是说设备上安装了一个使用 v1 签名的 APK,可以使用 v2 签名的 APK 进行覆盖安装,反之则不允许。
V4 签名:ADB 增量 APK 安装
Android11 引入,支持与流式传输兼容的签名方案。为了支持 ADB 增量 APK 安装功能。
因为需要流式传输,所以需要将文件分块,对每一块进行签名以便校验,使用的方式就是 Merkle 哈希树(https://www.kernel.org/doc/html/latest/filesystems/fsverity.html#merkle-tree),APK> v4 就是做这部分功能的。所以 APK v4 与 APK v2 或 APK v3 可以算是并行的,所以 APK v4 签名后还需要 v2 或 v3 签名作为补充。
ADB 增量 APK 安装
在设备上安装大型(2GB 以上)APK 可能需要很长的时间,即使应用只是稍作更改也是如此。ADB 增量 APK 安装可以安装足够的 APK 以启动应用,同时在后台流式传输剩余数据,从而加速这一过程。如果设备支持该功能,并且您安装了最新的 SDK 平台工具,adb install 将自动使用此功能。如果不支持,系统会自动使用默认安装方法。
小结
- v1 方案:基于 JAR 签名,签名信息写入到/META-INF 中,此目录不受签名保护
- v2 方案:在 Android7.0 引入,引入 APK Signing Blocking 区域,用于解决 v1 签名速度过慢(需要对所有文件 Hash 及签名;及/META-INF 下的文件不计入签名校验的,解决完整性保障不够的问题
- v3 方案:在 Android9.0 引入,用于支持密钥轮换
- v4 方案:支持 ADB 增量更新
其中 v1 到 v2 时颠覆性的,主要是为了解决 JAR 签名方案的安全性问题;v3 方案,结构上并没有太大的调整,可以理解为 v2 签名方案的升级版