feat(auth): verify inbound SigV4 presigned URLs - #125
Conversation
resolve_identity only authenticated requests carrying SigV4 material in the Authorization header; presigned URLs (all material in the query string) fell through to Anonymous. Browsers can't set an Authorization header on <img>/<a>/fetch, so presigned URLs are the only way to hand a data-viewer a directly-fetchable, time-limited link. - sigv4.rs: factor the credential-scope split into build_sigv4_auth and the signing core into verify_signed, shared by header and presigned paths. Add parse_sigv4_presigned (percent-decodes query values), verify_sigv4_presigned (strips X-Amz-Signature, UNSIGNED-PAYLOAD payload, date from X-Amz-Date), and is_presigned. - identity.rs: parse header or presigned query into a unified SigMode, then run one shared resolve->verify->Authenticated tail. Enforce the X-Amz-Date + X-Amz-Expires window (5s skew) via check_presigned_expiry. - mod.rs: export the new presigned symbols. Closes #53, closes #39 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sign_presigned helper builds a valid presigned query (self-checking round-trip). Covers: long-lived cred resolves, security-token resolves, tampered/injected query param -> SignatureDoesNotMatch, expired window -> ExpiredCredentials, access-key/token mismatch -> AccessDenied, and a non-SigV4 query staying Anonymous. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @alukach's task in 4m 12s —— View job Claude's review
❌ Changes requested — see findings below.
Simplify (ponytail)
💰 Estimated review cost: $1.28 · 4m12s · 33 turns |
|
📖 Docs preview deployed to https://multistore-docs-pr-125.development-seed.workers.dev
|
What I'm changing
resolve_identityonly authenticated requests whose SigV4 material was in theAuthorizationheader. Presigned URLs carry everything in the query string, so they fell through toResolvedIdentity::Anonymousand STS-signed links were treated as anonymous. Browsers can't set anAuthorizationheader on<img>/<a>/fetch, so presigned URLs are the only way to hand a browser data-viewer a directly-fetchable, time-limited link. This unblocks presigned-URL support in the Source Cooperative data proxy (source-cooperative/data.source.coop#136).Closes #53, closes #39.
How I did it
crates/core/src/auth/sigv4.rs— refactor to share machinery between the two request shapes, then add the presigned path:build_sigv4_auth— extracted theAKID/date/region/service/aws4_requestcredential-scope split so both parsers reuse it.verify_signed— extracted the canonical-request reconstruction + signing-key derivation + constant-time compare. Callers pass the already-canonicalized query, payload hash, and string-to-sign date, so the header-vs-presigned differences live entirely in the thin wrappers.verify_sigv4_signaturekeeps its public signature.parse_sigv4_presigned— pulls theX-Amz-*params; percent-decodes values for parsing (e.g.X-Amz-Credentialarrives asAKID%2F.../aws4_request) while the raw encoded query is what gets canonicalized during verification.verify_sigv4_presigned— stripsX-Amz-Signaturefrom the canonical query, usesUNSIGNED-PAYLOAD, and takes the date from theX-Amz-Datequery param.is_presigned— detectsX-Amz-Algorithm=AWS4-HMAC-SHA256.crates/core/src/auth/identity.rs— parse the header or the presigned query into a unifiedSigMode, then run one sharedresolve → verify → Authenticatedtail (temporary-credential and long-lived branches unchanged).check_presigned_expiryrejects onceX-Amz-Date + X-Amz-Expireselapses, with 5s clock-skew tolerance, via the existingExpiredCredentialserror. The session token (X-Amz-Security-Token) resolves through the sameTemporaryCredentialResolver, keeping the access-key match check.crates/core/src/auth/mod.rs— exportis_presigned,parse_sigv4_presigned,verify_sigv4_presigned,PresignedSig.docs/auth/— note presigned-URL client auth inindex.mdandproxy-auth.md.Everything downstream of identity resolution (authorize → presign-backend → stream) is unchanged.
Test plan
New tests in
crates/core/src/auth/tests.rs(self-checkingsign_presignedhelper):Authenticated(long-lived cred)X-Amz-Security-Tokenvia a stub resolver →AuthenticatedSignatureDoesNotMatchX-Amz-Date + X-Amz-Expiresin the past →ExpiredCredentialsAccessDeniedAnonymous(regression)Verification run:
cargo test -p multistore auth::— 30 passedcargo fmt,cargo clippy -p multistore— cleancargo checkcargo check -p multistore-cf-workers --target wasm32-unknown-unknownRelease
Lands as
feat(auth):→ release-please cuts a minor. Downstream proxy is on 0.4.0 and can bump once released.🤖 Generated with Claude Code