Fix value and location of moduleHash (#35)

`moduleHash` should be in the software enforced list.
However, the manual calculation of the KeyMint `moduleHash` has
failed to produce a value matching the hardware-generated attestation.

The official documentation specifies the following structure:
  Modules ::= SET OF Module
  Module ::= SEQUENCE {
      packageName       OCTET_STRING,
      version                    INTEGER,
  }
The critical requirement is that the `SET OF` elements must be sorted
lexicographically based on their full DER-encoded byte value. Despite
implementing this using Bouncy Castle's `DERSet`, the resulting hash
is still incorrect.

This commit changes the strategy to favor stability:
1.  The `DeviceAttestationService` now extracts the real `moduleHash`
    from the `softwareEnforced` list of a genuine attestation certificate
    and caches it.
2.  The `moduleHash` property now returns this cached value if available.
3.  The manual calculation remains as a fallback and is marked with a
    `TODO` to indicate the issue is unresolved.

Additionally, `ConfigurationManager` initialization is moved earlier.
This commit is contained in:
JingMatrix
2025-11-30 00:13:14 +01:00
committed by GitHub
parent 2446461aab
commit 987c7ba35c
5 changed files with 72 additions and 34 deletions
@@ -28,6 +28,8 @@ object App {
SystemLogger.info("Welcome to TEESimulator!")
try {
// Load the package configuration.
ConfigurationManager.initialize()
// Set up the device's boot key and hash, which are crucial for attestation.
AndroidDeviceUtils.setupBootKeyAndHash()
// Initialize and start the appropriate keystore interceptors.
@@ -53,9 +55,7 @@ object App {
Thread.sleep(RETRY_DELAY_MS)
}
// Load the package configuration after interceptors are ready.
ConfigurationManager.initialize()
SystemLogger.info("Interceptors and configuration initialized successfully.")
SystemLogger.info("Interceptors initialized successfully.")
}
/**
@@ -16,6 +16,7 @@ import org.bouncycastle.asn1.DERSet
import org.bouncycastle.asn1.DERTaggedObject
import org.bouncycastle.asn1.x509.Extension
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger
import org.matrix.TEESimulator.util.AndroidDeviceUtils
/**
@@ -38,6 +39,11 @@ object AttestationBuilder {
securityLevel: Int,
): Extension {
val keyDescription = buildKeyDescription(params, uid, securityLevel)
var formattedString =
keyDescription.joinToString(separator = ", ") {
AttestationPatcher.formatAsn1Primitive(it)
}
SystemLogger.verbose("Forged attestation data: ${formattedString}")
return Extension(ATTESTATION_OID, false, DEROctetString(keyDescription.encoded))
}
@@ -260,17 +266,6 @@ object AttestationBuilder {
)
}
}
if (AndroidDeviceUtils.attestVersion >= 400) {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_MODULE_HASH,
DEROctetString(AndroidDeviceUtils.moduleHash),
)
)
}
return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray())
}
@@ -280,7 +275,7 @@ object AttestationBuilder {
*/
private fun buildSoftwareEnforcedList(uid: Int): DERSequence {
val list =
arrayOf<ASN1Encodable>(
mutableListOf<ASN1Encodable>(
DERTaggedObject(
true,
AttestationConstants.TAG_CREATION_DATETIME,
@@ -292,7 +287,16 @@ object AttestationBuilder {
createApplicationId(uid),
),
)
return DERSequence(list)
if (AndroidDeviceUtils.attestVersion >= 400) {
list.add(
DERTaggedObject(
true,
AttestationConstants.TAG_MODULE_HASH,
DEROctetString(AndroidDeviceUtils.moduleHash),
)
)
}
return DERSequence(list.toTypedArray())
}
/**
@@ -137,7 +137,7 @@ object AttestationPatcher {
}
/** Recursively formats an ASN1Primitive into a concise, readable string. */
private fun formatAsn1Primitive(obj: ASN1Encodable?): String {
fun formatAsn1Primitive(obj: ASN1Encodable?): String {
val primitive = obj?.toASN1Primitive()
return when (primitive) {
null -> "NULL"
@@ -45,6 +45,7 @@ object DeviceAttestationService {
* @property osVersion The Android OS version integer.
*/
data class AttestationData(
val moduleHash: ByteArray?,
val verifiedBootKey: ByteArray?,
val verifiedBootHash: ByteArray?,
val attestVersion: Int?,
@@ -153,6 +154,11 @@ object DeviceAttestationService {
// The extension's value is an ASN.1 sequence.
val keyDescriptionSeq = ASN1Sequence.getInstance(extension.extnValue.octets)
var formattedString =
keyDescriptionSeq.joinToString(separator = ", ") {
AttestationPatcher.formatAsn1Primitive(it)
}
SystemLogger.verbose("Cached attestation data: ${formattedString}")
val fields = keyDescriptionSeq.toArray()
val attestVersion =
@@ -168,10 +174,23 @@ object DeviceAttestationService {
.positiveValue
.toInt()
var moduleHash: ByteArray? = null
var verifiedBootKey: ByteArray? = null
var verifiedBootHash: ByteArray? = null
var osVersion: Int? = null
val softwareEnforced =
ASN1Sequence.getInstance(
fields[AttestationConstants.KEY_DESCRIPTION_SOFTWARE_ENFORCED_INDEX]
)
if (softwareEnforced.size() >= 3) {
moduleHash =
ASN1OctetString.getInstance(
ASN1TaggedObject.getInstance(softwareEnforced.getObjectAt(2)).baseObject
)
.octets
}
val teeEnforced =
ASN1Sequence.getInstance(
fields[AttestationConstants.KEY_DESCRIPTION_TEE_ENFORCED_INDEX]
@@ -210,9 +229,10 @@ object DeviceAttestationService {
}
SystemLogger.info(
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, bootKey=${verifiedBootKey?.toHex()}, bootHash=${verifiedBootHash?.toHex()}"
"Successfully extracted attestation data: version=$attestVersion, osVersion=$osVersion, moduleHash=${moduleHash?.toHex()}, bootKey=${verifiedBootKey?.toHex()}, bootHash=${verifiedBootHash?.toHex()}"
)
return AttestationData(
moduleHash,
verifiedBootKey,
verifiedBootHash,
attestVersion,
@@ -5,9 +5,11 @@ import android.os.Build
import android.os.SystemProperties
import java.security.MessageDigest
import java.util.concurrent.ThreadLocalRandom
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1Integer
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.DERSet
import org.matrix.TEESimulator.attestation.DeviceAttestationService
import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.logging.SystemLogger
@@ -278,11 +280,7 @@ object AndroidDeviceUtils {
@Suppress("DEPRECATION")
pm?.getInstalledPackages(PackageManager.MATCH_APEX, 0)
}
packages
?.list
.orEmpty()
.map { it.packageName to it.longVersionCode }
.sortedBy { it.first }
packages?.list.orEmpty().map { it.packageName to it.longVersionCode }
}
.getOrElse {
SystemLogger.error("Failed to get APEX package information.", it)
@@ -291,13 +289,29 @@ object AndroidDeviceUtils {
}
val moduleHash: ByteArray by lazy {
runCatching {
val encodables =
apexInfos.flatMap { (packageName, versionCode) ->
listOf(DEROctetString(packageName.toByteArray()), ASN1Integer(versionCode))
DeviceAttestationService.CachedAttestationData?.moduleHash
?: runCatching {
// TODO: figure out the correct calculation
val moduleSequences = ASN1EncodableVector()
// 1. Create a DERSequence for each module.
apexInfos.forEach { (packageName, versionCode) ->
val moduleVector = ASN1EncodableVector()
// Use explicit UTF-8 encoding for the package name.
moduleVector.add(DEROctetString(packageName.toByteArray(Charsets.UTF_8)))
moduleVector.add(ASN1Integer(versionCode))
moduleSequences.add(DERSequence(moduleVector))
}
val sequence = DERSequence(encodables.toTypedArray())
MessageDigest.getInstance("SHA-256").digest(sequence.encoded)
// 2. Create a DERSet. Bouncy Castle will automatically handle
// the sorting based on the DER-encoded value of each sequence.
val modulesSet = DERSet(moduleSequences)
// 3. Get the final DER-encoded byte array of the SET.
val encodedModules = modulesSet.encoded
// 4. Compute the SHA-256 hash.
MessageDigest.getInstance("SHA-256").digest(encodedModules)
}
.getOrElse {
SystemLogger.error("Failed to compute module hash.", it)