Android 9 支持APK 密钥轮换,这使应用程序能够在 APK 更新过程中更改其签名密钥。为了使轮换切实可行,APK 必须指示新旧签名密钥之间的信任级别。为了支持密钥轮换,我们将APK 签名方案从 v2 更新为 v3,以允许使用新旧密钥。 V3 将有关支持的 SDK 版本的信息和旋转证明结构添加到 APK 签名块中。
APK签名块
为了保持与 v1 APK 格式的向后兼容性,v2 和 v3 APK 签名存储在紧邻 ZIP 中央目录之前的 APK 签名块内。
v3 APK 签名块格式与 v2 相同。 APK 的 v3 签名存储为 ID 值对,ID 为 0xf05368c0。
APK签名方案v3块
v3 方案的设计与v2 方案非常相似。它具有相同的通用格式,并支持相同的签名算法 ID 、密钥大小和 EC 曲线。
然而,v3方案添加了有关支持的SDK版本和proof-of-rotation结构的信息。
格式
APK 签名方案 v3 块存储在 ID 为0xf05368c0
的 APK 签名块内。
APK签名方案v3块的格式遵循v2的格式:
- 长度前缀
signer
的长度前缀序列:- 长度前缀的
signed data
:- 长度前缀
digests
的长度前缀序列:-
signature algorithm ID
(4字节) -
digest
(长度前缀)
-
- X.509
certificates
的长度前缀序列:- 带长度前缀的 X.509
certificate
(ASN.1 DER 形式)
- 带长度前缀的 X.509
-
minSDK
(uint32) - 如果平台版本低于此数字,则应忽略此签名者。 -
maxSDK
(uint32) - 如果平台版本高于此数字,则应忽略此签名者。 - 长度前缀附加
additional attributes
的长度前缀序列:-
ID
(uint32) -
value
(可变长度:附加属性的长度 - 4 个字节) -
ID - 0x3ba06f8c
-
value -
旋转证明结构
-
- 长度前缀
minSDK
(uint32) - 签名数据部分中 minSDK 值的重复 - 如果当前平台不在范围内,则用于跳过此签名的验证。必须匹配带符号的数据值。-
maxSDK
(uint32) - 签名数据部分中 maxSDK 值的重复 - 如果当前平台不在范围内,则用于跳过此签名的验证。必须匹配带符号的数据值。 - 长度前缀
signatures
的长度前缀序列:-
signature algorithm ID
(uint32) - 对
signed data
进行长度前缀signature
-
- 长度前缀的
public key
(SubjectPublicKeyInfo,ASN.1 DER 形式)
- 长度前缀的
轮换证明和自信任的旧证书结构
轮换证明结构允许应用程序轮换其签名证书,而不会被与其通信的其他应用程序阻止。为了实现这一点,应用程序签名包含两条新数据:
- 第三方断言应用程序的签名证书在其前身受信任的任何地方都可以信任
- 应用程序本身仍然信任的旧签名证书
签名数据部分中的proof-of-rotation 属性由一个单链表组成,每个节点都包含一个用于对应用程序的早期版本进行签名的签名证书。该属性旨在包含概念性的轮换证明和自信任的旧证书数据结构。该列表按版本排序,最旧的签名证书对应于根节点。旋转证明数据结构是通过让每个节点中的证书签署列表中的下一个节点来构建的,从而为每个新密钥注入证据,表明它应该与旧密钥一样受信任。
self-trusted-old-certs 数据结构是通过向每个节点添加标志来构建的,指示其在集合中的成员身份和属性。例如,可能存在一个标志,指示给定节点处的签名证书对于获取 Android 签名权限是可信的。此标志允许由旧证书签名的其他应用程序仍被授予由使用新签名证书签名的应用程序定义的签名权限。由于整个旋转证明属性驻留在 v3 signer
字段的签名数据部分中,因此它受到用于对包含的 apk 进行签名的密钥的保护。
这种格式排除了多个签名密钥以及不同祖先签名证书聚合为一个(多个起始节点到一个公共接收器)。
格式
旋转证明存储在 APK 签名方案 v3 块内,ID 为0x3ba06f8c
。其格式为:
- 长度前缀
levels
的长度前缀序列:- 长度前缀
signed data
(通过先前的证书 - 如果存在)- 带长度前缀的 X.509
certificate
(ASN.1 DER 形式) -
signature algorithm ID
(uint32) - 上一级证书使用的算法
- 带长度前缀的 X.509
flags
(uint32) - 指示此证书是否应位于 self-trusted-old-certs 结构中以及用于哪些操作的标志。-
signature algorithm ID
(uint32) - 必须与下一级中签名数据部分的 ID 匹配。 - 对上述
signed data
进行长度前缀signature
- 长度前缀
多项证书
Android 目前将使用多个证书签名的 APK 视为具有独立于组成证书的唯一签名身份。因此,签名数据部分中的旋转证明属性形成一个有向非循环图,最好将其视为单链表,给定版本的每组签名者代表一个节点。这增加了旋转证明结构的额外复杂性(下面的多签名者版本)。特别是,订购成为一个问题。更重要的是,不再可能独立签署 APK,因为轮换证明结构必须让旧的签名证书签署新的一组证书,而不是逐一签署它们。例如,由密钥 A 签名的 APK 希望由两个新密钥 B 和 C 签名,则 B 签名者不能只包含 A 或 B 的签名,因为这是与 B 和 C 不同的签名身份。这将意味着签名者必须在建立这样的结构之前进行协调。
多签名者旋转证明属性
- 长度前缀集合的长度前缀
sets
:-
signed data
(按之前的设置 - 如果存在)-
certificates
的长度前缀序列- 带长度前缀的 X.509
certificate
(ASN.1 DER 形式)
- 带长度前缀的 X.509
-
signature algorithm IDs
序列 (uint32) - 对应前一组中的每个证书,顺序相同。
-
-
flags
(uint32) - 指示这组证书是否应位于 self-trusted-old-certs 结构中以及用于哪些操作的标志。 - 长度前缀
signatures
的长度前缀序列:-
signature algorithm ID
(uint32) - 必须与签名数据部分中的算法 ID 匹配 - 对上述
signed data
进行长度前缀signature
-
-
旋转证明结构中的多个祖先
v3 方案也不处理同一应用程序的两个不同密钥轮换为同一签名密钥。这与收购的情况不同,收购公司希望将收购的应用程序转移到使用其签名密钥来共享权限。此次收购被视为受支持的用例,因为新应用程序将通过其包名称进行区分,并且可以包含自己的旋转证明结构。不受支持的情况是,同一应用程序有两个不同的路径来获取同一证书,这打破了密钥轮换设计中的许多假设。
确认
在 Android 9 及更高版本中,可以根据 APK 签名方案 v3、v2 方案或 v1 方案验证 APK。较旧的平台会忽略 v3 签名并尝试验证 v2 签名,然后验证 v1。
APK签名方案v3验证
- 找到 APK 签名块并验证:
- APK 签名块的两个大小字段包含相同的值。
- ZIP 中央目录后面紧跟着 ZIP 中央目录结束记录。
- ZIP 中央目录末尾后面没有更多数据。
- 找到 APK 签名块内的第一个 APK 签名方案 v3 块。如果存在 v3 块,请继续执行步骤 3。否则,请回退到使用 v2 方案验证 APK。
- 对于 APK 签名方案 v3 块中的每个
signer
,其最小和最大 SDK 版本都在当前平台的范围内:- 从
signatures
中选择支持的最强signature algorithm ID
。强度排序取决于每个实现/平台版本。 - 使用
public key
根据signatures
signed data
验证相应的signature
。 (现在可以安全地解析signed data
。) - 验证签名数据中的最小和最大 SDK 版本是否与为
signer
指定的版本匹配。 - 验证
digests
和signatures
中的签名算法 ID 的有序列表是否相同。 (这是为了防止签名剥离/添加。) - 使用与签名算法使用的摘要算法相同的摘要算法来计算 APK 内容的摘要。
- 验证计算出的摘要与
digests
中相应的digest
相同。 - 验证
certificate
中第一个certificates
的SubjectPublicKeyInfo 是否与public key
相同。 - 如果
signer
存在旋转证明属性,请验证该结构是否有效并且该signer
是列表中的最后一个证书。
- 从
- 如果在当前平台的范围内恰好找到一个
signer
并且步骤 3 对于该signer
成功,则验证成功。
验证
要测试您的设备是否正确支持 v3,请运行cts/hostsidetests/appsecurity/src/android/appsecurity/cts/
中的PkgInstallSignatureVerificationTest.java
CTS 测试。