From a811919d0cfe31bbe485d191d080a0d0493f92ea Mon Sep 17 00:00:00 2001 From: Enginex0 Date: Sat, 11 Jul 2026 12:08:00 +0100 Subject: [PATCH] fix(attestation): align KeyMint version to VINTF getAttestVersion/getKeymasterVersion now read the device VINTF manifest and derive attestationVersion=keymasterVersion=aidl*100 (KeyMint) or the legacy HIDL Keymaster pair, so the forged attestation certificate's version matches the device's declared IKeyMintDevice interface. This removes the version MISMATCH that Duck Detector flags (attested 400 vs declared 300). Falls back to the existing cache/SDK-map/400 chain when VINTF is absent or unreadable. Refs #40 --- .../TEESimulator/util/AndroidDeviceUtils.kt | 224 +++++++++++++++++- 1 file changed, 218 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt b/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt index 121736f..5b4f149 100644 --- a/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt +++ b/app/src/main/java/org/matrix/TEESimulator/util/AndroidDeviceUtils.kt @@ -8,6 +8,7 @@ import java.io.FileInputStream import java.security.MessageDigest import java.time.LocalDate import java.util.concurrent.ThreadLocalRandom +import javax.xml.parsers.DocumentBuilderFactory import org.bouncycastle.asn1.ASN1EncodableVector import org.bouncycastle.asn1.ASN1Integer import org.bouncycastle.asn1.DEROctetString @@ -15,6 +16,7 @@ import org.bouncycastle.asn1.DERSequence import org.matrix.TEESimulator.attestation.DeviceAttestationService import org.matrix.TEESimulator.config.ConfigurationManager import org.matrix.TEESimulator.logging.SystemLogger +import org.w3c.dom.Element /** * Provides utility functions for accessing Android system properties and device-specific @@ -433,15 +435,22 @@ object AndroidDeviceUtils { get() = attestVersionMap[Build.VERSION.SDK_INT] /** - * Retrieves the attestation version for the given security level. The value follows the device - * OS: cached attestation data wins, then attestVersionMap[SDK_INT], then 400 as last resort. A - * static StrongBox=300 floor would force a major-version mismatch with the TEE chain on Android - * 16 devices that report keymaster 400 across both security levels. + * Retrieves the attestation version for the given security level. A readable KeyMint VINTF + * declaration wins first, so local probes that compare the attested version against the + * device's manifest see a coherent pair. Otherwise the legacy chain applies: cached attestation + * data, then attestVersionMap[SDK_INT], then 400 as last resort. * * @param securityLevel The security level of the attestation (1 for TEE, 2 for StrongBox). * @return The appropriate attestation version number. */ fun getAttestVersion(securityLevel: Int): Int { + vintfKeyMintVersion?.let { version -> + SystemLogger.debug( + "attestVersion=${version.attestationVersion} source=vintf securityLevel=$securityLevel" + ) + return version.attestationVersion + } + val cached = DeviceAttestationService.CachedAttestationData?.attestVersion val version = cached ?: attestVersionMap[Build.VERSION.SDK_INT] ?: 400 // Default to a recent version @@ -456,12 +465,215 @@ object AndroidDeviceUtils { } /** - * Retrieves the Keymaster/KeyMint version based on the attestation version. + * Retrieves the Keymaster/KeyMint version. A readable KeyMint VINTF declaration is + * authoritative because recent local detectors compare the attested version directly against + * the manifest declaration. * * @param securityLevel The security level, used to determine the correct attestation version. * @return The appropriate Keymaster or KeyMint version number. */ - fun getKeymasterVersion(securityLevel: Int): Int = getAttestVersion(securityLevel) + fun getKeymasterVersion(securityLevel: Int): Int { + vintfKeyMintVersion?.let { version -> + SystemLogger.debug( + "keymasterVersion=${version.keymasterVersion} source=vintf securityLevel=$securityLevel" + ) + return version.keymasterVersion + } + return getAttestVersion(securityLevel) + } + + /** + * KeyMint/Keymaster version pair resolved from a device VINTF manifest. [attestationVersion] is + * the value written into the attestation record; [keymasterVersion] is the HAL version field. + * They coincide for AIDL KeyMint and diverge only for legacy HIDL Keymaster. + */ + private data class VintfKeyMintVersion( + val attestationVersion: Int, + val keymasterVersion: Int, + val sourcePath: String, + ) + + /** + * KeyMint version derived from the device's VINTF manifests, or null when none is readable. + * Resolved lazily so the manifest scan happens once, off the attestation hot path. + */ + private val vintfKeyMintVersion: VintfKeyMintVersion? by lazy { + readVintfKeyMintVersion().also { version -> + if (version != null) { + SystemLogger.info( + "Using KeyMint version from VINTF: attestation=${version.attestationVersion}, " + + "keymaster=${version.keymasterVersion}, source=${version.sourcePath}" + ) + } else { + SystemLogger.debug( + "No usable KeyMint VINTF declaration found; using attestation fallback" + ) + } + } + } + + private fun readVintfKeyMintVersion(): VintfKeyMintVersion? { + val files = linkedMapOf() + + VINTF_MANIFEST_DIRS.forEach { path -> + val dir = File(path) + if (!dir.exists() || !dir.isDirectory) return@forEach + val listed = + runCatching { + dir.listFiles { file -> + file.isFile && file.name.endsWith(".xml", ignoreCase = true) + } + } + .getOrElse { throwable -> + SystemLogger.debug("Unable to list VINTF dir $path: ${throwable.message}") + null + } + listed?.forEach { file -> files[file.absolutePath] = file } + } + + VINTF_MANIFEST_FILES.forEach { path -> + val file = File(path) + if (file.exists() && file.isFile) { + files[file.absolutePath] = file + } + } + + return files.values + .flatMap { file -> + runCatching { parseKeyMintVersions(file) } + .getOrElse { throwable -> + SystemLogger.debug( + "Unable to parse KeyMint VINTF ${file.absolutePath}: ${throwable.message}" + ) + emptyList() + } + } + .maxByOrNull { it.attestationVersion } + } + + private fun parseKeyMintVersions(file: File): List { + val document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file) + val root = document.documentElement ?: return emptyList() + + return directChildElements(root, "hal").flatMap { hal -> + val halName = directChildTexts(hal, "name").firstOrNull().orEmpty() + val versions = directChildTexts(hal, "version") + val fqnames = directChildTexts(hal, "fqname") + val interfaces = + directChildElements(hal, "interface").associate { interfaceElement -> + val name = directChildTexts(interfaceElement, "name").firstOrNull().orEmpty() + val instances = directChildTexts(interfaceElement, "instance").toSet() + name to instances + } + + when (halName) { + KEYMINT_HAL_NAME -> + if (hasDefaultInstance(fqnames, interfaces, KEYMINT_INTERFACE_NAME)) { + versions.mapNotNull { version -> + version + .toIntOrNull() + ?.takeIf { it > 0 } + ?.let { aidlVersion -> + val attestationVersion = aidlVersion * 100 + VintfKeyMintVersion( + attestationVersion = attestationVersion, + keymasterVersion = attestationVersion, + sourcePath = file.absolutePath, + ) + } + } + } else { + emptyList() + } + KEYMASTER_HAL_NAME -> + if (hasDefaultInstance(fqnames, interfaces, KEYMASTER_INTERFACE_NAME)) { + (versions.flatMap(::expandHidlVersions) + + fqnames.mapNotNull(::versionFromFqname)) + .distinct() + .mapNotNull { version -> + expectedLegacyVersions(version)?.let { expected -> + VintfKeyMintVersion( + attestationVersion = expected.second, + keymasterVersion = expected.first, + sourcePath = file.absolutePath, + ) + } + } + } else { + emptyList() + } + else -> emptyList() + } + } + } + + private fun directChildElements(parent: Element, tagName: String): List = buildList { + val children = parent.childNodes + for (index in 0 until children.length) { + val child = children.item(index) + if (child is Element && child.tagName == tagName) { + add(child) + } + } + } + + private fun directChildTexts(parent: Element, tagName: String): List = + directChildElements(parent, tagName) + .map { it.textContent.trim() } + .filter { it.isNotEmpty() } + + private fun hasDefaultInstance( + fqnames: List, + interfaces: Map>, + interfaceName: String, + ): Boolean = + fqnames.any { fqname -> + fqname.substringAfter("::", fqname).substringBefore("/") == interfaceName && + fqname.substringAfter("/", "") == DEFAULT_INSTANCE + } || interfaces[interfaceName]?.contains(DEFAULT_INSTANCE) == true + + private fun versionFromFqname(fqname: String): String? = + FQNAME_VERSION_REGEX.find(fqname)?.groupValues?.getOrNull(1) + + private fun expectedLegacyVersions(version: String): Pair? = + when (version) { + "3.0" -> 3 to 2 + "4.0" -> 4 to 3 + "4.1" -> 41 to 4 + else -> null + } + + private fun expandHidlVersions(version: String): List { + val range = HIDL_VERSION_RANGE_REGEX.matchEntire(version) ?: return listOf(version) + val major = range.groupValues[1] + val firstMinor = range.groupValues[2].toInt() + val lastMinor = range.groupValues[3].toInt() + return (firstMinor..lastMinor).map { minor -> "$major.$minor" } + } + + private val VINTF_MANIFEST_DIRS = + listOf( + "/system/etc/vintf/manifest", + "/system_ext/etc/vintf/manifest", + "/product/etc/vintf/manifest", + "/vendor/etc/vintf/manifest", + "/odm/etc/vintf/manifest", + ) + private val VINTF_MANIFEST_FILES = + listOf( + "/system/etc/vintf/manifest.xml", + "/system_ext/etc/vintf/manifest.xml", + "/product/etc/vintf/manifest.xml", + "/vendor/etc/vintf/manifest.xml", + "/odm/etc/vintf/manifest.xml", + ) + private const val KEYMINT_HAL_NAME = "android.hardware.security.keymint" + private const val KEYMASTER_HAL_NAME = "android.hardware.keymaster" + private const val KEYMINT_INTERFACE_NAME = "IKeyMintDevice" + private const val KEYMASTER_INTERFACE_NAME = "IKeymasterDevice" + private const val DEFAULT_INSTANCE = "default" + private val FQNAME_VERSION_REGEX = Regex("^@([0-9]+(?:\\.[0-9]+)?)::") + private val HIDL_VERSION_RANGE_REGEX = Regex("^([0-9]+)\\.([0-9]+)-([0-9]+)$") // --- APEX and Module Hash Properties ---