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:
@@ -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<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 {
|
||||
val fields = seq.toArray()
|
||||
return "attestVer=${formatAsn1Primitive(fields[AttestationConstants.KEY_DESCRIPTION_ATTESTATION_VERSION_INDEX])} " +
|
||||
|
||||
+17
@@ -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 =
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user