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 invokeodrefresh
to check whether any artifacts need to be generated or updated. For any new or updated artifacts thatodrefresh
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 thatodsign
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:
- On first boot, Keystore creates a symmetric key K0 with the
MAX_USES_PER_BOOT
tag set to1
. This means that the key can be used only once per boot. - 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. 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.