Uh oh!
There was an error while loading. Please reload this page.
fix(dig-node): serve tip-authoritative to close the Case-A §13 downgrade (#2211) - #192
Merged
Merged
Conversation
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…ade (#2211) The #2088 leaf binding + the #184 lineage cross-check bind a redirected serve to a genuine lineage generation, but not the path's canonical one: §13 is additive and NOT committed into the chain-anchored current_root, so a forged tip §13 can redirect a tip-committed path at a genuine-but-superseded prior generation and downgrade it. Serve TIP-FIRST with no §13 leaf binding (bind purely by proof.root == tip), and consult the §13 redirect + expected_leaf ONLY on a genuine tip miss. A path the tip's current_root commits is served from the chain-anchored tip (forged redirect never reached, Case A closed); an older-generation file misses at the tip and falls through to the still-lineage-authenticated §13 redirect exactly as #2088 intends (Case B preserved). Case B rollback stays open on #2211, blocked on the per-path current-state commitment (#2203). Co-Authored-By: Claude <noreply@anthropic.com>
… root (#2211) The tip-authoritative Case-A closure assumes a tip serve MISS means the path is legitimately absent from the tip generation. That premise is unenforced: the capsule anchor gate compares only the 32-byte CurrentRoot HEADER against the chain, never recomputing the tree from MerkleNodes. A malicious holder can craft a tip capsule whose header still names the genuine tip while its data is tampered so a tip-committed path no longer folds to it -- the node admits + caches it, the tip serve misses that path, and the forged §13 drives a redirect at a genuine-but- superseded prior generation: the rollback Case A was meant to prevent. Refuse a §13 redirect unless the tip capsule's own data folds to its committed current_root (verify_module_root recomputes MerkleNodes root == committed CurrentRoot, and that root == the chain-anchored tip). A tampered tip yields a clean miss, never a downgrade. Placed at the §13-trust boundary so it covers a tip capsule however it entered the cache. Also restore the #2088 fall-through: the tip-first / §13-redirect two-pass now DEFERS a non-Served tip outcome (a decoy miss OR an upstream error) while a redirect candidate remains, so a tip-pass upstream error no longer pre-empts a legitimate older-generation read. Regression test: a tampered-tip capsule forced to miss must never serve the rolled-back v1 (red without the gate: serves V1-OLD). The #2088 older-gen read, Case-A honest downgrade, #184 out-of-lineage, and client-supplied-superseded tests stay green. SPEC §14.4a corrected; DEVELOPMENT_LOG updated. Co-Authored-By: Claude <noreply@anthropic.com>
Silences clippy::single_match on the _2211 tampered-capsule test; logic unchanged (the gate + adversarial coverage are identical). Co-Authored-By: Claude <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs DIG-Network/dig_ecosystem#2211 (interim; Case B stays open, blocked on #2203). Anti-rollback (NC-9 / #127) defense-in-depth.
The residual (from #2088)
The #2088 read path follows the §13
PublicManifest's per-pathlatest_root/sha256_latestto serve each path from the generation holding it; #184 chain-authenticates thatlatest_rootis in the store's lineage (closing arbitrary substitution). But §13 is a forgeable additive section, not committed into the chain-anchoredcurrent_root(current_root = MerkleTree::from_leaves(merkle_leaves)commits only the tip generation's own leaves). So a forged §13 naming a genuine-but-superseded prior generation is honored → a downgrade served asX-Dig-Verified: true.Two sub-cases: Case A — the path's current version is at the tip (its leaf IS committed by
current_root); Case B — the current version lives in an older generation (no chain-anchored per-path fact to distinguish it). This PR closes Case A in dig-node; Case B requires the digstore #2203 change (commit §13 intocurrent_root) and stays open on #2211.Fix (Piece 2 — tip-authoritative serve)
The generation-resolution redirect no longer honors a §13 redirect for a path whose current bytes are committed by the tip's chain-anchored
current_root: the node serves tip-first, bound purely viaproof.root == tip(the chain-anchored tip), and consults the §13 redirect +expected_leafonly on a genuine tip miss (the tip capsule's constant-time decoy fall-through). A path present at the tip is served from the chain-anchored tip and the forged redirect is never reached; a legitimately-older-generation file misses at the tip and falls through to the (still §13-driven, Case-B) redirect exactly as #2088 intends.Preserves: #2088 (older-generation-unchanged files still resolve via the fall-through), #1764/#1765 (tip serve runs the full anchor-gate / uniform enforcement; unanchored/superseded →
-32005;X-Dig-Source⊥X-Dig-Verified), #2088 lineage-auth #184 (verify_lineage_rootuntouched, still gates the fall-through), andX-Dig-Generationfor the fall-through case.SPEC honesty correction (Piece 1)
SPEC.md §14.4ano longer overstates the guarantee — it now states the lineage cross-check binds served bytes to a genuine lineage generation, not the canonical/maximal one; a forged §13 naming a genuine-but-superseded prior generation is honored for a path whose current version lives in an older generation (a rollback bounded to owner-committed content, not arbitrary injection); full closure requires the per-path current-state commitment tracked in #2203.Test (TDD, red → green)
lib.rsCase-A regression: the tip generation holdsasset.jsv2 (leaf committed bycurrent_root); a §13 forged to a genuine prior-generationlatest_root+sha256_latest(v1) must serve v2 (or a miss), never v1. It fails onmain(forged redirect honored) and passes with this fix. The #2088-preserved + lineage-auth regressions stay green.Blast radius
crates/dig-node-core/src/seams/content/content_serve.rs(the generation-resolution redirect reorder),crates/dig-node-core/src/lib.rs(the Case-A test + resolution),SPEC.md§14.4a,DEVELOPMENT_LOG.md. No wire/format change → no docs.dig.net / SYSTEM.md / canonical companion; §13 semantics unchanged.Version
[workspace.package].version0.99.7 → 0.99.8 (patch,fix:— a serve-path tightening, no wire/API change).Cargo.lockregenerated.Generated by Claude Code