feat(rest): add a SigV4 request signer - #3082
Conversation
70b5cd9 to
47b0bc4
Compare
The crate traces the request it signs, and its redaction list covers `authorization` but not the `Original-` copy we make, so a delegate's bearer token could reach trace logs. Also reject non-UTF-8 headers rather than leave them unsigned, and hash a present-but-empty body as Java does.
There was a problem hiding this comment.
I think AWS-specific dependencies are worth to hide behind a feature flag like we already do for different FileIO deps with OpenDAL
iceberg-rust/crates/storage/opendal/Cargo.toml
Lines 30 to 48 in a5f162f
There was a problem hiding this comment.
@DerGut Good point, this wasn't on my radar. Added — sigv4, off by default, gating aws-sigv4, aws-credential-types, base64 and sha2.
There was a problem hiding this comment.
Since this file is empty, wouldn't an /auth/sigv4.rs suffice? Unless we expect to add a lot of logic soon
CTTY
left a comment
There was a problem hiding this comment.
Thanks for breaking this down to a smaller PR!
Just took a pass, and I'm not sure how we can allow the usage of non-static credentials. We should explore if we could use aws rust sdk directly
|
|
||
| /// Static AWS-style credentials used for SigV4 signing of catalog requests. | ||
| #[derive(Clone)] | ||
| pub struct AwsCredentials { |
There was a problem hiding this comment.
We should not add this in iceberg and should use predefined credentials: https://docs.rs/aws-credential-types/latest/aws_credential_types/struct.Credentials.html
There was a problem hiding this comment.
@CTTY Agreed, and it turned out to go further than just the type. Removed; sign takes aws_credential_types::Credentials, and following that through, the signer now carries no credential state at all — matching Java, where one Aws4Signer is shared across sessions and the session resolves credentials per request. It keeps only region, service and payload mode.
| /// AWS SigV4 signer following Iceberg Java's `RESTSigV4AuthSession`: it adds the | ||
| /// required amz headers and signs all request headers except a small blacklist. | ||
| #[derive(Clone)] | ||
| pub struct SigV4Signer { |
There was a problem hiding this comment.
Should this be pub(crate) ? I only expect sigv4AuthSession to use this
There was a problem hiding this comment.
@CTTY Not in this PR — nothing in the crate uses the signer yet, so pub(crate) makes it all dead code and -D warnings implies -D dead-code.
Works once #3092 lands, but new then can't take a SigV4Signer either — a public fn can't take a private type. Taking region/service/mode instead passes clippy and drops 8 lines from the public API, at the cost of a five-argument constructor.
| /// required amz headers and signs all request headers except a small blacklist. | ||
| #[derive(Clone)] | ||
| pub struct SigV4Signer { | ||
| credentials: AwsCredentials, |
There was a problem hiding this comment.
How do we handle role based credentials? I think we should introduce https://docs.rs/aws-credential-types/latest/aws_credential_types/provider/future/struct.ProvideCredentials.html to the SigV4AuthSession and have users provide their own credential provider
There was a problem hiding this comment.
| /// | ||
| /// Fails rather than sign a request whose body is streaming or whose | ||
| /// headers are not UTF-8, since neither can be canonicalized faithfully. | ||
| pub fn sign(&self, request: &mut reqwest::Request) -> Result<()> { |
There was a problem hiding this comment.
This should take HttpRequest?
| self.sign_at(request, Utc::now()) | ||
| } | ||
|
|
||
| fn sign_at(&self, request: &mut reqwest::Request, now: DateTime<Utc>) -> Result<()> { |
There was a problem hiding this comment.
Same as above, this should use HttpRequest
| // not itself signed. | ||
| let displaced_content_hash: Vec<_> = request | ||
| .headers() | ||
| .get_all("x-amz-content-sha256") |
There was a problem hiding this comment.
@CTTY Pulled out, with x-amz-date and x-amz-security-token. No header-name literals left outside tests.
| value.set_sensitive(true); | ||
| request.headers_mut().append(RELOCATED_AUTHORIZATION, value); | ||
| } | ||
| } |
There was a problem hiding this comment.
Let's create more helpers like java's converHeaders and updateHeaders to make the main function body more readable
There was a problem hiding this comment.
@CTTY Split out convert_headers and update_request_headers after Java's, plus four smaller ones.
Java holds one `Aws4Signer` across sessions and resolves credentials from an `AwsCredentialsProvider` per request, so the signer itself carries no credential state. Follow that: drop `AwsCredentials`, take the AWS crate's `Credentials` as a `sign` argument, and leave the provider to the auth session. Also sign `HttpRequest` rather than the concrete request type, name the amz headers, split `convert_headers`/`update_request_headers` after their Java counterparts, collapse the one-file `sigv4` module, and put the AWS dependencies behind a `sigv4` feature.
Which issue does this PR close?
Split out of #2660 so the signing can be reviewed on its own.
What changes are included in this PR?
SigV4Signersigns areqwest::Requestfor AWS SigV4, built on the officialaws-sigv4crate (already in the workspace lock viaaws-config). It handles the parts Iceberg needs on top of the crate's defaults:PayloadHashMode::IcebergRestputs a base64 checksum inx-amz-content-sha256while the canonical request hashes the body in hex, matching Java'sSignerChecksumParams;StandardAwsuses hex in both.Aws4Signerdefaults: path normalization and double URL-encoding.expect,connectionandx-forwarded-forare excluded from signing, as Java'sAbstractAws4Signerdoes — a proxy may rewrite them.+in the query is rewritten to%20before signing, since verifiers disagree on whether it means a literal plus or a space and reqwest writes spaces as+.The auth manager that uses this, and its catalog wiring, follow in #2660.