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.
This commit is contained in:
Enginex0
2026-03-20 06:23:05 +01:00
parent 94c8e5b182
commit 38e9b547a5
5 changed files with 207 additions and 16 deletions
+7 -7
View File
@@ -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<Sync>("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>("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<Exec>("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<Exec>("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<Exec>("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")
@@ -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()
@@ -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.
@@ -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) }
@@ -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<KeyPair, List<Certificate>>? {
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.")
}
}