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 ---