Skip to content

feat(rest): add a SigV4 request signer - #3082

Open
plusplusjiajia wants to merge 5 commits into
apache:mainfrom
plusplusjiajia:feat/rest-sigv4-signer
Open

feat(rest): add a SigV4 request signer#3082
plusplusjiajia wants to merge 5 commits into
apache:mainfrom
plusplusjiajia:feat/rest-sigv4-signer

Conversation

@plusplusjiajia

Copy link
Copy Markdown
Member

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?

SigV4Signer signs a reqwest::Request for AWS SigV4, built on the official aws-sigv4 crate (already in the workspace lock via aws-config). It handles the parts Iceberg needs on top of the crate's defaults:

  • PayloadHashMode::IcebergRest puts a base64 checksum in x-amz-content-sha256 while the canonical request hashes the body in hex, matching Java's SignerChecksumParams; StandardAws uses hex in both.
  • Aws4Signer defaults: path normalization and double URL-encoding.
  • expect, connection and x-forwarded-for are excluded from signing, as Java's AbstractAws4Signer does — a proxy may rewrite them.
  • A + in the query is rewritten to %20 before 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.

@plusplusjiajia

Copy link
Copy Markdown
Member Author

@CTTY This is the signing half of #2660 (#2660), split out so it can be reviewed on its own — and it now builds on the aws-sigv4 crate rather than a hand-rolled implementation, which was your concern there.

@plusplusjiajia
plusplusjiajia marked this pull request as ready for review August 27, 2026 05:37
@plusplusjiajia
plusplusjiajia force-pushed the feat/rest-sigv4-signer branch from 70b5cd9 to 47b0bc4 Compare August 27, 2026 05:49
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think AWS-specific dependencies are worth to hide behind a feature flag like we already do for different FileIO deps with OpenDAL

[features]
default = ["opendal-memory", "opendal-fs", "opendal-s3"]
opendal-all = [
"opendal-memory",
"opendal-fs",
"opendal-s3",
"opendal-gcs",
"opendal-oss",
"opendal-azdls",
"opendal-hf",
]
opendal-azdls = ["opendal/services-azdls"]
opendal-fs = ["opendal/services-fs"]
opendal-gcs = ["opendal/services-gcs"]
opendal-hf = ["opendal/services-hf"]
opendal-memory = ["opendal/services-memory"]
opendal-oss = ["opendal/services-oss"]
opendal-s3 = ["opendal/services-s3", "reqsign-aws-v4", "reqsign-core"]

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DerGut Good point, this wasn't on my radar. Added — sigv4, off by default, gating aws-sigv4, aws-credential-types, base64 and sha2.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this file is empty, wouldn't an /auth/sigv4.rs suffice? Unless we expect to add a lot of logic soon

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DerGut Done — collapsed to auth/sigv4.rs

@CTTY CTTY left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

@CTTY CTTY Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be pub(crate) ? I only expect sigv4AuthSession to use this

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CTTY This shaped the follow-up, thanks. Provider lives in the session, not the signer: #3092's SigV4AuthManager holds a SharedCredentialsProvider and resolves it per request, as Java does inside sign. Static credentials come from Java's property names; anything else goes to new.

///
/// 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<()> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should take HttpRequest?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CTTY Switched, thanks!

self.sign_at(request, Utc::now())
}

fn sign_at(&self, request: &mut reqwest::Request, now: DateTime<Utc>) -> Result<()> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, this should use HttpRequest

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CTTY Same change.

// not itself signed.
let displaced_content_hash: Vec<_> = request
.headers()
.get_all("x-amz-content-sha256")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a constant

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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);
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's create more helpers like java's converHeaders and updateHeaders to make the main function body more readable

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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.
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.

3 participants