深入 Android APK 签名机制全链路:从 V1 JAR 签名到 V4 流式签名的演进与验证原理

去年帮团队排查一个线上崩溃:用户从应用商店下载的 APK 安装时提示”签名不一致”。排查后发现是 CI 流程中 V2 签名和 V1 签名用了不同证书。这个问题让我把四种签名方案从头到尾捋了一遍,发现很多细节在日常开发中容易被忽略。

APK 签名不是一次性设计出来的,而是随着 Android 系统演进逐步叠加了四层方案。每一层都解决了一个具体痛点,理解它们的演化逻辑比死记硬背格式重要得多。

签名的本质:完整性证明

签名要回答两个问题:这个 APK 有没有被篡改,以及它是否来自声称的开发者

实现思路很直接:用私钥对内容摘要加密,系统用公钥解密比对。但”对什么内容做摘要”才是各版本签名的核心差异。

V1 对整个 JAR 条目做摘要,V2/V3 对整个 APK 文件做摘要,V4 干脆不构建完整的签名块。方案不同,但验证链路都遵循同一模式:

内容 → 哈希摘要 → 私钥签名 → 嵌入 APK
APK → 提取签名 → 公钥验签 → 比对摘要

V1:活在 JAR 规范里的签名

V1(JAR 签名)从 JDK 的 jarsigner 工具继承而来。APK 本质上是 ZIP 文件,ZIP 兼容 JAR 格式,JAR 签名可以直接复用。

APK 中每个文件作为一个 JAR 条目,在 META-INF/MANIFEST.MF 中记录每个条目的 Base64 编码 SHA1 摘要:

Name: res/layout/activity_main.xml
SHA1-Digest: n4x5bH3K9...base64...

CERT.SF 文件对 MANIFEST.MF 整体做 SHA1 摘要并签名,同时标注每个条目的摘要值。CERT.RSA 包含 PKCS#7 格式的数字签名和 X.509 证书链。

验证时先校验 CERT.RSA 中的签名是否与 CERT.SF 匹配,再校验 CERT.SF 中的条目摘要是否与 MANIFEST.MF 一致,最后验证 MANIFEST.MF 中的摘要与每个实际文件内容是否吻合。这是一个三级哈希链

V1 有三个硬伤:不保护 META-INF 目录之外的未签名文件、签名验证需要解压全部条目导致安装缓慢、以及 ZIP 条目顺序可被重排绕过部分检测。

Janus 漏洞(CVE-2017-13156)把 V1 的弱点暴露得很彻底:攻击者在 APK 中注入一个 classes.dex,系统只校验原始 dex 的摘要,却可能加载伪造的 dex。这个漏洞直接催生了 V2。

V2:把整个 APK 压进一个签名块

V2(APK Signature Scheme v2)不再关心 ZIP 内部有什么文件,它直接对 APK 文件的字节流做哈希。

APK 被划分为三个区段:❶ ZIP 条目内容 → ❷ APK Signing Block → ❸ ZIP 中央目录(Central Directory)+ EOCD。签名块插入在 ❶ 和 ❸ 之间,不破坏 ZIP 结构,旧工具仍能解压 APK。

[ZIP Entries (1~n)]
[APK Signing Block]
  size of block (8 bytes, 不含自身)
  [ID-value pairs]
    ID: 0x7109871a (V2签名魔数)
    value: V2 Signature
  size of block (8 bytes)
  magic: "APK Sig Block 42" (16 bytes)
[Central Directory]
[EOCD]

APK Signing Block 中可以有多个 ID-value 对,V2 的魔数是 0x7109871a。这对后面 V3 在同一块里共存很重要。

V2 签名时,将 APK 中区域 ❶、❸ 以及签名块中”V2 签名数据之前的字节”拼接起来做哈希,保证签名块自身也被保护在内。最终数据包含三个部分:

// V2 签名核心结构
data class V2Signature(
    val signers: List<SignerBlock>,
    // 每个 signer 包含:
    // - signedData: 由 digests + certificates + attributes 组成
    // - signatures: 多种算法签名(至少一种)
    // - publicKey
)

data class SignedData(
    val digests: List<Digest>,      // 对❶+②+③部分的哈希
    val certificates: List<X509Cert>,
    val attributes: List<Attribute> // 可扩展属性
)

验证时系统先定位 APK Signing Block,提取 V2 签名数据,用公钥验证 signedData 的签名,再验证 digests 与 APK 指定区段是否匹配。整个过程只需要有限次 I/O,不需要遍历 ZIP 条目。

V2 把签名块放在 APK 末尾附近,但 ZIP 中央目录之前的区域还没有被保护。另外 V2 不支持密钥轮转——证书过期就得换签名重新发布。

V3:密钥轮转与签名块的可扩展性

V3(APK Signature Scheme v3)与 V2 共享同一个 APK Signing Block,但结构上有本质区别。

V3 引入了 proof-of-rotation 结构。每个 signer 的 signedData 里可以包含一个 rotation 属性,记录签名证书链的变更历史:

data class ProofOfRotation(
    val minSdkVersion: Int,           // 新证书的最低 SDK
    val maxSdkVersion: Int,           // 旧证书的最高 SDK(可选)
    val newSigner: SignerBlock,       // 新密钥签名的 signer
    val previousProof: ProofOfRotation? // 递归链接的历史记录
)

新旧两个 signer 都对 signedData 签名,系统根据设备 SDK 版本选择对应证书。旧设备不受影响,新设备认新证书。这让同一个包名的应用可以在不丢失身份的前提下完成密钥迁移。

V3 的另一个改进是签名块中用 level 区分签名类型:V2 的结构里 level=0 表示直接签名者,V3 扩展为 level=1 表示轮转后的签名、level=2 表示更深的轮转层。

支持密钥轮转意味着应用商店的包名归属校验不再只认单一指纹,需要维护一个可信证书链。这个思路在 Android App Bundle 的 Play Signing 中得到了更激进的应用——开发者用自己的 upload key 签名,Google Play 用 release key 重签名。

V4:丢掉签名块,直接从文件系统和 ADB 读

V4(APK Signature Scheme v4)的出发点很明确:V2/V3 验证仍然需要读取 APK 的多个区段,对于 GB 级别的游戏 APK,I/O 开销不可忽略。

V4 把签名数据从 APK 内部移出,生成一个独立的 .idsig 文件。Android 11+ 设备安装时直接读这个小文件,不需要对 APK 本身做流式计算。

.idsig 文件的魔数是 0x6e6740d4,内部结构是一个扁平化的 merkle tree:

[.idsig File]
  magic: 0x6e6740d4
  [V4 Signature Block]
    hashingAlgorithm
    signingAlgorithm
    signingKeyBlock
    merkleTree
    flags

merkle tree 对 APK 文件做分块哈希,每块默认 1 MB。验证时只需要读取需要校验的块,配合树根哈希即可确认完整性。增量安装(如 ADB install 的增量传输)受益最大——只传输变化的块,同时只验证那些块的哈希。

V4 还通过 flags 字段引入了 merkle tree only 模式。这种模式下不包含传统签名,只提供完整性哈希树。配合 fs-verity 等内核特性,可以在安装后持续保护 APK 不被修改。

四种方案的验证优先级

Android 系统验证 APK 时,按 V4 → V3 → V2 → V1 的优先级逐个尝试。一个 APK 可以同时包含四种签名的任意组合,但只要找到一种可验证的签名就停止:

apkVerity(apk):
    if androidVersion >= 11 AND .idsig exists:
        verifyV4(apk, idsig) → return true/throw
    if v3Block exists in Signing Block:
        verifyV3(apk, v3Block) → return true/throw
    if v2Block exists in Signing Block:
        verifyV2(apk, v2Block) → return true/throw
    // fallback
    verifyV1(apk, CERT.RSA) → return true/throw

实践建议

apksigner 默认同时生成 V1+V2+V3 签名,jarsigner 只生成 V1。

证书管理方面,CI 中不同构建节点用了不同证书,会导致 V2 和 V1 签名不匹配——这是我踩过的坑。apksigner verify --verbose 可以明确列出每种签名方案是否通过,上线前跑一遍这个命令比事后查崩溃日志划算得多。

兼容性方面,如果你的 APK 面向国内市场,不要用 V2-only 签名。去掉 V1 后,Android 7.0 以下设备完全无法验证签名。Google Play 允许上传 V2-only APK 是因为 Play 渠道的存量设备已经足够新。保持 V1+V2+V3 的组合,覆盖率最稳。

多渠道打包的原理并没有因为签名版本演进而改变:找到签名区域中一个不影响验证的位置插入额外数据。V1 时代往 META-INF 写空文件,V2 时代往 Signing Block 的 ID-value 区域写入自定义 ID(美团的 Walle 就是这样做的)。V3 和 V4 没打破这个思路,只是签名块结构更复杂了,注入点仍然存在。

深入解析 APK 反编译与重新编译:从基础到进阶实战

Android 应用的反编译与重新编译是应用开发和逆向工程中的重要技能。无论是修改现有应用,还是对问题进行调试与修复,掌握 APK 的完整工作流程都至关重要。本教程将从基础知识入手,详细讲解 APK 反编译与重新编译的核心技术,并逐步深入到 AAR 依赖的整合及 APK 签名等高级操作。

Android应用安全加固与攻防

Android 作为开放的移动操作系统,为开发者提供了巨大的自由度和创新空间,但同时也使应用程序暴露在各种安全威胁之下。逆向工程、代码篡改、数据窃取、动态调试与注入、网络中间人攻击等手段层出不穷,对应用的知识产权、商业逻辑、用户数据安全以及整体生态构成了严峻挑战。

深入 Android 端侧 AI 的独立进程推理架构:从进程隔离到 AIDL 通信的稳定性保障

将端侧 LLM 推理迁移到独立进程,通过内存隔离解决 OOM 问题,通过崩溃隔离保护主进程稳定性。本文详细记录了 AIDL 接口设计、跨进程生命周期绑定、Binder 通信陷阱及多模型管理等实战经验。

深入 Android ARCore 增强现实全链路:从 SLAM 空间定位到 Light Estimation 光照估计的虚实融合架构

深入剖析 ARCore 运动追踪、环境理解与光照估计三条核心链路的技术原理,结合 Compose 集成与性能优化实践,构建稳定的虚实融合 AR 应用。