Uh oh!
There was an error while loading. Please reload this page.
fix(service): anchor identity + cache under the state dir, not $HOME - #127
Merged
Conversation
A stock `.deb` install never joined the P2P network. The packaged systemd unit sets ProtectHome=true, but the node writes its identity seed to dirs::config_dir()/dig — i.e. $HOME/.config/dig — which the sandbox has just made unreadable. Seed creation fails EROFS, Node::from_env() falls back to identity_seed: None, and peer bring-up refuses with "no identity seed; peer network needs a stable identity". Observed identically on three fresh installs: dig-node: no identity key (Read-only file system (os error 30)) dig-node: peer network bring-up failed: no identity seed The cache collides the same way, falling back to a /tmp dir that does not survive a restart — so even a node that did start could never accumulate anything to reshare. Fixed as a property of a SERVICE RUN rather than of the Linux packaging, because the macOS launchd daemon and the Windows service inherit the same home-relative default: on a service run the identity and cache are anchored under the machine state dir the service already resolves and hardens (#501), before the identity is first loaded. A CLI run is untouched — the user's identity stays shared with their other DIG tools — and an operator-set DIG_IDENTITY_DIR / DIG_NODE_CACHE is never overridden. The new tests assert the DECISION, not a Unix-only filesystem effect, so the guard stays falsifiable on every host CI runs on rather than silently unexercised on Windows. Refs DIG-Network/dig_ecosystem#1928 Co-Authored-By: Claude <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
MichaelTaylor3d pushed a commit
that referenced
this pull request
Aug 5, 2026
…2088) A >1-generation store was unreadable for every file not in its latest commit: the plaintext serve pinned every read to the chain-anchored tip, but a file unchanged since an earlier commit lives in an OLDER capsule (its root != tip), so the tip fetch folded to a decoy and the file read as a 404 for every generation but the latest. After the #127 pin resolves the tip, read the tip capsule's PublicManifest (section 13) to resolve, per path, the latest_root that holds the file (serve_root) and its sha256_latest leaf (expected_leaf). Serve from serve_root through all three tiers, and bind older-generation bytes to the chain-anchored tip by requiring proof.leaf == expected_leaf in verify_and_decrypt. The older capsule's own root is not chain-anchored and is attacker-choosable in isolation, so sha256_latest -- read from the trusted tip manifest -- is what ties the served bytes to the tip. Anti-rollback is untouched: a client-supplied superseded root still fails -32005; only the node's own trusted tip manifest may redirect a read to an older capsule. Legacy/private/absent-manifest reads are byte-identical to before. X-Dig-Generation now reports the generation that holds the file, which may be below the tip ordinal. Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d pushed a commit
that referenced
this pull request
Aug 5, 2026
…tion redirect (#2088) The #2088 generation-resolution serve read both serve_root (entry.latest_root) and expected_leaf (entry.sha256_latest) from the tip capsule's §13 PublicManifest — an additive section NOT committed into current_root and NOT checked by the capsule anchor gate. A malicious holder could serve a genuine tip capsule with a FORGED §13 pointing latest_root/sha256_latest at attacker content, substituting arbitrary bytes (and rolling back) on any multi-generation public store while stamping X-Dig-Verified: true — re-opening the #127 anti-rollback invariant. Add the missing gate: before honoring a redirect to an older serve_root, verify that root is a GENUINE root in the store's authenticated on-chain singleton lineage via a new AnchoredRootResolver::verify_lineage_root (CoinsetResolver walks sync_datastore_with_history + membership; the trait default and test mock accept only the current tip). If serve_root is not in the lineage the serve fails closed to the tip (decoy/miss), never the attacker-named bytes. expected_leaf stays a fail-closed defense-in-depth routing guard; the client-root -32005 pin is unchanged. #2203 (bind §13 into current_root) remains the full closure. Regression test: a forged §13 whose latest_root is out-of-lineage is refused (never serves the attacker bytes); the genuine two-generation store still serves real plaintext (its older root is authenticated in the lineage). SPEC §14.4a. Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit
that referenced
this pull request
Aug 5, 2026
) Closes dig_ecosystem#2088 (P0). A store with more than one generation was unreadable for every file not changed in its latest commit — every store broke on its second commit for files that commit didn't touch. decrypt_local rejected any local capsule whose roothash != the pinned tip, so a file living in an older generation's capsule was dropped as a miss and the tip then blind-served a decoy. Fix: after the #127 pin resolves the chain-anchored tip, read the tip capsule's PublicManifest (§13) to find each path's owning generation, and serve from that capsule (serve_root). serve_root is chain-authenticated against the store's on-chain singleton lineage before any redirect is honored (verify_lineage_root over the coinset root_history — the same chain authority the pin uses); a latest_root not in the store's authenticated lineage fails closed to the tip serve. Served bytes are additionally bound to the tip manifest's sha256_latest via an expected_leaf check in verify_and_decrypt (defense-in-depth). The #127 client-root pin (-32005 ROOT_NOT_ANCHORED) is untouched and serve_root is never client-derivable. Dual gate PASS: loop-reviewer (correctness) + loop-security. The security gate initially caught a content-substitution hole (serve_root/expected_leaf sourced from the unauthenticated §13 manifest); the serve_root lineage-authentication commit closes it — re-audit confirms a forged §13 naming an out-of-lineage root is refused (non-vacuous regression test), the genuine multi-generation case still serves real plaintext, and client-supplied stale roots still return -32005. Two non-gating follow-ups tracked: a residual genuine-prior-version rollback (source expected_leaf from current_root) and lineage-walk caching. root [workspace.package].version 0.99.0 -> 0.99.1 (patch, fix). Closes #2088 Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d pushed a commit
that referenced
this pull request
Aug 6, 2026
The peer-serve arm (dig.fetchRange) validated only request shape then served any range that Merkle-verified against the CLIENT-named root, so a peer could fetch bytes of a forged or superseded generation that the local /s and dig.getContent read paths already refuse. Factor the read arm's #127 pin resolution into a shared resolve_enforced_pin helper and apply it to BOTH the dig.getContent read arm (pure refactor) and the dig.fetchRange serve arm (the fix), so one policy governs read and serve with no drift. Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d pushed a commit
that referenced
this pull request
Aug 6, 2026
The peer-serve arm (dig.fetchRange) validated only request shape then served any range that Merkle-verified against the CLIENT-named root, so a peer could fetch bytes of a forged or superseded generation that the local /s and dig.getContent read paths already refuse. Factor the read arm's #127 pin resolution into a shared resolve_enforced_pin helper and apply it to BOTH the dig.getContent read arm (pure refactor) and the dig.fetchRange serve arm (the fix), so one policy governs read and serve with no drift. Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit
that referenced
this pull request
Aug 6, 2026
…e, gateway paths (#1764, #1765) Closes dig_ecosystem#1764. Closes dig_ecosystem#1765. The dig.fetchRange peer-serve arm had no chain-anchor gate — it served any range Merkle-verifying against a client-named root, while /s and dig.getContent fail-closed on the #127 pin. Factor the pin resolution into a shared resolve_enforced_pin helper applied to both dig.getContent (refactor) and dig.fetchRange (the fix), so unanchored/superseded/unreachable-chain content fails closed with -32005 uniformly across all three serve paths. x-dig-source stays orthogonal to x-dig-verified. Anti-rollback (#127/#2088) preserved. v0.99.5. Both gates PASS. Co-Authored-By: Claude <noreply@anthropic.com>
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.
Fixes DIG-Network/dig_ecosystem#1928.
What was wrong
A stock
.debinstall never joins the P2P network. Found on three fresh installs while standing up the 3-region node fleet (DIG-Network/dig_ecosystem#1927) — nothing customised, justapt install ./dig-node_0.71.0_arm64.deb:Two individually-reasonable halves that are jointly fatal:
packaging/linux/systemd/net.dignetwork.dig-node.servicesetsProtectHome=true; systemd runs the root service withHOME=/root, so/rootis unreadable.dirs::config_dir()/dig/identity_key.bin→$HOME/.config/dig— inside the directory the sandbox just blanked.So
load_or_create_seed()failsEROFS,Node::from_env()setsidentity_seed: None, andpeer.rsrefuses bring-up. The cache collides identically, falling back to/tmp/DigNode-<pid>/cache, which does not survive a restart — so even a node that did start could never accumulate anything to reshare.Confirmed by differential, same binary, same box:
The fix
Anchored as a property of a SERVICE RUN, not of the Linux packaging — the macOS launchd daemon and the Windows service inherit the same home-relative default, so fixing only the unit would leave two of three platforms carrying the bug. On a service run the identity and cache resolve under the machine state dir the service already resolves and hardens (#501), before the identity is first loaded.
DIG_IDENTITY_DIR/DIG_NODE_CACHEis never overridden.How verified
Live, on the #1927 fleet — same package, only the equivalent drop-in added:
All three regions went from
running:falsetorunning:truewith a held relay reservation.Suite: 294
dig-node-servicelib tests green,fmt+clippy --all-targets -D warningsclean. The four new tests assert the decision rather than a#[cfg(unix)]filesystem effect, so the guard stays falsifiable on every host CI runs on instead of being silently unexercised on Windows.SPEC.md§7.3a gains the normative rule.Minor bump: 0.71.1 → 0.72.0.
Co-Authored-By: Claude noreply@anthropic.com