From f43424f39383e0e5a30399f4af25a4dad0ed735c Mon Sep 17 00:00:00 2001 From: Michael Taylor Date: Sat, 4 Jul 2026 07:47:18 -0700 Subject: [PATCH] feat(protocol): specify the rpc.dig.net dual-mode gateway + ephemeral read certs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The peer-network spec already had the two RPC tiers (mTLS peer/control vs anonymous public-read) but treated rpc.dig.net as purely the anonymous front. Node-class clients (digstore CLI, dig-sdk, any DIG-identity-key holder) must never downgrade to that anonymous path, even at the public gateway, per the client->node connection order contract. - New peer-network.md §0a: rpc.dig.net is dual-mode — an mTLS front (CERT_REQUIRED) for node-class-client identity certs, reached at all three ladder tiers (dig.local/localhost/rpc.dig.net), alongside the existing plain-HTTPS+CORS front for browsers. - Ephemeral self-signed client certificate flow: an anonymous reader may use the mTLS front for channel-bound request integrity without any persisted identity or reputation, binding a signed request to its TLS session. - Design-status note: implementations gate on the gateway mTLS endpoint's existence and must not hard-break before it is deployed. - Cross-link the dual-mode note into dig-rpc.md's public-read-tier tip and add a Related link + conformance/tier-map notes in peer-network.md. Minor bump (additive normative spec content, no removed/renumbered sections). --- docs/protocol/dig-rpc.md | 2 +- docs/protocol/peer-network.md | 41 ++++++++++++++++++++++++++++++++--- package.json | 2 +- static/knowledge-graph.json | 5 +++++ 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/docs/protocol/dig-rpc.md b/docs/protocol/dig-rpc.md index 02498e2..d3ff16b 100644 --- a/docs/protocol/dig-rpc.md +++ b/docs/protocol/dig-rpc.md @@ -47,7 +47,7 @@ Content-Type: application/json - CORS `*`, **no credentials** (content is public ciphertext); `OPTIONS` → 204 (`bootstrap.rs:468`). :::tip This is the PUBLIC READ tier -`rpc.dig.net` and this network profile ARE the anonymous, browser-reachable **public read tier** of the [dual-transport model](./peer-network.md#dual-transport): no client certificate, CORS-`*`, read-only, decoy-on-miss, and integrity self-verified client-side against the chain-anchored root. The **peer / write / config / control** surface (peer exchange, DHT, PEX, availability-for-sync, PUSH/WRITE, node control) lives on the separate **mTLS peer/control tier** and is never reachable here — see the [tier map](./peer-network.md#tier-map). +`rpc.dig.net` and this network profile ARE the anonymous, browser-reachable **public read tier** of the [dual-transport model](./peer-network.md#dual-transport): no client certificate, CORS-`*`, read-only, decoy-on-miss, and integrity self-verified client-side against the chain-anchored root. The **peer / write / config / control** surface (peer exchange, DHT, PEX, availability-for-sync, PUSH/WRITE, node control) lives on the separate **mTLS peer/control tier** and is never reachable here — see the [tier map](./peer-network.md#tier-map). `rpc.dig.net` additionally answers on a separate mTLS front for node-class clients (and ephemeral-cert anonymous readers) — this plain-HTTPS endpoint is not that front; see [gateway dual-mode](./peer-network.md#gateway-dual-mode). ::: ## 2 · Method catalogue (network profile) diff --git a/docs/protocol/peer-network.md b/docs/protocol/peer-network.md index f868975..58d6102 100644 --- a/docs/protocol/peer-network.md +++ b/docs/protocol/peer-network.md @@ -1,13 +1,16 @@ --- sidebar_position: 13 title: "L7 · DIG Node peer network" -description: "The normative node↔node protocol: mTLS peer identity (peer_id = SHA-256(TLS SPKI DER)), the two RPC tiers (mTLS-authenticated PEER/CONTROL vs anonymous PUBLIC-READ so browsers can retrieve content), the ordered NAT-traversal ladder (direct → UPnP → NAT-PMP → PCP → relay-coordinated hole-punch (signalling only) → relayed/TURN transport), the relay's four roles (STUN, introducer, hole-punch signalling, relayed transport), STUN reflexive-address discovery, introducer + gossip peer discovery, PEX peer-exchange (node↔node stream + the RLY-008 relay introducer binding), the Kademlia DHT with provider records that locate which peers hold content (find_node/find_providers/add_provider/ping over a framed dig-nat mTLS stream; content-key = SHA-256(domain-tag ‖ store_id[‖root[‖retrieval_key]])), the relay RelayMessage wire (RLY-001..RLY-008), the peer RPC methods (dig.getPeers/dig.announce/dig.getNetworkInfo/dig.getAvailability/dig.listInventory/dig.fetchRange), and the relay-last-fallback invariant (prefer hole-punch signalling over full relaying)." +description: "The normative node↔node protocol: mTLS peer identity (peer_id = SHA-256(TLS SPKI DER)), the two RPC tiers (mTLS-authenticated PEER/CONTROL vs anonymous PUBLIC-READ so browsers can retrieve content), the dual-mode public gateway (rpc.dig.net's mTLS front for node-class clients — the digstore CLI, the SDK, any DIG-identity-key holder — across the full dig.local/localhost/rpc.dig.net ladder, plus its plain-HTTPS+CORS front for browsers, with an ephemeral self-signed client certificate for channel-bound anonymous reads), the ordered NAT-traversal ladder (direct → UPnP → NAT-PMP → PCP → relay-coordinated hole-punch (signalling only) → relayed/TURN transport), the relay's four roles (STUN, introducer, hole-punch signalling, relayed transport), STUN reflexive-address discovery, introducer + gossip peer discovery, PEX peer-exchange (node↔node stream + the RLY-008 relay introducer binding), the Kademlia DHT with provider records that locate which peers hold content (find_node/find_providers/add_provider/ping over a framed dig-nat mTLS stream; content-key = SHA-256(domain-tag ‖ store_id[‖root[‖retrieval_key]])), the relay RelayMessage wire (RLY-001..RLY-008), the peer RPC methods (dig.getPeers/dig.announce/dig.getNetworkInfo/dig.getAvailability/dig.listInventory/dig.fetchRange), and the relay-last-fallback invariant (prefer hole-punch signalling over full relaying)." keywords: - peer network - peer_id - mTLS - dual transport - public read tier + - gateway dual-mode + - ephemeral client certificate + - node-class client - CORS - NAT traversal - STUN @@ -94,8 +97,8 @@ A `dig-node` runs **two listeners**: The **recommended endpoint split** (design-first call): the public read tier is the **same JSON-RPC endpoint shape** as the network profile — one `POST` endpoint speaking JSON-RPC 2.0 — but the anonymous serve layer answers **only the read subset**; the peer/write/control methods return `-32601` there. This reuses the existing [node-profile / `dig.methods` gating](./dig-rpc.md#node-profile) (an agent already gates on `dig.methods` rather than assuming one uniform surface) instead of inventing a parallel protocol, and it is exactly what a browser already speaks to `rpc.dig.net`. The peer/control tier is **not** a JSON-RPC-over-HTTPS surface at all — it is the dig-nat mTLS mux — so it cannot be reached by an anonymous HTTP client even by accident. -:::note `rpc.dig.net` IS the public read tier -Today `rpc.dig.net` ([the dig RPC network profile](./dig-rpc.md)) is exactly this anonymous, CORS-enabled, decoy-on-miss, client-verified public read tier. This section names the tier the browser has always used and states the invariant that keeps the peer/write/control surface off it. +:::note `rpc.dig.net` is dual-mode: the public read tier plus an mTLS gateway +`rpc.dig.net` ([the dig RPC network profile](./dig-rpc.md)) is the anonymous, CORS-enabled, decoy-on-miss, client-verified public read tier — the tier a browser has always used, and this section states the invariant that keeps the peer/write/control surface off it. It is also the **public mTLS gateway**: node-class clients, and anonymous readers holding an ephemeral certificate, reach the very same hostname over the mTLS peer/control transport instead. [§0a](#gateway-dual-mode) is the two-front contract. ::: ### Browser specifics — fetch + verify without mTLS @@ -108,6 +111,35 @@ A browser (or an agent with no client certificate) reads content like this, need Because the read tier is CORS-`*` + anonymous, the exact same fetch works from a web page, a service worker, the extension, the SDK, or a headless agent — none of which can present a client certificate. +### 0a · `rpc.dig.net` is dual-mode — the public gateway serves both tiers {#gateway-dual-mode} + +`rpc.dig.net` is one public, network-wide hostname, but it is not one listener — it is the **public gateway**, answering on two independently-authenticated fronts so a node-class client and a browser both reach it correctly: + +| | **mTLS gateway front** | **Public-read front** | +|---|---|---| +| Who dials it | node-class clients — any DIG-identity-key holder: the `digstore` CLI, `dig-sdk`, a standalone `dig-node`, DIG Browser in standalone-node mode — plus anonymous readers using an [ephemeral certificate](#ephemeral-read-cert) | browsers, the extension's fetch path, headless agents with no client certificate | +| Auth | mTLS, `CERT_REQUIRED` — the same handshake as the [peer/control tier](#dual-transport) | none — plain HTTPS, CORS-`*` | +| Identity carried | the caller's `peer_id` derived from its presented cert ([§1](#peer-identity)) — persistent for a node-class client's own identity-derived cert, disposable for an ephemeral read cert | none | +| Methods reachable | the full [PEER/CONTROL surface](#tier-map) **and** the PUBLIC-READ methods, all over the mTLS channel | the [PUBLIC-READ surface](#tier-map) only | +| Write authorization | [§21.9 signed-request](./transport-and-push.md#per-request-auth), checked against the caller's identity | none — this front carries no write route | + +**A node-class client never uses the public-read front — not even for reads.** It resolves its endpoint through the fixed three-tier ladder — an explicit override wins outright, then `dig.local`, then `localhost`, then `rpc.dig.net` as the final fallback (see [Point a consumer at your node](../run-a-node/point-a-consumer.md) and [the digstore CLI's ladder](../digstore/cli/command-reference.md#which-node-digstore-talks-to)) — and at **every** tier of that ladder, including `rpc.dig.net`, it dials over mTLS with the client certificate derived from its DIG identity key ([§1](#peer-identity)), layering [§21.9](./transport-and-push.md#per-request-auth) over the channel for any guarded call. This is a hard rule, not an optimization: a node-class client downgraded to the anonymous front would lose its stable `peer_id` (never recognized as a returning peer for allowlisting, PEX, or reputation), lose access to the PEER/CONTROL methods a full read path may need (DHT lookups, availability-for-sync, multi-source [`dig.fetchRange`](#range)), and lose the channel-binding described next. + +#### Anonymous reads over the mTLS front — the ephemeral client certificate {#ephemeral-read-cert} + +An anonymous reader is not required to use the plain-HTTPS public-read front — it MAY instead dial the mTLS gateway front for **channel-bound** request integrity, at the cost of running a TLS client-cert handshake. The mTLS listener requires *a* certificate (`CERT_REQUIRED`), not a *known* one — peer identity is verified by the `peer_id` hash, not a CA ([§1](#peer-identity)) — so an anonymous caller satisfies the handshake with an **ephemeral, self-signed client certificate**: + +1. **Generate a throwaway keypair and a self-signed leaf**, scoped to the TLS session (or a short-lived batch of sessions) about to be opened. No registration and no persisted identity — the certificate exists only to complete the handshake. +2. **Complete the mTLS handshake** with it. The gateway derives a `peer_id` from it exactly as it would for any peer ([§1](#peer-identity)); this `peer_id` is disposable and carries **no reputation, no allowlisting, and no authorization** — it is a transport artifact, not a claimed identity. +3. **Issue the read** (`dig.getContent` or another [PUBLIC-READ](#tier-map) method) inside that authenticated channel, optionally carrying the same [§21.9-shaped](./transport-and-push.md#per-request-auth) signed-request headers a node-class client would send. +4. **The TLS session binds the request.** A signed request that travels inside a channel authenticated by that specific ephemeral certificate cannot be replayed over a *different* TLS session while still inside its [300-second freshness window](./transport-and-push.md#per-request-auth) — the channel itself becomes part of what a verifier can require to match. This is strictly additive integrity over the plain-HTTPS front, which binds a request to nothing. + +The read itself is governed by the same rules regardless of which front carried it: read-only, no mutation, [client-side self-verification](#dual-transport) against the chain-anchored root, decoy-on-miss ([§7a](#tier-map)). **Reaching the mTLS front never grants an anonymous caller a PEER/CONTROL method** — the [boundary invariant](#the-boundary-invariant) is enforced by what the request names, not by which front carried it; an ephemeral-cert caller naming a peer/write/config method gets the same [`-32601`](./dig-rpc.md#error-model) a plain-HTTPS caller would. + +:::caution Design status — gate on the gateway mTLS endpoint +The `rpc.dig.net` mTLS front described in this section is a **normative design contract**, not yet a live endpoint — today `rpc.dig.net` serves only the plain-HTTPS public-read front, and existing node-class clients still reach it that way for reads. This is a rollout transition, not a standing exception to the rule above: an implementation MUST NOT hard-break (crash, refuse to run, or treat the gateway as unreachable) merely because the mTLS front does not exist yet. It gates on the front's presence — a capability probe or a handshake attempt — and, only while that probe fails, continues reaching `rpc.dig.net` over the plain-HTTPS front for reads, never for peer/write/config methods (which simply are not reachable there, [§7a](#tier-map)). Once the mTLS front is deployed, every node-class client switches to it at the `rpc.dig.net` tier, closing this transition. +::: + ## 1 · Peer identity + mTLS {#peer-identity} A peer is identified by the SHA-256 of the DER-encoded `SubjectPublicKeyInfo` of the certificate it presents in the TLS handshake: @@ -386,6 +418,7 @@ Notes: - **`dig.getAvailability` / `dig.fetchRange` are PEER/CONTROL**, not public read: they are the node↔node **sync/multi-source** surface, ridden over the mTLS mux. The browser/agent public read path is the [dig RPC](./dig-rpc.md) (`dig.getContent` et al.) — a browser does not fan byte-ranges across peers; that is a node-side operation ([multi-source download](#multi-source)). - **The peer JSON-RPC surface is an allowlist.** Because the mTLS client-cert verifier accepts any well-formed self-signed leaf, "authenticated" means only "some `peer_id`", not "authorized". The peer surface therefore answers ONLY the read / discovery / announce methods above (`dig.getContent`, `dig.getAvailability`, `dig.listInventory`, `dig.fetchRange`, `dig.getNetworkInfo`, `dig.getPeers`, `dig.announce`, `dig.getAnchoredRoot`, `dig.getCollection`, `dig.listCollectionItems`) and returns [`-32601`](./dig-rpc.md#error-model) for every `cache.*` / `control.*` / `dig.stage` method — those are loopback / in-process only. `dig.getAnchoredRoot` / `dig.getCollection` / `dig.listCollectionItems` are read-only, owner-independent chain reads and are peer-reachable; `cache.*` / `control.*` / `dig.stage` mutate or read local state and are not. - **The read subset is identical in shape to the [network profile](./dig-rpc.md)** — this is why one JSON-RPC endpoint serves both a browser and the local consumer; only the *set of methods the anonymous listener answers* differs. +- **`rpc.dig.net` answers on two fronts, not two tiers.** The dual-mode gateway ([§0a](#gateway-dual-mode)) exposes the PEER/CONTROL column over its mTLS front (to node-class clients and ephemeral-cert anonymous readers) alongside the PUBLIC-READ column over its plain-HTTPS front — a method's tier assignment in this table never changes; only which transport a given caller uses to reach it differs. ## 7 · Peer RPC methods (node profile) {#peer-rpc} @@ -686,6 +719,7 @@ The peer network is implemented by several crates that must interoperate byte-fo | **`peer_id`** | `SHA-256(SubjectPublicKeyInfo DER)` → `Bytes32`, 64-hex on text surfaces | the identity every peer derives for every other peer; a mismatch means no interop | | **mTLS handshake** | `wss://` + client cert required + Chia `Handshake` (hex `network_id`, protocol version, node type) | that a peer link is authenticated and network-scoped before any message is processed | | **Two RPC tiers** | PEER/CONTROL = mTLS-authenticated (peer/DHT/PEX/availability-for-sync/write/config); PUBLIC-READ = anonymous, CORS-`*`, read-only, client-verified, decoy-on-miss ([§0](#dual-transport), [tier map §7a](#tier-map)). No peer/write/config method reachable without mTLS; content read requires no mTLS | that a browser can retrieve+verify content with no client cert while every mutation/identity/peer surface stays mTLS-gated — the boundary invariant is uniform across every serve layer + client | +| **Gateway dual-mode** | `rpc.dig.net` serves a plain-HTTPS anonymous public-read front (CORS-`*`, no client cert) and an mTLS front (`CERT_REQUIRED`) for node-class-client identity certs and ephemeral anonymous read certs ([§0a](#gateway-dual-mode)) | that a node-class client is never silently downgraded to the anonymous path even at the public gateway, and an anonymous mTLS reader gets channel-bound request integrity while carrying no identity or reputation | | **RelayMessage wire** | the RLY-001..RLY-008 JSON shapes in [§6](#relayed), `type`-tagged, `payload` as a byte array, `from` re-stamped, `network_id`-scoped; RLY-008 = the additive [PEX](#pex) binding | that any relay + any node speak the same rendezvous/hole-punch/relayed/PEX wire | | **PEX** | the four `pex_*` messages ([§4d](#pex)) with the `dig-pex/SPEC.md` shapes; two bindings — dig-nat mux stream (u32-BE+JSON, `PEX_MAX_FRAME` 262144) and the relay [RLY-008](#relayed) (additive WS frames, introducer-only, registration-backed `via:"introducer"`); entries are hints (first-hand rule, `via` provenance, strike-mute); `addresses[]` byte-compatible with `dig.getPeers`/DHT `Contact` | that peer gossip is anti-flood + first-hand across both transports, entries are verified-before-trusted, and PEX-learned contacts drop straight into a dial target | | **Relay error codes** | `1..4` (`NOT_REGISTERED`/`BAD_MESSAGE`/`PEER_NOT_FOUND`/`CAPACITY`) | deterministic relay-side failure signalling | @@ -704,6 +738,7 @@ A reimplementation of any peer crate conforms iff it reproduces these — the sa ## Related +- [Point a consumer at your node](../run-a-node/point-a-consumer.md) — the three-tier client→node resolution ladder (`dig.local` → `localhost` → `rpc.dig.net`) and how a node-class client dials mTLS across all of it, including the `rpc.dig.net` gateway's [mTLS front](#gateway-dual-mode) - [Private retrieval (onion routing)](./onion-routing.md) — the PRIVACY retrieval mode: telescoping onion circuits over these mTLS peer links, the `dig.onion` stream, and the onion-relay directory - [The dig RPC](./dig-rpc.md) — the PUBLIC READ tier: what a browser/agent reads over anonymous JSON-RPC; the node profile the peer RPC extends - [§21 transport & push](./transport-and-push.md) — the §21 GET (public read) vs PUSH/WRITE (mTLS peer/control) split diff --git a/package.json b/package.json index 6a3471d..3825bab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "docs-dig-net", - "version": "0.2.0", + "version": "0.3.0", "private": true, "scripts": { "docusaurus": "docusaurus", diff --git a/static/knowledge-graph.json b/static/knowledge-graph.json index 6fc4238..8566bc1 100644 --- a/static/knowledge-graph.json +++ b/static/knowledge-graph.json @@ -4018,6 +4018,11 @@ "to": "/docs/support/error-codes", "type": "see-also" }, + { + "from": "/docs/protocol/peer-network", + "to": "/docs/run-a-node/point-a-consumer", + "type": "see-also" + }, { "from": "/docs/protocol/peer-network", "to": "/docs/protocol/onion-routing",