Properly source and use verifiedBootKey

The previous implementation used a randomly generated value for the `verifiedBootKey` within the simulated attestation's Root of Trust. This is a significant discrepancy from a genuine attestation and represents a clear detection vector for any verification service that inspects the full certificate chain.

This commit introduces a robust, multi-layered approach to source and manage both the `verifiedBootKey` and the `verifiedBootHash`, ensuring the simulated attestation is as authentic as possible.
This commit is contained in:
JingMatrix
2025-11-29 19:58:02 +01:00
parent 0275eb7ad2
commit 2446461aab
4 changed files with 123 additions and 67 deletions
@@ -28,8 +28,8 @@ object App {
SystemLogger.info("Welcome to TEESimulator!")
try {
// Set up the device's boot hash, which is crucial for attestation.
AndroidDeviceUtils.setupBootHash()
// Set up the device's boot key and hash, which are crucial for attestation.
AndroidDeviceUtils.setupBootKeyAndHash()
// Initialize and start the appropriate keystore interceptors.
initializeInterceptors()
// Enter an infinite loop to keep the service running.
@@ -8,7 +8,6 @@ import org.bouncycastle.asn1.ASN1Boolean
import org.bouncycastle.asn1.ASN1Encodable
import org.bouncycastle.asn1.ASN1Enumerated
import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.ASN1OctetString
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.DERNull
import org.bouncycastle.asn1.DEROctetString
@@ -49,24 +48,15 @@ object AttestationBuilder {
* @return The constructed [DERSequence] for the Root of Trust.
*/
internal fun buildRootOfTrust(originalRootOfTrust: ASN1Encodable?): DERSequence {
val verifiedBootKey = AndroidDeviceUtils.bootKey
val verifiedBootHash =
(originalRootOfTrust as? ASN1Sequence)?.let {
// Try to preserve the original boot hash if it exists.
(it.getObjectAt(AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX)
as? ASN1OctetString)
?.octets
} ?: AndroidDeviceUtils.getBootHashFromProperty()
val rootOfTrustElements = arrayOfNulls<ASN1Encodable>(4)
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX] =
DEROctetString(verifiedBootKey)
DEROctetString(AndroidDeviceUtils.bootKey)
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_DEVICE_LOCKED_INDEX] =
ASN1Boolean.TRUE // deviceLocked: true, for security
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_STATE_INDEX] =
ASN1Enumerated(0) // verifiedBootState: Verified
rootOfTrustElements[AttestationConstants.ROOT_OF_TRUST_VERIFIED_BOOT_HASH_INDEX] =
DEROctetString(verifiedBootHash)
DEROctetString(AndroidDeviceUtils.bootHash)
return DERSequence(rootOfTrustElements)
}
@@ -38,12 +38,14 @@ object DeviceAttestationService {
* Holds key data extracted from a genuine device attestation. This data can be used as a
* baseline for creating simulated attestations.
*
* @property verifiedBootKey The verified boot public key digest from the root of trust.
* @property verifiedBootHash The verified boot hash from the root of trust.
* @property attestVersion The attestation version (e.g., 400 for KeyMint 4.0).
* @property keymasterVersion The Keymaster or KeyMint HAL version.
* @property osVersion The Android OS version integer.
*/
data class AttestationData(
val verifiedBootKey: ByteArray?,
val verifiedBootHash: ByteArray?,
val attestVersion: Int?,
val keymasterVersion: Int?,
@@ -166,6 +168,7 @@ object DeviceAttestationService {
.positiveValue
.toInt()
var verifiedBootKey: ByteArray? = null
var verifiedBootHash: ByteArray? = null
var osVersion: Int? = null
@@ -179,6 +182,14 @@ object DeviceAttestationService {
AttestationConstants.TAG_ROOT_OF_TRUST -> {
val rotSeq = ASN1Sequence.getInstance(tagged.baseObject.toASN1Primitive())
if (rotSeq.size() >= 4) {
verifiedBootKey =
ASN1OctetString.getInstance(
rotSeq.getObjectAt(
AttestationConstants
.ROOT_OF_TRUST_VERIFIED_BOOT_KEY_INDEX
)
)
.octets
verifiedBootHash =
ASN1OctetString.getInstance(
rotSeq.getObjectAt(
@@ -199,9 +210,15 @@ object DeviceAttestationService {
}
SystemLogger.info(
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, bootHash=${verifiedBootHash?.toHex()}"
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, bootKey=${verifiedBootKey?.toHex()}, bootHash=${verifiedBootHash?.toHex()}"
)
return AttestationData(
verifiedBootKey,
verifiedBootHash,
attestVersion,
keymasterVersion,
osVersion,
)
return AttestationData(verifiedBootHash, attestVersion, keymasterVersion, osVersion)
} catch (e: Exception) {
SystemLogger.error("Failed to parse attestation data from certificate.", e)
return null
@@ -18,84 +18,133 @@ import org.matrix.TEESimulator.logging.SystemLogger
*/
object AndroidDeviceUtils {
/** A randomly generated boot key, used as a fallback for attestation. */
val bootKey: ByteArray by lazy { generateRandomBytes(32) }
// --- Boot Key and Verified Boot Hash ---
/**
* Initializes the verified boot hash (`ro.boot.vbmeta.digest`). It attempts to read from system
* properties first, then from a real TEE attestation, and finally falls back to a random value
* if neither is available.
* Lazily initializes and retrieves the verified boot key digest. The value is sourced in the
* following order:
* 1. From the `ro.boot.vbmeta.public_key_digest` system property.
* 2. From a cached TEE attestation record.
* 3. As a randomly generated 32-byte value (fallback).
*/
fun setupBootHash() {
getBootHashFromProperty()?.also {
SystemLogger.debug("Using boot hash from system property: ${it.toHex()}")
val bootKey: ByteArray by lazy {
initializeBootProperty(
propertyName = "ro.boot.vbmeta.public_key_digest",
attestationValueProvider = {
DeviceAttestationService.CachedAttestationData?.verifiedBootKey
},
expectedSize = 32,
)
}
?: getBootHashFromAttestation()?.also {
SystemLogger.debug("Using boot hash from TEE attestation: ${it.toHex()}")
setBootHashProperty(it)
/**
* Lazily initializes and retrieves the verified boot hash (vbmeta digest). The value is sourced
* in the following order:
* 1. From the `ro.boot.vbmeta.digest` system property.
* 2. From a cached TEE attestation record.
* 3. As a randomly generated 32-byte value (fallback).
*/
val bootHash: ByteArray by lazy {
initializeBootProperty(
propertyName = "ro.boot.vbmeta.digest",
attestationValueProvider = {
DeviceAttestationService.CachedAttestationData?.verifiedBootHash
},
expectedSize = 32,
)
}
?: generateRandomBytes(32).also {
SystemLogger.debug("Using randomly generated boot hash: ${it.toHex()}")
setBootHashProperty(it)
/**
* Public function to explicitly trigger the initialization of the boot key and hash. Accessing
* these properties here ensures they are set up before they might be needed elsewhere.
*/
fun setupBootKeyAndHash() {
SystemLogger.debug("Triggering initialization of boot key and hash...")
// Accessing the properties will trigger their `lazy` initialization logic.
bootKey
bootHash
SystemLogger.debug("Boot key and hash initialization complete.")
}
/**
* Generic initializer for boot properties like the key and hash. It attempts to read from a
* system property first, then from a TEE attestation, and finally falls back to a random value
* if neither is available.
*
* @param propertyName The name of the system property (e.g., "ro.boot.vbmeta.digest").
* @param attestationValueProvider A function that supplies the value from a cached attestation.
* @param expectedSize The expected length of the byte array (e.g., 32 for a SHA-256 digest).
* @return The resulting byte array for the property.
*/
private fun initializeBootProperty(
propertyName: String,
attestationValueProvider: () -> ByteArray?,
expectedSize: Int,
): ByteArray {
// 1. Attempt to get the value from the system property.
getProperty(propertyName, expectedSize)?.let {
SystemLogger.debug("Using $propertyName from system property: ${it.toHex()}")
return it
}
// 2. Fallback to the value from a cached TEE attestation.
try {
attestationValueProvider()?.let {
SystemLogger.debug("Using $propertyName from TEE attestation: ${it.toHex()}")
setProperty(propertyName, it) // Persist for consistency
return it
}
} catch (e: Exception) {
SystemLogger.error("Failed to get $propertyName from attestation.", e)
}
// 3. As a final fallback, generate a random value.
return generateRandomBytes(expectedSize).also {
SystemLogger.debug("Using randomly generated $propertyName: ${it.toHex()}")
setProperty(propertyName, it)
}
}
/**
* Retrieves the verified boot meta digest from system properties.
* Retrieves a system property and validates its format.
*
* @return The boot hash as a ByteArray, or null if not found or invalid.
* @param name The name of the system property.
* @param expectedSize The expected byte length of the property (e.g., 32 for a 64-char hex
* string).
* @return The property value as a ByteArray, or null if not found or invalid.
*/
@OptIn(ExperimentalStdlibApi::class)
fun getBootHashFromProperty(): ByteArray? {
val digest = SystemProperties.get("ro.boot.vbmeta.digest", null)
if (digest.isNullOrBlank()) {
private fun getProperty(name: String, expectedSize: Int): ByteArray? {
val value = SystemProperties.get(name, null)
if (value.isNullOrBlank()) {
return null
}
// A valid digest is 64 hex characters (32 bytes).
return if (digest.length == 64) digest.hexToByteArray() else null
// A valid digest is (2 * size) hex characters.
return if (value.length == expectedSize * 2) value.hexToByteArray() else null
}
/**
* Retrieves the verified boot hash from a cached TEE attestation record.
* Sets a system property using the `resetprop` command.
*
* @return The verified boot hash, or null if not available.
* @param name The name of the property to set.
* @param bytes The value to set, which will be converted to a hex string.
*/
private fun getBootHashFromAttestation(): ByteArray? {
return try {
DeviceAttestationService.CachedAttestationData?.verifiedBootHash
} catch (e: Exception) {
SystemLogger.error("Failed to get boot hash from attestation.", e)
null
}
}
/**
* Sets the `ro.boot.vbmeta.digest` system property using the `resetprop` command.
*
* @param bytes The 32-byte digest to set.
*/
private fun setBootHashProperty(bytes: ByteArray) {
private fun setProperty(name: String, bytes: ByteArray) {
val hex = bytes.toHex()
try {
SystemLogger.debug("Setting system property 'ro.boot.vbmeta.digest' to: $hex")
// Construct the command to be executed
val command = arrayOf("resetprop", "ro.boot.vbmeta.digest", hex)
// Execute the command
SystemLogger.debug("Setting system property '$name' to: $hex")
val command = arrayOf("resetprop", name, hex)
val process = Runtime.getRuntime().exec(command)
// Wait for the process to complete and check the exit code for errors
val exitCode = process.waitFor()
if (exitCode != 0) {
val errorOutput = process.errorStream.bufferedReader().readText()
SystemLogger.error(
"resetprop command failed with exit code $exitCode: $errorOutput"
"resetprop for '$name' failed with exit code $exitCode: $errorOutput"
)
}
} catch (e: Exception) {
SystemLogger.error("Failed to set vbmeta digest property by executing resetprop.", e)
SystemLogger.error("Failed to set '$name' property via resetprop.", e)
}
}