Skip to content

fix(remote): clear error when a remote returns non-JSON/CloudFront HTML#4

Merged
MichaelTaylor3d merged 1 commit into
mainfrom
fix/123-124-remote-clear-error-help
Jul 6, 2026
Merged

fix(remote): clear error when a remote returns non-JSON/CloudFront HTML#4
MichaelTaylor3d merged 1 commit into
mainfrom
fix/123-124-remote-clear-error-help

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

TLDR

When a remote push is rejected at a CDN edge (rpc.dig.net's CloudFront returning a 403 "This distribution is not configured to allow the HTTP request method that was used for this request" HTML page — the DIG-Network/dig_ecosystem#123 live-user block), the CLI printed error: unauthorized: <raw HTML dump> — mistaking the HTML body for a server auth message. This surfaces a clean, catalogued error instead. Also records the #124 remote-CLI audit outcome. Refs #123, #124.

Root cause

digstore-remote read the error-response body and, when it wasn't JSON, returned the raw body verbatim; a 403/401 then mapped to CliError::Unauthorized(<that body>). A CloudFront HTML error page therefore printed as a giant HTML dump labelled "unauthorized".

Fix (TDD)

  • New pure, unit-tested classifier classify_remote_error(status, body) (in digstore-remote::client), shared by every remote error path (push-init POST /module/upload, inline finalize PUT /module, POST /module/complete, POST /tombstone):
    • JSON {"message"|"error": …}ClientError::Remote { status, message }genuine 401/403 reasons are preserved and still map to Unauthorized.
    • HTML / CloudFront page → new ClientError::NonJsonResponse { status, hint } with a clean, actionable hint (the raw markup is discarded, never shown), e.g. "the request was answered by a CDN/edge (CloudFront), not the DIG RPC origin (HTTP 403) — the remote's push/write endpoint or its allowed HTTP methods may be misconfigured".
    • empty body → bare ClientError::Status.
  • The CLI maps NonJsonResponseCliError::Network(hint) (exit 6), NOT Unauthorized.
  • Removed the old extract_server_message; the three error sites now call one classifier.

New tests (digstore-remote::client::error_classify_tests): CloudFront HTML 403 → NonJsonResponse (asserts NO raw HTML, names the edge); generic HTML 502 → NonJsonResponse; JSON message/error 4xx → Remote (auth reasons preserved); empty → Status; short plaintext → Remote. All 49 digstore-remote lib tests pass; cargo fmt/clippy -D warnings clean.

#124 — remote-CLI help/impl vs rpc.dig.net contract audit (verified clean)

Audited remote (add/list/remove), push, pull, clone, deploy, config node.url, and the --node/$DIG_NODE_URL override:

  • Help text matches the implementation and the real endpoints: origin defaults to https://rpc.dig.net; push origin; clone https://host/stores/<id>; remote add origin https://<user>@rpc.dig.net (userinfo stripped, store id taken from the local store).
  • The §5.3 node ladder + custom-node config surface (--node > $DIG_NODE_URL > config node.url > dig.local > localhost > rpc.dig.net) is present and documented.
  • rpc.dig.net is described as the content/store gateway + §21 write path, not a coinset proxy (#62); auth wording (§21 signed-request + §21.9 per-request identity) is accurate.
  • docs.dig.net (protocol/transport-and-push.md, rpc/dig-remote.md) already document the current 2-leg push (module/upload → inline/presigned → module/complete) accurately.
    No stale endpoints/flags/defaults found → no help/docs code changes needed beyond this clear-error fix. SPEC.md is correctly scoped to the .dig format; the remote transport contract lives in SYSTEM.md + the crate docs + docs.dig.net, which agree.

Version

patch 0.7.1 → 0.7.2 (a fix:), workspace Cargo.toml + Cargo.lock refreshed.

Verification

cargo test -p digstore-remote green (49 tests incl. the new classifier); cargo clippy --all-targets --all-features -D warnings clean; reinstalled via cargo install --path crates/digstore-cli --force --locked and ran the installed-binary integration tests (§3.5).

A push blocked at a CDN edge (e.g. rpc.dig.net's CloudFront returning a
403 "distribution is not configured to allow the HTTP request method"
HTML page) was surfaced as `unauthorized: <raw HTML dump>` — the HTML
body was mistaken for a server auth message.

Add a pure, unit-tested classifier (`classify_remote_error`) shared by
every remote error path (push-init, inline/finalize PUT, tombstone): a
JSON `{message|error}` envelope still maps to `Remote` (genuine 401/403
reasons preserved), an HTML/CloudFront page maps to a new
`ClientError::NonJsonResponse` carrying a clean hint (never the raw
markup), an empty body to a bare `Status`. The CLI maps NonJsonResponse
to a network error, not "unauthorized".

Refs #123, #124.
@MichaelTaylor3d MichaelTaylor3d merged commit 826322b into main Jul 6, 2026
10 checks passed
@MichaelTaylor3d MichaelTaylor3d deleted the fix/123-124-remote-clear-error-help branch July 6, 2026 15:12
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
…dual #4/#5)

Residual #4 — JWT signature verification (guest gate):
- verify_request_jwt now fetches the store's trusted JWKS over the
  session-gated jwks_fetch import (URL from the embedded AuthInfo section,
  §6.2), parses it, selects the key by kid (or the sole key), and verifies the
  token signature via jwt::verify_signature (RS256 = rsa PKCS#1 v1.5 over
  SHA-256, verify-only; ES256 = p256). Claims are still enforced first.
- The gate is no longer hardcoded off: GateConfig::from_embedded derives
  require_jwt from AuthInfo.requires_jwt, wired into get_content/get_proof.
- Fails closed -> Decoy on tampered/absent signature, wrong/unknown key,
  missing JWKS URL, or any claim failure.
- Tests: valid-sig+claims -> Real; tampered-sig/wrong-key/unknown-kid/absent-
  url/expired -> Decoy; existing session-gate + claim tests kept green. Test
  RSA private key is generated and signs in-test only; guest verifies via (n,e).
- Stays no_std (alloc); wasm32-unknown-unknown release build green.

Residual #5 — re-evaluate accepted advisories:
- deny.toml: removed the stale RUSTSEC-2025-0141 (bincode) ignore — bincode is
  not in the dependency tree. Kept RUSTSEC-2023-0071 (rsa/Marvin) with a
  comment: rsa is a direct verify-only dep (JWT RS256), so the decryption-oracle
  timing attack is N/A; re-evaluate if rsa is ever used to decrypt.
- SECURITY.md: residual #4 marked implemented; residual #5 updated (bincode
  removed, rsa verify-only/Marvin N/A; corrected "transitive" to direct dep).

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
…ML (#4)

A push blocked at a CDN edge (e.g. rpc.dig.net's CloudFront returning a
403 "distribution is not configured to allow the HTTP request method"
HTML page) was surfaced as `unauthorized: <raw HTML dump>` — the HTML
body was mistaken for a server auth message.

Add a pure, unit-tested classifier (`classify_remote_error`) shared by
every remote error path (push-init, inline/finalize PUT, tombstone): a
JSON `{message|error}` envelope still maps to `Remote` (genuine 401/403
reasons preserved), an HTML/CloudFront page maps to a new
`ClientError::NonJsonResponse` carrying a clean hint (never the raw
markup), an empty body to a bare `Status`. The CLI maps NonJsonResponse
to a network error, not "unauthorized".

Refs #123, #124.

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
…dual #4/#5)

Residual #4 — JWT signature verification (guest gate):
- verify_request_jwt now fetches the store's trusted JWKS over the
  session-gated jwks_fetch import (URL from the embedded AuthInfo section,
  §6.2), parses it, selects the key by kid (or the sole key), and verifies the
  token signature via jwt::verify_signature (RS256 = rsa PKCS#1 v1.5 over
  SHA-256, verify-only; ES256 = p256). Claims are still enforced first.
- The gate is no longer hardcoded off: GateConfig::from_embedded derives
  require_jwt from AuthInfo.requires_jwt, wired into get_content/get_proof.
- Fails closed -> Decoy on tampered/absent signature, wrong/unknown key,
  missing JWKS URL, or any claim failure.
- Tests: valid-sig+claims -> Real; tampered-sig/wrong-key/unknown-kid/absent-
  url/expired -> Decoy; existing session-gate + claim tests kept green. Test
  RSA private key is generated and signs in-test only; guest verifies via (n,e).
- Stays no_std (alloc); wasm32-unknown-unknown release build green.

Residual #5 — re-evaluate accepted advisories:
- deny.toml: removed the stale RUSTSEC-2025-0141 (bincode) ignore — bincode is
  not in the dependency tree. Kept RUSTSEC-2023-0071 (rsa/Marvin) with a
  comment: rsa is a direct verify-only dep (JWT RS256), so the decryption-oracle
  timing attack is N/A; re-evaluate if rsa is ever used to decrypt.
- SECURITY.md: residual #4 marked implemented; residual #5 updated (bincode
  removed, rsa verify-only/Marvin N/A; corrected "transitive" to direct dep).

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d added a commit that referenced this pull request Jul 10, 2026
…ML (#4)

A push blocked at a CDN edge (e.g. rpc.dig.net's CloudFront returning a
403 "distribution is not configured to allow the HTTP request method"
HTML page) was surfaced as `unauthorized: <raw HTML dump>` — the HTML
body was mistaken for a server auth message.

Add a pure, unit-tested classifier (`classify_remote_error`) shared by
every remote error path (push-init, inline/finalize PUT, tombstone): a
JSON `{message|error}` envelope still maps to `Remote` (genuine 401/403
reasons preserved), an HTML/CloudFront page maps to a new
`ClientError::NonJsonResponse` carrying a clean hint (never the raw
markup), an empty body to a bare `Status`. The CLI maps NonJsonResponse
to a network error, not "unauthorized".

Refs #123, #124.

Co-Authored-By: Claude <noreply@anthropic.com>
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