dig-urn-protocol
The canonical DIG content-addressing + server-untrusted verification contract: the one definition of how a DIG URN names content and how a blind client turns opaque gateway bytes into verified plaintext, fail-closed.
dig-urn-protocol is a leaf crate — it has NO dig-* dependencies and NO transport
(reqwest/tokio). The merkle-fold and AES primitives are INJECTED by the caller (digstore_core), so
this crate reimplements no merkle or AES crypto and can never skew from the canonical read-crypto. It
performs only SHA-256 (the retrieval key + the content leaf).
Licensed under Apache-2.0 OR MIT. Normative contract: SPEC.md.
Protocol interface — at-a-glance reference
Everything below is the complete public contract. An implementation in any language conforms by
matching it (and passing the frozen vectors in tests/fixtures/).
The urn:dig: grammar (ABNF, RFC 5234)
dig-urn = "urn:dig:" chain ":" store-id [ ":" root-hash ] [ "/" resource ]
chain = 1*chain-char ; non-empty; canonical value is "chia"
chain-char = ALPHA / DIGIT / "-"
store-id = 64HEXDIG ; CHIP-0035 singleton launcher id, 32 bytes
root-hash = 64HEXDIG ; a capsule's on-chain root, 32 bytes (OPTIONAL)
resource = *pchar ; verbatim after the FIRST "/" (OPTIONAL)
HEXDIG = DIGIT / "a".."f" ; lowercase, canonical
Full form with the out-of-band salt query (peeled OUTSIDE the canonical identity):
urn:dig:<chain>:<store-id>[:<root-hash>][/<resource>][?salt=<64hex>]
Normative parse rules:
| Rule | Behaviour |
|---|---|
| Prefix | Literal urn:dig:. Anything else is rejected. |
| Resource split | At the FIRST /. The resource may itself contain /. |
| Colon arity (head) | Exactly 2 or 3 segments (chain:store-id or chain:store-id:root-hash). A 4th is rejected. |
| store-id / root-hash | Exactly 64 lowercase hex (32 bytes). Non-hex / wrong-length rejected. |
| Resource optionality | Resource is OPTIONAL. Bare urn:dig:chia:<store> names the store. |
| Trailing slash | …/ → resource = "" (empty), distinct from an absent resource. |
| Chain back-compat | chia (canonical), mainnet, testnet all accepted. Do not tighten — it would break the frozen corpus. |
?salt= |
NOT part of the URN identity. Peeled off before parsing; a surfaced salt MUST be exactly 32 bytes / 64 hex. The core parser leaves an un-peeled ?salt= inside the resource. |
DigUrn type
Derivation
| Function | Definition |
|---|---|
canonical() |
urn:dig:<chain>:<store-hex>[:<root-hex>][/<resource>], lowercase hex, absent fields omitted. Idempotent. |
canonical_rootless() |
canonical() with root_hash = None and resource_key = effective_resource_key(). |
effective_resource_key() |
The resource, defaulting absent/empty → index.html (DEFAULT_RESOURCE_KEY). |
retrieval_key() |
SHA-256(canonical()) — the URN-identity key (pins the root). Matches digstore_core::Urn and the frozen corpus. |
content_key() |
SHA-256(canonical_rootless()) — root-independent; the fetch identifier + AES-key seed, stable across generations. |
salt_bytes(hex) |
Validate a peeled salt → SecretSalt (exactly 64 hex). |
Constants: URN_PREFIX = "urn:dig:", CANONICAL_CHAIN = "chia", DEFAULT_RESOURCE_KEY = "index.html", SALT_QUERY_MARKER = "?salt=", URN_ABNF (the grammar text).
Resolution interface
// .kind() → "success" | "integrity_failure" | "unreachable"
ResolveError (catalogued, stable): Parse · Transport · Rpc · NotFound · RootRequired ·
VerifyFailed(String) · DecryptFailed. IntegrityFailure (reached network, unverifiable) and
Unreachable (network down) are never conflated.
Browser content-verification contract
Injected primitives (supplied by digstore_core — this crate reimplements neither):
Verification input (conceptual VerificationInput): { urn, salt?, ciphertext, inclusion_proof (wire bytes), chunk_lens (untrusted per-chunk ciphertext lengths), trusted_root (from the CHAIN) }.
Normative verify rules (all fail-closed, enforced by this crate):
| # | Rule | Enforced by |
|---|---|---|
| 1 | Rootless rejection — a rootless URN cannot be verified on the blind tier → RootRequired. |
require_blind_root(urn) |
| 2 | Leaf binding — proof.leaf == SHA-256(ciphertext). |
verify_inclusion (resource_leaf) |
| 3 | Path fold — the proof path folds consistently to proof.root. |
injected decode_and_fold |
| 4 | Root anchoring — proof.root == trusted_root (the chain-anchored root; NEVER the gateway's). |
verify_inclusion |
| 5 | Gate-then-decrypt — decrypt ONLY after 1–4 pass; the AEAD tag is the final gate. | verify_and_decrypt |
| 6 | u64-bounded chunk split — chunk_lens summed/bounded in u64 (no usize wrap on wasm32), total must equal the buffer, each window sliced defensively → DecryptFailed on any inconsistency, never a panic. |
chunk_ranges |
Pipeline entry points:
; // rule 2 (SHA-256)
; // rule 1
; // 2–4
; // 6
; // 5,6
; // full blind pipeline (1–6)
The node tier does NOT use this contract — a loopback node decrypts + verifies server-side and returns plaintext under a loopback trust boundary.
Relationship to other crates
digstore_core— supplies the injected merkle-fold + AES primitives;Bytes32is byte-compatible (Bytes32::from(other.0)).dig-rpc-protocol— this crate consumes itsPublicReadfetch contract conceptually; it does not depend on it or duplicate any RPC method. A concreteUrnResolverwires the two together.
Conformance
tests/fixtures/urn_conformance.json— the frozen URN vectors, imported byte-identically fromdigstore_core(URN → canonical → retrieval-key). Every URN implementation must pass these.tests/fixtures/inclusion_proof_vectors.json— goldenresource_leaf(SHA-256) +chunk_rangesvectors pinning the security-critical logic this crate owns directly.