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:
@@ -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 {
|
||||||
|
|||||||
+23
-2
@@ -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) {
|
||||||
|
|||||||
+16
@@ -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.")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user