Layer 7 · Private retrieval — onion routing
Canonical references:
dig-onion(the onion-circuit crypto, cell wire, telescoping circuit builder, privacy-aware path selector, and onion-relay directory),dig-nat(the mTLSPeerConnectioneach hop rides),dig-dht(the provider-record store the onion-relay directory reuses), anddig-download(the multi-source fetch the exit runs). This page is the ecosystem-wide protocol spec for privacy-mode content retrieval; the peer network is the transport it rides and the dig RPC is the surface the toggle lives on.
Content retrieval on the DIG Network takes one of two modes, chosen per request. This page is the normative contract for the second one — privacy mode, an onion-routed read that hides who is reading from the hosts and from any single relay. A conforming reimplementation reproduces the wire and the rules below exactly.
- SPEED — the fast path documented in the peer network: locate holders in the DHT, fan byte-ranges across multiple providers, verify each range. The requesting node dials providers directly, so providers (and any on-path observer) see the requester's IP and
peer_id. This is the default for public content where the reader has nothing to hide. - PRIVACY — a telescoping onion circuit built through
k(default 3) DIG nodes acting as onion relays. The requester's node is the only party that knows both "who is asking" and "what is being asked for." Each relay peels exactly one encryption layer and learns only its immediate predecessor and successor. The exit relay runs an ordinary SPEED fetch on the requester's behalf against the real providers, so providers see the exit, never the requester. Content stays end-to-end encrypted at the DIG content layer and is merkle-verified by the requester, so a malicious relay can withhold bytes but never forge them.
Privacy mode costs latency (a 3-hop telescoping build plus per-hop crypto) and throughput (a circuit is a single ordered pipe on the requester→exit leg, not an N-way fan-out). Its anonymity set is the population of honest onion relays. The guarantees are weaker than Tor on some axes and stronger on others — stated plainly in §7.
0 · The two modes on the RPC surface
The mode is a first-class, optional field on the content-fetch methods, defaulting to speed. A legacy client that omits it gets speed unchanged.
// dig.getContent / dig.fetchRange params — additive fields (legacy clients omit them → speed)
{
"store_id": "<64hex>",
"retrieval_key": "<64hex>",
"root": "<64hex>|null",
"offset": 0, "length": 3145728,
"mode": "speed", // "speed" (default) | "privacy"
"privacy": { // present only when mode == "privacy"
"hops": 3, // circuit length: min 2, default 3, max 5
"reuse_circuit": true, // MAY bind to a prebuilt/existing suitable circuit
"cover_traffic": false // opt-in padding (best-effort obfuscation, not a guarantee)
}
}
Normative rules:
- An absent
modeMUST be treated asspeed.mode:"speed"MUST use the multi-source path and MUST NOT route through a circuit (no silent upgrade — no surprise latency).mode:"privacy"MUST route through an onion circuit per this page. hopsMUST lie in[2, 5](default 3); out of range fails with-32022.
Where the toggle lives — the local node only
The subtle, load-bearing rule: mode:"privacy" is meaningful only on the caller's own trusted local node (the in-process DIG Browser node, or the OS-service node on the loopback dig.local / 127.0.0.1). That node is the originator — it builds the circuit outward and is the only party holding both the reader's identity and the query.
- A node MUST NOT honor
mode:"privacy"received from a remote/anonymous caller by fetching privately on that caller's behalf — doing so would hand this node the caller's identity and their query, the exact linkage privacy mode exists to break. A caller with no trusted local originator (for example a browser talking only to a public read endpoint) cannot obtain privacy mode; the node MUST reject with-32021. Privacy mode therefore requires a local DIG node. - The circuit hops themselves are ordinary PEER/CONTROL-tier traffic: each hop is a
dig-natmTLS peer connection carrying onion cells on a dedicateddig.onionlogical stream. Onion relaying is an mTLS-gated peer capability, never anonymous-reachable, disjoint from the public read tier.
Hard invariants
- No silent downgrade. A
mode:"privacy"request that cannot be served privately MUST fail with-32020; it MUST NOT fall back to a direct fetch (that would deanonymize exactly the user who asked for privacy). Downgrading to a fast fetch is an explicit user/client choice only. - No silent upgrade.
mode:"speed"MUST NOT route through a circuit. - Exit-side provider resolution. For a private read, the requester's node MUST NOT issue a content-specific DHT
find_providersfrom its own node (that would reveal to the DHT what it wants to read); the exit performs the lookup (§1 step 2). The requester's only DHT activity for privacy is the content-independent onion-directory refresh (§3). - Integrity identical to SPEED. A private read ends in the same client-side verification (chain-anchored root pin + merkle inclusion + per-chunk AES-256-GCM-SIV decrypt) as a fast read. Privacy adds anonymity; it subtracts nothing from integrity.
1 · How a private read is routed end-to-end
A requester R (the user's local node) fetches through a prebuilt circuit R → E → M → X — guard/entry E, middle M, exit X:
User (browser / SDK) ──mode:privacy──▶ LOCAL dig-node R (trusted, on loopback)
│
│ 1. bind the request to a prebuilt circuit R→E→M→X
│ (guard E; M, X drawn from the onion directory §3–§6)
│ 2. R sends RESOLVE_PROVIDERS{content} down the circuit so the
│ DHT find_providers runs AT THE EXIT, not at R (invariant §0)
│ 3. R sends BEGIN_FETCH{content, root, ranges}
▼
E (guard / entry) ──mTLS──▶ M (middle) ──mTLS──▶ X (exit)
knows: R's IP + peer_id; knows: E and X knows: M (its predecessor),
that R runs an onion are relays; the content id + root; runs the
circuit; NOT the content NOT R, NOT the ordinary SPEED fetch against providers
id, NOT the content content id, NOT (find_providers → select → multi-source)
id, NOT the exit. the content. │
▼
Providers P1..Pn (ordinary, unchanged nodes)
know: the EXIT asked for this content id (ranges,
ciphertext served) — indistinguishable from
any normal fetch; do NOT know R exists.
│ ciphertext + inclusion proofs
▼
Response X──▶M──▶E──▶R: X wraps each returned range frame's CIPHERTEXT (still DIG-layer
encrypted!) in onion layers; M and E each add a layer; R peels all three, then runs the
ORDINARY client-side verification — chain-anchored root pin, merkle inclusion proof, and
AES-256-GCM-SIV decrypt with the URN-derived key. No hop (including X) ever saw plaintext
or the decryption key.
Who knows what — the honest table
| Party | Learns | Does NOT learn |
|---|---|---|
Guard / entry E | R's IP + peer_id; that R is running an onion circuit; cell timing / volume | The content id; the content; who the exit is; anything decryptable |
Middle M | That E and X are relays; cell timing / volume | R's existence / identity; the content id; the content |
Exit X | The content id + root; that some circuit wants it; runs the real fetch | R's identity / IP; that a specific person/app wants it; the decryption key; the plaintext (content stays DIG-layer ciphertext through X) |
Providers P | The exit fetched this content id (ranges) — identical to any normal fetch | R's existence; that this was an onion fetch at all |
| On-path observer (single link) | Fixed-size cells between two adjacent hops; their volume / timing | The content; the path beyond the two endpoints it watches |
Two integrity facts make privacy mode stronger than Tor here: (a) the content stays encrypted at the DIG content layer (AES-256-GCM-SIV, URN-derived key) the entire way, so the exit and every hop handle ciphertext — even the exit does not read the content; and (b) R verifies the merkle inclusion proof against the chain-anchored root it resolves itself, so a malicious exit can withhold or stall but cannot forge (a forged byte fails the merkle check, R tears the circuit and retries on a new one).
2 · Circuit construction
2.1 Telescoping build
A k-hop circuit is built one hop at a time, each downstream hop key-exchanged through the already-built prefix, so no relay ever sees the requester's raw key material for a hop beyond itself:
R wants circuit R → E → M → X
1. R dials E over dig-nat mTLS (an ordinary peer connection; E sees R's IP — mitigated by guards §4).
R ⇄ E: ntor handshake (CREATE / CREATED) → shared key K_E. R now holds a 1-hop circuit.
2. R sends E an EXTEND cell (encrypted under K_E) naming M + R's handshake for M.
E dials M over dig-nat mTLS, relays R's handshake, returns M's reply (EXTENDED).
R ⇄ M (through E): ntor handshake → K_M. M sees only E as its predecessor.
3. R sends (through E→M) an EXTEND cell naming X + R's handshake for X.
M dials X over dig-nat mTLS, relays the handshake.
R ⇄ X (through E→M): ntor handshake → K_X. X sees only M as its predecessor.
Circuit ready. R holds the ordered keys [K_E, K_M, K_X].
Telescoping (rather than a single-packet onion) is chosen because each EXTEND is "open a normal mTLS peer connection to the next hop" — exactly what the NAT-traversal ladder already does, NAT traversal and relay fallback included — and because each hop's key is an ephemeral key exchange giving forward secrecy per hop.