# Security Model
This page documents the security defenses the crate applies **automatically**
and how to (carefully) relax them for tests. The threat model and the normative
requirements are specified in
[RFC-ACDP-0008 (Security & Threat Model)](https://github.com/agentcontextdistributionprotocol/agentcontextdistributionprotocol/blob/main/rfcs/RFC-ACDP-0008-security.md)
and the cross-registry SSRF rules in
[RFC-ACDP-0006 §7](https://github.com/agentcontextdistributionprotocol/agentcontextdistributionprotocol/blob/main/rfcs/RFC-ACDP-0006-cross-registry.md).
This crate does not invent policy — it enforces what those RFCs require, by
default, on every public client API.
## The trust boundary
ACDP is a zero-trust substrate. **A registry is not a trusted party.** The only
thing you trust is the producer's signature, verified against a key resolved
from the producer's own `did:web` document. Everything the
[verification pipeline](consuming.md#verifiedcontext--the-verification-pipeline)
does follows from that: recompute the hash yourself, resolve the key yourself,
verify the signature yourself.
The second trust concern is **outbound requests**. The library makes outbound
HTTPS calls in three places, and each is SSRF-guarded identically:
1. **Producer DID resolution** — `WebResolver` fetching `did.json`.
2. **Cross-registry resolution** — `CrossRegistryResolver` fetching foreign
contexts and capabilities.
3. **Data-ref fetching** — `HttpsDataRefFetcher` fetching referenced data.
## Defenses applied by default
Every public client API (`RegistryClient`, `WebResolver`,
`CrossRegistryResolver`, `HttpsDataRefFetcher`) applies these out of the box —
you do not opt in:
| **HTTPS-only** | `http://` URLs are rejected. | RFC-ACDP-0008 |
| **IP-literal rejection** | `https://1.2.3.4/…` is rejected — forces a DNS lookup so the resolved IP can be filtered. | RFC-ACDP-0006 §7 |
| **Private/loopback/link-local/multicast/IMDS blocking** | Resolved IPs in RFC 1918, loopback, link-local, CGNAT, multicast, ULA (`fc00::/7`), `fe80::/10`, and the metadata endpoint (`169.254.169.254`) are refused — IPv4 **and** IPv6, including IPv4-mapped. | RFC-ACDP-0008 §4.8/§4.9 |
| **DNS-rebinding pin** | IPs are filtered **at DNS-resolution time, before any TCP connect** — a hostname whose answers fall in a forbidden range is refused. See below. | RFC-ACDP-0008 §7.6 |
| **Body-size caps** | 1 MB for context retrievals; 64 KB for capabilities and DID documents. | RFC-ACDP-0006 §7 |
| **Redirect cap** | Max 3 redirects, **same-authority only**. | RFC-ACDP-0006 §7 |
| **Timeouts** | 5 s connect, 30 s total. | RFC-ACDP-0006 §7.4 |
| **Algorithm-downgrade rejection** | The signature algorithm must match the algorithm of the resolved DID verification method. | RFC-ACDP-0001 §5.10 |
| **Ed25519 mandatory** | Ed25519 is always supported; downgrade attacks are rejected. | RFC-ACDP-0001 §5.10 |
> The size, redirect, and timeout constants are exposed as
> `acdp::registry::{MAX_CONTEXT_BYTES, MAX_METADATA_BYTES, MAX_REDIRECTS}` and
> in `src/limits.rs`.
## DNS-rebinding protection is active
DNS-rebinding (RFC-ACDP-0008 §7.6) is **on**. `crate::safe_http::SafeDnsResolver`
is wired into reqwest's `dns_resolver` hook by every HTTP client the crate
builds (`WebResolver`, `RegistryClient`, `HttpsDataRefFetcher`,
`CrossRegistryResolver`). Each resolved IP is filtered through the active
`SsrfPolicy` *at DNS time, before the socket is opened*, so a host that
DNS-rebinds to a forbidden range can never be connected to.
A host whose answers fall in a forbidden range is refused with
`AcdpError::KeyResolution` (permanent — HTTP 400, `is_transient == false`), not
a connect error.
## SsrfPolicy
`SsrfPolicy` is the knob behind all of this. Its default is the secure posture;
you rarely construct it directly.
| `reject_ip_literals` | `true` | refuse URLs with a literal IP host |
| `allow_http` | `false` | HTTPS-only |
| `allow_loopback_resolved` | `false` | refuse hosts that resolve to loopback |
`policy.check_url(url)` validates a URL up front; `classify_url` returns a
structured `SsrfRejection { reason, detail }` for diagnostics.
## Testing against localhost
Because loopback is blocked by default, tests that POST to a local listener
need to opt in explicitly. The intended seams:
- **`SsrfPolicy::allow_test_loopback()`** — the default policy with
`allow_loopback_resolved = true`. Pass it to a fetcher/resolver that accepts
a custom policy (e.g. `HttpsDataRefFetcher::with_ssrf_policy(...)`,
`CrossRegistryResolver::with_ssrf_policy(...)`).
- **`RegistryClient::with_test_transport(base_url)`** — a client that permits
HTTP and loopback for in-process test servers.
- **`RegistryClient::with_test_endpoint(...)`** / **`new_pinned(...)`** — pin a
DNS answer / custom CA so a TLS test server on `127.0.0.1` is reachable while
the production path stays locked down.
> These are **test-only**. Never construct a loopback-permitting policy in
> production code — it reopens the SSRF surface the defaults close. The TLS
> conformance suite (`tests/tls_conformance.rs`) uses exactly these seams to
> drive the `fed-*` and `did-ssrf-*` fixtures against an in-process server.
## What the crate does *not* do
Per RFC-ACDP-0008, some responsibilities sit with the registry or the operator,
not this client library:
- **Rate limiting** (RFC-ACDP-0008 §4.3) — a registry concern. The `server`
feature exposes a `RateLimiter` trait; see [Implementing a registry](registry.md).
- **Authentication / authorization of cross-registry calls** — out of scope for
v0.1.0 (RFC-ACDP-0006). Cross-registry resolution is unauthenticated.
- **Visibility enforcement at rest** — the registry enforces visibility on
retrieval; the consumer applies visibility rules client-side but cannot see
what a registry refuses to serve.
For the full threat enumeration (replay, Sybil/spam, existence-leak,
supersession races), read RFC-ACDP-0008 directly — these docs do not duplicate
it.