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 就可以了;
- 各个 keystore 部分代表的意思

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
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
gradle signingReport
通过代码
/**
* 获取签名工具类
*/
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,请运行以下命令:
zipalign -p -f -v 4 infile.apk outfile.apk
- 如需确认 existing.apk 的对齐方式,请运行以下命令:
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
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
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 签名方案的升级版