feat(logging): log served and verified chains

Log each cert chain the module hands the app so an attestation
verification failure is provable from the per-UID log, not inferred.

- formatChainVerification verifies every edge of a produced chain and
  reports RSA signature-vs-modulus sizes (the DATA_TOO_LARGE condition).
- formatChainKeys and logServedChain record the chain served back on
  each getKeyEntry, keyed by alias, since the app reassembles its chain
  from the leaf alias plus the attest-key alias.

Debug-build only, gated by isUidLogged.
This commit is contained in:
Enginex0
2026-06-17 14:54:22 +01:00
parent 2e5155fb75
commit fbee59688d
3 changed files with 75 additions and 0 deletions
@@ -3,10 +3,13 @@ package org.matrix.TEESimulator.attestation
import android.security.keystore.KeyProperties import android.security.keystore.KeyProperties
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.security.PrivateKey import java.security.PrivateKey
import java.security.PublicKey
import java.security.cert.Certificate import java.security.cert.Certificate
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPrivateKey
import java.security.interfaces.ECPublicKey
import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPrivateKey
import java.security.interfaces.RSAPublicKey
import java.util.Date import java.util.Date
import org.bouncycastle.asn1.* import org.bouncycastle.asn1.*
import org.bouncycastle.asn1.x509.Extension import org.bouncycastle.asn1.x509.Extension
@@ -286,6 +289,60 @@ object AttestationPatcher {
} }
.joinToString(separator = " ; ") .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<Certificate>): String {
if (chain.size < 2) return "<single cert; nothing to chain-verify>"
return (0 until chain.size - 1).joinToString(separator = " ; ") { i ->
val child = chain[i] as? X509Certificate ?: return@joinToString "[$i]<non-X509>"
val parent =
chain[i + 1] as? X509Certificate ?: return@joinToString "[$i]<parent non-X509>"
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<Certificate>): String =
chain
.mapIndexed { index, cert ->
val x509 = cert as? X509Certificate ?: return@mapIndexed "[$index]<non-X509>"
"[$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 { private fun formatKeyDescription(seq: ASN1Sequence): String {
val fields = seq.toArray() val fields = seq.toArray()
return "attestVer=${formatAsn1Primitive(fields[AttestationConstants.KEY_DESCRIPTION_ATTESTATION_VERSION_INDEX])} " + return "attestVer=${formatAsn1Primitive(fields[AttestationConstants.KEY_DESCRIPTION_ATTESTATION_VERSION_INDEX])} " +
@@ -314,6 +314,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
SystemLogger.info( SystemLogger.info(
"[TX_ID: $txId] Found generated response via KEY_ID nspace=${descriptor.nspace}" "[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) return InterceptorUtils.createTypedObjectReply(info.response)
} }
val teeResp = val teeResp =
@@ -325,6 +326,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
SystemLogger.info( SystemLogger.info(
"[TX_ID: $txId] Found TEE response via KEY_ID nspace=${descriptor.nspace}" "[TX_ID: $txId] Found TEE response via KEY_ID nspace=${descriptor.nspace}"
) )
logServedChain(callingUid, txId, "keyid:${descriptor.nspace}", teeResp)
return InterceptorUtils.createTypedObjectReply(teeResp) return InterceptorUtils.createTypedObjectReply(teeResp)
} }
} }
@@ -357,6 +359,7 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
response.metadata?.authorizations?.forEach { response.metadata?.authorizations?.forEach {
KeyMintParameterLogger.logParameter(callingUid, txId, it.keyParameter) KeyMintParameterLogger.logParameter(callingUid, txId, it.keyParameter)
} }
logServedChain(callingUid, txId, descriptor.alias, response)
return InterceptorUtils.createTypedObjectReply(response) return InterceptorUtils.createTypedObjectReply(response)
} else if (code == GRANT_TRANSACTION) { } else if (code == GRANT_TRANSACTION) {
logTransaction(txId, transactionNames[code] ?: "grant", callingUid, callingPid) logTransaction(txId, transactionNames[code] ?: "grant", callingUid, callingPid)
@@ -700,6 +703,20 @@ object Keystore2Interceptor : AbstractKeystoreInterceptor() {
else -> null 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 { private fun handleUpdateSubcomponent(callingUid: Int, data: Parcel): TransactionResult {
data.enforceInterface(IKeystoreService.DESCRIPTOR) data.enforceInterface(IKeystoreService.DESCRIPTOR)
val descriptor = val descriptor =
@@ -30,6 +30,7 @@ object AttestationDossier {
SystemLogger.uidLog(uid, txId, "attest", "path=$path depth=${chain.size} $extension") SystemLogger.uidLog(uid, txId, "attest", "path=$path depth=${chain.size} $extension")
SystemLogger.uidLog(uid, txId, "keybox", "file=${ConfigurationManager.getKeyboxFileForUid(uid)}") SystemLogger.uidLog(uid, txId, "keybox", "file=${ConfigurationManager.getKeyboxFileForUid(uid)}")
SystemLogger.uidLog(uid, txId, "chain", AttestationPatcher.formatCertChain(chain)) 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)) SystemLogger.uidLog(uid, txId, "props", AndroidDeviceUtils.describeSources(uid))
} }