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