Error Handling
Project Structure
See project structure details for info on components, their names, functionality, and interdependencies.
Attestation has many expected failure modes. Makoto, the unified server-side Android and iOS verifier at the core of Warden Supreme, groups them into semantic categories that can be handled consistently across platforms.
In the integrated flow, malformed proofs and invalid challenges may be rejected before Makoto receives the platform attestation statement.
Back-end integrators should understand where failures occur even when using the integrated verifier and client. This is necessary to define opaque client-facing error codes and diagnose failures observed in the field.
This page first describes server-side errors and then maps them to the categories communicated to clients.
Server-Side (Low-Level Errors)
The low-level hierarchy covers failures from platform verification. Pre-attestation errors cover checks performed by the Supreme verifier before Makoto receives the platform evidence.
Attestation Error Hierarchy
Makoto uses exceptions because navigable stack traces are valuable during diagnosis. All attestation failures derive
from the sealed AttestationException class, whose subclasses provide the semantic categories.
Every such exception features:
- a
platformproperty indicating whether an iOS or Android attestation failed to verify - a nullable
messageproviding human-readable debugging context rather than an end-user message - a
causecarrying the underlying platform-specific exception
On a high level, three different categories of attestation errors exist across iOS and Android:
- Certificate errors
- Trust
- Time
- Configuration errors
- May be thrown during initialisation. Example reasons include:
- No apps were configured
- Negative validity duration was specified
- Neither iOS nor Android attestation was setup
- Illegal team identifier for iOS attestation
- Thrown when an attestation statement is received for a platform that is not configured (i.e. only iOS attestation is configured, but an Android attestation statement is received or vice versa).
- May be thrown during initialisation. Example reasons include:
- Invalid attestation statement contents
- Most of the time containing platform specifics
- Also thrown for nonsensical/invalid inputs
The snippet below shows the attestation exceptions that may be thrown. Its annotations explain each case.
Note that the onAttestationError callback is side-effect-free except that it allows for returning a (nullable) string
to customise the error message/error code conveyed to the client.
val result = verifier.verifyAttestation(
csr = csr,
onAttestationError = { debugStatement ->
/*(1)!*/logger.log(Level.WARNING, debugStatement.serializeCompact(), cause)
when (cause) {
/*(2)!*/is AttestationException.Certificate.Time -> TODO()
/*(3)!*/is AttestationException.Certificate.Trust -> TODO()
/*(4)!*/is AttestationException.Configuration -> TODO()
is AttestationException.Content.Android ->
when ((cause as AttestationException.Content.Android).cause.reason) {
/*(5)!*/AttestationValueException.Reason.OS_VERSION -> TODO()
/*(6)!*/AttestationValueException.Reason.STATEMENT_TIME -> TODO()
/*(7)!*/AttestationValueException.Reason.CHALLENGE -> TODO()
/*(8)!*/AttestationValueException.Reason.PACKAGE_NAME -> TODO()
/*(9)!*/AttestationValueException.Reason.APP_SIGNER_DIGEST -> TODO()
/*(10)!*/AttestationValueException.Reason.APP_VERSION -> TODO()
/*(11)!*/AttestationValueException.Reason.ROLLBACK_RESISTANCE -> TODO()
/*(12)!*/AttestationValueException.Reason.SEC_LEVEL -> TODO()
/*(13)!*/AttestationValueException.Reason.SYSTEM_INTEGRITY -> TODO()
/*(14)!*/AttestationValueException.Reason.APP_UNEXPECTED -> TODO()
}
is AttestationException.Content.iOS ->
when ((cause as AttestationException.Content.iOS).cause.reason) {
/*(15)!*/IosAttestationException.Reason.OS_VERSION -> TODO()
/*(16)!*/IosAttestationException.Reason.STATEMENT_TIME -> TODO()
/*(17)!*/IosAttestationException.Reason.CHALLENGE -> TODO()
/*(18)!*/IosAttestationException.Reason.IDENTIFIER -> TODO()
/*(19)!*/IosAttestationException.Reason.SIG_CTR -> TODO()
/*(20)!*/IosAttestationException.Reason.APP_UNEXPECTED -> TODO()
}
/*(21)!*/is AttestationException.Content.Unknown -> TODO("Unsupported Input")
}
}
) { TODO("Refer to minimum example for certificate issuance") }
- Refer to Debugging
- Certificate is not yet valid or expired. Clock drift is the main source for this error.
- An untrusted root certificate was encountered. E.g., an Android Emulator was used in production.
- Thrown when an attestation statement is received for a platform that is not configured.
- The client OS is too old (with respect to the configured minimum OS version)
- The attestation statement creation timestamp (not the certificate validity!) is too far in the past or absent.
Warden Supreme's defaults account for ordinary clock drift. - The challenge encoded into the attestation statement payload does not match the expected challenge.
- The app's package name does not match the expected package name.
I.e., an unauthorised app is trying to attest to the back-end. - The app was signed with an unknown key.
Could be an indicator for a repackaging attack. - The client app is too old (i.e., minimum version constraint not fulfilled).
- Rollback resistance was enforced, but the client device is not rollback-resistant.
- In theory, this will happen when the attestation or keymaster security level does not match the expected level
(e.g. hardware attestation is enforced, but an emulator is trying to attest). In practice, however, this will never
occur because hardware and software attestation use different trust anchors.
Hence, an
AttestationException.Certificate.Trustis thrown before this check can even be triggered. - A client's bootloader lock state or verified boot state is unlocked/unverified, even though the attestation policy expects a locked bootloader and a factory image.
- This usually indicates a structural error in the attestation statement and therefore requires manual debugging to make sense of.
- Minimum iOS version/build number not satisfied.
- The attestation statement creation timestamp (not the certificate validity!) is too far in the past. This is usually due to a clock drift between client and server.
The Supreme Verifier prevents the client from even attempting to send an attestation, as clock drift detection is implemented as client-side functionality. - The challenge encoded into the attestation statement payload does not match the expected challenge.
- The team ID and/or bundle identifier and/or stage (sandbox vs. production) of the client app do not match.
- The signature counter encoded into the assertion is too high. See iOS technical deep dive.
- This usually indicates a structural error in the attestation statement and therefore requires manual debugging to make sense of.
- This is usually triggered by structurally invalid input, such as an empty proof or CSR or misencoded certificates, and requires manual debugging. It has not occurred in production with a legitimate client app.
Debugging
Refer to Debugging for detailed information and guidance on debugging.
All platform-specific exceptions are contained in the AttestationException.Content hierarchy. Applications rarely
need this detail when reacting to an error, but it is essential for diagnosis.
In particular, the call in line 4 will produce a log entry with a self-contained, replayable attestation call for
offline analysis.
Pre-Attestation Errors
When using fully integrated attestation, preprocessing steps are automatically performed to extract, check, and invalidate
the received challenge, parse the signed CSR or unsigned TBS CSR, extract the attestation statement, validate the selected
authentication mode and canonical CSR structure, match the attested public key, and decode requested attributes.
Arbitrary input can fail any of these steps. The following snippet shows how to handle pre-attestation errors.
Note that the onPreAttestationError callback is side-effect-free except that it allows for returning a (nullable) string
to customise the error message/error code conveyed to the client.
val result = verifier.verifyAttestation(
attestationProof = proof,
onPreAttestationError = {
when(this) {
/*(1)!*/is PreAttestationError.AttestationStatementExtraction -> TODO()
is PreAttestationError.AugmentedAttestationStatementExtraction -> TODO()
/*(2)!*/is PreAttestationError.ChallengeExtraction -> TODO()
/*(3)!*/is PreAttestationError.ChallengeVerification -> TODO()
/*(4)!*/is PreAttestationError.ClientDataValidation -> when (reason) {
PreAttestationError.ClientDataValidation.Reason.AUTHENTICATION_METHOD_MISMATCH -> TODO()
PreAttestationError.ClientDataValidation.Reason.DUPLICATE_CSR_ATTRIBUTE_OID -> TODO()
PreAttestationError.ClientDataValidation.Reason.MALFORMED_CSR_EXTENSION_REQUEST -> TODO()
PreAttestationError.ClientDataValidation.Reason.DUPLICATE_CSR_EXTENSION_OID -> TODO()
PreAttestationError.ClientDataValidation.Reason.NON_CANONICAL_CSR_ATTRIBUTE_ORDER -> TODO()
PreAttestationError.ClientDataValidation.Reason.ATTESTATION_BINDING -> TODO()
PreAttestationError.ClientDataValidation.Reason.ATTESTED_PUBLIC_KEY_MISMATCH -> TODO()
PreAttestationError.ClientDataValidation.Reason.REQUESTED_ATTRIBUTES_EXTRACTION -> TODO()
PreAttestationError.ClientDataValidation.Reason.REQUESTED_ATTRIBUTES_MISMATCH -> TODO()
}
/*(3)!*/is PreAttestationError.OperationalError -> TODO()
}
}
) { TODO("Refer to minimum example for certificate issuance") }
- The attestation statement could not be extracted from the received transport. New code receives
AugmentedAttestationStatementExtraction, which containsAttestationProof; the CSR-only class is retained for the deprecated verifier overload. - The nonce/challenge could not be extracted from the received TBS CSR.
- Challenge verification or an operational pre-attestation step failed.
ClientDataValidationexposes a precisereason, the received transport, the validated challenge, and the underlying throwable. Reasons cover authentication-mode mismatch, duplicate attribute/extension OIDs, malformed extension requests, non-canonical attribute order, hash binding, public-key mismatch, and requested-attribute extraction/matching.
The callback's non-null return value becomes the client-facing explanation. Exceptions thrown by this observation callback are ignored and the verifier uses its safe fallback explanation.
Client-Side (Generic, High-Level Error Categories)
Using fully integrated attestation only ever returns either an AttestationResponse.Success
or an AttestationResponse.Failure. The latter indicates one of four error reasons:
TRUSTencompassing untrusted roots, revoked certificates, invalid certificate chains, invalid CSR signatures, a public key that does not match the attested key, or a transport that does not provide the authentication mode required by the challengeTIMEencompassing temporal validity errors with respect to certificates and attestation statementsCONTENTencompassing cases where the attestation statement fails to parse or verify against policy, and invalid proof transport content such as ambiguous/malformed CSR attributes or missing/invalid requested attributesINTERNALencompassing errors on a more fundamental level, such as a structurally valid CSR, but using unsupported signature algorithms, for example, or outright implementation issues in Warden Supreme.
At no point are exceptions related to attestations transmitted to the client.
Instead, a nullable explanation string property is present, which can be used to convey context and/or error codes and
all server-side exceptions are automatically mapped to one of those four error types based on their semantics.