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 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])} " +
|
||||||
|
|||||||
+17
@@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user