Layer 2/3 · The self-defending module
Canonical reference:
digstore-compiler(pipeline.rs,inject.rs,filler.rs,config.rs) for the module;digstore-guest::content+digstore-hostfor the serving guest. The data it carries is the DIGS data section.
Fixed-size obfuscation
Every module's injected DIGS blob is padded to exactly uniform_blob_len (default FIXED_BLOB_LEN = 128 MiB), so all production stores compile to the same module size — leaking nothing about content size (config.rs:11-15,38-44; override DIGSTORE_UNIFORM_BLOB_LEN for tests).
Pipeline math (pipeline.rs:106-136): encode the blob with an empty Filler body → blob_len_without_filler; reject if it exceeds the budget (never truncate); filler_len = budget − blob_len_without_filler. Filler (id 11, unreferenced) is the ONLY variable section — it never touches leaves or CurrentRoot, so padding changes nothing served or proven.
Filler bytes are deterministic: a ChaCha20 keystream with seed = SHA-256(store_id || roothash || "digstore-filler-v1"), key = seed, nonce = 12 zero bytes, positional (so a shorter request is a prefix of a longer one) — filler.rs:7-28. Determinism is what makes compilation byte-identical.
WASM injection & memory layout (BINDING D2)
DIGS_DATA_OFFSET = 0x0020_0000(2 MiB), chosen above the guest's rodata (the linker places rodata at the 1 MiB default global base).- The guest bump heap floats dynamically at
align_up(DIGS_DATA_OFFSET + blob_len, 64 KiB), so it never overlaps the data section for any blob size (datasection.rs:32-47). inject_data_sectionappends the DIGS blob as an active data segment ati32.const DIGS_DATA_OFFSETlast (later active segments win on overlap), drops the originalDataCountsection, and re-emits the Memory section withminimum = max(template_min, ceil((offset+blob)/65536))and alwaysmaximum = Some(6144)(= 384 MiB ceiling,MAX_MEMORY_PAGES); rejects if needed pages > 6144 (inject.rs:16-206).
History: the original 1 MiB offset collided with guest rodata and a fixed 8 MiB heap overlapped the chunk pool → the module dropped chunks and failed to self-serve; fixed by 2 MiB + a floating heap.
The self-serving guest pipeline
The capsule WASM self-serves: it runs its own serve flow for a requested retrieval key inside a bounded host runtime, and the host only decodes the envelope framing — never decrypting (content.rs:39-456).
The gate chain
opaque-true predicate
→ temporal window
→ attestation [DISABLED by default]
→ JWT [only when AuthInfo.requires_jwt]
→ KeyTable lookup → oblivious gather → ContentResponse
else → an indistinguishable, success-shaped decoy