设备端签名架构

从 Android 12 开始,Android 运行时 (ART) 模块是一个 Mainline 模块。更新该模块可能需要重建启动类路径 jar 和系统服务器的预先 (AOT) 编译工件。由于这些工件对安全至关重要,因此 Android 12 采用了一种名为设备端签名的功能,以防止这些工件被篡改。本页面介绍了设备端签名架构及其与其他 Android 安全功能之间的互动。

高级设计

设备端签名有两个核心组件

  • odrefresh 是 ART Mainline 模块的一部分。它负责生成运行时工件。它会根据已安装的 ART 模块版本、启动类路径 jar 和系统服务器 jar 检查现有工件,以确定它们是否是最新的,或者是否需要重新生成。如果需要重新生成,odrefresh 会生成它们并存储起来。

  • odsign 是 Android 平台的一部分二进制文件。它在早期启动期间(紧随 /data 分区挂载之后)运行。它的主要职责是调用 odrefresh 以检查是否需要生成或更新任何工件。对于 odrefresh 生成的任何新的或更新的工件,odsign 都会计算哈希函数。此类哈希计算的结果称为文件摘要。对于任何已存在的工件,odsign 都会验证现有工件的摘要是否与 odsign 之前计算出的摘要相匹配。这可确保工件未被篡改。

在错误情况下(例如,当文件的摘要不匹配时),odrefreshodsign 会丢弃 /data 上的所有现有工件,并尝试重新生成它们。如果失败,系统会回退到 JIT 模式。

odrefreshodsigndm-verity 保护,并且是 Android 验证启动链的一部分。

使用 fs-verity 计算文件摘要

fs-verity 是 Linux 内核的一项功能,可对文件数据进行基于 Merkle 树的验证。在文件上启用 fs-verity 会导致文件系统使用 SHA-256 哈希算法在文件数据上构建 Merkle 树,将其存储在文件旁边的隐藏位置,并将文件标记为只读。fs-verity 会在按需读取文件数据时,自动根据 Merkle 树验证文件数据。fs-verity 将 Merkle 树的根哈希值作为名为fs-verity 文件摘要的值提供,并且 fs-verity 确保从文件读取的任何数据都与此文件摘要一致。

odsign 使用 fs-verity 通过优化启动时设备端编译工件的加密身份验证来提高启动性能。生成工件时,odsign 会在其上启用 fs-verity。当 odsign 验证工件时,它会验证 fs-verity 文件摘要,而不是完整的文件哈希值。这消除了在启动时读取工件的完整数据并对其进行哈希处理的需要。工件数据改为由 fs-verity 在使用时按需(逐块)进行哈希处理。

在内核不支持 fs-verity 的设备上,odsign 会回退到在用户空间中计算文件摘要。odsign 使用与 fs-verity 相同的基于 Merkle 树的哈希算法,因此在任何一种情况下,摘要都是相同的。所有搭载 Android 11 及更高版本的设备都必须支持 fs-verity。

文件摘要的存储

odsign 将工件的文件摘要存储在一个名为 odsign.info 的单独文件中。为了确保 odsign.info 不被篡改,odsign.info 使用具有重要安全属性的签名密钥进行签名。特别是,密钥只能在早期启动期间(此时只有受信任的代码在运行)生成和使用;有关详情,请参阅受信任的签名密钥

文件摘要的验证

在每次启动时,如果 odrefresh 确定现有工件是最新的,则 odsign 会确保文件自生成以来未被篡改。odsign 通过验证文件摘要来实现此目的。首先,它会验证 odsign.info 的签名。如果签名有效,则 odsign 会验证每个文件的摘要是否与 odsign.info 中的相应摘要相匹配。

受信任的签名密钥

Android 12 引入了一项名为启动阶段密钥的新密钥库功能,解决了以下安全问题

  • 是什么阻止攻击者使用我们的签名密钥来签署他们自己的 odsign.info 版本?
  • 是什么阻止攻击者生成他们自己的签名密钥并使用它来签署他们自己的 odsign.info 版本?

启动阶段密钥将 Android 的启动周期分为多个级别,并在加密方面将密钥的创建和使用与指定的级别联系起来。odsign 在早期级别(此时只有受信任的代码在运行,并通过 dm-verity 保护)创建其签名密钥。

启动阶段级别从 0 编号到幻数 1000000000。在 Android 的启动过程中,您可以通过从 init.rc 设置系统属性来提高启动级别。例如,以下代码将启动级别设置为 10

setprop keystore.boot_level 10

密钥库的客户端可以创建绑定到特定启动级别的密钥。例如,如果您为启动级别 10 创建密钥,则该密钥只能在设备处于启动级别 10 时使用。

odsign 使用启动级别 30,并且它创建的签名密钥绑定到该启动级别。在使用密钥对工件进行签名之前,odsign 会验证密钥是否绑定到启动级别 30。

这可以防止本节前面描述的两种攻击

  • 攻击者无法使用生成的密钥,因为当攻击者有机会运行恶意代码时,启动级别已超过 30,并且密钥库会拒绝使用该密钥的操作。
  • 攻击者无法创建新密钥,因为当攻击者有机会运行恶意代码时,启动级别已超过 30,并且密钥库会拒绝创建启动级别为该级别的新密钥。如果攻击者创建了未绑定到启动级别 30 的新密钥,odsign 会拒绝该密钥。

密钥库确保启动级别得到正确强制执行。以下部分将更详细地介绍针对不同 Keymaster 版本执行此操作的方式。

Keymaster 4.0 实现

不同版本的 Keymaster 以不同的方式处理启动阶段密钥的实现。在具有 Keymaster 4.0 TEE/Strongbox 的设备上,Keymaster 按如下方式处理实现

  1. 在首次启动时,密钥库会创建一个对称密钥 K0,并将 MAX_USES_PER_BOOT 标记设置为 1。这意味着该密钥在每次启动时只能使用一次。
  2. 在启动期间,如果启动级别提高,则可以使用 HKDF 函数从 K0 生成该启动级别的新密钥:Ki+i=HKDF(Ki, "some_fixed_string")。例如,如果您从启动级别 0 移动到启动级别 10,则会调用 HKDF 10 次以从 K0 派生 K10。
  3. 当启动级别更改时,上一个启动级别的密钥会从内存中删除,并且与先前启动级别关联的密钥不再可用。

    密钥 K0 是一个 MAX_USES_PER_BOOT=1 密钥。这意味着稍后在启动中使用该密钥也是不可能的,因为至少会发生一次启动级别转换(转换为最终启动级别)。

当密钥库客户端(如 odsign)请求在启动级别 i 中创建密钥时,其 blob 将使用密钥 Ki 进行加密。由于密钥 Ki 在启动级别 i 之后不可用,因此无法在以后的启动阶段创建或解密此密钥。

Keymaster 4.1 和 KeyMint 1.0 实现

Keymaster 4.1 和 KeyMint 1.0 实现与 Keymaster 4.0 实现基本相同。主要区别在于 K0 不是 MAX_USES_PER_BOOT 密钥,而是 Keymaster 4.1 中引入的 EARLY_BOOT_ONLY 密钥。EARLY_BOOT_ONLY 密钥只能在早期启动阶段(此时没有不受信任的代码在运行)使用。这提供了额外的保护级别:在 Keymaster 4.0 实现中,攻击者如果危害了文件系统和 SELinux,则可以修改密钥库数据库以创建自己的 MAX_USES_PER_BOOT=1 密钥来签署工件。使用 Keymaster 4.1 和 KeyMint 1.0 实现,这种攻击是不可能的,因为 EARLY_BOOT_ONLY 密钥只能在早期启动期间创建。

受信任的签名密钥的公共组件

odsign 从密钥库检索签名密钥的公钥组件。但是,密钥库不会从持有相应私钥的 TEE/SE 检索该公钥。相反,它会从其自己的磁盘数据库中检索公钥。这意味着,危害了文件系统的攻击者可能会修改密钥库数据库,使其包含属于他们控制下的公钥/私钥对的公钥。

为了防止这种攻击,odsign 创建了一个额外的 HMAC 密钥,其启动级别与签名密钥相同。然后,在创建签名密钥时,odsign 使用此 HMAC 密钥创建公钥的签名,并将其存储在磁盘上。在后续启动时,当检索签名密钥的公钥时,它会使用 HMAC 密钥验证磁盘签名是否与检索到的公钥的签名匹配。如果匹配,则公钥是可信的,因为 HMAC 密钥只能在早期启动级别中使用,因此不可能是由攻击者创建的。