AAuth attestation
Attestation is how Neotoma cryptographically verifies that an AAuth-signing key is bound to a hardware root of trust before promoting a request to the hardware tier. This page specifies the JSON-native cnf.attestation envelope, the per-format verifiers, the verification cascade, and the operator configuration knobs that select which roots are trusted.
For the wider AAuth wire format and trust-tier definitions see the AAuth spec. For CLI-side attestation generation see CLI keys.
Verification cascade
Server-side tier resolution after the AAuth signature is verified:
- If the JWT carries
cnf.attestationAND the verifier returns{ verified: true }, resolve tohardware. - Else, if the verified
iss(oriss:subcomposite) is in the operator allowlist (NEOTOMA_OPERATOR_ATTESTED_ISSUERS/NEOTOMA_OPERATOR_ATTESTED_SUBS), resolve tooperator_attested. - Else, resolve to
software.
Verifier failures (chain invalid, key not bound, format unsupported) MUST fall through rather than rejecting the request, the underlying signature is still valid, the client just does not earn the higher tier. The decision.attestation diagnostic block records the verifier reason so operators can debug failed promotions.
cnf.attestation envelope
JSON-native (no CBOR). Lives inside the cnf claim of the aa-agent+jwt agent token alongside cnf.jwk. Discriminator-routed so the same envelope shape carries every supported format.
{
"cnf": {
"jwk": { "kty": "EC", "crv": "P-256", "x": "...", "y": "..." },
"attestation": {
"format": "apple-secure-enclave",
"statement": {
"attestation_chain": ["<base64url DER>", "<base64url DER>"],
"signature": "<base64url over SHA-256(challenge || jkt)>"
},
"challenge": "<base64url SHA-256(jwt.iss || jwt.sub || jwt.iat)>"
}
}
}Field rules
format(string, required), discriminator. One ofapple-secure-enclave,webauthn-packed,tpm2. Unknown values fail withunsupported_format.statement(object, required), opaque to the envelope; the per-format verifier defines the inner shape.challenge(base64url, required), server recomputes from JWT claims (SHA-256(iss || sub || iat)) and compares against the per-format statement. Mismatch fails withchallenge_mismatch.
Key-binding rule
The credential public key extracted from statement MUST match cnf.jwk by RFC 7638 thumbprint. Without this binding, an attacker who replayed a leaked attestation could ride a different signing key past the verifier. Mismatch fails with key_binding_failed.
Verifier responsibility ordering inside one format dispatch:
- Parse statement, extract credential public key.
- Compare its RFC 7638 thumbprint to the JWT's
cnf.jwkthumbprint. Mismatch →key_binding_failed. - Recompute challenge from JWT claims and compare to the value in the statement. Mismatch →
challenge_mismatch. - Verify the format-specific cryptographic chain / quote.
Supported formats
After v0.12.0 every attestation format is fully verified end-to-end and participates in revocation under the policy described below.
apple-secure-enclave, verifier ships in v0.8.0; CLI sources viaaauth-mac-seon darwin; trust root bundled atconfig/aauth/apple_attestation_root.pem; revocation via Apple's anonymous-attestation revocation endpoint.webauthn-packed, verifier ships in v0.9.0; CLI sources viaaauth-yubikey(cross-platform, v0.10.x); trust = AAGUID allowlist plus operator CA bundle; revocation via OCSP from the leaf's AIA, CRL fallback via the leaf's CDP.tpm2, verifier ships in v0.9.0; CLI sources viaaauth-tpm2(linux) andaauth-win-tbs(win32) from v0.10.0; trust roots bundled inconfig/aauth/tpm_attestation_roots/; revocation via OCSP from the AIK leaf's AIA, CRL fallback via the AIK leaf's CDP.
apple-secure-enclave
Implemented by src/services/aauth_attestation_apple_se.ts. Statement carries:
attestation_chain, base64url-encoded DER X.509 certificates, leaf first, terminating at an Apple-rooted intermediate.signature, base64url-encoded ECDSA-P256 signature overSHA-256(challenge || jkt)wherejktis the RFC 7638 thumbprint of the credential public key.
After the shared key-binding and challenge checks, the verifier:
- Decodes the chain. Rejects if the leaf does not declare an EC P-256 public key.
- Walks the chain with
node:crypto.X509Certificate#verify(issuerKey). Rejects (chain_invalid) on any signature failure or if the chain terminates outside the merged trust set. - Verifies the leaf's ECDSA signature over
SHA-256(challenge || jkt). Rejects (signature_invalid) on mismatch.
webauthn-packed
Implemented in src/services/aauth_attestation_webauthn_packed.ts. Statement layout mirrors W3C WebAuthn §8.2:
{
"alg": -7,
"sig": "<base64url DER signature>",
"x5c": ["<base64url leaf>", "<base64url intermediates...>"]
}Verifier flow:
- Parse and validate the statement.
malformedon missing fields;unsupported_formatfor ECDAA-only attestations (nox5c) or COSE algs outside the v0.9.0 admission set. - Resolve COSE
algto a Node crypto primitive. Supported values:-7(ES256),-35(ES384),-36(ES512),-8(EdDSA),-257(RS256),-258(RS384),-259(RS512),-37(PS256). - Walk the leaf → intermediates chain against the merged trust roots from
aauth_attestation_trust_config.ts. Untrusted chains returnchain_invalid. - Extract the FIDO
id-fido-gen-ce-aaguidextension (1.3.6.1.4.1.45724.1.1.4) using a hand-written DER parser. When the operator allowlist (NEOTOMA_AAUTH_AAGUID_TRUST_LIST_PATH) is non-empty, the parsed AAGUID MUST match an entry; mismatches returnaaguid_not_trusted. When empty, AAGUID is logged but not gated, so operators can ramp gradually. - Bind the leaf's public key to the JWT-bound
cnf.jwkvia RFC 7638 thumbprint. Mismatches returnkey_binding_failed. - Verify the statement signature over the bound challenge using the resolved primitive. Mismatches return
signature_invalid.
Successful runs return verified: true with aaguid and key_model populated, plus the human-readable trust-chain summary.
tpm2
Verifies WebAuthn tpm attestation statements (TPM 2.0 quotes + AIK chains). Implementation in src/services/aauth_attestation_tpm2.ts; uses the in-repo big-endian length-prefixed parser at src/services/aauth_tpm_structures.ts (no external TPM library, so the parsing surface remains auditable in TypeScript).
{
"ver": "2.0",
"alg": -7,
"x5c": ["…AIK leaf…", "…intermediates…", "…root…"],
"sig": "…raw AIK signature over certInfo bytes…",
"certInfo": "…raw TPMS_ATTEST bytes…",
"pubArea": "…raw TPMT_PUBLIC bytes describing the bound key…"
}Pipeline:
- Parse the statement. Missing or non-string fields →
malformed. Unsupportedver→unsupported_format. - Resolve the COSE
algto a Node crypto digest + primitive. Unsupported algs returnsignature_invalidrather than throwing so the cascade keeps moving. - Decode the
x5cchain intoX509Certificateobjects. Decode failures or missing certs →chain_invalid. - Parse
pubAreaasTPMT_PUBLICand lift it into a NodeKeyObject(RSA(n, e)and ECC P-256/P-384/P-521 supported). Truncated or unsupported key types →malformed. - RFC 7638 thumbprint of the lifted public key MUST equal
ctx.boundJkt. Mismatches returnkey_binding_failed. - Walk the AIK chain against the merged trust set. Roots come from
config/aauth/tpm_attestation_roots/plus operator PEMs fromNEOTOMA_AAUTH_ATTESTATION_CA_PATH. Untrusted chains returnchain_invalid. - Verify
sigover the rawcertInfobytes using the AIK leaf's public key. Mismatches returnsignature_invalid. - Parse
certInfoasTPMS_ATTEST. Magic MUST beTPM_GENERATED_VALUE(0xff544347) and type MUST beTPM_ST_ATTEST_QUOTE(0x8018) orTPM_ST_ATTEST_CERTIFY(0x8017); other shapes →malformed. extraDataMUST equalSHA-256(challenge || jkt). Mismatches returnchallenge_mismatch.- For
TPM_ST_ATTEST_CERTIFYonly, the certifiedattested.nameMUST equalnameAlg || digest(pubArea). Mismatches returnpubarea_mismatch, this catches CLI sources that quote a different key than the one they signed.
Revocation
Implemented in src/services/aauth_attestation_revocation.ts. After every successful chain validation the per-format verifier consults the shared revocation service and folds the result back into the AttestationOutcome via applyRevocationPolicy().
Channels
- apple-secure-enclave, Apple anonymous-attestation revocation endpoint (
https://data.appattest.apple.com/v1/revoked-list, override viaNEOTOMA_AAUTH_APPLE_REVOCATION_URL). POST{ "serial_numbers": [...] }; returns{ "revoked": [...] }. Cache keyed bySHA-256(leaf DER). - webauthn-packed, OCSP via the leaf's AIA, CRL fallback via the leaf's CDP. Standard X.509 path used by YubiKey and external authenticators.
- tpm2, OCSP via the AIK leaf's AIA, CRL fallback via the AIK leaf's CDP. Vendor AIK chains (Infineon, STMicro, Intel, AMD, Microsoft) consistently advertise OCSP responders.
Lookups short-circuit when NEOTOMA_AAUTH_REVOCATION_MODE is disabled , the verifier never opens a network socket and the diagnostic stays absent. In log_only and enforce modes the service runs and the diagnostic rides on the outcome regardless of verification result.
Policy
disabled, diagnostic omitted; outcome forwarded as-is.log_only, diagnostic attached (checked: true,mode: "log_only",demoted: false) but never demotes the tier. Operator-audit window before the v0.12.0 flip.enforce, arevokedstatus (andunknownwhenNEOTOMA_AAUTH_REVOCATION_FAIL_OPEN=0) demotes a previously-verified outcome to{ verified: false, reason: "revoked"... }. The cascade then falls through to the operator allowlist orsoftwaretier exactly as it does for any otherverified: falsereason.
Caching
In-memory LRU keyed by SHA-256(leaf DER fingerprint || channel). TTL via NEOTOMA_AAUTH_REVOCATION_CACHE_TTL_SECONDS (default 3600). Cache hits surface as source: "cache". Process-local; restarts re-validate every chain through the upstream channel before re-populating.
Diagnostics
interface AttestationRevocationDiagnostic {
checked: boolean; // false when mode === "disabled"
status?: "good" | "revoked" | "unknown";
source?: "disabled" | "cache" | "apple" | "ocsp" | "crl" | "no_endpoint" | "error";
detail?: string; // free-form responder/network detail
mode?: "disabled" | "log_only" | "enforce";
demoted?: boolean; // true iff enforce mode demoted hardware → software
}Migration plan
- v0.10.x, default
disabled. No network calls, no diagnostic. Pre-FU-7 behaviour. - v0.11.0, default
log_only. Network calls run, diagnostic surfaces, tiers unchanged. Operators audit before the flip. - v0.12.0, default
enforce. Revoked / fail-closed-unknown demotehardwaretosoftware. Operators who need to fall back can pinNEOTOMA_AAUTH_REVOCATION_MODE=log_only.
Trust configuration
Loaded by src/services/aauth_attestation_trust_config.ts. Always includes the bundled Apple Attestation Root; operator inputs are additive. The trust loader is fail-open: missing or unreadable operator inputs log a single warning and continue with the bundled root.
NEOTOMA_AAUTH_ATTESTATION_CA_PATH, absolute path to a PEM file or directory of PEM files. Adds operator-managed CAs to the merged trust set used for chain validation.config/aauth/tpm_attestation_roots/, bundled TPM 2.0 AIK root CAs (.pem/.crt, recursive). Always merged into the trust set for thetpm2verifier. Vendor sub-directories document provenance perREADME.md.NEOTOMA_AAUTH_AAGUID_TRUST_LIST_PATH, absolute path to a JSON file containing an array of WebAuthn AAGUIDs (RFC 4122 lower-case hyphenated). Restricts which authenticator AAGUIDs thewebauthn-packedverifier admits. Empty/missing file = no AAGUID gating.NEOTOMA_OPERATOR_ATTESTED_ISSUERS, CSV ofissvalues. Promotes verified AAuth signatures whoseissmatches tooperator_attested.NEOTOMA_OPERATOR_ATTESTED_SUBS, CSV ofiss:subcomposite values. Same as above but pinned to a specific(iss, sub)pair.
config/aauth/apple_attestation_root.pem ships in-repo. The first lines of the file MUST be a comment referencing the Apple documentation page the cert was sourced from and the SHA-256 fingerprint, so operators can audit the bundled material at a glance.
Diagnostics surface
The verifier returns a structured outcome:
type AttestationOutcome =
| { verified: true; format: AttestationFormat; revocation?: AttestationRevocationDiagnostic }
| { verified: false; format: AttestationFormat | "unknown"; reason: AttestationFailureReason; revocation?: AttestationRevocationDiagnostic };
type AttestationFailureReason =
| "not_present"
| "unsupported_format"
| "key_binding_failed"
| "challenge_mismatch"
| "chain_invalid"
| "signature_invalid"
| "aaguid_not_trusted"
| "pubarea_mismatch"
| "not_implemented"
| "malformed"
| "revoked";The middleware mirrors this onto AttributionDecisionDiagnostics.attestation so operators can see exactly why a promotion failed without log-spelunking. not_present means the JWT did not carry cnf.attestation at all (the cascade then evaluates the operator allowlist). revoked is populated by the revocation service in enforce mode. AttributionDecisionDiagnostics.attestation.revocation carries the underlying status, channel, mode, and demoted flag.
Back to AAuth overview. See also AAuth spec, CLI keys, integration.