This document records the security posture of the digstore workspace: the hardening applied in the 2026-06-09 audit, the residual risks that are tracked but not yet closed, and how to run the supply-chain checks.
Report vulnerabilities privately to the maintainer (see Cargo.toml authors).
Do not open public issues for unpatched security bugs.
- Storage/serving host is untrusted for confidentiality ("provider blind"): it stores ciphertext and serves it without decrypting. Chunk plaintext and the per-URN keys never leave the client.
- Remote sync peers are untrusted: a
clone/pullmust not be able to install or execute attacker-chosen content. Transport is TLS; modules are verified against the requested store identity before use. - WASM guest modules are untrusted: the host sandboxes them with memory / table / fuel / wall-clock limits and a restricted host-import surface.
- Store identity is the on-chain Chia launcher id (§20.1):
store_idis the launcher id of the store's singleton, minted bydigstore init. It is NOT a hash of the publisher key. Head authorization — advancing a store to a served root — is a BLS signature overSHA-256(root || store_id)verified against the module's embedded publisher key.
Crypto
- Chunk AEAD switched to AES-256-GCM-SIV (RFC 8452). The previous
AES-256-GCM used a fixed all-zero nonce across every chunk and every version of
a resource; under GCM that is catastrophic (keystream reuse + GHASH
authentication-key recovery / "forbidden attack"). GCM-SIV is nonce-misuse
resistant, so a fixed nonce is safe while keeping encryption deterministic for
the ciphertext-committed merkle root. (
digstore-crypto/src/aead.rs) - Identity (point-at-infinity) BLS public keys are rejected by
validate_public_key. (digstore-crypto/src/bls.rs)
Remote sync (clone / pull / client)
- Downloaded modules are cryptographically verified before use.
cloneandpullnow require: embeddedStoreId == requested store id(the launcher id), and the merkle root recomputed from the module's own content equals both the embeddedCurrentRootand the served root. Previously the client installed and executed whatever bytes the server returned. (digstore-compiler::verify_module_root, wired indigstore-cli/.../remote_ops.rs) - Authenticated head (closes the former residual #1). The remote now persists
the verified publisher push signature per root and returns the served-head
signature in the store descriptor;
clone/pullre-verify it (verify_push) against the module's embedded publisher key and fail closed on an absent or invalid signature. This upgrades clone/pull from "self-consistent module" to "publisher-authorized content", so a malicious origin that does not hold the publisher's private key can no longer serve fabricated content. A regression test (clone_rejects_unauthenticated_or_forged_head) proves the fail-closed path. (digstore-remotewire/backends/handlers,remote_ops.rs::verify_head_signature) - Transport policy enforced. Remote URLs must be
https://(plaintexthttp://is allowed only for loopback). The HTTP client follows no redirects, so a malicious server cannot bounce a push (signature + body + bearer) to an attacker host or use redirects for SSRF. (remote_ops.rs,client.rs) - Delta chunk integrity verified: each delta chunk must hash to its
advertised content id before it is accepted. (
client.rs) - Rate limiter is now a time-based token bucket that refills over a window
(a one-time burst can no longer permanently lock out a store) and the bucket
map is bounded (no unbounded-growth memory DoS). (
ratelimit.rs) - 5xx responses no longer echo internal detail (filesystem paths, IO/join
errors); detail is logged server-side only. (
server.rs)
Host / WASM sandbox
jwks_fetchSSRF guard: the guest-controlled URL must behttpsand resolve only to public addresses; loopback/private/link-local/CGNAT and the cloud-metadata endpoint are refused. (digstore-host/src/imports.rs)- Store limits bound all growable resources (linear memory, table elements,
table/memory/instance counts), and the WASM threads/shared-memory proposal is
disabled for serve-only modules. (
runtime.rs) - Fetched modules are size-bounded before validate/compile in
dighost. - Blind-serve host RNG uses OS entropy instead of a hardcoded seed, so decoys
returned on a retrieval miss are not distinguishable from real content.
(
serve_blind.rs)
Multi-store workspaces and resource limits
- Per-store 128 MB content cap. Each store enforces a hard cap of
MAX_STORE_BYTES = 128_000_000(decimal) on staged plaintext content. It is enforced atomically atadd(staging that would exceed the cap stages nothing and errors with the remaining headroom) and defensively atcommit. The cap bounds the worst-case data-section blob, so a store can never produce a module that exceeds the module memory ceiling. (digstore-coreis the single source of truth for the constant; the CLI enforces it.) - Module memory ceiling raised to a configurable 384 MiB. The module-declared
linear-memory cap is 6144 pages (384 MiB), sized to hold the embedded data
section (up to the 128 MB cap plus overhead) and a single-copy serve of a
worst-case ~122 MB resource. The host's outer limit defaults to the same 384 MiB
and is operator-configurable via
ExecutionLimits.memory_bytes_max— the real DoS bound for an untrusted module. The guest heap base is placed dynamically above the data section, so heap growth can never corrupt the embedded chunk pool for any blob size. (digstore-compiler,digstore-host,digstore-guest) - Uniform module size. Size-obfuscation filler pads every module's data blob
to one fixed budget, so every store compiles to the same module size regardless
of content. A full-cap store carries ~no filler; smaller stores carry
deterministic filler up to the budget. The module size therefore reveals nothing
about how much content a store holds. (
digstore-compiler)
CLI / filesystem
- Key material uses the OS CSPRNG (
getrandom); the previous time/pid/pointer "RNG" produced predictable BLS signing keys and private salts. The weak fallback was removed (fail closed). (store_ops.rs) - Secret files (
signing_key.bin, salt) are written0600on Unix. checkoutrejects path-traversing resource keys (.., absolute paths, Windows drive/ADS), so a malicious cloned store cannot write outside the output directory. (checkout.rs)
Supply chain / build
Cargo.lockis now committed (this workspace ships binaries).[profile.release]enablesoverflow-checks— this code does offset/length arithmetic on untrusted serialized input.deny.tomladded forcargo deny check(advisories, licenses, sources, wildcard bans).
These are known and NOT yet fixed. They are intentionally called out so they are not mistaken for closed.
-
Key rotation / root revocation. With the authenticated head in place (below), a leaked store key still cannot be rotated without minting a new store id, and there is no signed tombstone to retract a previously published root. The store key is effectively long-lived.
-
Merkle tree has no leaf/node domain separation (
digstore-core/src/merkle.rs) and BLS signing messages lack per-role domain-separation tags (digstore-crypto/src/bls.rs). Both are defense-in-depth against second-preimage / cross-protocol signature reuse. Deferred because the change alters every root/signature and must be made in lockstep across the host and the guest verifier plus all fixtures. -
Proof backend is
MockProver(forgeable) on the default serve path — but the chain source, clock, and backend selection are now real and injectable.- Real chain source.
digstore_prover::CoinsetChainSourcefetches the current Chia peak + on-chain block records fromhttps://api.coinset.org(POST /get_blockchain_state,POST /get_block_record_by_height), supplying the real block header hash / height / timestamp the attestation freshness gate anchors to (§13.7/§16). Best-effort, short timeout, clearChainRpcerrors; parsing + HTTP-mocked tests run in CI and an#[ignore]d live test hits the real mirror. (digstore-prover/src/coinset.rs,tests/coinset_parse.rs,tests/coinset_live.rs) - Real clock.
digstore_host::SystemClock(OS wall clock) replaces theFixedClockwhenever a real chain is wired. (digstore-host/src/clock.rs) - Injectable serve path.
serve_blindno longer hardcodes the trio.BlindServeDepsmakes the prover, chain source, and clock injectable; it defaults to the mock/fixed trio (so existing tests + the toolchain-free default build stay green) and a caller can swap inCoinsetChainSource+SystemClock(with_real_chain_clock) and a realRisc0Prover(with_risc0_prover).serve_blind_withis the injection entry point. (digstore-host/src/serve_blind.rs) - The backend SELECTION compiles in both modes. With the
risc0feature OFF (the default) the backend isMockProver; with it ON, a realRisc0Proveris available — guarded by#[cfg(feature = "risc0")]so the default build never pulls the toolchain.
Still required for trustworthy proofs (the toolchain boundary): real RISC0 proving needs the RISC0 toolchain (
r0vm/rzup) and is enabled via therisc0cargo feature (digstore-host/risc0->digstore-prover/risc0), which triggersrisc0-build'sembed_methodsto compile the zkVM guest ELF. It is NOT built or tested in CI here because the toolchain is not installed in this environment. The wiring is done: flip therisc0feature, install the toolchain, and supplyCoinsetChainSource+SystemClock+Risc0Provertoserve_blind_withto produce real execution proofs. Until then the default serve path's proofs remain forgeable (mock backend). - Real chain source.
-
JWT signature verification — implemented (closes the former residual #4). The guest JWT gate (
digstore-guest/src/content.rs) now verifies the token's cryptographic signature, not just its claims. RS256 (rsaPKCS#1 v1.5 over SHA-256) and ES256 (p256) are supported; the verifying key is reconstructed from the store's trusted JWKS, which the guest fetches over the session-gatedjwks_fetchhost import using thejwks_urladvertised in the embedded AuthInfo section (§6.2). The gate is no longer hardcoded off:get_content/get_proofderiverequire_jwtfromAuthInfo.requires_jwt. A token with a valid signature from a trusted key and valid claims releases real content; a tampered/absent signature, a key not in the JWKS, an unknownkid, a missing JWKS URL, or any claim failure fails closed -> Decoy (never real content, never a 404). -
Dependency advisories currently accepted (see
deny.toml):rsa(RUSTSEC-2023-0071, Marvin timing side channel).rsav0.9 is a direct, verify-only dependency ofdigstore-guest— it is used solely for JWT RS256 signature verification (public-keyverify, no decryption or private-key operations). The Marvin attack is a timing oracle against RSA decryption, so it does not apply to signature verification; the ignore is retained with that rationale and must be re-evaluated ifrsais ever used to decrypt. The formerbincode 1.xadvisory (RUSTSEC-2025-0141) ignore has been removed:bincodeis no longer in the dependency tree. Re-evaluate each audit. -
Clone/pull chain-verified head authentication — Closed (2026-06-11, Phase B). Now that
store_idis the on-chain Chia launcher id (no longerSHA-256(publisher key)), the integrity gate (verify_module_root) checksStoreId == requested+ merkle self-consistency, and the head signature is verified against the publisher key the module itself carries. On top of that,clone/pullnow verify that the served root equals the store singleton's current on-chain root, read from the chain via the launcher id embedded in the module'sChainStatesection (Phase A). The check fails closed on a mismatch or an unreachable chain (CliError::VerificationFailed, exit code 5) — it never silently falls back to trusting the served head. The chain (not the serving origin) is therefore the authority for the latest authorized root, closing the earlier first-use-trust gap: a malicious origin can no longer serve a self-consistent module with an attacker-chosen root for the correct launcher id, because the on-chain root will not match.Backward-compat caveat: a module that carries no embedded
ChainState(older modules predating Phase A) has no on-chain pointer to verify against, so the chain-root gate is a no-op for it and the head-signature gate remains the authority. Offline test seam: verification is exercised entirely offline via theDIGSTORE_ANCHOR_MOCKseam (DIGSTORE_ANCHOR_MOCK_CHAIN_ROOT/DIGSTORE_ANCHOR_MOCK_CHAIN_UNREACHABLE; seecrates/digstore-cli/tests/cli_chain_verify.rs).
cargo test --workspace
cargo install cargo-deny --locked && cargo deny check advisories bans sourcesCI (.github/workflows/ci.yml) runs fmt, clippy, build+test on Linux+Windows,
and the cargo deny supply-chain checks on every PR and push to main. The
cargo deny license-compliance check is not yet gated — the workspace crates
need explicit license fields and the full transitive license set must be
enumerated first; this is a tracked follow-up.