fix(remote): clear error when a remote returns non-JSON/CloudFront HTML#4
Merged
Merged
Conversation
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
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>
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 free
to 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.
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 printederror: 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-remoteread the error-response body and, when it wasn't JSON, returned the raw body verbatim; a403/401then mapped toCliError::Unauthorized(<that body>). A CloudFront HTML error page therefore printed as a giant HTML dump labelled "unauthorized".Fix (TDD)
classify_remote_error(status, body)(indigstore-remote::client), shared by every remote error path (push-initPOST /module/upload, inline finalizePUT /module,POST /module/complete,POST /tombstone):{"message"|"error": …}→ClientError::Remote { status, message }— genuine 401/403 reasons are preserved and still map toUnauthorized.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".ClientError::Status.NonJsonResponse→CliError::Network(hint)(exit 6), NOTUnauthorized.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; JSONmessage/error4xx → Remote (auth reasons preserved); empty → Status; short plaintext → Remote. All 49digstore-remotelib tests pass;cargo fmt/clippy -D warningsclean.#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_URLoverride:origindefaults tohttps://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).--node>$DIG_NODE_URL>config node.url> dig.local > localhost > rpc.dig.net) is present and documented.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
.digformat; the remote transport contract lives in SYSTEM.md + the crate docs + docs.dig.net, which agree.Version
patch
0.7.1 → 0.7.2(afix:), workspaceCargo.toml+Cargo.lockrefreshed.Verification
cargo test -p digstore-remotegreen (49 tests incl. the new classifier);cargo clippy --all-targets --all-features -D warningsclean; reinstalled viacargo install --path crates/digstore-cli --force --lockedand ran the installed-binary integration tests (§3.5).