Date: 2026-04-26
Tested against: sandbox.neotoma.io (v0.7.1)
Agent: aauth:local@dickhardt.github.io (Secure Enclave ES256 key)
Packages: @aauth/fetch 0.8.1, @aauth/local-keys 0.8.0, @hellocoop/httpsig 1.5.0
| Test | Result |
|---|---|
Discovery (/.well-known/aauth-resource.json) |
✅ Live, returns valid JSON |
Unsigned request → AAUTH_REQUIRED (401) |
✅ Correct |
Signed write to /sandbox/aauth-only/store |
✅ 200 — entity created, attributed |
Agent identity (agent_sub, agent_iss) |
✅ Correctly resolved from agent token |
| Signature verification | ✅ Neotoma extracts ephemeral key from cnf.jwk |
| Tier detection | ✅ "software" (correct — no hardware attestation) |
The core protocol chain — key → agent token → HTTP signature → verification → attribution — is fully operational end-to-end.
Every aauth-fetch CLI invocation generates a new ephemeral keypair (per the spec — agents rotate ephemeral keys). The agent token wraps the new ephemeral public key in cnf.jwk, signed by the durable root key. This is correct per spec.
The problem: Neotoma partitions user identity by sha256("aauth:<thumbprint>") where <thumbprint> is the ephemeral key's JWK Thumbprint. Since each invocation has a new ephemeral key:
- Write succeeds → entity stored under
user_idderived from thumbprint A - Read (next invocation) → new ephemeral key → thumbprint B → different
user_id�� 404
I confirmed this with three successive /session calls — each returned a different agent_thumbprint and user_id:
Call 1: agent_thumbprint: "Rx1E5ie_...", user_id: "aeb71ce5-..."
Call 2: agent_thumbprint: "F30g7nHx...", user_id: "5f719d59-..."
Call 3: agent_thumbprint: "IUJ_gqnj...", user_id: "a0f24e8b-..."
What the spec says (Section 5, "Agent Identifiers"):
sub: Agent identifier (stable across key rotations)
The agent identifier aauth:local@dickhardt.github.io is the stable identity anchor. Ephemeral keys are proof-of-possession for a single session — they are designed to rotate frequently.
Fix: Partition by agent_sub (from the verified agent token), or by the root key kid resolved via the published JWKS at {iss}/.well-known/jwks.json. The ephemeral thumbprint should NOT be used as a stable identity anchor.
Verification: Neotoma already extracts agent_sub correctly (it appears in the /session response). It also has agent_iss which can be used to fetch the JWKS and confirm the root key.
Neotoma returns:
{
"issuer": "https://sandbox.neotoma.io",
"client_name": "Neotoma",
"signature_window": 60,
"supported_algs": ["ES256", "EdDSA"],
"supported_typ": ["aa-agent+jwt"],
"jwks_uri": null,
"jwks_uri_reason": "Neotoma is verifier-only; agent JWKs are conveyed per-request..."
}Spec defines (Section "Resource Metadata"):
| Field | Spec Status | Neotoma |
|---|---|---|
issuer |
REQUIRED | ✅ Present |
jwks_uri |
REQUIRED | null (spec says required URL) |
client_name |
OPTIONAL | ✅ Present |
signature_window |
OPTIONAL | ✅ Present, correct |
supported_algs |
Not in spec | ❓ Non-standard addition |
supported_typ |
Not in spec | ❓ Non-standard addition |
jwks_uri_reason |
Not in spec | ❓ Non-standard addition |
authorization_endpoint |
OPTIONAL | Not present (fine) |
scope_descriptions |
OPTIONAL | Not present (fine) |
Suggestions:
jwks_uri: For verifier-only resources, either omit entirely or publish an empty JWKS ({"keys":[]}). Setting it tonullviolates the REQUIRED constraint. We could also discuss updating the spec to make it OPTIONAL for resources that don't sign resource tokens.supported_algs/supported_typ: These are potentially useful for agents. If we want them in the spec, let's discuss adding them. For now they're non-standard extensions (which is fine — the spec doesn't forbid additional fields).
@aauth/local-keys caches the agent token + ephemeral key in an in-memory Map. For long-running processes (MCP agents, servers), this works perfectly — same ephemeral key for the cache TTL. For CLI usage, every invocation = new process = new ephemeral key.
This isn't a bug — it's spec-correct behavior (agents SHOULD rotate ephemeral keys). But combined with issue #1, it means the CLI can never read back what it wrote on Neotoma's sandbox.
Potential CLI enhancement: Persist ephemeral key + agent token to ~/.aauth/sessions/<agent-url-hash>.json with TTL, so multiple CLI calls within the token lifetime share the same signing key. This is optional — the real fix is #1.
# Confirm discovery
curl -s https://sandbox.neotoma.io/.well-known/aauth-resource.json | jq
# Signed session — shows your agent identity
aauth-fetch --agent-only https://sandbox.neotoma.io/session
# Unsigned write to AAuth-only route — correctly rejects
curl -s -X POST https://sandbox.neotoma.io/sandbox/aauth-only/store \
-H 'content-type: application/json' \
-d '{"entities":[{"entity_type":"note","content":"unsigned"}]}'
# → {"error_code":"AAUTH_REQUIRED",...}
# Signed write — succeeds
aauth-fetch --agent-only -X POST \
-d '{"entities":[{"entity_type":"note","canonical_name":"AAuth test","content":"signed"}],"idempotency_key":"test-'$(date +%s)'"}' \
https://sandbox.neotoma.io/sandbox/aauth-only/store
# → 200, entity createdNote: Reading back the entity will 404 due to issue #1 (new ephemeral key = new partition). Once Neotoma partitions by agent_sub, the full write→read round-trip will work.
The AAuth protocol implementation is solid. The agent bootstrap, key management, token signing, HTTP Message Signatures, and verification all work correctly. The one blocking issue is Neotoma's identity partitioning using ephemeral key thumbprints instead of the stable agent identifier — a straightforward fix on Neotoma's side.