APK 签名方案 v2 是一种全文件签名方案,通过检测对受保护 APK 部分的任何更改,提高验证速度并增强完整性保证。
使用 APK 签名方案 v2 进行签名会将 APK 签名块插入 APK 文件中,紧邻 ZIP 中央目录部分之前。在 APK 签名块内,v2 签名和签名者身份信息存储在 APK 签名方案 v2 块中。
图 1. 签名前后的 APK
APK 签名方案 v2 在 Android 7.0 (Nougat) 中引入。为了使 APK 可以在 Android 6.0 (Marshmallow) 和更早版本的设备上安装,应先使用 JAR 签名对 APK 进行签名,然后再使用 v2 方案进行签名。
APK 签名块
为了保持与 v1 APK 格式的向后兼容性,v2 和更新的 APK 签名存储在 APK 签名块内,这是一个为支持 APK 签名方案 v2 而引入的新容器。在 APK 文件中,APK 签名块位于紧邻 ZIP 中央目录之前,而 ZIP 中央目录又位于文件末尾。
该块包含 ID-值对,这些 ID-值对以使其更容易在 APK 中找到该块的方式进行封装。0x7109871a
ID 的 ID-值对中存储了 APK 的 v2 签名。
格式
APK 签名块的格式如下(所有数字字段均为小端序)
- 块的
size of block
(以字节为单位)(不包括此字段)(uint64) - uint64 长度前缀 ID-值对序列
ID
(uint32)value
(可变长度:对的长度 - 4 字节)
- 块的
size of block
(以字节为单位)—与第一个字段相同 (uint64) magic
“APK Sig Block 42”(16 字节)
APK 的解析方式为:首先找到 ZIP 中央目录的起始位置(通过在文件末尾查找 ZIP 中央目录结尾记录,然后从记录中读取中央目录的起始偏移量)。magic
值提供了一种快速确定中央目录之前的内容很可能是 APK 签名块的方法。size of block
值随后有效地指向文件中块的起始位置。
解释块时,应忽略具有未知 ID 的 ID-值对。
APK 签名方案 v2 块
APK 由一个或多个签名者/身份签名,每个签名者/身份由一个签名密钥表示。此信息存储为 APK 签名方案 v2 块。对于每个签名者,存储以下信息
- (签名算法、摘要、签名)元组。存储摘要是为了将签名验证与 APK 内容的完整性检查分离开来。
- 表示签名者身份的 X.509 证书链。
- 其他属性,以键值对形式表示。
对于每个签名者,使用来自提供的列表的受支持签名来验证 APK。具有未知签名算法的签名将被忽略。在遇到多个受支持签名时,由每个实现来选择要使用的签名。这使得将来能够以向后兼容的方式引入更强大的签名方法。建议的方法是验证最强的签名。
格式
APK 签名方案 v2 块存储在 ID 为 0x7109871a
的 APK 签名块内。
APK 签名方案 v2 块的格式如下(所有数值均为小端序,所有长度前缀字段均使用 uint32 作为长度)
- 长度前缀
signer
的长度前缀序列- 长度前缀
signed data
- 长度前缀
digests
的长度前缀序列signature algorithm ID
(uint32)- (长度前缀)
digest
—请参阅完整性保护内容
- X.509
certificates
的长度前缀序列- 长度前缀 X.509
certificate
(ASN.1 DER 形式)
- 长度前缀 X.509
- 长度前缀
additional attributes
的长度前缀序列ID
(uint32)value
(可变长度:其他属性的长度 - 4 字节)
- 长度前缀
- 长度前缀
signatures
的长度前缀序列signature algorithm ID
(uint32)- 长度前缀
signature
(通过signed data
)
- 长度前缀
public key
(SubjectPublicKeyInfo,ASN.1 DER 形式)
- 长度前缀
签名算法 ID
- 0x0101—RSASSA-PSS,带 SHA2-256 摘要、SHA2-256 MGF1、32 字节盐值、trailer: 0xbc
- 0x0102—RSASSA-PSS,带 SHA2-512 摘要、SHA2-512 MGF1、64 字节盐值、trailer: 0xbc
- 0x0103—RSASSA-PKCS1-v1_5,带 SHA2-256 摘要。这适用于需要确定性签名的构建系统。
- 0x0104—RSASSA-PKCS1-v1_5,带 SHA2-512 摘要。这适用于需要确定性签名的构建系统。
- 0x0201—ECDSA,带 SHA2-256 摘要
- 0x0202—ECDSA,带 SHA2-512 摘要
- 0x0301—DSA,带 SHA2-256 摘要
Android 平台支持上述所有签名算法。签名工具可以支持算法的子集。
支持的密钥大小和 EC 曲线
- RSA:1024、2048、4096、8192、16384
- EC:NIST P-256、P-384、P-521
- DSA:1024、2048、3072
完整性保护内容
为了保护 APK 内容,APK 由四个部分组成
- ZIP 条目的内容(从偏移量 0 到 APK 签名块的起始位置)
- APK 签名块
- ZIP 中央目录
- ZIP 中央目录结尾
图 2. 签名后的 APK 部分
APK 签名方案 v2 保护第 1、3、4 部分以及第 2 部分中包含的 APK 签名方案 v2 块的 signed data
块的完整性。
第 1、3 和 4 部分的完整性受到存储在 signed data
块中的一个或多个内容摘要的保护,而 signed data
块又受到一个或多个签名的保护。
第 1、3 和 4 部分的摘要计算方式如下,类似于两级 Merkle 树。每个部分被分成连续的 1 MB(220 字节)块。每个部分的最后一个块可能较短。每个块的摘要是在字节 0xa5
、块的长度(以字节为单位,小端序 uint32)和块的内容的串联上计算得出的。顶层摘要是在字节 0x5a
、块的数量(小端序 uint32)以及 APK 中块的出现顺序的块摘要的串联上计算得出的。摘要以分块方式计算,以便通过并行化来加速计算。
图 3. APK 摘要
第 4 部分(ZIP 中央目录结尾)的保护很复杂,因为该部分包含 ZIP 中央目录的偏移量。当 APK 签名块的大小发生变化时(例如,添加新签名时),偏移量会发生变化。因此,在计算 ZIP 中央目录结尾的摘要时,包含 ZIP 中央目录偏移量的字段必须视为包含 APK 签名块的偏移量。
回滚保护
攻击者可能会尝试在支持验证 v2 签名 APK 的 Android 平台上将 v2 签名 APK 验证为 v1 签名 APK。为了缓解这种攻击,同时进行 v1 签名的 v2 签名 APK 必须在其 META-INF/*.SF 文件的主要部分中包含 X-Android-APK-Signed 属性。该属性的值是以逗号分隔的 APK 签名方案 ID 集(此方案的 ID 为 2)。验证 v1 签名时,APK 验证程序需要拒绝未从此集合中获得验证程序首选的 APK 签名方案(例如,v2 方案)的签名的 APK。此保护依赖于 META-INF/*.SF 文件的内容受 v1 签名保护这一事实。请参阅关于 JAR 签名 APK 验证的部分。
攻击者可能会尝试从 APK 签名方案 v2 块中剥离更强的签名。为了缓解这种攻击,APK 签名所用的签名算法 ID 列表存储在 signed data
块中,该块受到每个签名的保护。
验证
在 Android 7.0 及更高版本中,可以根据 APK 签名方案 v2+ 或 JAR 签名(v1 方案)验证 APK。旧平台会忽略 v2 签名,仅验证 v1 签名。
图 4. APK 签名验证过程(红色为新步骤)
APK 签名方案 v2 验证
- 找到 APK 签名块并验证以下内容:
- APK 签名块的两个大小字段包含相同的值。
- ZIP 中央目录紧随 ZIP 中央目录结尾记录之后。
- ZIP 中央目录结尾之后没有更多数据。
- 找到 APK 签名块内的第一个 APK 签名方案 v2 块。如果 v2 块存在,则继续执行步骤 3。否则,回退到使用 v1 方案验证 APK。
- 对于 APK 签名方案 v2 块中的每个
signer
- 从
signatures
中选择最强的受支持signature algorithm ID
。强度排序取决于每个实现/平台版本。 - 使用
public key
针对signed data
验证来自signatures
的相应signature
。(现在可以安全地解析signed data
。) - 验证
digests
和signatures
中签名算法 ID 的有序列表是否相同。(这是为了防止签名剥离/添加。) - 使用与签名算法使用的摘要算法相同的摘要算法计算 APK 内容的摘要。
- 验证计算出的摘要是否与来自
digests
的相应digest
相同。 - 验证
certificates
的第一个certificate
的 SubjectPublicKeyInfo 是否与public key
相同。
- 从
- 如果找到至少一个
signer
并且每个找到的signer
的步骤 3 都成功,则验证成功。
注意:如果在步骤 3 或 4 中发生失败,则不得使用 v1 方案验证 APK。
JAR 签名 APK 验证(v1 方案)
JAR 签名 APK 是一个 标准签名 JAR,它必须完全包含 META-INF/MANIFEST.MF 中列出的条目,并且所有条目都必须由同一组签名者签名。其完整性验证方式如下:
- 每个签名者都由 META-INF/<signer>.SF 和 META-INF/<signer>.(RSA|DSA|EC) JAR 条目表示。
- <signer>.(RSA|DSA|EC) 是一个 PKCS #7 CMS ContentInfo,带有 SignedData 结构,其签名在 <signer>.SF 文件上进行验证。
- <signer>.SF 文件包含 META-INF/MANIFEST.MF 的全文件摘要以及 META-INF/MANIFEST.MF 每个部分的摘要。MANIFEST.MF 的全文件摘要经过验证。如果验证失败,则改为验证每个 MANIFEST.MF 部分的摘要。
- META-INF/MANIFEST.MF 包含每个受完整性保护的 JAR 条目的相应命名部分,其中包含条目未压缩内容的摘要。所有这些摘要都经过验证。
- 如果 APK 包含未在 MANIFEST.MF 中列出且不属于 JAR 签名的 JAR 条目,则 APK 验证失败。
因此,保护链是 <signer>.(RSA|DSA|EC) -> <signer>.SF -> MANIFEST.MF -> 每个受完整性保护的 JAR 条目的内容。