Skip to content

Commit f566903

Browse files
authored
Expose AwsAuthorizer (#4237)
* Expose AWSAuthorizer * Review feedback
1 parent a5c1a33 commit f566903

3 files changed

Lines changed: 86 additions & 48 deletions

File tree

object_store/src/aws/client.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ impl S3Client {
238238
&self.config.region,
239239
"s3",
240240
self.config.sign_payload,
241-
payload_sha256,
241+
payload_sha256.as_deref(),
242242
)
243243
.send_retry(&self.config.retry_config)
244244
.await
@@ -315,7 +315,6 @@ impl S3Client {
315315

316316
let mut query = Vec::with_capacity(4);
317317

318-
// Note: the order of these matters to ensure the generated URL is canonical
319318
if let Some(token) = token {
320319
query.push(("continuation-token", token))
321320
}

object_store/src/aws/credential.rs

Lines changed: 83 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
use crate::aws::{STORE, STRICT_ENCODE_SET};
18+
use crate::aws::{STORE, STRICT_ENCODE_SET, STRICT_PATH_ENCODE_SET};
1919
use crate::client::retry::RetryExt;
2020
use crate::client::token::{TemporaryToken, TokenCache};
2121
use crate::client::TokenProvider;
@@ -39,7 +39,8 @@ type StdError = Box<dyn std::error::Error + Send + Sync>;
3939
/// SHA256 hash of empty string
4040
static EMPTY_SHA256_HASH: &str =
4141
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
42-
static UNSIGNED_PAYLOAD_LITERAL: &str = "UNSIGNED-PAYLOAD";
42+
static UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
43+
static STREAMING_PAYLOAD: &str = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD";
4344

4445
/// A set of AWS security credentials
4546
#[derive(Debug, Eq, PartialEq)]
@@ -72,8 +73,12 @@ impl AwsCredential {
7273
}
7374
}
7475

75-
struct RequestSigner<'a> {
76-
date: DateTime<Utc>,
76+
/// Authorize a [`Request`] with an [`AwsCredential`] using [AWS SigV4]
77+
///
78+
/// [AWS SigV4]: https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
79+
#[derive(Debug)]
80+
pub struct AwsAuthorizer<'a> {
81+
date: Option<DateTime<Utc>>,
7782
credential: &'a AwsCredential,
7883
service: &'a str,
7984
region: &'a str,
@@ -85,47 +90,86 @@ const HASH_HEADER: &str = "x-amz-content-sha256";
8590
const TOKEN_HEADER: &str = "x-amz-security-token";
8691
const AUTH_HEADER: &str = "authorization";
8792

88-
impl<'a> RequestSigner<'a> {
89-
fn sign(&self, request: &mut Request, pre_calculated_digest: Option<Vec<u8>>) {
93+
impl<'a> AwsAuthorizer<'a> {
94+
/// Create a new [`AwsAuthorizer`]
95+
pub fn new(credential: &'a AwsCredential, service: &'a str, region: &'a str) -> Self {
96+
Self {
97+
credential,
98+
service,
99+
region,
100+
date: None,
101+
sign_payload: true,
102+
}
103+
}
104+
105+
/// Controls whether this [`AwsAuthorizer`] will attempt to sign the request payload,
106+
/// the default is `true`
107+
pub fn with_sign_payload(mut self, signed: bool) -> Self {
108+
self.sign_payload = signed;
109+
self
110+
}
111+
112+
/// Authorize `request` with an optional pre-calculated SHA256 digest by attaching
113+
/// the relevant [AWS SigV4] headers
114+
///
115+
/// # Payload Signature
116+
///
117+
/// AWS SigV4 requests must contain the `x-amz-content-sha256` header, it is set as follows:
118+
///
119+
/// * If not configured to sign payloads, it is set to `UNSIGNED-PAYLOAD`
120+
/// * If a `pre_calculated_digest` is provided, it is set to the hex encoding of it
121+
/// * If it is a streaming request, it is set to `STREAMING-AWS4-HMAC-SHA256-PAYLOAD`
122+
/// * Otherwise it is set to the hex encoded SHA256 of the request body
123+
///
124+
/// [AWS SigV4]: https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html
125+
pub fn authorize(&self, request: &mut Request, pre_calculated_digest: Option<&[u8]>) {
90126
if let Some(ref token) = self.credential.token {
91127
let token_val = HeaderValue::from_str(token).unwrap();
92128
request.headers_mut().insert(TOKEN_HEADER, token_val);
93129
}
94130

95-
let host_val = HeaderValue::from_str(
96-
&request.url()[url::Position::BeforeHost..url::Position::AfterPort],
97-
)
98-
.unwrap();
131+
let host = &request.url()[url::Position::BeforeHost..url::Position::AfterPort];
132+
let host_val = HeaderValue::from_str(host).unwrap();
99133
request.headers_mut().insert("host", host_val);
100134

101-
let date_str = self.date.format("%Y%m%dT%H%M%SZ").to_string();
135+
let date = self.date.unwrap_or_else(Utc::now);
136+
let date_str = date.format("%Y%m%dT%H%M%SZ").to_string();
102137
let date_val = HeaderValue::from_str(&date_str).unwrap();
103138
request.headers_mut().insert(DATE_HEADER, date_val);
104139

105-
let digest = if self.sign_payload {
106-
if let Some(digest) = pre_calculated_digest {
107-
hex_encode(&digest)
108-
} else {
109-
match request.body() {
140+
let digest = match self.sign_payload {
141+
false => UNSIGNED_PAYLOAD.to_string(),
142+
true => match pre_calculated_digest {
143+
Some(digest) => hex_encode(digest),
144+
None => match request.body() {
110145
None => EMPTY_SHA256_HASH.to_string(),
111-
Some(body) => hex_digest(body.as_bytes().unwrap()),
112-
}
113-
}
114-
} else {
115-
UNSIGNED_PAYLOAD_LITERAL.to_string()
146+
Some(body) => match body.as_bytes() {
147+
Some(bytes) => hex_digest(bytes),
148+
None => STREAMING_PAYLOAD.to_string(),
149+
},
150+
},
151+
},
116152
};
117153

118154
let header_digest = HeaderValue::from_str(&digest).unwrap();
119155
request.headers_mut().insert(HASH_HEADER, header_digest);
120156

157+
// Each path segment must be URI-encoded twice (except for Amazon S3 which only gets URI-encoded once).
158+
// see https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
159+
let canonical_uri = match self.service {
160+
"s3" => request.url().path().to_string(),
161+
_ => utf8_percent_encode(request.url().path(), &STRICT_PATH_ENCODE_SET)
162+
.to_string(),
163+
};
164+
121165
let (signed_headers, canonical_headers) = canonicalize_headers(request.headers());
122166
let canonical_query = canonicalize_query(request.url());
123167

124168
// https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
125169
let canonical_request = format!(
126170
"{}\n{}\n{}\n{}\n{}\n{}",
127171
request.method().as_str(),
128-
request.url().path(), // S3 doesn't percent encode this like other services
172+
canonical_uri,
129173
canonical_query,
130174
canonical_headers,
131175
signed_headers,
@@ -135,22 +179,22 @@ impl<'a> RequestSigner<'a> {
135179
let hashed_canonical_request = hex_digest(canonical_request.as_bytes());
136180
let scope = format!(
137181
"{}/{}/{}/aws4_request",
138-
self.date.format("%Y%m%d"),
182+
date.format("%Y%m%d"),
139183
self.region,
140184
self.service
141185
);
142186

143187
let string_to_sign = format!(
144188
"AWS4-HMAC-SHA256\n{}\n{}\n{}",
145-
self.date.format("%Y%m%dT%H%M%SZ"),
189+
date.format("%Y%m%dT%H%M%SZ"),
146190
scope,
147191
hashed_canonical_request
148192
);
149193

150194
// sign the string
151195
let signature =
152196
self.credential
153-
.sign(&string_to_sign, self.date, self.region, self.service);
197+
.sign(&string_to_sign, date, self.region, self.service);
154198

155199
// build the actual auth header
156200
let authorisation = format!(
@@ -171,7 +215,7 @@ pub trait CredentialExt {
171215
region: &str,
172216
service: &str,
173217
sign_payload: bool,
174-
payload_sha256: Option<Vec<u8>>,
218+
payload_sha256: Option<&[u8]>,
175219
) -> Self;
176220
}
177221

@@ -182,21 +226,15 @@ impl CredentialExt for RequestBuilder {
182226
region: &str,
183227
service: &str,
184228
sign_payload: bool,
185-
payload_sha256: Option<Vec<u8>>,
229+
payload_sha256: Option<&[u8]>,
186230
) -> Self {
187231
let (client, request) = self.build_split();
188232
let mut request = request.expect("request valid");
189233

190-
let date = Utc::now();
191-
let signer = RequestSigner {
192-
date,
193-
credential,
194-
service,
195-
region,
196-
sign_payload,
197-
};
234+
AwsAuthorizer::new(credential, service, region)
235+
.with_sign_payload(sign_payload)
236+
.authorize(&mut request, payload_sha256);
198237

199-
signer.sign(&mut request, payload_sha256);
200238
Self::from_parts(client, request)
201239
}
202240
}
@@ -539,15 +577,15 @@ mod tests {
539577
.build()
540578
.unwrap();
541579

542-
let signer = RequestSigner {
543-
date,
580+
let signer = AwsAuthorizer {
581+
date: Some(date),
544582
credential: &credential,
545583
service: "ec2",
546584
region: "us-east-1",
547585
sign_payload: true,
548586
};
549587

550-
signer.sign(&mut request, None);
588+
signer.authorize(&mut request, None);
551589
assert_eq!(request.headers().get(AUTH_HEADER).unwrap(), "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20220806/us-east-1/ec2/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=a3c787a7ed37f7fdfbfd2d7056a3d7c9d85e6d52a2bfbec73793c0be6e7862d4")
552590
}
553591

@@ -577,15 +615,15 @@ mod tests {
577615
.build()
578616
.unwrap();
579617

580-
let signer = RequestSigner {
581-
date,
618+
let authorizer = AwsAuthorizer {
619+
date: Some(date),
582620
credential: &credential,
583621
service: "ec2",
584622
region: "us-east-1",
585623
sign_payload: false,
586624
};
587625

588-
signer.sign(&mut request, None);
626+
authorizer.authorize(&mut request, None);
589627
assert_eq!(request.headers().get(AUTH_HEADER).unwrap(), "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20220806/us-east-1/ec2/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=653c3d8ea261fd826207df58bc2bb69fbb5003e9eb3c0ef06e4a51f2a81d8699")
590628
}
591629

@@ -614,15 +652,15 @@ mod tests {
614652
.build()
615653
.unwrap();
616654

617-
let signer = RequestSigner {
618-
date,
655+
let authorizer = AwsAuthorizer {
656+
date: Some(date),
619657
credential: &credential,
620658
service: "s3",
621659
region: "us-east-1",
622660
sign_payload: true,
623661
};
624662

625-
signer.sign(&mut request, None);
663+
authorizer.authorize(&mut request, None);
626664
assert_eq!(request.headers().get(AUTH_HEADER).unwrap(), "AWS4-HMAC-SHA256 Credential=H20ABqCkLZID4rLe/20220809/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=9ebf2f92872066c99ac94e573b4e1b80f4dbb8a32b1e8e23178318746e7d1b4d")
627665
}
628666

object_store/src/aws/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ const STORE: &str = "S3";
8282

8383
/// [`CredentialProvider`] for [`AmazonS3`]
8484
pub type AwsCredentialProvider = Arc<dyn CredentialProvider<Credential = AwsCredential>>;
85-
pub use credential::AwsCredential;
85+
pub use credential::{AwsAuthorizer, AwsCredential};
8686

8787
/// Default metadata endpoint
8888
static METADATA_ENDPOINT: &str = "http://169.254.169.254";
@@ -160,6 +160,7 @@ impl From<Error> for super::Error {
160160
}
161161

162162
/// Get the bucket region using the [HeadBucket API]. This will fail if the bucket does not exist.
163+
///
163164
/// [HeadBucket API]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadBucket.html
164165
pub async fn resolve_bucket_region(
165166
bucket: &str,

0 commit comments

Comments
 (0)