<!--
Full-page Markdown export (rendered HTML → GFM).
Source: https://neotoma.io/de/aauth/attestation
Generated: 2026-04-27T12:48:51.650Z
-->
# 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](/aauth/spec). For CLI-side attestation generation see [CLI keys](/aauth/cli-keys).
## Verification cascade
Server-side tier resolution after the AAuth signature is verified:
1. If the JWT carries `cnf.attestation` AND the verifier returns `{ verified: true }`, resolve to `hardware`.
2. Else, if the verified `iss` (or `iss:sub` composite) is in the operator allowlist (`NEOTOMA_OPERATOR_ATTESTED_ISSUERS` / `NEOTOMA_OPERATOR_ATTESTED_SUBS`), resolve to `operator_attested`.
3. 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 of `apple-secure-enclave`, `webauthn-packed`, `tpm2`. Unknown values fail with `unsupported_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 with `challenge_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:
1. Parse statement, extract credential public key.
2. Compare its RFC 7638 thumbprint to the JWT's `cnf.jwk` thumbprint. Mismatch → `key_binding_failed`.
3. Recompute challenge from JWT claims and compare to the value in the statement. Mismatch → `challenge_mismatch`.
4. 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 via `aauth-mac-se` on darwin; trust root bundled at `config/aauth/apple_attestation_root.pem`; revocation via Apple's anonymous-attestation revocation endpoint.
- **`webauthn-packed`** , verifier ships in v0.9.0; CLI sources via `aauth-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 via `aauth-tpm2` (linux) and `aauth-win-tbs` (win32) from v0.10.0; trust roots bundled in `config/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 over `SHA-256(challenge || jkt)` where `jkt` is the RFC 7638 thumbprint of the credential public key.
After the shared key-binding and challenge checks, the verifier:
1. Decodes the chain. Rejects if the leaf does not declare an EC P-256 public key.
2. 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.
3. 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:
1. Parse and validate the statement. `malformed` on missing fields; `unsupported_format` for ECDAA-only attestations (no `x5c`) or COSE algs outside the v0.9.0 admission set.
2. Resolve COSE `alg` to a Node crypto primitive. Supported values: `-7` (ES256), `-35` (ES384), `-36` (ES512), `-8` (EdDSA), `-257` (RS256), `-258` (RS384), `-259` (RS512), `-37` (PS256).
3. Walk the leaf → intermediates chain against the merged trust roots from `aauth_attestation_trust_config.ts`. Untrusted chains return `chain_invalid`.
4. Extract the FIDO `id-fido-gen-ce-aaguid` extension (`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 return `aaguid_not_trusted`. When empty, AAGUID is logged but not gated, so operators can ramp gradually.
5. Bind the leaf's public key to the JWT-bound `cnf.jwk` via RFC 7638 thumbprint. Mismatches return `key_binding_failed`.
6. 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:
1. Parse the statement. Missing or non-string fields → `malformed`. Unsupported `ver` → `unsupported_format`.
2. Resolve the COSE `alg` to a Node crypto digest + primitive. Unsupported algs return `signature_invalid` rather than throwing so the cascade keeps moving.
3. Decode the `x5c` chain into `X509Certificate` objects. Decode failures or missing certs → `chain_invalid`.
4. Parse `pubArea` as `TPMT_PUBLIC` and lift it into a Node `KeyObject` (RSA `(n, e)` and ECC P-256/P-384/P-521 supported). Truncated or unsupported key types → `malformed`.
5. RFC 7638 thumbprint of the lifted public key MUST equal `ctx.boundJkt`. Mismatches return `key_binding_failed`.
6. Walk the AIK chain against the merged trust set. Roots come from `config/aauth/tpm_attestation_roots/` plus operator PEMs from `NEOTOMA_AAUTH_ATTESTATION_CA_PATH`. Untrusted chains return `chain_invalid`.
7. Verify `sig` over the raw `certInfo` bytes using the AIK leaf's public key. Mismatches return `signature_invalid`.
8. Parse `certInfo` as `TPMS_ATTEST`. Magic MUST be `TPM_GENERATED_VALUE` (`0xff544347`) and type MUST be `TPM_ST_ATTEST_QUOTE` (`0x8018`) or `TPM_ST_ATTEST_CERTIFY` (`0x8017`); other shapes → `malformed`.
9. `extraData` MUST equal `SHA-256(challenge || jkt)`. Mismatches return `challenge_mismatch`.
10. For `TPM_ST_ATTEST_CERTIFY` only, the certified `attested.name` MUST equal `nameAlg || digest(pubArea)`. Mismatches return `pubarea_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 via `NEOTOMA_AAUTH_APPLE_REVOCATION_URL`). POST `{ "serial_numbers": [...] }`; returns `{ "revoked": [...] }`. Cache keyed by `SHA-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`** , a `revoked` status (and `unknown` when `NEOTOMA_AAUTH_REVOCATION_FAIL_OPEN=0`) demotes a previously-verified outcome to `{ verified: false, reason: "revoked"... }`. The cascade then falls through to the operator allowlist or `software` tier exactly as it does for any other `verified: false` reason.
### 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 demote `hardware` to `software`. Operators who need to fall back can pin `NEOTOMA_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 the `tpm2` verifier. Vendor sub-directories document provenance per `README.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 the `webauthn-packed` verifier admits. Empty/missing file = no AAGUID gating.
- **`NEOTOMA_OPERATOR_ATTESTED_ISSUERS`** , CSV of `iss` values. Promotes verified AAuth signatures whose `iss` matches to `operator_attested`.
- **`NEOTOMA_OPERATOR_ATTESTED_SUBS`** , CSV of `iss:sub` composite 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](/aauth). See also [AAuth spec](/aauth/spec), [CLI keys](/aauth/cli-keys), [integration](/aauth/integration).