Prevent detection via inconsistent certificate signatures (#45)

Fixes a detection vector where the simulator could be identified by comparing certificate signatures from different API calls.

Previously, the simulator would re-patch and re-sign a certificate on-the-fly for both `generateKey` and `getKeyEntry` calls. Due to the non-deterministic nature of ECDSA signing, this resulted in different signatures for the same certificate, which is a detectable anomaly not present in a real TEE.

This is resolved by caching the patched certificate chain after its initial creation in `KeyMintSecurityLevelInterceptor`. The `getKeyEntry` hook in `Keystore2Interceptor` now retrieves the chain from this cache, guaranteeing that subsequent calls return a byte-for-byte identical certificate.

Cache cleanup logic was also integrated into key deletion and clearing functions to maintain state consistency.
This commit is contained in:
JingMatrix
2025-12-04 00:59:59 +01:00
committed by GitHub
parent e66e558ce5
commit d0cc5e3b56
3 changed files with 46 additions and 3 deletions
@@ -124,8 +124,14 @@ object AttestationPatcher {
// Sign the newly built certificate with the private key from our keybox. // Sign the newly built certificate with the private key from our keybox.
val signer = JcaContentSignerBuilder(sigAlgName).build(keybox.keyPair.private) val signer = JcaContentSignerBuilder(sigAlgName).build(keybox.keyPair.private)
val newCertificate = JcaX509CertificateConverter().getCertificate(builder.build(signer))
return JcaX509CertificateConverter().getCertificate(builder.build(signer)) // Log the signature of the newly created certificate to observe its non-deterministic
// nature.
val signatureBytes = (newCertificate as X509Certificate).signature
SystemLogger.verbose("Signature of patched leaf cert: ${signatureBytes.toHex()}")
return newCertificate
} }
private fun getKeyboxForUidAndAlgorithm(uid: Int, algorithm: String): KeyBox { private fun getKeyboxForUidAndAlgorithm(uid: Int, algorithm: String): KeyBox {
@@ -9,6 +9,7 @@ import android.os.Parcel
import android.system.keystore2.IKeystoreService import android.system.keystore2.IKeystoreService
import android.system.keystore2.KeyDescriptor import android.system.keystore2.KeyDescriptor
import android.system.keystore2.KeyEntryResponse import android.system.keystore2.KeyEntryResponse
import java.security.cert.Certificate
import org.matrix.TEESimulator.attestation.AttestationPatcher import org.matrix.TEESimulator.attestation.AttestationPatcher
import org.matrix.TEESimulator.config.ConfigurationManager import org.matrix.TEESimulator.config.ConfigurationManager
import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor import org.matrix.TEESimulator.interception.keystore.shim.KeyMintSecurityLevelInterceptor
@@ -185,8 +186,28 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
} }
// Perform the attestation patch. // Perform the attestation patch.
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid) val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
CertificateHelper.updateCertificateChain(response.metadata, newChain).getOrThrow()
// First, try to retrieve the already-patched chain from our cache to ensure
// consistency.
val cachedChain = KeyMintSecurityLevelInterceptor.getPatchedChain(keyId)
val finalChain: Array<Certificate>
if (cachedChain != null) {
SystemLogger.debug(
"[TX_ID: $txId] Using cached patched certificate chain for $keyId."
)
finalChain = cachedChain
} else {
// If no chain is cached (e.g., key existed before simulator started),
// perform a live patch as a fallback. This may still be detectable.
SystemLogger.info(
"[TX_ID: $txId] No cached chain for $keyId. Performing live patch as a fallback."
)
finalChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
}
CertificateHelper.updateCertificateChain(response.metadata, finalChain).getOrThrow()
InterceptorUtils.createTypedObjectReply(response) InterceptorUtils.createTypedObjectReply(response)
} catch (e: Exception) { } catch (e: Exception) {
@@ -101,6 +101,14 @@ class KeyMintSecurityLevelInterceptor(
?: return TransactionResult.SkipTransaction ?: return TransactionResult.SkipTransaction
if (originalChain.size > 1) { if (originalChain.size > 1) {
val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid) val newChain = AttestationPatcher.patchCertificateChain(originalChain, callingUid)
// Cache the newly patched chain to ensure consistency across subsequent API calls.
data.enforceInterface(IKeystoreSecurityLevel.DESCRIPTOR)
val keyDescriptor = data.readTypedObject(KeyDescriptor.CREATOR)!!
val keyId = KeyIdentifier(callingUid, keyDescriptor.alias)
patchedChains[keyId] = newChain
SystemLogger.debug("Cached patched certificate chain for $keyId.")
CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow() CertificateHelper.updateCertificateChain(metadata, newChain).getOrThrow()
return InterceptorUtils.createTypedObjectReply(metadata) return InterceptorUtils.createTypedObjectReply(metadata)
@@ -217,6 +225,8 @@ class KeyMintSecurityLevelInterceptor(
// Stores keys generated entirely in software. // Stores keys generated entirely in software.
val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>() val generatedKeys = ConcurrentHashMap<KeyIdentifier, GeneratedKeyInfo>()
// Caches patched certificate chains to prevent re-generation and signature inconsistencies.
private val patchedChains = ConcurrentHashMap<KeyIdentifier, Array<Certificate>>()
// A set to quickly identify keys that were generated for attestation purposes. // A set to quickly identify keys that were generated for attestation purposes.
private val attestationKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>() private val attestationKeys = ConcurrentHashMap.newKeySet<KeyIdentifier>()
@@ -224,12 +234,17 @@ class KeyMintSecurityLevelInterceptor(
fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? = fun getGeneratedKeyResponse(keyId: KeyIdentifier): KeyEntryResponse? =
generatedKeys[keyId]?.response generatedKeys[keyId]?.response
fun getPatchedChain(keyId: KeyIdentifier): Array<Certificate>? = patchedChains[keyId]
fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId) fun isAttestationKey(keyId: KeyIdentifier): Boolean = attestationKeys.contains(keyId)
fun cleanupKeyData(keyId: KeyIdentifier) { fun cleanupKeyData(keyId: KeyIdentifier) {
if (generatedKeys.remove(keyId) != null) { if (generatedKeys.remove(keyId) != null) {
SystemLogger.debug("Remove generated key ${keyId}") SystemLogger.debug("Remove generated key ${keyId}")
} }
if (patchedChains.remove(keyId) != null) {
SystemLogger.debug("Remove patched chain for ${keyId}")
}
if (attestationKeys.remove(keyId)) { if (attestationKeys.remove(keyId)) {
SystemLogger.debug("Remove cached attestaion key ${keyId}") SystemLogger.debug("Remove cached attestaion key ${keyId}")
} }
@@ -240,6 +255,7 @@ class KeyMintSecurityLevelInterceptor(
val count = generatedKeys.size val count = generatedKeys.size
val reasonMessage = reason?.let { " due to $it" } ?: "" val reasonMessage = reason?.let { " due to $it" } ?: ""
generatedKeys.clear() generatedKeys.clear()
patchedChains.clear()
attestationKeys.clear() attestationKeys.clear()
SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.") SystemLogger.info("Cleared all cached keys ($count entries)$reasonMessage.")
} }