From 38e9b547a57e22f6039f0199ec6cecb8bd802253 Mon Sep 17 00:00:00 2001 From: Enginex0 Date: Fri, 20 Mar 2026 06:23:05 +0100 Subject: [PATCH] feat(certgen): wire NativeCertGen and key persistence onto PR 157 base NativeCertGen serves as a fast path before BouncyCastle in doSoftwareGeneration, falling back gracefully when libcertgen.so is absent. GeneratedKeyPersistence saves software keys to disk asynchronously and restores them on daemon restart. Bump version to v5.1, restore TEESimulator-RS naming convention. --- app/build.gradle.kts | 14 +- .../main/java/org/matrix/TEESimulator/App.kt | 3 + .../attestation/AttestationBuilder.kt | 2 +- .../keystore/Keystore2Interceptor.kt | 2 + .../shim/KeyMintSecurityLevelInterceptor.kt | 202 +++++++++++++++++- 5 files changed, 207 insertions(+), 16 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7b87329..cce73d4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -29,7 +29,7 @@ val gitExecutor = objects.newInstance(GitExecutor::class.java) val gitCommitCount = gitExecutor.execute("git rev-list HEAD --count", rootDir).toInt() val gitCommitHash = gitExecutor.execute("git rev-parse --verify --short HEAD", rootDir) -val verName = "v3.2" +val verName = "v5.1" android { namespace = "org.matrix.TEESimulator" @@ -79,13 +79,13 @@ androidComponents { // --- Define output locations and file names --- // Stage all files in a temporary directory inside 'build' before zipping val tempModuleDir = project.layout.buildDirectory.dir("module/${variant.name}") - val zipFileName = "TEESimulator-$verName-$gitCommitCount-$gitCommitHash-$capitalized.zip" + val zipFileName = "TEESimulator-RS-$verName-$gitCommitCount-$gitCommitHash-$capitalized.zip" // Task 1: Prepare all module files in the temporary build directory. // Using Sync ensures that stale files from previous runs are removed. val prepareModuleFilesTask = tasks.register("prepareModuleFiles${capitalized}") { - group = "TEESimulator Module Packaging" + group = "TEESimulator-RS Module Packaging" description = "Prepares all files for the ${variant.name} module zip." if (isDebug) { @@ -143,7 +143,7 @@ androidComponents { // Task 2: Zip the prepared files from the temporary directory. val zipTask = tasks.register("zip${capitalized}") { - group = "TEESimulator Module Packaging" + group = "TEESimulator-RS Module Packaging" description = "Creates the flashable zip for the ${variant.name} module." dependsOn(prepareModuleFilesTask) @@ -156,7 +156,7 @@ androidComponents { fun createInstallTasks(rootProvider: String, installCli: String) { val pushTask = tasks.register("push${rootProvider}Module${capitalized}") { - group = "TEESimulator Module Installation" + group = "TEESimulator-RS Module Installation" description = "Pushes the ${variant.name} module to the device for $rootProvider." dependsOn(zipTask) @@ -170,7 +170,7 @@ androidComponents { val installTask = tasks.register("install${rootProvider}${capitalized}") { - group = "TEESimulator Module Installation" + group = "TEESimulator-RS Module Installation" description = "Installs the ${variant.name} module via $rootProvider." dependsOn(pushTask) commandLine( @@ -183,7 +183,7 @@ androidComponents { } tasks.register("install${rootProvider}AndReboot${capitalized}") { - group = "TEESimulator Module Installation" + group = "TEESimulator-RS Module Installation" description = "Installs the ${variant.name} module via $rootProvider and reboots." dependsOn(installTask) commandLine("adb", "reboot") diff --git a/app/src/main/java/org/matrix/TEESimulator/App.kt b/app/src/main/java/org/matrix/TEESimulator/App.kt index 0ff5675..1c7f0ce 100644 --- a/app/src/main/java/org/matrix/TEESimulator/App.kt +++ b/app/src/main/java/org/matrix/TEESimulator/App.kt @@ -13,6 +13,7 @@ import org.matrix.TEESimulator.interception.keystore.AbstractKeystoreInterceptor import org.matrix.TEESimulator.interception.keystore.Keystore2Interceptor import org.matrix.TEESimulator.interception.keystore.KeystoreInterceptor import org.matrix.TEESimulator.logging.SystemLogger +import org.matrix.TEESimulator.pki.NativeCertGen import org.matrix.TEESimulator.util.AndroidDeviceUtils /** @@ -49,6 +50,8 @@ object App { Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) Security.addProvider(BouncyCastleProvider()) + NativeCertGen.initialize("/data/adb/modules/tricky_store/libcertgen.so") + // This starts the message queue processing. It blocks here indefinitely // processing messages until Looper.myLooper().quit() is called. Looper.loop() 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 499a14d..f2539b0 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationBuilder.kt @@ -553,7 +553,7 @@ object AttestationBuilder { * retrieved. */ @Throws(Throwable::class) - private fun createApplicationId(uid: Int): DEROctetString { + internal fun createApplicationId(uid: Int): DEROctetString { // AOSP keystore_attestation_id.cpp: gather_attestation_application_id() // uses a hardcoded identity for AID_SYSTEM (1000) and AID_ROOT (0): // packageName = "AndroidSystem", versionCode = 1, no signing digests. diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt index 65917f4..eda4b29 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/Keystore2Interceptor.kt @@ -98,6 +98,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { interceptor, KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES, ) + interceptor.loadPersistedKeys() } } .onFailure { SystemLogger.error("Failed to intercept TEE SecurityLevel.", it) } @@ -114,6 +115,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { interceptor, KeyMintSecurityLevelInterceptor.INTERCEPTED_CODES, ) + interceptor.loadPersistedKeys() } } .onFailure { SystemLogger.error("Failed to intercept StrongBox SecurityLevel.", it) } diff --git a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt index 5b64fed..29daf3d 100644 --- a/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt +++ b/app/src/main/java/org/matrix/TEESimulator/interception/keystore/shim/KeyMintSecurityLevelInterceptor.kt @@ -14,7 +14,14 @@ import java.security.KeyPair import java.security.SecureRandom import java.security.cert.Certificate import java.util.concurrent.CompletableFuture +import android.util.Pair as AndroidPair +import java.io.ByteArrayInputStream +import java.security.KeyFactory +import java.security.cert.CertificateFactory +import java.security.spec.PKCS8EncodedKeySpec import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors +import org.matrix.TEESimulator.attestation.AttestationBuilder import org.matrix.TEESimulator.attestation.AttestationPatcher import org.matrix.TEESimulator.attestation.KeyMintAttestation import org.matrix.TEESimulator.config.ConfigurationManager @@ -22,8 +29,11 @@ import org.matrix.TEESimulator.interception.core.BinderInterceptor import org.matrix.TEESimulator.interception.keystore.InterceptorUtils import org.matrix.TEESimulator.interception.keystore.KeyIdentifier import org.matrix.TEESimulator.logging.SystemLogger +import org.matrix.TEESimulator.pki.CertGenConfig import org.matrix.TEESimulator.pki.CertificateGenerator import org.matrix.TEESimulator.pki.CertificateHelper +import org.matrix.TEESimulator.pki.KeyBoxManager +import org.matrix.TEESimulator.pki.NativeCertGen import org.matrix.TEESimulator.util.AndroidDeviceUtils import org.matrix.TEESimulator.util.TeeLatencySimulator @@ -547,14 +557,18 @@ class KeyMintSecurityLevelInterceptor( return InterceptorUtils.createTypedObjectReply(metadata) } - val keyData = + val keyData = if (NativeCertGen.isAvailable && attestationKey == null) { + generateAttestedKeyPairNative(callingUid, parsedParams) + ?: CertificateGenerator.generateAttestedKeyPair( + callingUid, keyDescriptor.alias, attestationKey?.alias, + parsedParams, securityLevel, + ) + } else { CertificateGenerator.generateAttestedKeyPair( - callingUid, - keyDescriptor.alias, - attestationKey?.alias, - parsedParams, - securityLevel, - ) ?: throw Exception("CertificateGenerator failed to create key pair.") + callingUid, keyDescriptor.alias, attestationKey?.alias, + parsedParams, securityLevel, + ) + } ?: throw Exception("Certificate generation failed.") val response = buildKeyEntryResponse(callingUid, keyData.second, parsedParams, keyDescriptor) @@ -562,12 +576,182 @@ class KeyMintSecurityLevelInterceptor( GeneratedKeyInfo(keyData.first, null, keyDescriptor.nspace, response, parsedParams) if (isAttestKeyRequest) attestationKeys.add(keyId) + val certChainCopy = keyData.second.toList() + persistExecutor.execute { + GeneratedKeyPersistence.save( + keyId = keyId, + keyPair = keyData.first, + nspace = keyDescriptor.nspace, + securityLevel = securityLevel, + certChain = certChainCopy, + algorithm = parsedParams.algorithm, + keySize = parsedParams.keySize, + ecCurve = parsedParams.ecCurve ?: 0, + purposes = parsedParams.purpose, + digests = parsedParams.digest, + isAttestationKey = isAttestKeyRequest, + ) + } + TeeLatencySimulator.simulateGenerateKeyDelay( parsedParams.algorithm, System.nanoTime() - genStartNanos ) return InterceptorUtils.createTypedObjectReply(response.metadata) } + private fun generateAttestedKeyPairNative( + callingUid: Int, + params: KeyMintAttestation, + ): AndroidPair>? { + return runCatching { + val algorithmName = when (params.algorithm) { + Algorithm.EC -> "EC" + Algorithm.RSA -> "RSA" + else -> return null + } + val keyboxFile = ConfigurationManager.getKeyboxFileForUid(callingUid) + val keybox = KeyBoxManager.getAttestationKey(keyboxFile, algorithmName) ?: return null + + val keyboxCertChainBytes = keybox.certificates + .map { it.encoded } + .fold(ByteArray(0)) { acc, der -> acc + der } + + val attestVersion = AndroidDeviceUtils.getAttestVersion(securityLevel) + val config = CertGenConfig( + algorithm = params.algorithm, + keySize = params.keySize, + ecCurve = params.ecCurve ?: 0, + rsaPublicExponent = params.rsaPublicExponent?.toLong() ?: 65537L, + attestationChallenge = params.attestationChallenge, + purposes = params.purpose.toIntArray(), + digests = params.digest.toIntArray(), + certSerial = params.certificateSerial?.toByteArray(), + certSubject = params.certificateSubject?.encoded, + certNotBefore = params.certificateNotBefore?.time ?: -1L, + certNotAfter = params.certificateNotAfter?.time ?: -1L, + keyboxPrivateKey = keybox.keyPair.private.encoded, + keyboxCertChain = keyboxCertChainBytes, + securityLevel = securityLevel, + attestVersion = attestVersion, + keymasterVersion = AndroidDeviceUtils.getKeymasterVersion(securityLevel), + osVersion = AndroidDeviceUtils.osVersion, + osPatchLevel = AndroidDeviceUtils.getPatchLevel(callingUid), + vendorPatchLevel = AndroidDeviceUtils.getVendorPatchLevelLong(callingUid), + bootPatchLevel = AndroidDeviceUtils.getBootPatchLevelLong(callingUid), + bootKey = AndroidDeviceUtils.bootKey, + bootHash = AndroidDeviceUtils.bootHash, + creationDatetime = System.currentTimeMillis(), + attestationApplicationId = AttestationBuilder.createApplicationId(callingUid).octets, + moduleHash = if (attestVersion >= 400) AndroidDeviceUtils.moduleHash else null, + idBrand = params.brand, + idDevice = params.device, + idProduct = params.product, + idSerial = params.serial, + idImei = params.imei, + idMeid = params.meid, + idManufacturer = params.manufacturer, + idModel = params.model, + idSecondImei = if (attestVersion >= 300) params.secondImei else null, + activeDatetime = params.activeDateTime?.time ?: -1L, + originationExpireDatetime = params.originationExpireDateTime?.time ?: -1L, + usageExpireDatetime = params.usageExpireDateTime?.time ?: -1L, + usageCountLimit = params.usageCountLimit ?: -1, + callerNonce = params.callerNonce == true, + unlockedDeviceRequired = params.unlockedDeviceRequired == true, + noAuthRequired = params.noAuthRequired != false, + ) + + val resultBytes = NativeCertGen.generateAttestedKeyPair(config) ?: return null + val (keyPair, certs) = NativeCertGen.parseNativeResult(resultBytes) + SystemLogger.info("NativeCertGen: ${certs.size} certs generated") + AndroidPair(keyPair, certs) + }.onFailure { + SystemLogger.error("NativeCertGen failed, falling back to BouncyCastle", it) + }.getOrNull() + } + + fun loadPersistedKeys() { + val records = GeneratedKeyPersistence.loadAll(securityLevel) + if (records.isEmpty()) return + + SystemLogger.info("Restoring ${records.size} persisted keys for security level $securityLevel") + for (record in records) { + runCatching { + val keyId = KeyIdentifier(record.uid, record.alias) + if (generatedKeys.containsKey(keyId)) return@runCatching + + val algorithmName = when (record.algorithm) { + Algorithm.EC -> "EC" + Algorithm.RSA -> "RSA" + else -> throw IllegalArgumentException("Unknown algorithm: ${record.algorithm}") + } + + val keyFactory = KeyFactory.getInstance(algorithmName) + val privateKey = keyFactory.generatePrivate(PKCS8EncodedKeySpec(record.privateKeyBytes)) + + val certFactory = CertificateFactory.getInstance("X.509") + val certChain = record.certChainBytes.map { bytes -> + certFactory.generateCertificate(ByteArrayInputStream(bytes)) + } + require(certChain.isNotEmpty()) { "Empty certificate chain" } + + val keyPair = KeyPair(certChain[0].publicKey, privateKey) + val descriptor = KeyDescriptor().apply { + domain = Domain.APP + nspace = record.nspace + alias = record.alias + blob = null + } + + val attestation = KeyMintAttestation( + algorithm = record.algorithm, + ecCurve = if (record.algorithm == Algorithm.EC) record.ecCurve else null, + ecCurveName = "", + keySize = record.keySize, + origin = null, + noAuthRequired = null, + blockMode = emptyList(), + padding = emptyList(), + purpose = record.purposes, + digest = record.digests, + rsaPublicExponent = null, + certificateSerial = null, + certificateSubject = null, + certificateNotBefore = null, + certificateNotAfter = null, + attestationChallenge = null, + brand = null, device = null, product = null, serial = null, + imei = null, meid = null, manufacturer = null, model = null, + secondImei = null, + activeDateTime = null, + originationExpireDateTime = null, + usageExpireDateTime = null, + usageCountLimit = null, + callerNonce = null, + unlockedDeviceRequired = null, + includeUniqueId = null, + rollbackResistance = null, + earlyBootOnly = null, + allowWhileOnBody = null, + trustedUserPresenceRequired = null, + trustedConfirmationRequired = null, + maxUsesPerBoot = null, + maxBootLevel = null, + minMacLength = null, + rsaOaepMgfDigest = emptyList(), + ) + + val response = buildKeyEntryResponse(record.uid, certChain, attestation, descriptor) + generatedKeys[keyId] = GeneratedKeyInfo(keyPair, null, record.nspace, response, attestation) + if (record.isAttestationKey) attestationKeys.add(keyId) + SystemLogger.debug("Restored persisted key: $keyId") + }.onFailure { + SystemLogger.error("Failed to restore key: uid=${record.uid} alias=${record.alias}", it) + } + } + SystemLogger.info("Key restoration complete. Total in memory: ${generatedKeys.size}") + } + /** * Races TEE hardware generation against software generation concurrently for AUTO mode. * If TEE succeeds, the software future is cancelled and TEE is marked functional. @@ -690,8 +874,8 @@ class KeyMintSecurityLevelInterceptor( companion object { private val secureRandom = SecureRandom() + private val persistExecutor = Executors.newSingleThreadExecutor() - /** Once set to true, AUTO mode skips the race and uses PATCH directly. */ @Volatile var teeFunctional = false private const val INVALID_ARGUMENT = 20 @@ -770,6 +954,7 @@ class KeyMintSecurityLevelInterceptor( fun cleanupKeyData(keyId: KeyIdentifier) { if (generatedKeys.remove(keyId) != null) { SystemLogger.debug("Remove generated key ${keyId}") + GeneratedKeyPersistence.delete(keyId) } if (patchedChains.remove(keyId) != null) { SystemLogger.debug("Remove patched chain for ${keyId}") @@ -801,6 +986,7 @@ class KeyMintSecurityLevelInterceptor( importedKeys.clear() usageCounters.clear() teeResponses.clear() + GeneratedKeyPersistence.deleteAll() SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.") } }