feat(logging): per-UID forge diagnostics

The attestation dossier only fired on a successfully produced chain, so
the StrongBox/BHIM failures left nothing on the per-UID plane and had to
be reconstructed from marshalled .bin dumps offline. Add three records,
all debug- and target-gated like the existing dossier:

- keybox-pick: which keybox signs the forge (requested algo, exact match
  vs EC fail-safe, signer subject) -- makes an EC-only-keybox RSA
  fallback visible instead of silent.
- forge-fail: emit the failure reason on the per-UID plane when a forge
  throws (e.g. ATTESTATION_KEYS_NOT_PROVISIONED), paired with dispatch.
- auth-shape: the emitted authorization list (count, ordered tags,
  per-auth securityLevel) -- the surface the duck generate-mode parcel
  fingerprint stride-walks, readable without offline decode.
This commit is contained in:
Enginex0
2026-06-04 20:39:49 +01:00
parent 79e4fe905e
commit 9561f7d9c0
3 changed files with 62 additions and 8 deletions
@@ -746,8 +746,12 @@ class KeyMintSecurityLevelInterceptor(
}
}
}
.getOrElse {
SystemLogger.error("Error during generateKey handling for UID $callingUid.", it)
.getOrElse { ex ->
SystemLogger.error("Error during generateKey handling for UID $callingUid.", ex)
// The dossier only fires on a produced chain; a forge that throws (e.g. a missing
// keybox) would otherwise leave no per-UID record. Pair this with the preceding
// `dispatch` line to see which request failed and why.
SystemLogger.uidLog(callingUid, txId, "forge-fail") { "ex=${ex.message}" }
InterceptorUtils.createServiceSpecificErrorReply(SECURE_HW_COMMUNICATION_FAILED)
}
}
@@ -914,6 +918,7 @@ class KeyMintSecurityLevelInterceptor(
val response =
buildKeyEntryResponse(callingUid, keyData.second, parsedParams, keyDescriptor)
AttestationDossier.logAuthShape(callingUid, txId, response.metadata?.authorizations)
generatedKeys[keyId] =
GeneratedKeyInfo(keyData.first, null, keyDescriptor.nspace, response, parsedParams)
Keystore2Interceptor.forgetDeletedKey(keyId)
@@ -1,5 +1,7 @@
package org.matrix.TEESimulator.logging
import android.hardware.security.keymint.Tag
import android.system.keystore2.Authorization
import java.security.cert.Certificate
import java.security.cert.X509Certificate
import org.matrix.TEESimulator.attestation.AttestationPatcher
@@ -30,4 +32,42 @@ object AttestationDossier {
SystemLogger.uidLog(uid, txId, "chain", AttestationPatcher.formatCertChain(chain))
SystemLogger.uidLog(uid, txId, "props", AndroidDeviceUtils.describeSources(uid))
}
/**
* Records the *shape* of the emitted authorization list — count, ordered tags, and per-auth
* securityLevel. This is the exact surface the duck detector's generate-mode parcel fingerprint
* stride-walks, so logging it readably lets a "fingerprint" detection be compared against the
* known genuine-TEE shape without decoding the marshalled reply offline.
*/
fun logAuthShape(uid: Int, txId: Long, authorizations: Array<Authorization>?) {
if (!SystemLogger.isUidLogged(uid)) return
val auths = authorizations ?: return
val shape = auths.joinToString(",") { "${tagName(it.keyParameter.tag)}/${it.securityLevel}" }
SystemLogger.uidLog(uid, txId, "auth-shape", "n=${auths.size} [$shape]")
}
/** Names the authorization tags that occur in generate-mode replies; others render as numbers. */
private fun tagName(tag: Int): String =
when (tag) {
Tag.PURPOSE -> "PURPOSE"
Tag.ALGORITHM -> "ALGORITHM"
Tag.KEY_SIZE -> "KEY_SIZE"
Tag.DIGEST -> "DIGEST"
Tag.PADDING -> "PADDING"
Tag.EC_CURVE -> "EC_CURVE"
Tag.RSA_PUBLIC_EXPONENT -> "RSA_PUBLIC_EXPONENT"
Tag.NO_AUTH_REQUIRED -> "NO_AUTH_REQUIRED"
Tag.ORIGIN -> "ORIGIN"
Tag.OS_VERSION -> "OS_VERSION"
Tag.OS_PATCHLEVEL -> "OS_PATCHLEVEL"
Tag.VENDOR_PATCHLEVEL -> "VENDOR_PATCHLEVEL"
Tag.BOOT_PATCHLEVEL -> "BOOT_PATCHLEVEL"
Tag.CREATION_DATETIME -> "CREATION_DATETIME"
Tag.ROOT_OF_TRUST -> "ROOT_OF_TRUST"
Tag.USER_ID -> "USER_ID"
Tag.USAGE_COUNT_LIMIT -> "USAGE_COUNT_LIMIT"
Tag.UNLOCKED_DEVICE_REQUIRED -> "UNLOCKED_DEVICE_REQUIRED"
Tag.ACTIVE_DATETIME -> "ACTIVE_DATETIME"
else -> "tag${tag and 0x0FFFFFFF}"
}
}
@@ -191,12 +191,21 @@ object CertificateGenerator {
// certificate"). Mirrors the patch path's fail-safe
// (AttestationPatcher.getKeyboxForUidAndAlgorithm) and the RSA-leaf-under-EC-keybox handling
// in commit e6d5e4d.
return KeyBoxManager.getAttestationKey(keyboxFile, algorithmName)
val matched = KeyBoxManager.getAttestationKey(keyboxFile, algorithmName)
val keybox =
matched
?: KeyBoxManager.getAnyAttestationKey(keyboxFile)
?: throw android.os.ServiceSpecificException(
-75, // ATTESTATION_KEYS_NOT_PROVISIONED
"No usable attestation key in $keyboxFile",
)
// Surface which keybox actually signs the forge, so an EC-only-keybox fallback (an RSA leaf
// rooted under the EC key) is visible on the per-UID plane instead of silent.
SystemLogger.uidLog(uid, null, "keybox-pick") {
"req=$algorithmName ${if (matched != null) "matched" else "fellback-to-any"} " +
"signer=${getIssuerFromKeybox(keybox)}"
}
return keybox
}
/** Retrieves the key pair and issuer name for a given attestation key alias. */