diff --git a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationPatcher.kt b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationPatcher.kt index 22e4278..24e0b78 100644 --- a/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationPatcher.kt +++ b/app/src/main/java/org/matrix/TEESimulator/attestation/AttestationPatcher.kt @@ -3,10 +3,13 @@ package org.matrix.TEESimulator.attestation import android.security.keystore.KeyProperties import java.nio.charset.StandardCharsets import java.security.PrivateKey +import java.security.PublicKey import java.security.cert.Certificate import java.security.cert.X509Certificate import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey import java.util.Date import org.bouncycastle.asn1.* import org.bouncycastle.asn1.x509.Extension @@ -286,6 +289,60 @@ object AttestationPatcher { } .joinToString(separator = " ; ") + /** + * Verifies every certificate in [chain] against its issuer and renders the outcome for the + * dossier. The forged chain is [leaf] + keybox certs, so edge 0<-1 proves the leaf was signed by + * the key matching the issuer cert and later edges test the keybox's own chain. For an RSA issuer + * it also reports signature-bytes vs modulus-bytes: a signature longer than the modulus is the + * exact DATA_TOO_LARGE_FOR_KEY_SIZE the app's verifier throws, so the offending edge is + * identifiable from the log alone. + */ + fun formatChainVerification(chain: List): String { + if (chain.size < 2) return "" + return (0 until chain.size - 1).joinToString(separator = " ; ") { i -> + val child = chain[i] as? X509Certificate ?: return@joinToString "[$i]" + val parent = + chain[i + 1] as? X509Certificate ?: return@joinToString "[$i]" + val outcome = + runCatching { + child.verify(parent.publicKey) + "OK" + } + .getOrElse { "FAIL(${it.javaClass.simpleName}: ${it.message?.take(80)})" } + val rsaSizes = + (parent.publicKey as? RSAPublicKey)?.let { + val sigBytes = child.signature.size + val modBytes = (it.modulus.bitLength() + 7) / 8 + " sig=${sigBytes}B mod=${modBytes}B" + if (sigBytes > modBytes) " OVERSIZE" else "" + } ?: "" + "[$i]${describeKey(child.publicKey)}<-[${i + 1}]${describeKey(parent.publicKey)}:" + + "$outcome$rsaSizes" + } + } + + /** + * Per-cert key type/size, subject, issuer, and signature length, for reconstructing the chain a + * caller verifies. The signature length reveals the signer's key size, so a 4096-bit signature + * landing on a 2048-bit issuer (DATA_TOO_LARGE) is visible without the certificate bytes. + */ + fun formatChainKeys(chain: List): String = + chain + .mapIndexed { index, cert -> + val x509 = cert as? X509Certificate ?: return@mapIndexed "[$index]" + "[$index]${describeKey(x509.publicKey)} " + + "subj=${x509.subjectX500Principal.name} " + + "iss=${x509.issuerX500Principal.name} " + + "sigLen=${x509.signature.size}B" + } + .joinToString(separator = " ; ") + + private fun describeKey(key: PublicKey): String = + when (key) { + is RSAPublicKey -> "RSA${key.modulus.bitLength()}" + is ECPublicKey -> "EC${key.params.curve.field.fieldSize}" + else -> key.algorithm + } + private fun formatKeyDescription(seq: ASN1Sequence): String { val fields = seq.toArray() return "attestVer=${formatAsn1Primitive(fields[AttestationConstants.KEY_DESCRIPTION_ATTESTATION_VERSION_INDEX])} " + 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 aba7f67..c628cb8 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 @@ -314,6 +314,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { SystemLogger.info( "[TX_ID: $txId] Found generated response via KEY_ID nspace=${descriptor.nspace}" ) + logServedChain(callingUid, txId, "keyid:${descriptor.nspace}", info.response) return InterceptorUtils.createTypedObjectReply(info.response) } val teeResp = @@ -325,6 +326,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { SystemLogger.info( "[TX_ID: $txId] Found TEE response via KEY_ID nspace=${descriptor.nspace}" ) + logServedChain(callingUid, txId, "keyid:${descriptor.nspace}", teeResp) return InterceptorUtils.createTypedObjectReply(teeResp) } } @@ -357,6 +359,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { response.metadata?.authorizations?.forEach { KeyMintParameterLogger.logParameter(callingUid, txId, it.keyParameter) } + logServedChain(callingUid, txId, descriptor.alias, response) return InterceptorUtils.createTypedObjectReply(response) } else if (code == GRANT_TRANSACTION) { logTransaction(txId, transactionNames[code] ?: "grant", callingUid, callingPid) @@ -700,6 +703,20 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() { else -> null } + /** + * Records the certificate chain actually served back to [uid] on a getKeyEntry, keyed by + * [alias]. The app reassembles its final chain from these served chains (the leaf alias plus + * the attest-key alias), so logging each one with key sizes and a per-edge verification makes a + * verification failure in the app's combined chain reproducible from the log, not inferred. + */ + private fun logServedChain(uid: Int, txId: Long, alias: String, response: KeyEntryResponse?) { + if (response == null || !SystemLogger.isUidLogged(uid)) return + val chain = CertificateHelper.getCertificateChain(response)?.asList() ?: return + SystemLogger.uidLog(uid, txId, "served", "alias=$alias depth=${chain.size}") + SystemLogger.uidLog(uid, txId, "served-keys", AttestationPatcher.formatChainKeys(chain)) + SystemLogger.uidLog(uid, txId, "served-verify", AttestationPatcher.formatChainVerification(chain)) + } + private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult { data.enforceInterface(IKeystoreService.DESCRIPTOR) val descriptor = diff --git a/app/src/main/java/org/matrix/TEESimulator/logging/AttestationDossier.kt b/app/src/main/java/org/matrix/TEESimulator/logging/AttestationDossier.kt index f6dfeba..344ba6b 100644 --- a/app/src/main/java/org/matrix/TEESimulator/logging/AttestationDossier.kt +++ b/app/src/main/java/org/matrix/TEESimulator/logging/AttestationDossier.kt @@ -30,6 +30,7 @@ object AttestationDossier { SystemLogger.uidLog(uid, txId, "attest", "path=$path depth=${chain.size} $extension") SystemLogger.uidLog(uid, txId, "keybox", "file=${ConfigurationManager.getKeyboxFileForUid(uid)}") SystemLogger.uidLog(uid, txId, "chain", AttestationPatcher.formatCertChain(chain)) + SystemLogger.uidLog(uid, txId, "chain-verify", AttestationPatcher.formatChainVerification(chain)) SystemLogger.uidLog(uid, txId, "props", AndroidDeviceUtils.describeSources(uid)) }