Debugging, Recording,
and Replaying Attestation Checks
Attestation failures often depend on the exact certificate chain, configuration, and revocation state present during verification. Warden Supreme can capture these inputs and replay the failed check offline.
For an interactive look at real-world Android attestation data before building an integration, use the Attestation Collector. It displays the parsed statement and verification result.
The workflow is the same whether you use Warden Supreme's integrated flow or the raw makoto or roboto attestation checkers (see Usage without Integrated Clients):
- When verification fails, capture a debug statement containing the exact proof, configuration, verification time, and revocation-list snapshot used for the check.
- Call
serializeCompact()on that statement and persist the resulting single-line string. This is the portable replay artifact to copy from the deployed service to a development machine. - Deserialize the compact string and call
replay()locally. The original check can then be stepped through in a debugger without access to the device or the original deployment. - Use
serialize()only when you want a human-readable JSON representation for inspection.
Enrich the surrounding log entry with device make/model, OS, and patch levels so OEM-specific quirks can be correlated over time.
Privacy Risks and Risk of Chasing Phantoms
Three restrictions apply when recording or sharing debug statements.
- Attestation information is personally identifying data if the contained public key is associated with a person.
- If you publicly paste a debug statement, it will contain your whole configuration, and it will leak information about your service, apps, and the environment of your backend that performs the attestation checks.
- For actual debugging, only ever share debug statements in
serializeCompact()format, not the human-readableserialize()output.- The
serializeCompact()format uses a self-describing, forward-compatible multibase encoding. - This encoding is transport-safe. It survives Microsoft Outlook's sometimes creative way of "improving" message formatting, as well as "helpful" IDEs and editors being extra-smart when pasting.
- We once lost a full day of debugging to encoding errors in transit. This is exactly why
serializeCompact()is the way to record and share debug statements for analysis, whileserialize()is for a human-readable representation.
- The
Collecting Debug Info
Regardless of whether you are using Warden Supreme's integrated attestation flow, or raw makoto/roboto,
all flows produce a debug statement with the same serialize(), serializeCompact(), and replay() lifecycle. Only
the point at which the statement is collected and its concrete type differ:
| Verification flow | How debug info is obtained | Debug statement type |
|---|---|---|
| Warden Supreme | Supplied automatically to onAttestationError |
WardenDebugAttestationStatement |
| Raw makoto | Call collectDebugInfo(...) after the failed verification |
WardenDebugAttestationStatement |
| Raw roboto | Call collectDebugInfo(...) after the failed verification |
AndroidDebugAttestationStatement |
In Warden Supreme's integrated flow, the attestation error is the receiver of the onAttestationError lambda and the ready-made
debug statement is its argument. Persist the compact replay artefact directly in that callback:
With raw makoto or roboto, call the matching collectDebugInfo(...) overload using the same proof and challenge that were
passed to verification, then persist debugInfo.serializeCompact(). Collect it after the failed verification so the statement
can include the revocation-list snapshot used by that check.
The resulting compact string is the boundary between production and offline debugging: store or transfer it as one unchanged
line. Do not copy the pretty-printed serialize() output for replay.
Debugging Integrated Attestation
Warden Supreme already contains code to load compact WardenDebugAttestationStatements and replay() them. The replay utility
expects a file containing one serializeCompact() result per line.
Run the replay utility in an IDE to step through the attestation workflow and inspect the stack trace behind a failure:
- Import this project into IntelliJ IDEA
- Add a breakpoint here (line 19)
- Run it in debug mode
Pass the debug-statement file as the single argument described in Diag.kt.
The equivalent direct API flow is:
val statement = WardenDebugAttestationStatement.deserializeCompact(recordedLine)
println(statement.serialize()) // optional human-readable JSON
val replayedResult = statement.replay()
For a raw roboto recording, use AndroidDebugAttestationStatement.deserializeCompact(recordedLine) instead; the remaining
workflow is identical.
Debugging Raw Android Attestations
Stand-Alone Attestation Parser
To use androidAttestationExtension on all platforms (e.g., to implement client-side checks on Android),
include :supreme-common in your project, and you are ready to go!
A similar utility for printing the contents of an Android attestation statement lives in /utils/roboto-diag. It pretty-prints the leaf certificate's Android attestation extension and expects either:
-f path/to/leaf/certificate.pem- a base64-encoded certificate as the sole argument
It will then pretty-print the attestation extension's contents.
There is also a nullable androidAttestationExtension extension property on the Java X509Certificate
and on Signum's X509Certificate class (and certificate chains). It exposes prettyPrint(), so you can peek into Android attestation extensions at any time.
It even parses malformed values and prints their DER-encoded hex representation.
The parser and renderer remain experimental, but they have been tested against thousands of attestation proofs captured
from real devices. This corpus includes malformed values that Google's old and current parsers reject. Warden's parser
retains these values and renders their DER representation where necessary, making it considerably more useful for
investigating certificates encountered in production.
Example of a Pretty-Printed Attestation Record from an Emulator
AttestationKeyDescription(
attestationVersion = 3
attestationSecurityLevel = TRUSTED_ENVIRONMENT
keyMintVersion = 41
keyMintSecurityLevel = TRUSTED_ENVIRONMENT
attestationChallenge = b5a4a68423c5d2f610328b3dcc8b408d352afc78fb7eb0d4803bd9ef581654eb
uniqueId =
softwareEnforced =
AuthorizationList(
purpose = null
algorithm = null
keySize = null
blockMode = null
digest = null
padding = null
callerNonce = false
minMacLength = null
ecCurve = null
rsaPublicExponent = null
mgfDigest = null
rollbackResistance = false
earlyBootOnly = false
activeDateTime = null
originationExpireDateTime = null
usageExpireDateTime = null
usageCountLimit = null
userSecureId = null
noAuthRequired = false
userAuthType = null
authTimeout = null
allowWhileOnBody = false
trustedUserPresenceRequired = false
trustedConfirmationRequired = false
unlockedDeviceRequired = false
allApplications = false
creationDateTime = CreationDateTime(intValue=1752703332000, timestamp=2025-07-16T22:02:12Z)
origin = null
rollbackResistant = null
rootOfTrust = null
osVersion = null
osPatchLevel = null
attestationApplicationId =
AttestationApplicationId(
packageInfos = [
AttestationPackageInfo(packageName='at.asitplus.atttest', version=1)
]
signatureDigests = [
34b9762c4d6c90d48431940c57bde7314258b26420efe16ac7f7274f0d330ad5
]
)
attestationIdBrand = null
attestationIdDevice = null
attestationIdProduct = null
attestationIdSerial = null
attestationIdImei = null
attestationIdMeid = null
attestationIdManufacturer = null
attestationIdModel = null
vendorPatchLevel = null
bootPatchLevel = null
deviceUniqueAttestation = false
attestationIdSecondImei = null
moduleHash = null
)
hardwareEnforced =
AuthorizationList(
purpose = [
- SIGN
- VERIFY
]
algorithm = EC
keySize = KeySize(intValue=256)
blockMode = null
digest = [
- SHA_2_256
]
padding = null
callerNonce = false
minMacLength = null
ecCurve = P_256
rsaPublicExponent = null
mgfDigest = null
rollbackResistance = false
earlyBootOnly = false
activeDateTime = null
originationExpireDateTime = null
usageExpireDateTime = null
usageCountLimit = null
userSecureId = null
noAuthRequired = false
userAuthType = UserAuth(authTypes=[FINGERPRINT], intValue=2)
authTimeout = null
allowWhileOnBody = false
trustedUserPresenceRequired = false
trustedConfirmationRequired = false
unlockedDeviceRequired = false
allApplications = false
creationDateTime = null
origin = GENERATED
rollbackResistant = null
rootOfTrust =
RootOfTrust(
verifiedBootKeyDigest=c2224571c9cd5c89200a7311b1e37aa9cf751e2e19753e8d3702bca00be1d42c
deviceLocked=true
verifiedBootState=Verified
verifiedBootHash=d415abd0e620d3e8a942a62920195ad893ff0515ab8236931a156bc1f49de444
)
osVersion = OsVersion(major=13, minor=0, sub=0, intValue=130000)
osPatchLevel = OsPatchLevel(year=2025, month=MAY, intValue=202505)
attestationApplicationId = null
attestationIdBrand = null
attestationIdDevice = null
attestationIdProduct = null
attestationIdSerial = null
attestationIdImei = null
attestationIdMeid = null
attestationIdManufacturer = null
attestationIdModel = null
vendorPatchLevel = PatchLevel(year=2025, month=MAY, day=1, intValue=20250501)
bootPatchLevel = PatchLevel(year=2025, month=MAY, day=1, intValue=20250501)
deviceUniqueAttestation = false
attestationIdSecondImei = null
moduleHash = null
)
)