From f06cb30b40eadbaf8e84296d2861da096f06d1c0 Mon Sep 17 00:00:00 2001 From: Enginex0 Date: Thu, 19 Mar 2026 07:32:53 +0100 Subject: [PATCH] feat(attestation): align attestation extension and cert generation with AOSP KeyMintAttestation now carries all 17 enforcement tags that AOSP's authorize_create and buildKeyDescription paths expect. AttestationBuilder populates BLOCK_MODE as SET OF INTEGER, gates version-guarded tags (RSA_OAEP_MGF_DIGEST >=100, ROLLBACK_RESISTANCE >=3, EARLY_BOOT_ONLY >=4), computes INCLUDE_UNIQUE_ID via HMAC-SHA256 per KeyMint HAL spec, and gates AAID on challenge presence. CertificateGenerator uses AOSP cert validity defaults (epoch notBefore, 9999-12-31 notAfter), returns ServiceSpecificException(-75) for missing keybox, and adds RSA exponent null safety. --- .../attestation/AttestationBuilder.kt | 221 ++++++++++++++---- .../attestation/AttestationConstants.kt | 6 + .../attestation/KeyMintAttestation.kt | 52 ++++- .../TEESimulator/pki/CertificateGenerator.kt | 47 ++-- 4 files changed, 258 insertions(+), 68 deletions(-) diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt index 166790b..b1b5112 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt @@ -2,8 +2,11 @@ package org.matrix.TEESimulator.attestation import android.content.pm.PackageManager import android.os.Build +import java.nio.ByteBuffer import java.nio.charset.StandardCharsets import java.security.MessageDigest +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec import org.bouncycastle.asn1.ASN1Boolean import org.bouncycastle.asn1.ASN1Encodable import org.bouncycastle.asn1.ASN1Enumerated @@ -127,33 +130,59 @@ object AttestationBuilder { return properties } - /** Constructs the main `KeyDescription` sequence, which is the core of the attestation. */ private fun buildKeyDescription( params: KeyMintAttestation, uid: Int, securityLevel: Int, ): ASN1Sequence { + val creationTime = System.currentTimeMillis() val teeEnforced = buildTeeEnforcedList(params, uid, securityLevel) - val softwareEnforced = buildSoftwareEnforcedList(uid, securityLevel) + val softwareEnforced = buildSoftwareEnforcedList(params, uid, securityLevel, creationTime) + + val uniqueId = + if (params.includeUniqueId == true && params.attestationChallenge != null) { + computeUniqueId(creationTime, createApplicationId(uid).octets) + } else { + ByteArray(0) + } val fields = arrayOf( - ASN1Integer( - AndroidDeviceUtils.getAttestVersion(securityLevel).toLong() - ), // attestationVersion - ASN1Enumerated(securityLevel), // attestationSecurityLevel - ASN1Integer( - AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong() - ), // keymasterVersion - ASN1Enumerated(securityLevel), // keymasterSecurityLevel - DEROctetString(params.attestationChallenge ?: ByteArray(0)), // attestationChallenge - DEROctetString(ByteArray(0)), // uniqueId + ASN1Integer(AndroidDeviceUtils.getAttestVersion(securityLevel).toLong()), + ASN1Enumerated(securityLevel), + ASN1Integer(AndroidDeviceUtils.getKeymasterVersion(securityLevel).toLong()), + ASN1Enumerated(securityLevel), + DEROctetString(params.attestationChallenge ?: ByteArray(0)), + DEROctetString(uniqueId), softwareEnforced, teeEnforced, ) return DERSequence(fields) } + private fun computeUniqueId(creationTimeMs: Long, aaidDer: ByteArray): ByteArray { + val temporalCounter = creationTimeMs / 2592000000L + val message = + ByteBuffer.allocate(8 + aaidDer.size + 1) + .putLong(temporalCounter) + .put(aaidDer) + .put(0x00) + .array() + val mac = Mac.getInstance("HmacSHA256") + mac.init(SecretKeySpec(hbk, "HmacSHA256")) + return mac.doFinal(message).copyOf(16) + } + + private val hbk: ByteArray by lazy { + val file = java.io.File(ConfigurationManager.CONFIG_PATH, "hbk") + if (file.exists() && file.length() == 32L) { + file.readBytes() + } else { + SystemLogger.warning("hbk not found, generating ephemeral HBK.") + ByteArray(32).also { java.security.SecureRandom().nextBytes(it) } + } + } + /** Builds the `TeeEnforced` authorization list. These are properties the TEE "guarantees". */ private fun buildTeeEnforcedList( params: KeyMintAttestation, @@ -194,6 +223,16 @@ object AttestationBuilder { ) } + if (params.blockMode.isNotEmpty()) { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_BLOCK_MODE, + DERSet(params.blockMode.map { ASN1Integer(it.toLong()) }.toTypedArray()), + ) + ) + } + if (params.padding.isNotEmpty()) { list.add( DERTaggedObject( @@ -214,14 +253,61 @@ object AttestationBuilder { ) } + val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel) + + if (params.rsaOaepMgfDigest.isNotEmpty() && attestVersion >= 100) { + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_RSA_OAEP_MGF_DIGEST, + DERSet(params.rsaOaepMgfDigest.map { ASN1Integer(it.toLong()) }.toTypedArray()), + ) + ) + } + + if (params.rollbackResistance == true && attestVersion >= 3) { + list.add( + DERTaggedObject(true, AttestationConstants.TAG_ROLLBACK_RESISTANCE, DERNull.INSTANCE) + ) + } + + if (params.earlyBootOnly == true && attestVersion >= 4) { + list.add( + DERTaggedObject(true, AttestationConstants.TAG_EARLY_BOOT_ONLY, DERNull.INSTANCE) + ) + } + + if (params.noAuthRequired == true) { + list.add( + DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE) + ) + } + + if (params.allowWhileOnBody == true) { + list.add( + DERTaggedObject(true, AttestationConstants.TAG_ALLOW_WHILE_ON_BODY, DERNull.INSTANCE) + ) + } + + if (params.trustedUserPresenceRequired == true && attestVersion >= 3) { + list.add( + DERTaggedObject(true, AttestationConstants.TAG_TRUSTED_USER_PRESENCE_REQUIRED, DERNull.INSTANCE) + ) + } + + if (params.trustedConfirmationRequired == true && attestVersion >= 3) { + list.add( + DERTaggedObject(true, AttestationConstants.TAG_TRUSTED_CONFIRMATION_REQUIRED, DERNull.INSTANCE) + ) + } + list.addAll( listOf( - DERTaggedObject(true, AttestationConstants.TAG_NO_AUTH_REQUIRED, DERNull.INSTANCE), DERTaggedObject( true, AttestationConstants.TAG_ORIGIN, - ASN1Integer(0L), - ), // KeyOrigin.GENERATED + ASN1Integer((params.origin ?: 0).toLong()), + ), DERTaggedObject( true, AttestationConstants.TAG_ROOT_OF_TRUST, @@ -325,20 +411,32 @@ object AttestationBuilder { * Builds the `SoftwareEnforced` authorization list. These are properties guaranteed by * Keystore. */ - private fun buildSoftwareEnforcedList(uid: Int, securityLevel: Int): DERSequence { - val list = - mutableListOf( - DERTaggedObject( - true, - AttestationConstants.TAG_CREATION_DATETIME, - ASN1Integer(System.currentTimeMillis()), - ), + private fun buildSoftwareEnforcedList( + params: KeyMintAttestation, + uid: Int, + securityLevel: Int, + creationTimeMs: Long = System.currentTimeMillis(), + ): DERSequence { + val list = mutableListOf() + + list.add( + DERTaggedObject( + true, + AttestationConstants.TAG_CREATION_DATETIME, + ASN1Integer(creationTimeMs), + ) + ) + + if (params.attestationChallenge != null) { + list.add( DERTaggedObject( true, AttestationConstants.TAG_ATTESTATION_APPLICATION_ID, createApplicationId(uid), - ), + ) ) + } + if (AndroidDeviceUtils.getAttestVersion(securityLevel) >= 400) { list.add( DERTaggedObject( @@ -348,7 +446,34 @@ object AttestationBuilder { ) ) } - return DERSequence(list.toTypedArray()) + + params.activeDateTime?.let { + list.add( + DERTaggedObject(true, AttestationConstants.TAG_ACTIVE_DATETIME, ASN1Integer(it.time)) + ) + } + params.originationExpireDateTime?.let { + list.add( + DERTaggedObject(true, AttestationConstants.TAG_ORIGINATION_EXPIRE_DATETIME, ASN1Integer(it.time)) + ) + } + params.usageExpireDateTime?.let { + list.add( + DERTaggedObject(true, AttestationConstants.TAG_USAGE_EXPIRE_DATETIME, ASN1Integer(it.time)) + ) + } + params.usageCountLimit?.let { + list.add( + DERTaggedObject(true, AttestationConstants.TAG_USAGE_COUNT_LIMIT, ASN1Integer(it.toLong())) + ) + } + if (params.unlockedDeviceRequired == true) { + list.add( + DERTaggedObject(true, AttestationConstants.TAG_UNLOCKED_DEVICE_REQUIRED, DERNull.INSTANCE) + ) + } + + return DERSequence(list.sortedBy { (it as DERTaggedObject).tagNo }.toTypedArray()) } /** @@ -376,6 +501,11 @@ object AttestationBuilder { */ @Throws(Throwable::class) internal fun createApplicationId(uid: Int): DEROctetString { + val appUid = uid % 100000 + if (appUid == 0 || appUid == 1000) { + return buildApplicationIdDer(listOf("AndroidSystem" to 1L), emptySet()) + } + val pm = ConfigurationManager.getPackageManager() ?: throw IllegalStateException("PackageManager not found!") @@ -383,12 +513,11 @@ object AttestationBuilder { pm.getPackagesForUid(uid) ?: throw IllegalStateException("No packages for UID $uid") val sha256 = MessageDigest.getInstance("SHA-256") - val packageInfoList = mutableListOf() + val packageInfoList = mutableListOf>() val signatureDigests = mutableSetOf() - // Process all packages associated with the UID in a single loop. + val userId = uid / 100000 packages.forEach { packageName -> - val userId = uid / 100000 val packageInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { pm.getPackageInfo( @@ -401,34 +530,36 @@ object AttestationBuilder { pm.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES, userId) } - // Add package information (name and version code) to our list. - packageInfoList.add( - DERSequence( - arrayOf( - DEROctetString(packageInfo.packageName.toByteArray(StandardCharsets.UTF_8)), - ASN1Integer(packageInfo.longVersionCode), - ) - ) - ) + packageInfoList.add(packageInfo.packageName to packageInfo.longVersionCode) - // Collect unique signature digests from the signing history. packageInfo.signingInfo?.signingCertificateHistory?.forEach { signature -> - val digest = sha256.digest(signature.toByteArray()) - signatureDigests.add(Digest(digest)) + signatureDigests.add(Digest(sha256.digest(signature.toByteArray()))) } } - // The application ID is a sequence of two sets: - // 1. A set of package information (name and version). - // 2. A set of SHA-256 digests of the signing certificates. + return buildApplicationIdDer(packageInfoList, signatureDigests) + } + + private fun buildApplicationIdDer( + packages: List>, + digests: Set, + ): DEROctetString { + val packageInfoList = + packages.map { (name, version) -> + DERSequence( + arrayOf( + DEROctetString(name.toByteArray(StandardCharsets.UTF_8)), + ASN1Integer(version), + ) + ) + } val applicationIdSequence = DERSequence( arrayOf( DERSet(packageInfoList.toTypedArray()), - DERSet(signatureDigests.map { DEROctetString(it.digest) }.toTypedArray()), + DERSet(digests.map { DEROctetString(it.digest) }.toTypedArray()), ) ) - return DEROctetString(applicationIdSequence.encoded) } } diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationConstants.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationConstants.kt index a899a12..3a56c0b 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationConstants.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationConstants.kt @@ -44,9 +44,11 @@ object AttestationConstants { // --- Key Lifetime and Usage Control --- const val TAG_ROLLBACK_RESISTANCE = 303 + const val TAG_EARLY_BOOT_ONLY = 305 const val TAG_ACTIVE_DATETIME = 400 const val TAG_ORIGINATION_EXPIRE_DATETIME = 401 const val TAG_USAGE_EXPIRE_DATETIME = 402 + const val TAG_MAX_BOOT_LEVEL = 403 const val TAG_MAX_USES_PER_BOOT = 404 const val TAG_USAGE_COUNT_LIMIT = 405 @@ -56,6 +58,10 @@ object AttestationConstants { const val TAG_NO_AUTH_REQUIRED = 503 const val TAG_USER_AUTH_TYPE = 504 const val TAG_AUTH_TIMEOUT = 505 + const val TAG_ALLOW_WHILE_ON_BODY = 506 + const val TAG_TRUSTED_USER_PRESENCE_REQUIRED = 507 + const val TAG_TRUSTED_CONFIRMATION_REQUIRED = 508 + const val TAG_UNLOCKED_DEVICE_REQUIRED = 509 // --- Attestation and Application Info --- const val TAG_APPLICATION_ID = 601 diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt index 6c3c549..0eaf31a 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/KeyMintAttestation.kt @@ -41,13 +41,29 @@ data class KeyMintAttestation( val manufacturer: ByteArray?, val model: ByteArray?, val secondImei: ByteArray?, + val activeDateTime: Date?, + val originationExpireDateTime: Date?, + val usageExpireDateTime: Date?, + val usageCountLimit: Int?, + val callerNonce: Boolean?, + val unlockedDeviceRequired: Boolean?, + val includeUniqueId: Boolean?, + val rollbackResistance: Boolean?, + val earlyBootOnly: Boolean?, + val allowWhileOnBody: Boolean?, + val trustedUserPresenceRequired: Boolean?, + val trustedConfirmationRequired: Boolean?, + val noAuthRequired: Boolean?, + val maxUsesPerBoot: Int?, + val maxBootLevel: Int?, + val minMacLength: Int?, + val rsaOaepMgfDigest: List, ) { /** Secondary constructor that populates the fields by parsing an array of `KeyParameter`. */ constructor( params: Array ) : this( - // AOSP: [key_param(tag = KEY_SIZE, field = Integer)] - keySize = params.findInteger(Tag.KEY_SIZE) ?: 0, + keySize = params.findInteger(Tag.KEY_SIZE) ?: params.deriveKeySizeFromCurve(), // AOSP: [key_param(tag = ALGORITHM, field = Algorithm)] algorithm = params.findAlgorithm(Tag.ALGORITHM) ?: 0, @@ -100,6 +116,23 @@ data class KeyMintAttestation( manufacturer = params.findBlob(Tag.ATTESTATION_ID_MANUFACTURER), model = params.findBlob(Tag.ATTESTATION_ID_MODEL), secondImei = params.findBlob(Tag.ATTESTATION_ID_SECOND_IMEI), + activeDateTime = params.findDate(Tag.ACTIVE_DATETIME), + originationExpireDateTime = params.findDate(Tag.ORIGINATION_EXPIRE_DATETIME), + usageExpireDateTime = params.findDate(Tag.USAGE_EXPIRE_DATETIME), + usageCountLimit = params.findInteger(Tag.USAGE_COUNT_LIMIT), + callerNonce = params.findBoolean(Tag.CALLER_NONCE), + unlockedDeviceRequired = params.findBoolean(Tag.UNLOCKED_DEVICE_REQUIRED), + includeUniqueId = params.findBoolean(Tag.INCLUDE_UNIQUE_ID), + rollbackResistance = params.findBoolean(Tag.ROLLBACK_RESISTANCE), + earlyBootOnly = params.findBoolean(Tag.EARLY_BOOT_ONLY), + allowWhileOnBody = params.findBoolean(Tag.ALLOW_WHILE_ON_BODY), + trustedUserPresenceRequired = params.findBoolean(Tag.TRUSTED_USER_PRESENCE_REQUIRED), + trustedConfirmationRequired = params.findBoolean(Tag.TRUSTED_CONFIRMATION_REQUIRED), + noAuthRequired = params.findBoolean(Tag.NO_AUTH_REQUIRED), + maxUsesPerBoot = params.findInteger(Tag.MAX_USES_PER_BOOT), + maxBootLevel = params.findInteger(Tag.MAX_BOOT_LEVEL), + minMacLength = params.findInteger(Tag.MIN_MAC_LENGTH), + rsaOaepMgfDigest = params.findAllDigests(Tag.RSA_OAEP_MGF_DIGEST), ) { // Log all parsed parameters for debugging purposes. params.forEach { KeyMintParameterLogger.logParameter(it) } @@ -156,6 +189,21 @@ private fun Array.findAllKeyPurpose(tag: Int): List = private fun Array.findAllDigests(tag: Int): List = this.filter { it.tag == tag }.map { it.value.digest } +private fun Array.findBoolean(tag: Int): Boolean? = + if (this.any { it.tag == tag }) true else null + +private fun Array.deriveKeySizeFromCurve(): Int { + val curveId = this.find { it.tag == Tag.EC_CURVE }?.value?.ecCurve ?: return 0 + return when (curveId) { + EcCurve.P_224 -> 224 + EcCurve.P_256 -> 256 + EcCurve.P_384 -> 384 + EcCurve.P_521 -> 521 + EcCurve.CURVE_25519 -> 256 + else -> 0 + } +} + /** * Derives the EC Curve name. Logic: Checks specific EC_CURVE tag first (field=EcCurve), falls back * to KEY_SIZE (field=Integer). diff --git a/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt b/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt index 003a0c5..4b27058 100644 --- a/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt +++ b/app/src/main/java/org/matrix/TEESimulator/pki/CertificateGenerator.kt @@ -8,7 +8,6 @@ import java.math.BigInteger import java.security.KeyPair import java.security.KeyPairGenerator import java.security.cert.Certificate -import java.security.cert.X509Certificate import java.security.spec.ECGenParameterSpec import java.security.spec.RSAKeyGenParameterSpec import java.util.Date @@ -36,6 +35,8 @@ import org.matrix.TEESimulator.logging.SystemLogger */ object CertificateGenerator { + private const val UNDEFINED_NOT_AFTER = 253402300799000L + /** * Generates a software-based cryptographic key pair. * @@ -49,7 +50,10 @@ object CertificateGenerator { Algorithm.EC -> "EC" to ECGenParameterSpec(params.ecCurveName) Algorithm.RSA -> "RSA" to - RSAKeyGenParameterSpec(params.keySize, params.rsaPublicExponent) + RSAKeyGenParameterSpec( + params.keySize, + params.rsaPublicExponent ?: RSAKeyGenParameterSpec.F4, + ) else -> throw IllegalArgumentException( "Unsupported algorithm: ${params.algorithm}" @@ -88,11 +92,9 @@ object CertificateGenerator { "Attestation challenge exceeds length limit (${challenge.size} > ${AttestationConstants.CHALLENGE_LENGTH_LIMIT})" ) - return runCatching { + return try { val keybox = getKeyboxForAlgorithm(uid, params.algorithm) - // Determine the signing key and issuer. If an attestKey is provided, use it. - // Otherwise, fall back to the root key from the keybox. val (signingKey, issuer) = if (attestKeyAlias != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { getAttestationKeyInfo(uid, attestKeyAlias)?.let { it.first to it.second } @@ -101,20 +103,20 @@ object CertificateGenerator { keybox.keyPair to getIssuerFromKeybox(keybox) } - // Build the new leaf certificate with the simulated attestation. val leafCert = buildCertificate(subjectKeyPair, signingKey, issuer, params, uid, securityLevel) - // If not self-attesting, the chain is just the leaf. Otherwise, append the keybox - // chain. if (attestKeyAlias != null) { listOf(leafCert) } else { listOf(leafCert) + keybox.certificates } + } catch (e: android.os.ServiceSpecificException) { + throw e + } catch (e: Exception) { + SystemLogger.error("Failed to generate certificate chain.", e) + null } - .onFailure { SystemLogger.error("Failed to generate certificate chain.", it) } - .getOrNull() } /** @@ -128,7 +130,7 @@ object CertificateGenerator { params: KeyMintAttestation, securityLevel: Int, ): Pair>? { - return runCatching { + return try { SystemLogger.info( "Generating new attested key pair for alias: '$alias' (UID: $uid)" ) @@ -144,11 +146,12 @@ object CertificateGenerator { "Successfully generated new certificate chain for alias: '$alias'." ) Pair(newKeyPair, chain) + } catch (e: android.os.ServiceSpecificException) { + throw e + } catch (e: Exception) { + SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", e) + null } - .onFailure { - SystemLogger.error("Failed to generate attested key pair for alias '$alias'.", it) - } - .getOrNull() } fun getIssuerFromKeybox(keybox: KeyBox) = @@ -163,7 +166,10 @@ object CertificateGenerator { else -> throw IllegalArgumentException("Unsupported algorithm ID: $algorithm") } return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName) - ?: throw Exception("Could not load keybox for UID $uid and algorithm $algorithmName") + ?: throw android.os.ServiceSpecificException( + -75, // ATTESTATION_KEYS_NOT_PROVISIONED + "No attestation key for algorithm $algorithmName in $keyboxFile", + ) } /** Retrieves the key pair and issuer name for a given attestation key alias. */ @@ -214,16 +220,15 @@ object CertificateGenerator { securityLevel: Int, ): Certificate { val subject = params.certificateSubject ?: X500Name("CN=Android Keystore Key") - val leafNotAfter = - (signingKeyPair.public as? X509Certificate)?.notAfter - ?: Date(System.currentTimeMillis() + 31536000000L) + val notBefore = params.certificateNotBefore ?: Date(0) + val notAfter = params.certificateNotAfter ?: Date(UNDEFINED_NOT_AFTER) val builder = JcaX509v3CertificateBuilder( issuer, params.certificateSerial ?: BigInteger.ONE, - params.certificateNotBefore ?: Date(), - params.certificateNotAfter ?: leafNotAfter, + notBefore, + notAfter, subject, subjectKeyPair.public, )