Skip to content

fix: route dign open through dig-urn-resolver (§5.3 ladder + branded errors) (#745) - #50

Merged
MichaelTaylor3d merged 1 commit into
mainfrom
fix/dign-open-via-urn-resolver
Jul 17, 2026
Merged

fix: route dign open through dig-urn-resolver (§5.3 ladder + branded errors) (#745)#50
MichaelTaylor3d merged 1 commit into
mainfrom
fix/dign-open-via-urn-resolver

Conversation

@MichaelTaylor3d

@MichaelTaylor3d MichaelTaylor3d commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

TLDR

Fixes P0 #745: dig-node/dign open <urn|chia://> no longer hard-rolls a single http://localhost:9778/s/… URL and no longer surfaces the raw 502: could not read the store's on-chain root. It now routes through the canonical dig-urn-resolver (§5.3 ladder + fail-closed verify) — resolving even when the local /s/ tier 502s (#747, non-blocking) — and shows a branded DIG error asset when content can't load.

Success-open flow (chosen + why)

On a verified Success, probe the browser-navigable tiers in §5.3 order and open the FIRST that actually serves (matches the extension URN bar #308):

  1. http://<storeId>.dig/<path> — offered ONLY for a rootless link (the host can't pin a capsule root);
  2. http://dig.local/s/<storeId>[:<root>]/<path>;
  3. http://localhost:9778/s/<storeId>[:<root>]/<path>.

If NO browser tier can serve it (e.g. the local chain-read 502s per #747) but the resolver still returned VERIFIED bytes via rpc.dig.net, write those verified bytes to a temp file and open THAT. Rationale: the resolver has already fail-closed-verified the bytes, so the user always sees the exact verified content and NEVER a raw error — while still preferring live browser navigation when a tier can serve.

Branded errors

IntegrityFailure / Unreachable / hard resolve errors (not-found / rpc) → a static branded PNG from dig_urn_resolver::images (assets/), written to a temp file + opened. No hand-rolled page, no raw 502 string. (Asserted: the integrity case writes exactly images::png(Integrity) and never probes/opens a browser tier.)

Layering (the sanctioned #668/#745 exception)

The dig-urn-resolver dependency is in dig-node-service ONLY (the client shell's open-command path) — confirmed NOT in dig-node-core (the server engine), where it would be the real cycle. Depends on the published crates.io dig-urn-resolver = "0.3.1" (no git dep, #681). The resolver's public API crosses only String/Vec<u8>, so its internal (crates.io) digstore-core instance never interoperates with core's git-pinned one.

Security (audit notes)

  • Strict untrusted-input validation runs FIRST and is unchanged: a hostile scheme/metachar/traversal link exits USAGE and NEVER reaches the resolver or the launcher (regression test hostile_link_rejected_before_resolve_or_launch).
  • Fail-closed honored: success bytes come only from the resolver's verified Success; unverified/tampered bytes are never opened (the branded integrity asset is a static const PNG).
  • No shell: targets (validated http URL or a temp file path) are passed as a single non-shell argv entry.

Blast radius (gitnexus + review)

Single symbol group: dig_node_service::open (run/run_with + new open_success/open_branded/candidate_urls). Sole caller is entrypoint.rs (Command::Openopen::run), whose signature is unchanged. No other module imports open::. dig-node-core untouched.

Version

0.38.20.38.3 (patch — fix:, no public-API break). apps/nightlies: rides the next nightly; a stable tag is a manual dispatch.

Evidence (this session, local Windows build via ninja generator)

  • cargo test -p dig-node-service open::15 passed (success-tier selection, .dig-priority, root-pin-skips-.dig, verified-bytes fallback, integrity/unreachable/not-found branded assets, hostile-input security, URN reconstruction, ext derivation, preserved normalize validation).
  • Full crate: cargo test -p dig-node-service216 lib + all integration passed, 0 failed.
  • cargo fmt -- --check clean; cargo clippy --all-targets -- -D warnings clean; cargo build green.
  • SPEC.md §8.5 updated to the resolver-routed behavior.

Do NOT merge — for the review + security gate.

🤖 Generated with Claude Code

@MichaelTaylor3d
MichaelTaylor3d force-pushed the fix/dign-open-via-urn-resolver branch from 092c540 to 87da503 Compare July 17, 2026 13:37
@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review July 17, 2026 13:38
…(#745)

`dig-node/dign open <urn|chia://>` hard-rolled a single
`http://localhost:9778/s/<store>:<root>/<path>` URL and surfaced the local
node's raw `502: could not read the store's on-chain root` when that tier
could not serve. Route it instead through the canonical dig-urn-resolver — the
§5.3 ladder (dig.local -> localhost -> rpc.dig.net) with fail-closed verify —
so it resolves EVEN WHEN the local /s/ chain-read 502s (#747, non-blocking),
matching the extension URN bar (#308) and every other URN client (#668).

On a verified Success it opens the best browser-navigable tier (.dig ->
dig.local -> localhost, cheaply probed, first that serves). When no local tier
can serve while the resolver succeeded via rpc, it serves the already-verified
bytes over an EPHEMERAL LOOPBACK HTTP endpoint (http://127.0.0.1:<port>) and
opens that. On IntegrityFailure/Unreachable/hard error it serves a branded DIG
error asset from the resolver (never a raw 502 string).

SECURITY (#745, closes the review-gate RCE): resolved bytes + content type +
resource name are all attacker-controlled and 'verified' means chain-inclusion,
NOT safe. The command NEVER writes resolved bytes to disk and NEVER OS-opens a
local file (which would run an attacker .hta/.js with no Mark-of-the-Web ->
RCE, or give HTML a file:// origin). It serves them over a sandboxed loopback
http origin so the browser applies its normal download/MOTW/origin handling;
attacker-influenced header values are stripped of CR/LF. The strict untrusted-
input gate still runs FIRST — a hostile link never reaches the resolver or the
launcher.

The dig-urn-resolver dependency is in dig-node-service (the client shell) ONLY,
never dig-node-core (the server engine), per the #668/#745 layering exception.
Depends on the published crates.io dig-urn-resolver 0.3.1 (no git dep, #681).

SPEC.md §8.5 updated. Version 0.38.2 -> 0.38.3 (patch: fix).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MichaelTaylor3d
MichaelTaylor3d force-pushed the fix/dign-open-via-urn-resolver branch from 87da503 to d796e8e Compare July 17, 2026 14:08
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Security fix pushed — RCE closed (fix (a): serve over ephemeral loopback HTTP)

Addressed the HIGH/near-CRITICAL RCE the security gate found in the open_success content-fallback (verified ≠ safe — attacker-controlled bytes + content_type + link.path written to %TEMP%\*.hta with no Mark-of-the-Web, then OS-default-opened → arbitrary code execution).

Chosen: preferred fix (a). The content-fallback (and every branded-error case) now serves the bytes over an ephemeral 127.0.0.1:<port> HTTP endpoint and opens that http://127.0.0.1:… URL — the process NEVER writes resolved bytes to disk and NEVER hands the OS a local file. The browser then applies its normal render-vs-download / Mark-of-the-Web / SmartScreen / sandboxed-origin handling — exactly the safety the pre-#745 localhost/s/ serve had. This also removed the temp-file write entirely, mooting the two non-gating disk DiD findings (CWE-377/59, no cleanup).

Server hardening: X-Content-Type-Options: nosniff, Cache-Control: no-store, Connection: close, short-lived (bounded lifetime + linger), and attacker-influenced header values (content_type, filename) are stripped of control chars / CR-LF to prevent HTTP response-header injection.

Regression test added: attacker_executable_is_served_over_http_never_written_or_os_opened — for payload.hta/evil.js/run.vbs with executable content types, asserts the opened target is a http://127.0.0.1: URL (never a file:/disk path) and the bytes are served (browser downloads w/ MOTW), never OS-opened as an executable. Plus real_local_server_serves_bytes_over_loopback_http (end-to-end proof the real server serves over http with nosniff) and sanitize_header_value_strips_crlf_injection.

Fail-closed + branded-error behavior (which passed) is preserved; the strict untrusted-input gate still runs FIRST (hostile link never reaches resolver/launcher/server).

Evidence (local, ninja generator): cargo test -p dig-node-service open::18 passed; full crate → 219 lib + all integration, 0 failed; cargo fmt --check clean; cargo clippy --all-targets -- -D warnings clean. SPEC.md §8.5 updated (disk-write forbidden; loopback-serve required). Version stays 0.38.3.

Please re-run the security + review gate. Do NOT merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant