On-device signing architecture

As of Android 12, the Android Runtime (ART) module is a Mainline module. Updating the module might require it to rebuild the ahead-of-time (AOT) compilation artifacts of bootclasspath jars and system server. Because these artifacts are security sensitive, Android 12 employs a feature called on-device signing to prevent these artifacts from being tampered with. This page covers the on-device signing architecture and its interactions with other Android security features.

High-level design

On-device signing has two core components:

  • odrefresh is part of the ART Mainline module. It is responsible for generating the runtime artifacts. It checks existing artifacts against the installed version of the ART module, bootclasspath jars, and system server jars to determine whether they're up to date or need to be regenerated. If they need to be regenerated, odrefresh generates them and stores them.

  • odsign is a binary that is part of the Android platform. It runs during early boot, right after the /data partition is mounted. Its main responsibility is to invoke odrefresh to check whether any artifacts need to be generated or updated. For any new or updated artifacts that odrefresh generates, odsign computes a hash function. The result of such a hash computation is called a file digest. For any artifacts that already exist, odsign verifies that the digests of the existing artifacts match the digests that odsign had previously computed. This ensures that the artifacts haven't been tampered with.

In error conditions, such as when the digest for a file doesn't match, odrefresh and odsign throw away all existing artifacts on /data and attempt to regenerate them. If that fails, the system falls back to JIT mode.

odrefresh and odsign are protected by dm-verity, and are a part of Android's Verified Boot chain.

Computation of file digests with fs-verity

fs-verity is a feature of the Linux kernel that does Merkle tree based verification of file data. Enabling fs-verity on a file causes the file system to build a Merkle tree over the file's data using SHA-256 hashes, store it in a hidden location alongside the file, and mark the file as read-only. fs-verity automatically verifies the file's data against the Merkle tree on demand as it's read. fs-verity makes the root hash of the Merkle tree available as a value called the fs-verity file digest, and fs-verity ensures that any data read from the file is consistent with this file digest.

odsign uses fs-verity to improve boot performance by optimizing the cryptographic authentication of on-device compiled artifacts at boot time. When an artifact is generated, odsign enables fs-verity on it. When odsign verifies an artifact, it verifies the fs-verity file digest instead of the full file hash. This eliminates the need to read and hash the full data of the artifact at boot time. Artifact data is instead hashed on demand by fs-verity as it's used, on a block-by-block basis.

On devices whose kernel doesn't support fs-verity, odsign falls back to computing file digests in userspace. odsign uses the same Merkle tree based hash algorithm as fs-verity, so the digests are the same in either case. fs-verity is required on all devices that launched with Android 11 and higher.

Storage of the file digests

odsign stores the file digests of the artifacts in a separate file called odsign.info. To make sure odsign.info isn't tampered with, odsign.info is signed with a signing key that has important security properties. In particular, the key can be generated and used only during early boot, at which point only trusted code is running; see Trusted signing keys for details.

Verification of file digests

On every boot, if odrefresh determines that the existing artifacts are up to date, odsign ensures that the files haven't been tampered with since they were generated. odsign does this by verifying the file digests. First, it verifies the signature of odsign.info. If the signature is valid, then odsign verifies that the digest of each file matches the corresponding digest in odsign.info.

Trusted signing keys

Android 12 introduces a new Keystore feature called boot stage keys that addresses the following security concerns:

  • What prevents an attacker from using our signing key to sign their own version of odsign.info?
  • What prevents an attacker from generating their own signing key and using that to sign their own version of odsign.info?

Boot stage keys split Android's boot cycle into levels, and cryptographically tie the creation and use of a key to a specified level. odsign creates its signing key at an early level, when only trusted code is running, protected through dm-verity.

Boot stage levels are numbered from 0 to the magic number 1000000000. During Android's boot process, you can increase the boot level by setting a system property from init.rc. For example, the following code sets the boot level to 10:

setprop keystore.boot_level 10

Clients of Keystore can create keys that are tied to a certain boot level. For example, if you create a key for boot level 10, then that key can be used only when the device is in boot level 10.

odsign uses boot level 30, and the signing key that it creates is tied to that boot level. Before using a key to sign artifacts, odsign verifies that the key is tied to boot level 30.

This prevents the two attacks described earlier in this section:

  • Attackers can't use the generated key, because by the time an attacker has a chance to run malicious code, the boot level has increased beyond 30, and Keystore refuses operations that use the key.
  • Attackers can't create a new key, because by the time an attacker has a chance to run malicious code, the boot level has increased beyond 30, and Keystore refuses to create a new key with that boot level. If an attacker creates a new key that isn't tied to boot level 30, odsign rejects it.

Keystore ensures that the boot level is properly enforced. The following sections go into more detail about how this is done for different Keymaster versions.

Keymaster 4.0 implementation

Different versions of Keymaster handle the implementation of boot stage keys differently. On devices with a Keymaster 4.0 TEE/Strongbox, Keymaster handles the implementation as follows:

  1. On first boot, Keystore creates a symmetric key K0 with the MAX_USES_PER_BOOT tag set to 1. This means that the key can be used only once per boot.
  2. During the boot, if the boot level is increased, a new key for that boot level can be generated from K0 using a HKDF function: Ki+i=HKDF(Ki, "some_fixed_string"). For example, if you move from boot level 0 to boot level 10, the HKDF is invoked 10 times to derive K10 from K0.
  3. When the boot level changes, the key for the previous boot level is erased from memory, and the keys associated with previous boot levels are no longer available.

    Key K0 is a MAX_USES_PER_BOOT=1 key. This means that it's also impossible to use that key later in boot, because at least one boot level transition (to the final boot level) always occurs.

When a Keystore client such as odsign requests a key to be created in boot level i, its blob is encrypted with key Ki. Because Ki isn't available after boot level i, this key can't be created or decrypted in later boot stages.

Keymaster 4.1 and KeyMint 1.0 implementation

The Keymaster 4.1 and KeyMint 1.0 implementations are largely the same as the Keymaster 4.0 implementation. The main difference is that K0 isn't a MAX_USES_PER_BOOT key, but an EARLY_BOOT_ONLY key, which was introduced in Keymaster 4.1. An EARLY_BOOT_ONLY key can be used only during early phases of boot, when no untrusted code is running. This provides an additional level of protection: in the Keymaster 4.0 implementation, an attacker that compromises the file system and SELinux can modify the Keystore database to create its own MAX_USES_PER_BOOT=1 key to sign artifacts with. Such an attack is impossible with the Keymaster 4.1 and KeyMint 1.0 implementations, because EARLY_BOOT_ONLY keys can be created only during early boot.

Public component of trusted signing keys

odsign retrieves the public key component of the signing key from Keystore. However, Keystore doesn't retrieve that public key from the TEE/SE that holds the corresponding private key. Instead, it retrieves the public key from its own on-disk database. This means that an attacker that compromises the file system could modify the Keystore database to contain a public key that is part of a public/private keypair under their control.

To prevent this attack, odsign creates an additional HMAC key with the same boot level as the signing key. Then, when creating the signing key, odsign uses this HMAC key to create a signature of the public key and stores that on disk. On subsequent boots, when retrieving the public key of the signing key, it uses the HMAC key to verify that the on-disk signature matches the signature of the retrieved public key. If they match, the public key is trustworthy, because the HMAC key can be used only in early boot levels and therefore can't have been created by an attacker.