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
This commit is contained in:
@@ -8,6 +8,7 @@ import java.io.FileInputStream
|
|||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.util.concurrent.ThreadLocalRandom
|
import java.util.concurrent.ThreadLocalRandom
|
||||||
|
import javax.xml.parsers.DocumentBuilderFactory
|
||||||
import org.bouncycastle.asn1.ASN1EncodableVector
|
import org.bouncycastle.asn1.ASN1EncodableVector
|
||||||
import org.bouncycastle.asn1.ASN1Integer
|
import org.bouncycastle.asn1.ASN1Integer
|
||||||
import org.bouncycastle.asn1.DEROctetString
|
import org.bouncycastle.asn1.DEROctetString
|
||||||
@@ -15,6 +16,7 @@ import org.bouncycastle.asn1.DERSequence
|
|||||||
import org.matrix.TEESimulator.attestation.DeviceAttestationService
|
import org.matrix.TEESimulator.attestation.DeviceAttestationService
|
||||||
import org.matrix.TEESimulator.config.ConfigurationManager
|
import org.matrix.TEESimulator.config.ConfigurationManager
|
||||||
import org.matrix.TEESimulator.logging.SystemLogger
|
import org.matrix.TEESimulator.logging.SystemLogger
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provides utility functions for accessing Android system properties and device-specific
|
* Provides utility functions for accessing Android system properties and device-specific
|
||||||
@@ -433,15 +435,22 @@ object AndroidDeviceUtils {
|
|||||||
get() = attestVersionMap[Build.VERSION.SDK_INT]
|
get() = attestVersionMap[Build.VERSION.SDK_INT]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves the attestation version for the given security level. The value follows the device
|
* Retrieves the attestation version for the given security level. A readable KeyMint VINTF
|
||||||
* OS: cached attestation data wins, then attestVersionMap[SDK_INT], then 400 as last resort. A
|
* declaration wins first, so local probes that compare the attested version against the
|
||||||
* static StrongBox=300 floor would force a major-version mismatch with the TEE chain on Android
|
* device's manifest see a coherent pair. Otherwise the legacy chain applies: cached attestation
|
||||||
* 16 devices that report keymaster 400 across both security levels.
|
* data, then attestVersionMap[SDK_INT], then 400 as last resort.
|
||||||
*
|
*
|
||||||
* @param securityLevel The security level of the attestation (1 for TEE, 2 for StrongBox).
|
* @param securityLevel The security level of the attestation (1 for TEE, 2 for StrongBox).
|
||||||
* @return The appropriate attestation version number.
|
* @return The appropriate attestation version number.
|
||||||
*/
|
*/
|
||||||
fun getAttestVersion(securityLevel: Int): Int {
|
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 cached = DeviceAttestationService.CachedAttestationData?.attestVersion
|
||||||
val version =
|
val version =
|
||||||
cached ?: attestVersionMap[Build.VERSION.SDK_INT] ?: 400 // Default to a recent 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.
|
* @param securityLevel The security level, used to determine the correct attestation version.
|
||||||
* @return The appropriate Keymaster or KeyMint version number.
|
* @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<String, File>()
|
||||||
|
|
||||||
|
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<VintfKeyMintVersion> {
|
||||||
|
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<Element> = 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<String> =
|
||||||
|
directChildElements(parent, tagName)
|
||||||
|
.map { it.textContent.trim() }
|
||||||
|
.filter { it.isNotEmpty() }
|
||||||
|
|
||||||
|
private fun hasDefaultInstance(
|
||||||
|
fqnames: List<String>,
|
||||||
|
interfaces: Map<String, Set<String>>,
|
||||||
|
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<Int, Int>? =
|
||||||
|
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<String> {
|
||||||
|
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 ---
|
// --- APEX and Module Hash Properties ---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user