安卓签名方案记录
Android的签名机制是保障应用安全和完整性的重要环节,经历了多个版本的演进。目前主流的签名方案包括V1、V2、V3、V4四种,它们适用于不同的Android版本和应用场景
V1签名是最初的签名方案;为了提高验证速度与覆盖度,在Android7.0引入了V2;为了实现密钥轮转,在Android9.0引入了V3;为了增量apk安装,在Android11.0引入了V4
V1签名方案
V1 签名会在 APK 的 META-INF/ 目录中新增或更新签名相关文件,其中会包括以下三个文件:
通常包括 MANIFEST.MF、一个或多个 *.SF,以及与其对应的 *.RSA / *.DSA / *.EC 签名块文件
MANIFEST.MF是清单摘要文件,对apk中除META-INF外的每个文件逐个计算哈希摘要,并以base64编码后保存,用于确保每个文件的完整性
*.SF是签名文件,用于记录 MANIFEST.MF 的摘要,以及 MANIFEST.MF 中各个条目 section 的摘要
*.RSA / *.DSA / *.EC是签名块文件,使用私钥对.SF进行数字签名,并且将签名结果连同包含公钥的X.509证书一起保存,用于验证签名合法性,可以通过openssl命令查看公钥以及签名的信息
- V1签名校验时,首先会在META-INF目录下查找后缀名为.DSA或者.RSA或者.EC的签名文件,找到之后根据签名文件的文件名推测出.SF的文件名;
- 之后读取.RSA的签名文件数据,构造出PKCS#7格式的对象pkcs7,并从其中的X.509证书中读取公钥、签名数据,使用证书中的公钥验证 PKCS#7/CMS 签名,确认 *.SF 文件确实由对应私钥签出且内容未被篡改;
- 计算MANIFEST.MF整个文件的摘要值,并且与.SF文件中记录的摘要值进行比较,如果相等,就可以肯定MANIFEST.MF文件没有被篡改,否则需要对MANIFEST.MF文件中的每个 manifest section计算摘要,依次与.SF文件记录的摘要进行比对,每一个都相等才能进行下一步;
- 之后先读取AndroidManifest.xml的文件数据计算出摘要,与MANIFEST.MF记录的摘要进行比对,如果相等,继续遍历文件计算摘要值进行比对,全部相等则校验成功
V2签名方案
V2签名方案是基于V1签名方案的不足提出的,在Android9引入。V1 签名通常不会保护 META-INF 目录下未被 MANIFEST.MF 登记的普通文件,因此攻击者新增 META-INF 下的额外文件,或修改某些未参与签名校验的 META-INF 普通文件,可能不会影响 V1 验签。此外,V1签名方案的apk安装速度较慢,因为需要解压并验证每个文件;同时内存占用较高,需要解压和缓存文件。
V2签名方案是基于zip文件的特性,对整个apk文件进行签名,该方案能发现对受保护apk所有地方的更改。
apk文件的大致结构如下图所示:
首先apk开头存储了所有的压缩数据,包括资源文件、代码文件等信息;签名块则是apk签名的保存位置;中央目录则相当于压缩包的索引表,记录了压缩包里每一个文件的名字、大小、压缩方式、在包内的偏移等信息。
zip文件解析时,首先需要找到中央目录结束的地方,会从文件末尾搜索0x06054b50;然后继续往前扫描,拿到中央目录条目总数,再把整块中央目录全部读进来;之后从前往后,根据每条索引,读取压缩的真实文件,所以签名块放在压缩数据和中央目录之间,不会影响压缩文件的解压
所以V2签名方案是基于zip文件的特性,将签名块放在压缩目录与中央目录之间。而相对于V1签名方案来说,V2签名不再以文件为单位计算摘要了,而是以1MB为单位将APK 的受保护区域拆分成多个连续的块,计算每个块的摘要,并对所有摘要进行签名,最后添加X.509开发者数字证书。验签时首先取出公钥,用公钥验证区块2的签名;再用摘要集依次验证每一块数据的摘要
V2签名的标志是在中央目录之前的标志位APK Sig Block 42,如果没有这个标志,则没有V2签名
假设一个 APK 同时具有 v1 和 v2 签名,攻击者如果删除其中的 v2 签名块,然后让 Android 7.0 设备只按照 v1 方案验证,可能绕过 v2 对 ZIP 结构的更强保护
为防止这种降级攻击,AOSP 规范要求:带 v2 且同时带 v1 签名的 APK,应在 META-INF/*.SF 的主属性部分加入:X-Android-APK-Signed: 2。这样,支持 v2 的系统在执行 v1 验证时,如果发现该属性声明 APK 本应存在 v2 签名,而实际 v2 签名块不存在,就必须拒绝安装
V3签名方案
V3签名方案是在9.0版本提出的,用于解决密钥轮转问题。V1和V2签名都不支持密钥轮转,当开发者需要更换新证书时,用户需要重新安装应用,否则会提示签名不一致,不可以直接覆盖安装
V3签名和V2签名大致相同,也是对整体zip文件进行签名,同样位于 APK Signing Block 中,但使用不同的签名方案 ID
V3签名比V2签名的签名块中多了一块attr块,存储证书链;V3 在 signed data 的 additional attributes 中加入 proof-of-rotation 记录,用于描述旧签名证书到新签名证书的轮转关系。系统只要在链中找到和已安装apk任意证书匹配的那一级,就允许覆盖安装,而且轮转一旦发布,旧私钥就可以废弃;以后再次轮转,只需要把本次的密钥当旧即可,链可无限加长。证书链则是用旧密钥给新证书/公钥进行签名声明,再把这段声明和新证书一块放在apk中,验证中只要发现某个安装的证书能在这条链里找到节点即可
原来APP使用证书 A:
1 | |
现在开发者要更换为证书 B,需要先持有:
1 | |
然后生成新版本:
1 | |
V3.1签名方案
V3 已经支持密钥轮转,但无法精确指定轮转从哪个 SDK 版本开始生效。V3.1签名方案在 Android 13.0提出,让新证书只在指定新系统上生效
V3.1签名方案的核心作用是:允许开发者指定密钥轮转从哪个 Android SDK 版本开始生效。例如,一个应用原本使用旧证书 A 签名,现在希望从 Android 13 开始切换到新证书 B,此时 APK 中可以同时存在:
- V3签名块:使用旧证书 A,供 Android 9 ~ Android 12L 使用
- V3.1签名块:使用新证书 B,供 Android 13 及以上使用
V4签名方案
V4 签名方案由 Android 11 引入,不取代 V2/V3 方案,而是用于支持流式传输和增量安装场景下的完整性校验
传统安装通常需要在 APK 完整下载后,再对其进行验签和安装。对于体积较大的应用,这会增加用户开始使用应用前的等待时间。Android 11 引入了增量文件系统 IncFS,并支持 adb install --incremental。V4 基于 APK 的全部字节构建符合 fs-verity 结构的 Merkle 哈希树,并通过受签名保护的根哈希作为校验基准。当安装或运行过程需要访问某个尚未完整下载的数据块时,系统可以按需接收该数据块及其对应的 Merkle 验证信息,并根据可信根哈希验证该数据块的完整性;验证通过后即可使用该数据块,其余数据可以继续按需传输
因此,V4 支持的是“按需下载、按块验证”的能力。应用能够启动前实际需要下载的数据量并不是固定值,而是取决于应用结构、启动阶段所需的代码和资源,以及系统的数据加载策略
与 V2/V3 将签名信息嵌入 APK 内部不同,V4 签名存储在独立的 <apk name>.apk.idsig 文件中,并与 APK 一同传输。V4 签名需要 V2 或 V3 签名作为补充,其主要职责是为增量安装阶段提供适合流式加载的数据完整性校验能力;V2/V3 则继续提供 APK 内部签名保护以及与签名者身份相关的验证基础
参考链接:
https://source.android.google.cn/docs/security/features/apksigning?hl=zh-cn