Skip to content
Merged
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ did:agent:QmfJZCnucexGhSZqdsAcKnFcv4RxyhPFMUiGjQmDRiMxux
└─ sha2-256 multihash of the agent's signed inception event
```

## Documentation

| | |
|---|---|
| [Why AgentAuth](docs/why.md) | The problem, and why API keys, OAuth and mTLS fall short for agents |
| [Architecture](docs/architecture.md) | Components, request flow, how authority and limits propagate |
| [Protocol spec](docs/protocol.md) | Wire formats and verification rules, for other implementations |
| [Integration guide](docs/integration-guide.md) | Adding AgentAuth to an agent or a service |
| [Threat model](docs/threat-model.md) | What's defended against, and the known gaps |
| [Operations](docs/operations.md) | Running the registry, key management, incident response, scaling |
| [Design decisions](docs/design-decisions.md) | Why things are the way they are |
| [Glossary](docs/glossary.md) · [Roadmap](docs/roadmap.md) | |

## Why not just API keys?

| Problem with API keys for agents | AgentAuth |
Expand Down Expand Up @@ -279,6 +292,7 @@ agentauth/
store.py SQLite persistence
server.py Registry (FastAPI)
cli.py `agentauth` command
docs/ Why, architecture, protocol spec, guides, threat model, decisions
examples/ tool_service.py: a demo service with scoped, resource- and spend-limited endpoints
tests/ End-to-end tests (attacks included)
```
Expand Down Expand Up @@ -313,9 +327,4 @@ tests/ End-to-end tests (attacks included)

## Roadmap

1. ~~Scoped, short-lived capability grants~~ ✅ v0.2
2. ~~Delegation chains with attenuation and cascading revocation~~ ✅ v0.2
3. ~~Resource, spend, rate and use limits~~ ✅ v0.3
4. **Hash-chained audit log** of authorized actions, recording the full principal → agent chain.
5. Production hardening: Redis nonce cache and usage ledger, Postgres, registration rate limits, multi-witness receipts, KMS-held recovery keys.
6. A written spec and a TypeScript verifier; MCP integration.
Shipped: identity (v0.1), delegated grants (v0.2), limits (v0.3). Next up: an audit log, production hardening (Redis/Postgres), protection against a dishonest registry, KMS-held recovery keys, and a TypeScript verifier. See the full [roadmap](docs/roadmap.md).
15 changes: 15 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# AgentAuth documentation

| Doc | Read it if you want to… |
|---|---|
| [Why AgentAuth](why.md) | understand the problem and why existing tools (API keys, OAuth, mTLS) fall short for autonomous agents |
| [Architecture](architecture.md) | see the moving parts: registry, SDK, relying services, and how a request is checked |
| [Protocol specification](protocol.md) | implement a compatible client or verifier in another language |
| [Integration guide](integration-guide.md) | give an agent an identity, or protect your service with AgentAuth |
| [Threat model](threat-model.md) | know what AgentAuth defends against, and what it doesn't (yet) |
| [Operations](operations.md) | run the registry and manage keys in a real deployment |
| [Design decisions](design-decisions.md) | learn why things are the way they are, and what was traded off |
| [Glossary](glossary.md) | look up a term (DID, KEL, pre-rotation, principal, attenuation, …) |
| [Roadmap](roadmap.md) | see what's done and what's next |

New here? Read **Why**, then **Architecture**, then follow the quick start in the [main README](../README.md).
119 changes: 119 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Architecture

## Components

```mermaid
flowchart LR
subgraph Principal["Human / org"]
H[Principal identity<br/>did:agent:…]
end
subgraph Agents
A[Agent<br/>SDK + keys]
S[Sub-agent<br/>SDK + keys]
end
R[(Registry<br/>key event logs<br/>revocations)]
SVC[Relying service<br/>RequestVerifier +<br/>GrantVerifier]

H -- "grant (signed)" --> A
A -- "delegated grant (narrower)" --> S
H & A & S -- "register / rotate / deactivate" --> R
S -- "signed request + AgentGrant chain" --> SVC
SVC -- "resolve keys, replay logs,<br/>check revocations" --> R
```

| Component | Code | Role |
|---|---|---|
| **Crypto primitives** | `agentauth/crypto.py` | Ed25519 keys, base58btc/multibase, multikeys, canonical JSON, SHA-256 multihash digests |
| **Identity** | `agentauth/identity.py` | Builds and verifies key event log (KEL) events; derives DIDs; produces DID documents. Pure functions with no I/O. |
| **Request signing** | `agentauth/httpsig.py` | `AgentSig` header: signing and verification, replay (nonce) cache |
| **Grants** | `agentauth/grants.py` | Capability grants, delegation chains, scope algebra, revocation events, `GrantVerifier` |
| **Limits** | `agentauth/limits.py` | Resource/spend/rate/use limits, attenuation rules, `UsageLedger` |
| **Keystore** | `agentauth/keystore.py` | Encrypted key files; current and recovery keys stored separately |
| **SDK** | `agentauth/sdk.py` | `AgentIdentity`, `RegistryClient`, httpx/httpx2 auth hook, caching resolvers for services |
| **Integrations** | `agentauth/integrations.py` | FastAPI dependencies: `fastapi_dependency` (identity) and `fastapi_authorizer` (scopes + limits) |
| **Registry** | `agentauth/server.py`, `agentauth/store.py` | HTTP service storing KELs and revocations (SQLite) |
| **CLI** | `agentauth/cli.py` | `agentauth` command wrapping the SDK and registry |

## The registry is a witness, not an authority

The registry stores each identity's key event log and serves it back. It **validates** every event before accepting it, but it **cannot forge** one:

- A DID is the digest of its own signed inception event, so the registry can't create or change one.
- Every later event is signed and hash-chained to the one before it.
- `RegistryKeyResolver` (used by services) downloads the **full log** and replays it locally by default, instead of trusting the registry's summary.

What the registry *can* do is withhold events or show different clients different logs. See [threat model: registry equivocation](threat-model.md#registry-equivocation).

## Lifecycle of an identity

```mermaid
stateDiagram-v2
[*] --> Active: inception<br/>(key₀, commits to H(key₁))
Active --> Active: rotation<br/>(reveals key₁, commits to H(key₂))
Active --> Deactivated: deactivation<br/>(recovery key or controller)
Deactivated --> [*]
```

1. **Inception.** The agent generates two keypairs: a *current* key and a *next* (recovery) key. It signs an inception event containing the current public key and a hash of the next one. If it has a controller, the controller countersigns. The DID is the digest of this event.
2. **Rotation.** To rotate, the agent reveals the pre-committed next key, signs the rotation with it, and commits to a new next key. Holding only the current key isn't enough.
3. **Deactivation.** Either the holder of the recovery key or the controller (with its current key) can permanently deactivate the identity. Nothing can be appended afterwards.

## What happens on a request

```mermaid
sequenceDiagram
participant S as Sub-agent
participant SVC as Relying service
participant R as Registry

S->>SVC: POST /purchase<br/>Authorization: AgentSig …<br/>AgentGrant: g_root,g_child
Note over SVC: 1. Authenticate
SVC->>R: resolve S (log replay, cached)
SVC->>SVC: audience ✓ timestamp ✓ kid current ✓<br/>signature ✓ nonce unused ✓
Note over SVC: 2. Authorize
SVC->>R: resolve each issuer (cached)
SVC->>SVC: each link signed by issuer's current key ✓<br/>chain unbroken ✓ only narrows ✓<br/>leaf.sub == S ✓ root principal trusted ✓
SVC->>R: revocation check (cached)
SVC->>SVC: scope covers "payments.buy" ✓
Note over SVC: 3. Enforce limits
SVC->>SVC: resource & per-call spend on every link ✓<br/>ledger: charge budget/rate/uses on every link (all-or-nothing)
SVC-->>S: 200 OK (or 401 / 402 / 403 / 429)
```

**Status codes:**

| Code | Meaning |
|---|---|
| **401** | Authentication failed: bad signature, stale timestamp, replayed nonce, wrong audience, unknown or deactivated agent, rotated key, or no grant presented |
| **403** | Authorization failed: grant invalid, untrusted principal, revoked, scope not covered, resource not covered, per-call spend exceeded, or uses exhausted |
| **402** | Budget exhausted |
| **429** | Rate limit reached (the message says when to retry) |
| **400** | Malformed input, or a requested resource containing `..` |

## How authority flows

```mermaid
flowchart TD
P["Principal: arunima<br/>trusted by service for tools.*, payments.*"]
G1["Grant 1 → researcher<br/>scope: payments.*<br/>budget 5000¢, depth 1"]
G2["Grant 2 → helper<br/>scope: payments.buy<br/>budget 3000¢, depth 0"]
G3["Grant 3 → shopper<br/>scope: payments.buy<br/>budget 3000¢, depth 0"]
P --> G1 --> G2
G1 --> G3
G2 -. "spends count against G2 and G1" .-> L[(Usage ledger)]
G3 -. "spends count against G3 and G1" .-> L
```

Every call is charged against **every** grant in its chain. Grants 2 and 3 each allow 30 USD, but together they can't exceed Grant 1's 50 USD. Revoking Grant 1 (or deactivating `researcher`) cuts off both sub-agents at once.

## Caching and freshness

Services cache lookups so they don't hit the registry on every request. The cache TTLs set how quickly a change takes effect:

| Cache | Default | What a stale entry means |
|---|---|---|
| `RegistryKeyResolver` (keys and active status) | 30 s | A deactivated agent keeps working for up to 30 s. A **rotation** is picked up immediately, because a newer `kid` forces a refresh. |
| `RegistryRevocationChecker` ("not revoked" answers) | 10 s (5 s in the example service) | A revoked grant keeps working for up to that long. "Revoked" answers are cached permanently. |
| Nonce cache | 2 × max clock skew (240 s) | In-memory and per process (see [operations](operations.md)). |

Set any TTL to `0` for immediate effect, at the cost of a registry round-trip per request.
125 changes: 125 additions & 0 deletions docs/design-decisions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Design decisions

Short records of the choices that shaped AgentAuth: what was decided, why, and what it costs. Add a new entry when a decision changes. Don't rewrite old ones; mark them superseded instead.

---

### DD-1: Self-certifying DIDs instead of registry-assigned IDs

**Decision.** An agent's DID is the digest of its own signed inception event (`did:agent:<multihash>`).

**Why.** An identifier the registry hands out is only as trustworthy as the registry. A self-certifying one can't be minted, reassigned or forged by anyone who doesn't hold the inception key, and anyone can check it offline.

**Cost.** DIDs aren't human-readable. Human-friendly names live in `meta` and aren't authoritative.

---

### DD-2: A KERI-style key event log with mandatory pre-rotation

**Decision.** Each identity has an append-only, hash-chained log. Every establishment event commits to the hash of the *next* key, and a rotation must reveal that key and be signed by it.

**Why.** Agents run in exposed environments, so their current key is the likeliest thing to leak. Pre-rotation means leaking it isn't enough to take over the identity. We borrowed the idea from KERI but kept the event model much smaller (no witnesses yet, one key per event, no multisig thresholds) to keep the protocol easy to implement.

**Alternatives.**
- `did:key`: no rotation at all.
- `did:web`: trusts DNS and the web host.
- Full KERI: powerful, but a large specification for a v0.

**Cost.** Operators have to look after two keys per identity. Losing the recovery key means the identity can't be rotated again, only deactivated.

---

### DD-3: Sign every request instead of issuing bearer tokens

**Decision.** Agents authenticate each request with an Ed25519 signature over its method, audience, path, body, timestamp, nonce and grant header (`AgentSig`).

**Why.** Bearer tokens leak through agent context: logs, prompts and tool outputs. A signature is worthless outside the one request it was made for.

**Alternatives.**
- **RFC 9421 HTTP Message Signatures.** Close in spirit. We chose a fixed, minimal signing string to avoid negotiating covered components. A future version may align with 9421.
- **DPoP.** Proof-of-possession for OAuth tokens, but it assumes an OAuth deployment.

**Cost.** Every request costs one signature (microseconds) and one verification. Services need a nonce cache.

---

### DD-4: Our own grant format rather than JWT, Macaroons or Biscuit

**Decision.** Grants are canonical-JSON bodies with detached Ed25519 signatures. Chains are a comma-separated list of links, each signed by the previous link's subject.

**Why.**
- **JWT:** allows many algorithms (a long history of `alg` confusion bugs), has no native model for multi-hop delegation, and its claims don't map cleanly onto per-link attenuation.
- **Macaroons:** attenuate beautifully, but rely on a shared HMAC root key, so only the issuer can verify them. We need anyone to be able to verify, with each hop signed by a *different* party.
- **Biscuit:** the closest match (public-key, attenuable, offline-verifiable) and a strong candidate for the future. Its Datalog policy language is more than v0 needed.

**Cost.** A custom format has to be implemented in each language. The [protocol spec](protocol.md) exists to make that feasible.

---

### DD-5: Services decide whom to trust (the root policy)

**Decision.** A grant chain is only accepted if its root issuer is a principal the *service* trusts, and only for the scopes the service allows that principal.

**Why.** There's no global authority on who may do what at your API. The service owner already knows which people and orgs are its customers. AgentAuth makes the chain from that principal to the calling agent verifiable, and leaves the root decision where it belongs.

**Cost.** Each service configures trust. `trust_principals({...})` covers the simple case, and a callable handles dynamic ones.

---

### DD-6: Grants are only valid under the issuer's current key

**Decision.** Each link records the issuer's `kid`. Verification fails if the issuer has rotated since the grant was issued.

**Why.** If a key was rotated *because* it leaked, grants signed with it must stop working. Accepting historical keys would require knowing *when* each grant was signed, which a compromised key could lie about.

**Cost.** Rotating an issuer invalidates everything it issued. Grants are meant to be short-lived (15 minutes by default, 24 hours at most), so re-issuing them is cheap.

---

### DD-7: Limits apply to the whole grant and fail closed

**Decision.** A grant's limits (resources, spend, rate, uses) apply to *every* scope in it. A resource-limited grant is refused at endpoints that don't declare a resource. Unknown limit fields make a grant invalid.

**Why.** Per-scope limits would make both the format and the narrowing rules much more complex. Failing open (ignoring a resource limit when an endpoint doesn't declare a resource) would silently widen authority, which is the one thing a limit must never do.

**Cost.** To give an agent differently limited capabilities, issue several grants. Services have to declare resources and costs on their endpoints.

---

### DD-8: Stateful limits are charged against every link, all-or-nothing

**Decision.** Budget, rate and use counters are kept per grant. Each call is charged atomically against every link in its chain that has one.

**Why.** Without this, an agent with a 50 USD budget could delegate 50 USD to each of ten sub-agents. Charging every link turns a parent's limit into a hard ceiling on everything beneath it.

**Cost.** The ledger is shared state that services must keep, and it has to be distributed (Redis or a database) once a service runs more than one replica.

---

### DD-9: A single registry as witness, not authority (for now)

**Decision.** One registry stores and serves logs. Clients replay logs rather than trusting its answers.

**Why.** It gives the security properties that matter most (no forgery, no key substitution) with a simple deployment.

**Cost.** The registry can withhold events or show different clients different logs. Multiple witnesses and clients that remember logs they've seen are on the [roadmap](roadmap.md).

---

### DD-10: Canonical JSON without floats

**Decision.** Signed payloads are JSON with sorted keys, no whitespace and UTF-8, and floats are rejected. Amounts are integers in the smallest unit.

**Why.** Float serialization differs across languages and is the classic way a "canonical" JSON scheme breaks. Without floats, and with the ASCII keys and safe-range integers AgentAuth uses, this produces the same bytes as RFC 8785 (JCS).

**Cost.** Money and other quantities have to be expressed as integers, such as cents, and the unit has to be stated.

---

### DD-11: Python and FastAPI first

**Decision.** The reference implementation is a Python package with FastAPI integrations and an SQLite-backed registry.

**Why.** Much agent tooling is written in Python, and FastAPI's dependency injection maps directly onto "authenticate, authorize, enforce limits".

**Cost.** Node/TypeScript agent ecosystems need a port. That's on the [roadmap](roadmap.md), and the [protocol spec](protocol.md) is the contract a port has to follow.
Loading
Loading