SafeHttpClient Egress & Anti-SSRF Model
Egress & SSRF Threat Model (SafeHttpClient)
Section titled “Egress & SSRF Threat Model (SafeHttpClient)”Every outbound HTTP request made by resolver-core passes through safe_client.rs.
The resolver treats all non-public network space as hostile. If DNS resolution for any endpoint (DID document, DIF domain configuration, or status list) yields an IP address within RFC 1918, RFC 6890, or cloud metadata CIDRs, the request is aborted prior to socket creation.
Live RFC 9457 Egress Interception Telemetry
Section titled “Live RFC 9457 Egress Interception Telemetry”When an untrusted input triggers an SSRF block, the resolver returns an instant RFC 9457 Problem Details payload with microsecond trace telemetry:
Outbound Egress Blocked by Security Policy
Request destination '10.0.4.15:80' resolves to RFC 1918 private IPv4 space. Outbound connection aborted by SafeHttpClient kernel filter.
{
"type": "https://did.is/errors/egress-blocked",
"title": "Outbound Egress Blocked by Security Policy",
"status": 403,
"code": "EGRESS_BLOCKED",
"detail": "Request destination '10.0.4.15:80' resolves to RFC 1918 private IPv4 space. Outbound connection aborted by SafeHttpClient kernel filter.",
"instance": "urn:didis:trace:9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"timestamp": "2026-10-10T20:58:42.821Z"
}Socket-Level Address Denylist
Section titled “Socket-Level Address Denylist”Prior to connection establishment, every IP address returned by DNS is evaluated against static CIDR denylists. If any IP falls within a non-public subnet, the request fails with ResolverError::SsrfDetected.
DNS Query Hostname ──► [ IP_1, IP_2, ... ] │ ▼ Validate ALL IP addresses Is ANY IP in blocked CIDR ranges? ├── YES ──► ABORT: SsrfDetected └── NO ──► Pin socket to IP_1Blocked IPv4 Subnets
Section titled “Blocked IPv4 Subnets”0.0.0.0/8(Local identification)10.0.0.0/8(RFC 1918 Private)100.64.0.0/10(RFC 6598 Carrier-Grade NAT)127.0.0.0/8(Loopback)169.254.0.0/16(Link-Local / Cloud Metadata169.254.169.254)172.16.0.0/12(RFC 1918 Private)192.0.0.0/24(IETF Protocol Assignments)192.0.2.0/24(TEST-NET-1)192.88.99.0/24(6to4 Relay Anycast)192.168.0.0/16(RFC 1918 Private)198.18.0.0/15(Network Benchmark Tests)198.51.100.0/24(TEST-NET-2)203.0.113.0/24(TEST-NET-3)224.0.0.0/4(Multicast)240.0.0.0/4(Reserved for future use)255.255.255.255/32(Limited Broadcast)
Blocked IPv6 Subnets
Section titled “Blocked IPv6 Subnets”::1/128(Loopback)::/128(Unspecified)::ffff:0:0/96(IPv4-mapped addresses; mapped IPv4 evaluated against IPv4 denylist)64:ff9b::/96(IPv4-IPv6 translation)64:ff9b:1::/48(Local IPv4-IPv6 translation)100::/64(Discard-only)2001::/32(TEREDO)2001:db8::/32(Documentation)2002::/16(6to4)fc00::/7(Unique Local Unicast ULA)fe80::/10(Link-Local Unicast)fec0::/10(Site-Local Unicast)ff00::/8(Multicast)
DNS Pre-Resolution & Address Pinning
Section titled “DNS Pre-Resolution & Address Pinning”To defeat Time-of-Check to Time-of-Use (TOCTOU) DNS-rebinding attacks:
SafeHttpClientperforms asynchronous DNS resolution viatokio::net::lookup_host.- Verifies that all returned
SocketAddrentries passvalidate_ip. - Selects the first vetted address (
addrs[0]) and pins the subsequent HTTP client socket connection directly to that specific socket address. - The remote server cannot rebind the DNS record to an internal IP between resolution and connection.
Redirect & Protocol Constraints
Section titled “Redirect & Protocol Constraints”- Manual Hop Re-Validation: Automatic redirect following is disabled. Redirects are handled manually up to
MAX_REDIRECTS = 2. - Hop Validation: Each redirect target URL undergoes full URL parsing, scheme checking, host extraction, DNS pre-resolution, and IP denylist validation.
- Downgrade Prohibition: Redirects from
https://tohttp://are rejected withResolverError::SsrfDetected. - Concurrency Bounds: Egress concurrency is bounded via a global semaphore (
MAX_CONCURRENT_EGRESS = 64) to prevent worker starvation under outbound loads.
Deterministic Offline Context Catalog
Section titled “Deterministic Offline Context Catalog”Per Architecture Decision Record 002 (ADR-002), DID.is never dereferences @context URLs at runtime.
All recognized vocabularies are compiled statically into context_catalog.rs:
1
pub const KNOWN_CONTEXTS: &[KnownContext] = &[
2
KnownContext { uri: "https://www.w3.org/ns/did/v1", name: "DID Core v1.0" },
3
KnownContext { uri: "https://www.w3.org/ns/did/v1.1", name: "DID Core v1.1" },
4
KnownContext { uri: "https://w3id.org/did/v1", name: "DID Core (legacy alias)" },
5
KnownContext { uri: "https://www.w3.org/ns/cid/v1", name: "Controlled Identifiers v1.0" },
6
KnownContext { uri: "https://w3id.org/security/multikey/v1", name: "Multikey" },
7
KnownContext { uri: "https://w3id.org/security/jwk/v1", name: "JsonWebKey" },
8
KnownContext { uri: "https://w3id.org/security/data-integrity/v2", name: "Data Integrity v2" },
9
KnownContext { uri: "https://w3id.org/security/data-integrity/v1", name: "Data Integrity v1" },
10
KnownContext { uri: "https://w3id.org/security/suites/ed25519-2020/v1", name: "Ed25519 2020 suite" },
11
KnownContext { uri: "https://w3id.org/security/suites/ed25519-2018/v1", name: "Ed25519 2018 suite" },
12
KnownContext { uri: "https://w3id.org/security/suites/x25519-2020/v1", name: "X25519 2020 suite" },
13
KnownContext { uri: "https://w3id.org/security/suites/x25519-2019/v1", name: "X25519 2019 suite" },
14
KnownContext { uri: "https://w3id.org/security/suites/jws-2020/v1", name: "JWS 2020 suite" },
15
KnownContext { uri: "https://w3id.org/security/suites/secp256k1-2019/v1", name: "secp256k1 2019 suite" },
16
KnownContext { uri: "https://w3id.org/security/v1", name: "Security vocabulary v1" },
17
KnownContext { uri: "https://w3id.org/security/v2", name: "Security vocabulary v2" },
18
KnownContext { uri: "https://www.w3.org/ns/credentials/v2", name: "VC Data Model v2.0" },
19
KnownContext { uri: "https://www.w3.org/2018/credentials/v1", name: "VC Data Model v1.1" },
20
KnownContext { uri: "https://www.w3.org/ns/credentials/examples/v2", name: "VC examples" },
21
KnownContext { uri: "https://identity.foundation/.well-known/did-configuration/v1", name: "DIF Well Known DID Configuration" },
22
KnownContext { uri: "https://identity.foundation/linked-vp/contexts/v1", name: "DIF Linked VP" },
23
KnownContext { uri: "https://identity.foundation/did-webvh/v1", name: "did:webvh v1" },
24
];
Unknown contexts are flagged in evidence output but never trigger network requests.