AAuth wire format and verification
This page is the canonical reference for Neotoma's AAuth wire format, signature verification rules, and trust-tier derivation. It is the upstream spec for the verifier in src/middleware/aauth_verify.ts, the agent-identity resolution in src/crypto/agent_identity.ts, and the attribution-policy seam in src/services/attribution_policy.ts.
For an integrator-oriented walkthrough see the integration guide. Cryptographic attestation envelopes (Apple SE, WebAuthn-packed, TPM 2.0) are specified separately on the attestation page; CLI keygen and hardware backends are on CLI keys.
What AAuth is (and is not)
- A writing agent owns a stable keypair (software or hardware-backed) and an
aa-agent+jwttoken carryingiss,sub, and acnf.jwkconfirmation key. - The agent signs each HTTP request per RFC 9421. Neotoma verifies the signature, derives a
trust_tier, and stamps(agent_thumbprint, agent_sub, agent_iss, trust_tier, …)onto every durable row. - Bearer tokens, OAuth, and MCP
connection_idcontinue to resolve the humanuser_id. AAuth never bypasses user-scope resolution. - When AAuth is absent, Neotoma falls back to MCP
clientInfoorX-Client-Name/X-Client-VersionHTTP headers as a self-reported attribution channel. Self-reported channels never reach thehardware,operator_attested, orsoftwaretiers, the highest they earn isunverified_client.
Wire format
Neotoma identifies a writing agent over two channels, in precedence order.
1. AAuth (signed request)
The request carries:
Signature, the signature bytes (RFC 9421).Signature-Input, signature parameters. MUST cover, at minimum:@authority,@method,@target-uri,content-digest(when the request has a body), and thesignature-keyheader itself.Signature-Key, the agent's JWK plus anaa-agent+jwtagent token. The JWT carriestyp: "aa-agent+jwt",iss,sub,iat,cnf.jwk, and optionallycnf.attestation.
Verification runs against the canonical authority configured via NEOTOMA_AUTH_AUTHORITY (defaults to the local dev host). The authority value MUST match the server's canonical host, using the request Host header is explicitly unsafe and is rejected.
2. MCP clientInfo fallback
On initialize the MCP transport self-reports { name, version }. Generic names (mcp, client, mcp-client, unknown, anonymous, …) are dropped through normaliseClientNameWithReason and treated as if clientInfo were absent. Non-MCP HTTP callers can pass the same information via the X-Client-Name and X-Client-Version headers.
A successful AAuth verification populates agent_thumbprint (RFC 7638), agent_sub, agent_iss, agent_algorithm, and agent_public_key. A populated clientInfo populates client_name and client_version. Both halves are persisted; the cascade below chooses the trust tier.
Verification rules
Performed by aauth_verify middleware:
- Parse
Signature-Inputand reject if any required component is missing. - Resolve
@authorityagainstNEOTOMA_AUTH_AUTHORITY. Mismatch fails withauthority_mismatch. - Recompute
content-digest(when the request has a body) and compare to the header. Mismatch fails withdigest_mismatch. - Parse the
Signature-Keyheader into JWK + JWT. Reject iftypis notaa-agent+jwtor the JWT is malformed. - Verify the JWT signature against the embedded JWK. The JWT MUST bind
cnf.jwkto the same key used to sign the request. - Check
iatagainst the configured AAuth clock-skew window (NEOTOMA_AUTH_AGENT_TOKEN_MAX_AGE_S, default 300 s). - Verify the request signature against the JWK. Failure produces
signature_invalid. - If
cnf.attestationis present, dispatch to the attestation verifier and capture the per-formatattestation_outcome.
Verifier failures never reject the request when the underlying signature is valid; they only prevent tier promotion. The attribution.decision block on GET /session records the failure reason so operators can debug from the Inspector.
Trust tiers
A single enum is stamped onto every durable row. Tier resolution is performed once per request, services and clients MUST read the already-resolved AgentIdentity.trust_tier from the per-request context and never re-derive it.
hardware, AAuth verified AND the JWT carries acnf.attestationthe verifier accepts AND (v0.12.0+) the bound key is not revoked.operator_attested, AAuth verified ANDiss(oriss:sub) is in the operator allowlist.software, AAuth verified, but no attestation envelope (or attestation failed and operator allowlist did not match), regardless of signing algorithm.unverified_client, No AAuth, butclientInfo.name(orX-Client-Name) survived normalisation.anonymous, Nothing distinctive: generic or absentclientInfo, no AAuth, no fallback header.
Verification cascade
Request ├── AAuth Signature header? │ no → non-generic clientInfo or X-Client-Name? │ yes → unverified_client │ no → anonymous │ yes → signature verifies? │ no → fall through to clientInfo channel │ yes → cnf.attestation present? │ yes → verifier accepts? │ yes → key not revoked? │ yes → hardware │ no → software │ no → iss / iss:sub in operator allowlist? │ yes → operator_attested │ no → software │ no → iss / iss:sub in operator allowlist? │ yes → operator_attested │ no → software
Per-request precedence
For each request Neotoma walks these inputs in order; the first populated field at each layer wins:
AAuth (verified signature + JWT) → agent_thumbprint, agent_sub, agent_iss,
agent_algorithm, agent_public_key
│
▼
clientInfo.name + version → client_name, client_version
│
▼
X-Client-Name + X-Client-Version → client_name, client_version
│
▼
OAuth connection id → connection_id
│
▼
(nothing) → anonymousBearer tokens resolve only user_id; they do not mint an attribution tier above anonymous on their own.
Operator policy
The server publishes the active policy on GET /session under policy:
NEOTOMA_ATTRIBUTION_POLICY(allow|warn|reject; defaultallow), global behaviour foranonymouswrites.NEOTOMA_MIN_ATTRIBUTION_TIER(hardware|software|unverified_client), minimum tier required.NEOTOMA_ATTRIBUTION_POLICY_JSON, per-path overrides, e.g.{"observations":"reject"}.
Behaviour: reject returns HTTP 403 ATTRIBUTION_REQUIRED with min_tier and current_tier in the error envelope. warn stamps an X-Neotoma-Attribution-Warning response header and emits an attribution_decision log line, but completes the write. allow is silent. min_tier and per_path compose: a per-path reject always wins over the global allow.
Per-agent fine-grained capability scoping ((op, entity_type) allow-lists) is layered on top, see agent capabilities.
Diagnostic surface
Every AAuth verification emits a structured attribution_decision log event with at least:
signature_present, was aSignatureheader on the request?signature_verified, did the cryptographic check pass?signature_error_code, when applicable: why verification failed (authority_mismatch,digest_mismatch,signature_invalid,agent_token_expired,unsupported_algorithm, …).attestation_outcome, when present: result of the per-format verifier (verified,format_unsupported,key_binding_failed,challenge_mismatch,chain_invalid, …).revocation_outcome(v0.12.0+),not_checked,live,revoked, orerror_skipped.resolved_tier, final tier as stamped onto the request context.
The same fields are exposed on GET /session under attribution.decision so client preflight tooling can surface them without scraping logs.
Transport parity
The same identity contract is threaded through every transport:
- HTTP
/mcp(MCP-over-HTTP). - Direct REST routes (
/store,/observations/create,/create_relationship,/correct,/session, …). - MCP stdio.
- CLI-over-MCP and CLI-over-HTTP.
Per-transport notes live on the integration page.
Back to AAuth overview.