APK 签名方案概述

1. APK 签名方案简介

在 Android 上,APK 签名是将 APK 放入 APK Sandbox 的第一步。已签名的 APK 证书定义了哪个用户 ID 与哪个 APK 关联,不同 APK 要以不同的用户 ID 运行。APK 签名可确保一个 APK 无法访问任何其他 APK 的数据。当 APK 安装到设备上时,PackageManagerService 会验证 APK 是否已经签过名。若其签名证书和其他 APK 的签名一致,则它们会共用一个UID。

Android 支持以下四种签名方案:
(1)v1: 是一种基于 JAR 签名的方案,Android 系统一开始对其提供支持。它不保护 APK 的 ZIP 元数据部分,存在的安全风险较大。
(2)v2: 是 Android 7.0 引入的一种全文件签名方案,它能够发现对 APK 受保护部分进行的所有更改,从而提升验证速度并增强完整性保护。在签名时,会在 APK 文件中插入一个 “APK Signing Block”,该分块位于 “ZIP Central Dirctory” 部分之前并金紧邻该部分。v2 签名和签名者身份信息会存储在 “APK Signing Block” 内。
(3)v3: 9.0 引入的,会将 APK 视为 blob,并对整个文件进行签名检查。对 APK 进行的任何修改都会使 APK 签名作废。其验证不仅速度要更快,而且能发现未经授权的篡改。
(4)v4: 11.0 引入的 APK 签名方案。

2. V1 签名方案

JAR 签名的 APK 是一种标准的签名 JAR,其中包含的条目必须与 META-INF/MANIFEST.MF 中列出的条目完全相同,并且所有条目都必须由同一组签名者签名。其完整性按照以下方式进行验证:
(1)每个签名者均由一个包含 META-INF/.SF 和 META-INF/.(RSA|DSA|EC) 的 JAR 条目来表示。
(2).(RSA|DSA|EC) 是具有 SignedData 结构的 PKCS #7 CMS ContentInfo,其签名通过 .SF 进行验证。
(3).SF 文件包含 META-INF/MANIFEST.MF 的全文件摘要和 META-INF/MANIFEST.MF 各个部分的摘要。需要验证 MANIFEST.MF 的全文件摘要。如果该验证失败,则改为验证 MANIFEST.MF 各个部分的摘要。
(4)对于每个受完整性保护的 JAR 条目,META-INF/MANIFEST.MF 都包含一个具有相应名称的部分,其中包含相应条目未压缩内容的摘要。所有这些摘要都需要验证。
(5)如果 APK 包含未在 MANIFEST.MF 中列出且不属于 JAR 签名一部分的 JAR 条目,APK 验证将会失败。

因此,保护链是每个受完整性保护的 JAR 条目的 .(RSA|DSA|EC) -> .SF -> MANIFEST.MF -> 内容。

3. V2 签名方案

3.1 APK Signing Block

“APK Signing Block” 包含多个 “ID-Value” 对,其中 v2 签名对应的 ID 为 “0x7109871a”(4个字节)。”APK Signing Block” 中所有数字字段均采用小端字节排序,其格式如下:

块大小 size of block : unit64 字节数;
“ID-Value” 对:unit32 ID 和 可变长度的 value;
魔数 magic:”APK Sig Block 42” 16字节。

3.2 解析 APK 过程

在文件末尾找到 “ZIP Central Dirctory” 记录,然后从该记录中读取 “ZIP Central Dirctory” 的起始偏移量。通过 “magic” 值,可快速确定 “ZIP Central Dirctory” 前面可能是 “APK Signing Block”,然后通过 “size of block” 值找到 “APK Signing Block” 在文件中的起始位置。

3.3 APK Signature Scheme v2 Block

APK 支持一个或多个签名者/身份签名,每个签名者/身份均由一个签名密钥来表示,它会以 “APK Signature Scheme v2 Block” 的形式存储。每个签名者会存储下面信息:
(1)签名算法、摘要、签名。
(2)表示签名者身份的 X.509 证书链。
(3)采用键值对形式的其它属性。

“APK Signature Scheme v2 Block” 所有数字均采用小端字节排序,所有带长度前缀的字段均使用 unit32 值表示长度:
带长度前缀的 signer序列:
(1)带长度前缀的签名过的数据:
a.带长度前缀的摘要序列:签名算法 ID (unit32) + (带长度前缀)摘要。
b.带长度前缀的 X.509 证书序列:(ASN.1 DER格式)。
c.带长度前缀的 additional attributes 序列:ID (unit32) + 可变长度 value。
(2)带长度前缀的签名序列:
a.签名算法 ID (unit32)位;
b.已签名数据上带长度前缀的签名;
(3)带长度前缀的公钥(SubjectPublicKeyInfo,ASN.1 DER形式)

3.4 签名算法

签名算法 ID
(1)0x0101 - 采用 SHA2-256 摘要、SHA2-256 MGF1、32 个字节的盐且尾部为 0xbc 的 RSASSA-PSS 算法。
(2)0x0102 - 采用 SHA2-512 摘要、SHA2-512 MGF1、64 个字节的盐且尾部为 0xbc 的 RSASSA-PSS 算法。
(3)0x0103 - 采用 SHA2-256 摘要的 RSASSA-PKCS1-v1_5 算法。此算法适用于需要确定性签名的构建系统。
(4)0x0104 - 采用 SHA2-512 摘要的 RSASSA-PKCS1-v1_5 算法。此算法适用于需要确定性签名的构建系统。
(5)0x0201 - 采用 SHA2-256 摘要的 ECDSA 算法。
(6)0x0202 - 采用 SHA2-512 摘要的 ECDSA 算法。
(7)0x0301 - 采用 SHA2-256 摘要的 DSA 算法。

支持的密钥大小和 EC 曲线:
(1)RSA:1024、2048、4096、8192、16384。
(2)EC:NIST P-256、P-384、P-521。
(3)DSA:1024、2048、3072。

3.5 组成部分

v2签名的 APK 包含下面4个部分:
(1) Contents of ZIP entries(从偏移量 0 处开始一直到 “APK Signing Block” 的起始位置);
(2) APK Signing Block;
(3) ZIP Central Directory;
(4) End of ZIP Central Directory;

v2签名保护(1)(3)(4)部分的完整性以及(2)的 “APK Signature Scheme v2 Block” 中已签名数据的完整性。通过一个或多个摘要来保护(1)(3)(4)部分,这些摘存储在(2)的已签名数据块中,然后这个签名数据库会通过一个或多个签名来保护。

(1)(3)(4)部分的摘要采用的计算方式类似两级 Merkle 树。每个部分会被分成多个 1MB 的连续块。每个块的摘要均通过字节 “0xa5” 的串联、块的长度(小端字节排序的 unit32 值)和块的内容进行计算。顶级摘要通过字节 “0xa5” 的串联、块数(小端字节排序的 unit32 值)以及块的摘要的连接(块在 APK 中显示的顺序)进行计算。摘要以分块方式计算,以便通过并行处理来加快计算速度。

(4)部分包含 “ZIP Central Directory” 的偏移量,所以该部分的保护比较复杂。当 “APK Signing Block” 的大小发生变化时,偏移量也会随之改变。因此,在通过 “ZIP Central Directory” 计算摘要时,必须将包含 “ZIP Central Directory” 偏移量的字段视为包含 “APK Signing Block” 的偏移量。

攻击者可能会试图在对将带v2签名的 APK 作为带v1签名的 APK 进行验证。为了防范此类攻击,带v2签名的 APK 如果还带v1签名,其 “META-INF/*.SF” 文件中必须包含 “X-Android-APK-Signed” 属性。该属性的值是一组以英文逗号 “,” 分隔的 APK 签名方案 ID(v2 ID 是 2)。在验证v1签名时,如果 APK 没有相应的签名,APK 验证程序必须要拒绝这些 APK。另外,攻击者可能会试图从 “APK Signature Scheme v2 Block” 中删除安全系数较高的签名。为了防范此类攻击,对 APK 进行签名时使用的签名算法 ID 的列表会存储在通过各个签名保护的已签名数据块中。

3.6 验证过程

(1)找到 “APK Signing Block” 并验证以下内容:
a.”APK Signing Block” 的两个大小字段包含相同的值。
b.”ZIP End of Central Directory” 紧跟在 “ZIP Central Directory” 记录后面。
c.”ZIP End of Central Directory” 之后没有任何数据。
(2)找到 “APK Signing Block” 中的第一个 “APK Signature Scheme v2 Block” 。如果 v2 分块存在,则继续执行第 3 步。否则,回退至使用 v1 方案验证 APK。
(3)对 “APK Signature Scheme v2 Block” 中的每个签名者执行以下操作:
a.从 signatures 中选择安全系数最高的受支持签名算法 ID。安全系数排序取决于各个实现/平台版本。
b.使用公钥并对照已签名的数据验证 signatures 的中对应的签名。
c.验证 digests 和 signatures 中的签名算法 ID 列表是否相同。
d.使用签名算法所用的同一种摘要算法计算 APK 内容的摘要。
e.验证计算出的摘要是否与 digests 中对应的摘要一致。
f.验证 certificates 中第一个证书的 SubjectPublicKeyInfo 是否与公钥相同。
(4)如果找到了至少一个签名者,并且对于每个找到的签名者,第(3)步都取得了成功,APK 验证将会成功。
注意:如果第(3)步或第(4)步失败了,则不得使用 v1 方案验证 APK。

4. v3 签名方案

4.1 组成部分

Android 9 引入的 v3 签名方案在 “APK Signing Block” 中添加了 “proof-of-rotation” 结构信息以支持 “APK key rotation”,这使 APK 能够在更新过程中更改其签名密钥。为了实现轮替,APK 必须指明新旧签名密钥之间的信任级别。

v3 “APK Signing Block” 的格式与v2相同,它们采用相同的常规格式,并支持相同的签名算法 ID、密钥大小和 EC 曲线。APK 的 v3 签名会存储为一个“ID-值”对,其 ID 为 “0xf05368c0”。
“APK Signature Scheme v3 Block” 存储在 “APK Signing Block” 内,ID 为 “0xf05368c0”。

“APK 签名方案 v3 分块”采用 v2 的格式:
带长度前缀的 signer 序列:
(1)带长度前缀的签名过的数据:
a.带长度前缀的摘要序列:签名算法 ID (unit32) + (带长度前缀)摘要。
b.带长度前缀的 X.509 证书序列:(ASN.1 DER格式)。
c.低于 minSDK(unit32) 或高于 maxSDK(unit32)则忽略该签名者。
c.带长度前缀的 additional attributes 序列:ID (unit32) + 可变长度 value + ID(0x3ba06f8c) + proof-of-rotation结构value。
(2)minSDK(uint32):签名数据部分中 minSDK 值的副本 - 用于在当前平台不在范围内时跳过对此签名的验证。必须与签名数据值匹配。
(3)maxSDK(uint32):签名数据部分中 maxSDK 值的副本 - 用于在当前平台不在范围内时跳过对此签名的验证。必须与签名数据值匹配。
(4)带长度前缀的签名序列:
a.签名算法 ID (unit32)位;
b.已签名数据上带长度前缀的签名;
(5)带长度前缀的公钥(SubjectPublicKeyInfo,ASN.1 DER形式)

4.2 Proof-of-rotation 和 self-trusted-old-certs 结构

proof-of rotation 结构允许 APK 轮替其签名证书,而不会使这些证书在与这些 APK 通信的其他 APK 上被屏蔽。为此,APK 签名包含两个新数据块:
(1)判断三方 APK 的签名证书是否可信(只要其先前证书可信);
(2)APK 的旧签名证书(APK 本身仍信任这些证书)

签名数据部分中的 proof-of-rotation 属性包含一个单链表,其中每个节点都包含用于为之前版本的 APK 签名的签名证书。此属性旨在包含概念性 proof-of-rotation 和 self-trusted-old-certs 数据结构。该单链表按版本排序,最旧的签名证书对应于根节点。在构建 proof-of-rotation 数据结构时,系统会让每个节点中的证书为列表中的下一个证书签名,从而为每个新密钥提供证据来证明它应该像旧密钥一样可信。
在构造 self-trusted-old-certs 数据结构时,系统会向每个节点添加标记来指示它在集合中的成员资格和属性。例如,可能存在一个标记,指示给定节点上的签名证书可信,可获得 Android 签名权限。此标记允许由旧证书签名的其他 APK 仍被授予由使用新签名证书签名的 APK 所定义的签名权限。由于整个 proof-of-rotation 属性都位于 v3 signer 字段的签名数据部分中,因此用于为所含 APK 签名的密钥会保护该属性。
此格式排除了多个签名密钥的情况和将不同祖先签名证书收敛到一个证书的情况(多个起始节点指向一个通用接收器)。

proof-of-rotation 存储在 “APK Signature Scheme v3 Block” 内,ID 为 “0x3ba06f8c”。其格式为:
带长度前缀的 levels(带长度前缀)序列:
(1)带长度前缀的已签名数据(由上一个证书签名 - 如果存在)
a.带长度前缀的 X.509 证书(ASN.1 DER 格式)
b.签名算法 ID (uint32) – 上一级证书使用的算法
(2)flags (uint32) - 用于指示此证书是否应该在 self-trusted-old-certs 结构中,以及针对哪些操作。
(3)签名算法 ID (uint32) - 必须与下一级中的签名数据部分的 ID 一致。
(4)上述已签名数据的带长度前缀的签名。

Android 目前将使用多个证书签名的 APK 视为具有与所含证书不同的签名身份。因此,签名数据部分中的 proof-of-rotation 属性构成了一个有向无环图,最好将其视为单链表,其中给定版本的每组签名者都表示一个节点。这使得 proof-of-rotation 结构(下面的多签名者版本)更复杂。排序成为一个特别突出的问题。更重要的是,无法再单独为 APK 签名,因为 proof-of-rotation 结构必须让旧签名证书为新的证书集签名,而不是逐个签名。例如,如果希望由两个新密钥 B 和 C 签名的 APK 是由密钥 A 签名的,则它不能让 B 签名者仅包含 A 或 B 的签名,因为这是与 B 和 C 不同的签名身份。这意味着签名者必须在构建此类结构之前进行协调。

带长度前缀的 sets(带长度前缀)序列:
(1)已签名数据(由上一组证书签名 - 如果存在)
a.带长度前缀的 certificates 序列:带长度前缀的 X.509 证书(ASN.1 DER 格式)
b.签名算法 IDs (uint32) 序列 - 上一组证书中的每个证书对应一个序列,且采用相同顺序。
(2)flags (uint32) - 这些标记用于指示这组证书是否应该在 self-trusted-old-certs 结构中,以及针对哪些操作。
(3)带长度前缀的 signatures(带长度前缀)序列:
a.签名算法 ID (uint32) - 必须与签名数据部分中的相应 ID 一致;
b. 上述已签名数据带长度前缀的签名。

v3 方案也无法处理两个不同密钥轮替到同一个应用的同一签名密钥的情形。这不同于收购情形,在收购情形中,收购公司希望转移收购的应用以使用其签名密钥来共享权限。收购被视为受支持的用例,因为新应用将通过其软件包名称来区分,并且可以包含自己的 proof-of-rotation 结构。不受支持的用例是,同一应用有两个不同的路径指向相同的证书,这打破了在密钥轮替设计中做出的许多假设。

4.3 验证过程

(1)找到 “APK Signing Block” 并验证以下内容:
a.”APK Signing Block” 的两个大小字段包含相同的值。
b.”ZIP End of Central Directory” 紧跟在 “ZIP Central Directory” 记录后面。
c.”ZIP End of Central Directory” 之后没有任何数据。
(2)找到 “APK Signing Block” 中的第一个 “APK Signature Scheme v3 Block” 。如果 v3 分块存在,则继续执行第 3 步。否则,回退至使用 v2 方案验证 APK。
(3)对 “APK Signature Scheme v3 Block” 中的每个签名者执行以下操作:
a.从 signatures 中选择安全系数最高的受支持签名算法 ID。安全系数排序取决于各个实现/平台版本。
b.使用公钥并对照已签名的数据验证 signatures 的中对应的签名。
c.验证签名数据中的最低和最高 SDK 版本是否与为签名者指定的版本匹配。
d.验证 digests 和 signatures 中的签名算法 ID 列表是否相同。
e.使用签名算法所用的同一种摘要算法计算 APK 内容的摘要。
f.验证计算出的摘要是否与 digests 中对应的摘要一致。
g.验证 certificates 中第一个证书的 SubjectPublicKeyInfo 是否与公钥相同。
h.如果签名者存在 proof-of-rotation 属性,则验证结构是否有效,以及此签名者是否为列表中的最后一个证书。
(4)如果找到了至少一个签名者,并且对于每个找到的签名者,第(3)步都取得了成功,APK 验证将会成功。

运行 cts/hostsidetests/appsecurity/src/android/appsecurity/cts/ 中的 PkgInstallSignatureVerificationTest.java CTS 测试。