简短回答:Android 会根据设备检查几种签名格式中的一种:v1(JAR 签名,逐文件签名,适用于 Android 6 及更早版本)、v2(存放在 APK 签名分块中的全文件签名,Android 7.0)、v3(v2 加上轮换证明谱系,Android 9)、v3.1(只面向 Android 13 及更高版本的轮换)、v3.2(经典算法与 ML-DSA 的混合签名,Android 17)和 v4(用于增量安装的单独 .idsig 文件,Android 11)。目标 API 级别为 30 或更高的应用必须具备 v2 或更新方案的签名。更新必须由同一个密钥签名,或由应用轮换谱系中的后继密钥签名,因此签名密钥在应用的整个生命周期中都是它的身份:让 Google Play 保管签名密钥(Play 应用签名),自己只保留上传密钥,并用 apksigner verify -v --print-certs 检查发布版本。
实验使用 Android SDK build-tools 37.0.0 中的 apksigner(它自报的版本号为 0.9),运行环境为 eclipse-temurin:25.0.4.1_1-jdk-noble 中的 Java 25.0.4.1。样本是2026年10月22日从 f-droid.org 下载的 F-Droid 客户端 APK;其余密钥均为本文专门生成。
签名方案一览
| 方案 | 起始版本 | 位置 | 新增内容 |
|---|---|---|---|
| v1 | Android 1.0 | META-INF/*.SF、*.RSA/*.EC | JAR 签名:每个 ZIP 条目一个哈希;不覆盖 ZIP 元数据 |
| v2 | Android 7.0(API 24) | APK 签名分块,ID 0x7109871a | 对除分块本身以外的整个文件签名 |
| v3 | Android 9(API 28) | ID 0xf05368c0 | 在 v2 基础上增加支持的 SDK 范围和轮换证明谱系 |
| v3.1 | Android 13(API 33) | 新的分块 ID | 轮换只在 Android 13 及更高版本上生效 |
| v3.2 | Android 17(API 37) | ID 0x70e1c89f | 恰好两个签名者,一个经典、一个后量子,两者都要验证 |
| v4 | Android 11(API 30) | APK 旁边的 app.apk.idsig | 基于 APK 的 Merkle 树,供 adb install --incremental 使用;需要 v2 或 v3 |
APK 签名方案 v2“将 APK 文件视为 blob,并对整个文件执行签名检查”,因此“对 APK 进行的任何修改(包括对 ZIP 元数据的修改)都会使 APK 签名作废”(AOSP)。它也更快:设备不必再逐个解压条目来检查。v1 只为早于 Android 7.0 的设备保留;minSdkVersion 为 24 或更高的 APK 不需要它,apksigner 也会省略它。保留 v1 并不能掩盖被剥离的 v2 签名:同时带有 v1 签名的 v2 签名 APK 会在其 .SF 文件中携带 X-Android-APK-Signed 属性,而该属性在 v1 签名的覆盖范围内;如果 APK 声明了某个方案却缺少该方案的签名,验证方必须拒绝它。
从 Android 11 开始,“如果目标平台为 Android 11(API 级别 30)的应用目前仅使用 APK 签名方案 v1 签名,现在还必须使用 APK 签名方案 v2 或更高版本签名”(行为变更)。
解读 apksigner verify 的输出
apksigner verify -v --print-certs 会针对 APK 所支持的 SDK 范围检查它携带的每种方案,并打印签名者。以已发布的 F-Droid 客户端为例(输出已筛选,只保留方案和签名者相关的行):
Verifies
Verified using v1 scheme (JAR signing): false
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): false
Verified using v3.2 scheme (APK Signature Scheme v3.2): false
Verified using v4 scheme (APK Signature Scheme v4): false
Verified for SourceStamp: false
Number of signers: 1
V3.0 Signer: certificate DN: CN=Ciaran Gultnieks, OU=Unknown, O=Unknown, L=Wetherby, ST=Unknown, C=UK
V3.0 Signer: certificate SHA-256 digest: 43238d512c1e5eb2d6569f4a3afbf5523418b82e0a3ed1552770abb9a9c9ccab
V3.0 Signer: key algorithm: RSA
v1 显示 false 并不是错误:该应用的最低 SDK 为 24,因此不需要 v1。证书摘要才是识别应用签名者的值:F-Droid 在其签名密钥页面上为客户端公布的正是 43238d51…a9c9ccab,而 Android 在每次更新时都会比对签名者。这里没有信任链:APK 证书通常是自签名的,v2 验证规则检查第一张证书的 SubjectPublicKeyInfo 与签名密钥是否一致,但没有检查颁发者或有效期的步骤(v2)。这张证书签发于2010年。
用我们自己的 P-256 密钥重新签名一个副本,再修改文件中的一个字节,就能看到这项检查的两个方面:
keytool -genkeypair -keystore key1.p12 -storetype PKCS12 -storepass changeit -alias app \
-dname "CN=Example App signing key 2026" -validity 10000 -keyalg EC -groupname secp256r1 -sigalg SHA256withECDSA
apksigner sign --ks key1.p12 --ks-pass pass:changeit --out app-key1.apk F-Droid.apk
apksigner verify -v --print-certs app-key1.apk
cp app-key1.apk tampered.apk
printf '\x00' | dd of=tampered.apk bs=1 seek=4000000 conv=notrunc status=none
apksigner verify -v tampered.apk
Verifies
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
V3.0 Signer: certificate DN: CN=Example App signing key 2026
V3.0 Signer: certificate SHA-256 digest: a93143a06f88894887d593f080c05b4ffd3aa9839c52378cee687987162d6d72
V3.0 Signer: key algorithm: EC
DOES NOT VERIFY
ERROR: APK Signature Scheme v3 signer #1: APK integrity check failed. CHUNKED_SHA256 digest mismatch. Expected: <66b1ea5c6952adc0c70f086346a76da06a5ab35dac770d104ff14d5ec09a9c97>, actual: <a5b1f3e628e868c150ade8cf1f3280f54ff4ae3771f20620cafbe97ad72ced57>
重新签名的副本完全通过了验证,只是证书不同。正因如此,判断 APK 是否来自正确的发布者,要看签名者摘要,而不是“Verifies”。如果把它覆盖安装到原版 F-Droid 上,Android 会把它视为由另一个密钥签名的更新而拒绝安装。CHUNKED_SHA256 摘要是对文件各部分中连续的 1 MB 数据块计算的,正是这一点让 v2 及更高版本能够快速、并行地完成验证。
轮换密钥:v3 与 v3.1
应用不能简单地通过重新签名来更换签名密钥:这样的更新会被拒绝。v3 增加了轮换证明(proof-of-rotation),即“一个单向链表,其中每个节点都包含一张曾用于为应用的先前版本签名的签名证书”,每张证书都为下一张签名(v3)。认得旧密钥的设备,只要谱系把新旧两个密钥连接起来,就会接受由新密钥签名的 APK。
apksigner rotate --out lineage.bin --old-signer --ks key1.p12 --ks-pass pass:changeit \
--new-signer --ks key2.p12 --ks-pass pass:changeit
apksigner sign --ks key1.p12 --ks-pass pass:changeit --next-signer --ks key2.p12 --ks-pass pass:changeit \
--lineage lineage.bin --out app-rotated.apk F-Droid.apk
apksigner verify -v --print-certs app-rotated.apk
apksigner lineage --in app-rotated.apk --print-certs
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): true
V3.1 Signer: (minSdkVersion=33, maxSdkVersion=2147483647) certificate DN: CN=Example App signing key 2027
V3.1 Signer: (minSdkVersion=33, maxSdkVersion=2147483647) key algorithm: EC
V3.0 Signer: (minSdkVersion=24, maxSdkVersion=32) certificate DN: CN=Example App signing key 2026
V3.0 Signer: (minSdkVersion=24, maxSdkVersion=32) key algorithm: EC
Signer #1 in lineage certificate DN: CN=Example App signing key 2026
Has installed data capability: true
Has shared UID capability : true
Has permission capability : true
Has rollback capability : false
Has auth capability : true
Signer #2 in lineage certificate DN: CN=Example App signing key 2027
Has installed data capability: true
Has shared UID capability : true
Has permission capability : true
Has rollback capability : false
Has auth capability : true
默认情况下,apksigner 会把新密钥放进面向 Android 13 及更高版本的 v3.1 分块,而在面向 Android 7 至 12 的 v3.0 分块中保留旧密钥。这是有意为之:AOSP 指出,“对于 Android 12(API 级别 31)及更低版本,不建议轮换 APK 密钥”,因为从 Android 13 起,checkSignatures 才会识别谱系并“返回最新的签名”。--rotation-min-sdk-version 28 会强制在 Android 9 及更高版本上应用轮换;apksigner 文档把这一选项留给已经在旧平台上轮换过密钥的应用(apksigner)。
这些能力项说明旧密钥还能做什么:在由它签名的已安装数据之上安装更新、共享 UID、持有签名权限,以及(此处为 false)回滚到旧密钥。如果旧密钥已被泄露,用 apksigner lineage 撤销这些能力。
后量子:混合签名 v3.2
Android 17 新增了 v3.2,这是“一种混合签名方案,旨在支持 Android 生态系统向后量子密码过渡”:APK“同时使用 RSA 或 ECDSA 等经典算法和一种 PQC 算法”签名,初期为 ML-DSA-65 或 ML-DSA-87,签名存放在 ID 为 0x70e1c89f 的分块中,该分块“必须恰好包含两个签名者”(v3.2)。Android 17 会验证这两个签名;更早的版本会跳过该分块,改用仍须保留的 v3.0 或 v3.1 分块。平台把 PQC 密钥视为经典密钥的一次轮换,两个签名者必须具有“完全相同的先前签名历史”。
Java 25 的 keytool 可以创建 ML-DSA 密钥(-keyalg ML-DSA-65),build-tools 37.0.0 也能用这些密钥签名。我们试了好几次才成功:在两个混合签名者都有谱系之前,apksigner 一直报 NullPointerException 错误;而且 PQC 谱系必须和经典签名者的谱系一样,从同一个先前的密钥轮换而来:
keytool -genkeypair -keystore pq.p12 -storetype PKCS12 -storepass changeit -alias app \
-dname "CN=Example App PQ signing key" -validity 10000 -keyalg ML-DSA-65
apksigner rotate --out lineage-pq.bin --old-signer --ks key1.p12 --ks-pass pass:changeit \
--new-signer --ks pq.p12 --ks-pass pass:changeit
apksigner sign --ks key1.p12 --ks-pass pass:changeit --next-signer --ks key2.p12 --ks-pass pass:changeit --lineage lineage.bin \
--next-signer --ks key2.p12 --ks-pass pass:changeit --hybrid-signer-role classical --signer-lineage lineage.bin \
--next-signer --ks pq.p12 --ks-pass pass:changeit --hybrid-signer-role pqc --signer-lineage lineage-pq.bin \
--hybrid-min-sdk-version 37 --out app-hybrid.apk F-Droid.apk
apksigner verify -v --print-certs app-hybrid.apk
Verifies
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme (APK Signature Scheme v3.1): true
Verified using v3.2 scheme (APK Signature Scheme v3.2): true
V3.2 Hybrid Classical Signer: (minSdkVersion=36 (dev release=true), maxSdkVersion=2147483647) certificate DN: CN=Example App signing key 2027
V3.2 Hybrid Classical Signer: (minSdkVersion=36 (dev release=true), maxSdkVersion=2147483647) key algorithm: EC
V3.2 Hybrid PQC Signer: (minSdkVersion=36 (dev release=true), maxSdkVersion=2147483647) certificate DN: CN=Example App PQ signing key
V3.2 Hybrid PQC Signer: (minSdkVersion=36 (dev release=true), maxSdkVersion=2147483647) key algorithm: ML-DSA
V3.1 Signer: (minSdkVersion=33, maxSdkVersion=2147483647) certificate DN: CN=Example App signing key 2027
V3.0 Signer: (minSdkVersion=24, maxSdkVersion=32) certificate DN: CN=Example App signing key 2026
现在一个 APK 里有三代密钥:key1 用于 Android 7 至 12,key2 用于 Android 13 及更高版本,key2 加上 ML-DSA-65 密钥用于支持 v3.2 的平台。与单一签名的 APK 相比,混合签名的 APK 大了 20,480 字节。即使指定了 --hybrid-min-sdk-version 37,这一版 apksigner 仍把 v3.2 的范围打印为“36 (dev release=true)”,这是它对仍在开发中的平台所用的编码方式;应检查你发布时所用的 build-tools 版本的输出。大多数开发者永远不会运行这些命令:Play 会生成并管理混合签名密钥(见下一节)。
v4 与增量安装
--v4-signing-enabled true 会在 APK 旁边写入一个 .idsig 文件。文件中包含 fs-verity 格式的 APK Merkle 树以及对树根的签名,因此 adb install --incremental 可以在所有字节到达之前就启动应用。AOSP 明确指出:“v4 签名需要一个补充的 v2 或 v3 签名。”在实验中,12.6 MB 的 APK 对应的签名文件为 103,041 字节,apksigner verify --v4-signature-file app-v4.apk.idsig 报告 Verified using v4 scheme (APK Signature Scheme v4): true。v4 是面向开发和流式安装的功能,不会上传到应用商店。
Play 应用签名与开发者验证
在 Google Play 上,为用户所安装内容签名的密钥通常不归你所有。使用 Play 应用签名时,你用上传密钥(“2048 位或以上的 RSA 密钥”)为 app bundle 签名,Google 再用它保管的应用签名密钥对分发出去的 APK 重新签名。由此带来两点影响:
- 上传密钥丢失或泄露后可以补救:“你不会被锁在应用之外”;你可以在 Play 管理中心(Play Console)用新证书申请重置上传密钥。
- 应用签名密钥的轮换(“密钥升级”)在 Play 管理中心中操作。Play 现在会让新应用“采用量子就绪的混合签名,并使用 Google 生成的密钥”:“一个经典的 4096 位 RSA 密钥和一个后量子 ML-DSA-65 密钥”,外加一个面向旧设备的经典密钥,“因此你的应用将使用三个不同的密钥”。Play 指出,混合签名目前与 v4 不兼容;对于 Android 17 及更高版本上的安装,Play 提供每年一次的密钥升级。
在 Play 之外,签名密钥更加重要。Android 的开发者验证会为安装在经过认证的设备上的应用,将开发者及其包名与签名密钥一起注册:相关保护措施已于2026年9月30日在巴西、印度尼西亚、新加坡和泰国启动,Google 计划在2027年把验证推广到全球。其常见问题解答说得很直白:“如果你丢失了签名密钥,就无法注册你的软件包。”
常见错误
- 签名后修改 APK:使用
apksigner时,“必须在为 APK 文件签名之前使用zipalign”(zipalign);任何会重写 ZIP 的工具都是如此。签名后的任何改动都会导致上文的摘要不匹配。 - 轻信“Verifies”:任何密钥都能产生有效的签名。应把签名者证书的 SHA-256 摘要与你预期的值比对(Play 管理中心会显示你的摘要;F-Droid 和许多开源项目会公布它们的摘要)。
- 无故在 Android 12 及更早版本上用 v3 轮换:应让
apksigner以 v3.1 为目标。 - 签名密钥只有一份:对于 Play 之外的应用,没有重置这回事——密钥就是应用本身。
- 把密钥放进仓库:应把上传密钥保存在硬件支持的密钥库或云端密钥库中,只给 CI 签名权限,而不是密钥文件。
- 在后端固定应用签名证书(API 许可名单、OAuth 客户端注册、按证书限制的 Firebase 或 Maps 密钥),却没有为密钥升级做规划:Play 文档要求你向 API 提供方注册新的指纹,采用混合签名时是两个新密钥的指纹。