Summary
Add support for verifying inbound SigV4 presigned URLs (signature carried in the query string) in the auth layer. Today resolve_identity only authenticates requests whose SigV4 material is in the Authorization header; presigned requests are treated as anonymous.
This unblocks presigned-URL support in the Source Cooperative data proxy — see downstream issue: source-cooperative/data.source.coop#136. The proxy wants to issue standard AWS SigV4 presigned GET URLs (generated with STS temp creds) that browser data-viewers can fetch directly, while the proxy still streams the bytes.
Current behavior (the gap)
crates/core/src/auth/identity.rs::resolve_identity:
- Returns
ResolvedIdentity::Anonymous whenever there is no Authorization header.
- Reads the session token (
x-amz-security-token) and payload hash (x-amz-content-sha256) from headers.
For a presigned URL, all SigV4 material is in the query string (X-Amz-Algorithm, X-Amz-Credential, X-Amz-Date, X-Amz-Expires, X-Amz-SignedHeaders, X-Amz-Security-Token, X-Amz-Signature), and the canonical request is constructed differently. So presigned requests currently fail to authenticate.
Much of the machinery already exists and is reusable: crates/core/src/auth/sigv4.rs::verify_sigv4_signature already takes a query_string and canonicalize_query_string already sorts it; the SigV4Auth struct, signing-key derivation, and constant_time_eq can all be shared.
Proposed change
In crates/core/src/auth/sigv4.rs + crates/core/src/auth/identity.rs:
- Detect presigned requests in
resolve_identity — if the query contains X-Amz-Algorithm=AWS4-HMAC-SHA256 (or X-Amz-Signature), take a presigned branch before the Anonymous early-return.
- Parse SigV4 from the query — new
parse_sigv4_presigned(query) producing a SigV4Auth (reuse the existing credential-scope split) plus expires and the security token. Pull X-Amz-Credential, X-Amz-SignedHeaders, X-Amz-Signature, X-Amz-Date, X-Amz-Expires, X-Amz-Security-Token.
- Presigned canonical-request variant — either parameterize
verify_sigv4_signature or add a sibling:
- Canonical query = all query params except
X-Amz-Signature, sorted/encoded (strip it, then canonicalize_query_string).
- Payload hash = the literal string
UNSIGNED-PAYLOAD.
string_to_sign date comes from the X-Amz-Date query param (not the x-amz-date header).
- Reuse the existing signing-key derivation and
constant_time_eq.
- Enforce expiry — reject when
X-Amz-Date + X-Amz-Expires < now (small clock-skew allowance).
- Session token — resolve
X-Amz-Security-Token through the existing credential_resolver (TemporaryCredentialResolver), keep the access-key↔token match check, and return the same ResolvedIdentity::Authenticated. Everything downstream (authorize → presign-backend → stream) is unchanged.
Tests
Add AWS SigV4 presigned test vectors alongside the existing header-auth vectors in crates/core/src/auth/tests.rs:
- A known presigned canonical request → expected signature (positive).
- Tampered query param → signature mismatch.
X-Amz-Date + X-Amz-Expires in the past → expired/denied.
- With
X-Amz-Security-Token resolved via a stub TemporaryCredentialResolver → Authenticated.
Release
Cut a minor release so downstream consumers (the data proxy, currently on 0.4.0) can bump.
Filed alongside source-cooperative/data.source.coop#136.
Summary
Add support for verifying inbound SigV4 presigned URLs (signature carried in the query string) in the auth layer. Today
resolve_identityonly authenticates requests whose SigV4 material is in theAuthorizationheader; presigned requests are treated as anonymous.This unblocks presigned-URL support in the Source Cooperative data proxy — see downstream issue: source-cooperative/data.source.coop#136. The proxy wants to issue standard AWS SigV4 presigned GET URLs (generated with STS temp creds) that browser data-viewers can fetch directly, while the proxy still streams the bytes.
Current behavior (the gap)
crates/core/src/auth/identity.rs::resolve_identity:ResolvedIdentity::Anonymouswhenever there is noAuthorizationheader.x-amz-security-token) and payload hash (x-amz-content-sha256) from headers.For a presigned URL, all SigV4 material is in the query string (
X-Amz-Algorithm,X-Amz-Credential,X-Amz-Date,X-Amz-Expires,X-Amz-SignedHeaders,X-Amz-Security-Token,X-Amz-Signature), and the canonical request is constructed differently. So presigned requests currently fail to authenticate.Much of the machinery already exists and is reusable:
crates/core/src/auth/sigv4.rs::verify_sigv4_signaturealready takes aquery_stringandcanonicalize_query_stringalready sorts it; theSigV4Authstruct, signing-key derivation, andconstant_time_eqcan all be shared.Proposed change
In
crates/core/src/auth/sigv4.rs+crates/core/src/auth/identity.rs:resolve_identity— if the query containsX-Amz-Algorithm=AWS4-HMAC-SHA256(orX-Amz-Signature), take a presigned branch before theAnonymousearly-return.parse_sigv4_presigned(query)producing aSigV4Auth(reuse the existing credential-scope split) plusexpiresand the security token. PullX-Amz-Credential,X-Amz-SignedHeaders,X-Amz-Signature,X-Amz-Date,X-Amz-Expires,X-Amz-Security-Token.verify_sigv4_signatureor add a sibling:X-Amz-Signature, sorted/encoded (strip it, thencanonicalize_query_string).UNSIGNED-PAYLOAD.string_to_signdate comes from theX-Amz-Datequery param (not thex-amz-dateheader).constant_time_eq.X-Amz-Date + X-Amz-Expires < now(small clock-skew allowance).X-Amz-Security-Tokenthrough the existingcredential_resolver(TemporaryCredentialResolver), keep the access-key↔token match check, and return the sameResolvedIdentity::Authenticated. Everything downstream (authorize → presign-backend → stream) is unchanged.Tests
Add AWS SigV4 presigned test vectors alongside the existing header-auth vectors in
crates/core/src/auth/tests.rs:X-Amz-Date + X-Amz-Expiresin the past → expired/denied.X-Amz-Security-Tokenresolved via a stubTemporaryCredentialResolver→Authenticated.Release
Cut a minor release so downstream consumers (the data proxy, currently on 0.4.0) can bump.
Filed alongside source-cooperative/data.source.coop#136.