/rust/registry/src/index.crates.io-6f17d22bba15001f/reqsign-0.16.3/src/aws/v4.rs
Line | Count | Source (jump to first uncovered line) |
1 | | //! AWS service sigv4 signer |
2 | | |
3 | | use std::fmt::Debug; |
4 | | use std::fmt::Write; |
5 | | use std::time::Duration; |
6 | | |
7 | | use anyhow::Result; |
8 | | use http::header; |
9 | | use http::HeaderValue; |
10 | | use log::debug; |
11 | | use percent_encoding::percent_decode_str; |
12 | | use percent_encoding::utf8_percent_encode; |
13 | | |
14 | | use super::constants::AWS_QUERY_ENCODE_SET; |
15 | | use super::constants::X_AMZ_CONTENT_SHA_256; |
16 | | use super::constants::X_AMZ_DATE; |
17 | | use super::constants::X_AMZ_SECURITY_TOKEN; |
18 | | use super::credential::Credential; |
19 | | use crate::ctx::SigningContext; |
20 | | use crate::ctx::SigningMethod; |
21 | | use crate::hash::hex_hmac_sha256; |
22 | | use crate::hash::hex_sha256; |
23 | | use crate::hash::hmac_sha256; |
24 | | use crate::request::SignableRequest; |
25 | | use crate::time::format_date; |
26 | | use crate::time::format_iso8601; |
27 | | use crate::time::now; |
28 | | use crate::time::DateTime; |
29 | | |
30 | | /// Singer that implement AWS SigV4. |
31 | | /// |
32 | | /// - [Signature Version 4 signing process](https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html) |
33 | | #[derive(Debug)] |
34 | | pub struct Signer { |
35 | | service: String, |
36 | | region: String, |
37 | | |
38 | | time: Option<DateTime>, |
39 | | } |
40 | | |
41 | | impl Signer { |
42 | | /// Create a builder. |
43 | 0 | pub fn new(service: &str, region: &str) -> Self { |
44 | 0 | Self { |
45 | 0 | service: service.to_string(), |
46 | 0 | region: region.to_string(), |
47 | 0 | time: None, |
48 | 0 | } |
49 | 0 | } |
50 | | |
51 | | /// Specify the signing time. |
52 | | /// |
53 | | /// # Note |
54 | | /// |
55 | | /// We should always take current time to sign requests. |
56 | | /// Only use this function for testing. |
57 | | #[cfg(test)] |
58 | | pub fn time(mut self, time: DateTime) -> Self { |
59 | | self.time = Some(time); |
60 | | self |
61 | | } |
62 | | |
63 | 0 | fn build( |
64 | 0 | &self, |
65 | 0 | req: &mut impl SignableRequest, |
66 | 0 | method: SigningMethod, |
67 | 0 | cred: &Credential, |
68 | 0 | ) -> Result<SigningContext> { |
69 | 0 | let now = self.time.unwrap_or_else(now); |
70 | 0 | let mut ctx = req.build()?; |
71 | | |
72 | | // canonicalize context |
73 | 0 | canonicalize_header(&mut ctx, method, cred, now)?; |
74 | 0 | canonicalize_query(&mut ctx, method, cred, now, &self.service, &self.region)?; |
75 | | |
76 | | // build canonical request and string to sign. |
77 | 0 | let creq = canonical_request_string(&mut ctx)?; |
78 | 0 | let encoded_req = hex_sha256(creq.as_bytes()); |
79 | 0 |
|
80 | 0 | // Scope: "20220313/<region>/<service>/aws4_request" |
81 | 0 | let scope = format!( |
82 | 0 | "{}/{}/{}/aws4_request", |
83 | 0 | format_date(now), |
84 | 0 | self.region, |
85 | 0 | self.service |
86 | 0 | ); |
87 | 0 | debug!("calculated scope: {scope}"); |
88 | | |
89 | | // StringToSign: |
90 | | // |
91 | | // AWS4-HMAC-SHA256 |
92 | | // 20220313T072004Z |
93 | | // 20220313/<region>/<service>/aws4_request |
94 | | // <hashed_canonical_request> |
95 | 0 | let string_to_sign = { |
96 | 0 | let mut f = String::new(); |
97 | 0 | writeln!(f, "AWS4-HMAC-SHA256")?; |
98 | 0 | writeln!(f, "{}", format_iso8601(now))?; |
99 | 0 | writeln!(f, "{}", &scope)?; |
100 | 0 | write!(f, "{}", &encoded_req)?; |
101 | 0 | f |
102 | 0 | }; |
103 | 0 | debug!("calculated string to sign: {string_to_sign}"); |
104 | | |
105 | 0 | let signing_key = |
106 | 0 | generate_signing_key(&cred.secret_access_key, now, &self.region, &self.service); |
107 | 0 | let signature = hex_hmac_sha256(&signing_key, string_to_sign.as_bytes()); |
108 | 0 |
|
109 | 0 | match method { |
110 | | SigningMethod::Header => { |
111 | 0 | let mut authorization = HeaderValue::from_str(&format!( |
112 | 0 | "AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}", |
113 | 0 | cred.access_key_id, |
114 | 0 | scope, |
115 | 0 | ctx.header_name_to_vec_sorted().join(";"), |
116 | 0 | signature |
117 | 0 | ))?; |
118 | 0 | authorization.set_sensitive(true); |
119 | 0 |
|
120 | 0 | ctx.headers |
121 | 0 | .insert(http::header::AUTHORIZATION, authorization); |
122 | | } |
123 | 0 | SigningMethod::Query(_) => { |
124 | 0 | ctx.query.push(("X-Amz-Signature".into(), signature)); |
125 | 0 | } |
126 | | } |
127 | | |
128 | 0 | Ok(ctx) |
129 | 0 | } Unexecuted instantiation: <reqsign::aws::v4::Signer>::build::<http::request::Request<opendal::types::buffer::Buffer>> Unexecuted instantiation: <reqsign::aws::v4::Signer>::build::<reqwest::async_impl::request::Request> |
130 | | |
131 | | /// Get the region of this signer. |
132 | 0 | pub fn region(&self) -> &str { |
133 | 0 | &self.region |
134 | 0 | } |
135 | | |
136 | | /// Signing request with header. |
137 | | /// |
138 | | /// # Example |
139 | | /// |
140 | | /// ```rust,no_run |
141 | | /// use anyhow::Result; |
142 | | /// use reqsign::AwsConfig; |
143 | | /// use reqsign::AwsDefaultLoader; |
144 | | /// use reqsign::AwsV4Signer; |
145 | | /// use reqwest::Client; |
146 | | /// use reqwest::Request; |
147 | | /// use reqwest::Url; |
148 | | /// |
149 | | /// #[tokio::main] |
150 | | /// async fn main() -> Result<()> { |
151 | | /// let client = Client::new(); |
152 | | /// let config = AwsConfig::default().from_profile().from_env(); |
153 | | /// let loader = AwsDefaultLoader::new(client.clone(), config); |
154 | | /// let signer = AwsV4Signer::new("s3", "us-east-1"); |
155 | | /// // Construct request |
156 | | /// let url = Url::parse("https://s3.amazonaws.com/testbucket")?; |
157 | | /// let mut req = reqwest::Request::new(http::Method::GET, url); |
158 | | /// // Signing request with Signer |
159 | | /// let credential = loader.load().await?.unwrap(); |
160 | | /// signer.sign(&mut req, &credential)?; |
161 | | /// // Sending already signed request. |
162 | | /// let resp = client.execute(req).await?; |
163 | | /// println!("resp got status: {}", resp.status()); |
164 | | /// Ok(()) |
165 | | /// } |
166 | | /// ``` |
167 | 0 | pub fn sign(&self, req: &mut impl SignableRequest, cred: &Credential) -> Result<()> { |
168 | 0 | let ctx = self.build(req, SigningMethod::Header, cred)?; |
169 | 0 | req.apply(ctx) |
170 | 0 | } Unexecuted instantiation: <reqsign::aws::v4::Signer>::sign::<http::request::Request<opendal::types::buffer::Buffer>> Unexecuted instantiation: <reqsign::aws::v4::Signer>::sign::<reqwest::async_impl::request::Request> |
171 | | |
172 | | /// Signing request with query. |
173 | | /// |
174 | | /// # Example |
175 | | /// |
176 | | /// ```rust,no_run |
177 | | /// use std::time::Duration; |
178 | | /// |
179 | | /// use anyhow::Result; |
180 | | /// use reqsign::AwsConfig; |
181 | | /// use reqsign::AwsDefaultLoader; |
182 | | /// use reqsign::AwsV4Signer; |
183 | | /// use reqwest::Client; |
184 | | /// use reqwest::Request; |
185 | | /// use reqwest::Url; |
186 | | /// |
187 | | /// #[tokio::main] |
188 | | /// async fn main() -> Result<()> { |
189 | | /// let client = Client::new(); |
190 | | /// let config = AwsConfig::default().from_profile().from_env(); |
191 | | /// let loader = AwsDefaultLoader::new(client.clone(), config); |
192 | | /// let signer = AwsV4Signer::new("s3", "us-east-1"); |
193 | | /// // Construct request |
194 | | /// let url = Url::parse("https://s3.amazonaws.com/testbucket")?; |
195 | | /// let mut req = reqwest::Request::new(http::Method::GET, url); |
196 | | /// // Signing request with Signer |
197 | | /// let credential = loader.load().await?.unwrap(); |
198 | | /// signer.sign_query(&mut req, Duration::from_secs(3600), &credential)?; |
199 | | /// // Sending already signed request. |
200 | | /// let resp = client.execute(req).await?; |
201 | | /// println!("resp got status: {}", resp.status()); |
202 | | /// Ok(()) |
203 | | /// } |
204 | | /// ``` |
205 | 0 | pub fn sign_query( |
206 | 0 | &self, |
207 | 0 | req: &mut impl SignableRequest, |
208 | 0 | expire: Duration, |
209 | 0 | cred: &Credential, |
210 | 0 | ) -> Result<()> { |
211 | 0 | let ctx = self.build(req, SigningMethod::Query(expire), cred)?; |
212 | 0 | req.apply(ctx) |
213 | 0 | } Unexecuted instantiation: <reqsign::aws::v4::Signer>::sign_query::<http::request::Request<opendal::types::buffer::Buffer>> Unexecuted instantiation: <reqsign::aws::v4::Signer>::sign_query::<_> |
214 | | } |
215 | | |
216 | 0 | fn canonical_request_string(ctx: &mut SigningContext) -> Result<String> { |
217 | 0 | // 256 is specially chosen to avoid reallocation for most requests. |
218 | 0 | let mut f = String::with_capacity(256); |
219 | 0 |
|
220 | 0 | // Insert method |
221 | 0 | writeln!(f, "{}", ctx.method)?; |
222 | | // Insert encoded path |
223 | 0 | let path = percent_decode_str(&ctx.path).decode_utf8()?; |
224 | 0 | writeln!( |
225 | 0 | f, |
226 | 0 | "{}", |
227 | 0 | utf8_percent_encode(&path, &super::constants::AWS_URI_ENCODE_SET) |
228 | 0 | )?; |
229 | | // Insert query |
230 | 0 | writeln!( |
231 | 0 | f, |
232 | 0 | "{}", |
233 | 0 | ctx.query |
234 | 0 | .iter() |
235 | 0 | .map(|(k, v)| { format!("{k}={v}") }) |
236 | 0 | .collect::<Vec<_>>() |
237 | 0 | .join("&") |
238 | 0 | )?; |
239 | | // Insert signed headers |
240 | 0 | let signed_headers = ctx.header_name_to_vec_sorted(); |
241 | 0 | for header in signed_headers.iter() { |
242 | 0 | let value = &ctx.headers[*header]; |
243 | 0 | writeln!( |
244 | 0 | f, |
245 | 0 | "{}:{}", |
246 | 0 | header, |
247 | 0 | value.to_str().expect("header value must be valid") |
248 | 0 | )?; |
249 | | } |
250 | 0 | writeln!(f)?; |
251 | 0 | writeln!(f, "{}", signed_headers.join(";"))?; |
252 | | |
253 | 0 | if ctx.headers.get(X_AMZ_CONTENT_SHA_256).is_none() { |
254 | 0 | write!(f, "UNSIGNED-PAYLOAD")?; |
255 | | } else { |
256 | 0 | write!(f, "{}", ctx.headers[X_AMZ_CONTENT_SHA_256].to_str()?)?; |
257 | | } |
258 | | |
259 | 0 | Ok(f) |
260 | 0 | } |
261 | | |
262 | 0 | fn canonicalize_header( |
263 | 0 | ctx: &mut SigningContext, |
264 | 0 | method: SigningMethod, |
265 | 0 | cred: &Credential, |
266 | 0 | now: DateTime, |
267 | 0 | ) -> Result<()> { |
268 | | // Header names and values need to be normalized according to Step 4 of https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html |
269 | 0 | for (_, value) in ctx.headers.iter_mut() { |
270 | 0 | SigningContext::header_value_normalize(value) |
271 | | } |
272 | | |
273 | | // Insert HOST header if not present. |
274 | 0 | if ctx.headers.get(header::HOST).is_none() { |
275 | 0 | ctx.headers |
276 | 0 | .insert(header::HOST, ctx.authority.as_str().parse()?); |
277 | 0 | } |
278 | | |
279 | 0 | if method == SigningMethod::Header { |
280 | | // Insert DATE header if not present. |
281 | 0 | if ctx.headers.get(X_AMZ_DATE).is_none() { |
282 | 0 | let date_header = HeaderValue::try_from(format_iso8601(now))?; |
283 | 0 | ctx.headers.insert(X_AMZ_DATE, date_header); |
284 | 0 | } |
285 | | |
286 | | // Insert X_AMZ_CONTENT_SHA_256 header if not present. |
287 | 0 | if ctx.headers.get(X_AMZ_CONTENT_SHA_256).is_none() { |
288 | 0 | ctx.headers.insert( |
289 | 0 | X_AMZ_CONTENT_SHA_256, |
290 | 0 | HeaderValue::from_static("UNSIGNED-PAYLOAD"), |
291 | 0 | ); |
292 | 0 | } |
293 | | |
294 | | // Insert X_AMZ_SECURITY_TOKEN header if security token exists. |
295 | 0 | if let Some(token) = &cred.session_token { |
296 | 0 | let mut value = HeaderValue::from_str(token)?; |
297 | | // Set token value sensitive to valid leaking. |
298 | 0 | value.set_sensitive(true); |
299 | 0 |
|
300 | 0 | ctx.headers.insert(X_AMZ_SECURITY_TOKEN, value); |
301 | 0 | } |
302 | 0 | } |
303 | | |
304 | 0 | Ok(()) |
305 | 0 | } |
306 | | |
307 | 0 | fn canonicalize_query( |
308 | 0 | ctx: &mut SigningContext, |
309 | 0 | method: SigningMethod, |
310 | 0 | cred: &Credential, |
311 | 0 | now: DateTime, |
312 | 0 | service: &str, |
313 | 0 | region: &str, |
314 | 0 | ) -> Result<()> { |
315 | 0 | if let SigningMethod::Query(expire) = method { |
316 | 0 | ctx.query |
317 | 0 | .push(("X-Amz-Algorithm".into(), "AWS4-HMAC-SHA256".into())); |
318 | 0 | ctx.query.push(( |
319 | 0 | "X-Amz-Credential".into(), |
320 | 0 | format!( |
321 | 0 | "{}/{}/{}/{}/aws4_request", |
322 | 0 | cred.access_key_id, |
323 | 0 | format_date(now), |
324 | 0 | region, |
325 | 0 | service |
326 | 0 | ), |
327 | 0 | )); |
328 | 0 | ctx.query.push(("X-Amz-Date".into(), format_iso8601(now))); |
329 | 0 | ctx.query |
330 | 0 | .push(("X-Amz-Expires".into(), expire.as_secs().to_string())); |
331 | 0 | ctx.query.push(( |
332 | 0 | "X-Amz-SignedHeaders".into(), |
333 | 0 | ctx.header_name_to_vec_sorted().join(";"), |
334 | 0 | )); |
335 | | |
336 | 0 | if let Some(token) = &cred.session_token { |
337 | 0 | ctx.query |
338 | 0 | .push(("X-Amz-Security-Token".into(), token.into())); |
339 | 0 | } |
340 | 0 | } |
341 | | |
342 | | // Return if query is empty. |
343 | 0 | if ctx.query.is_empty() { |
344 | 0 | return Ok(()); |
345 | 0 | } |
346 | 0 |
|
347 | 0 | // Sort by param name |
348 | 0 | ctx.query.sort(); |
349 | 0 |
|
350 | 0 | ctx.query = ctx |
351 | 0 | .query |
352 | 0 | .iter() |
353 | 0 | .map(|(k, v)| { |
354 | 0 | ( |
355 | 0 | utf8_percent_encode(k, &AWS_QUERY_ENCODE_SET).to_string(), |
356 | 0 | utf8_percent_encode(v, &AWS_QUERY_ENCODE_SET).to_string(), |
357 | 0 | ) |
358 | 0 | }) |
359 | 0 | .collect(); |
360 | 0 |
|
361 | 0 | Ok(()) |
362 | 0 | } |
363 | | |
364 | 0 | fn generate_signing_key(secret: &str, time: DateTime, region: &str, service: &str) -> Vec<u8> { |
365 | 0 | // Sign secret |
366 | 0 | let secret = format!("AWS4{secret}"); |
367 | 0 | // Sign date |
368 | 0 | let sign_date = hmac_sha256(secret.as_bytes(), format_date(time).as_bytes()); |
369 | 0 | // Sign region |
370 | 0 | let sign_region = hmac_sha256(sign_date.as_slice(), region.as_bytes()); |
371 | 0 | // Sign service |
372 | 0 | let sign_service = hmac_sha256(sign_region.as_slice(), service.as_bytes()); |
373 | 0 | // Sign request |
374 | 0 | let sign_request = hmac_sha256(sign_service.as_slice(), "aws4_request".as_bytes()); |
375 | 0 |
|
376 | 0 | sign_request |
377 | 0 | } |
378 | | |
379 | | #[cfg(test)] |
380 | | mod tests { |
381 | | use std::time::SystemTime; |
382 | | |
383 | | use anyhow::Result; |
384 | | use aws_credential_types::Credentials; |
385 | | use aws_sigv4::http_request::PayloadChecksumKind; |
386 | | use aws_sigv4::http_request::PercentEncodingMode; |
387 | | use aws_sigv4::http_request::SignableBody; |
388 | | use aws_sigv4::http_request::SignableRequest; |
389 | | use aws_sigv4::http_request::SignatureLocation; |
390 | | use aws_sigv4::http_request::SigningSettings; |
391 | | use aws_sigv4::sign::v4; |
392 | | use http::header; |
393 | | use macro_rules_attribute::apply; |
394 | | use reqwest::Client; |
395 | | |
396 | | use super::super::AwsDefaultLoader; |
397 | | use super::*; |
398 | | use crate::aws::AwsConfig; |
399 | | |
400 | | fn test_get_request() -> http::Request<&'static str> { |
401 | | let mut req = http::Request::new(""); |
402 | | *req.method_mut() = http::Method::GET; |
403 | | *req.uri_mut() = "http://127.0.0.1:9000/hello" |
404 | | .parse() |
405 | | .expect("url must be valid"); |
406 | | |
407 | | req |
408 | | } |
409 | | |
410 | | fn test_get_request_with_sse() -> http::Request<&'static str> { |
411 | | let mut req = http::Request::new(""); |
412 | | *req.method_mut() = http::Method::GET; |
413 | | *req.uri_mut() = "http://127.0.0.1:9000/hello" |
414 | | .parse() |
415 | | .expect("url must be valid"); |
416 | | req.headers_mut().insert( |
417 | | "x-amz-server-side-encryption", |
418 | | "a".parse().expect("must be valid"), |
419 | | ); |
420 | | req.headers_mut().insert( |
421 | | "x-amz-server-side-encryption-customer-algorithm", |
422 | | "b".parse().expect("must be valid"), |
423 | | ); |
424 | | req.headers_mut().insert( |
425 | | "x-amz-server-side-encryption-customer-key", |
426 | | "c".parse().expect("must be valid"), |
427 | | ); |
428 | | req.headers_mut().insert( |
429 | | "x-amz-server-side-encryption-customer-key-md5", |
430 | | "d".parse().expect("must be valid"), |
431 | | ); |
432 | | req.headers_mut().insert( |
433 | | "x-amz-server-side-encryption-aws-kms-key-id", |
434 | | "e".parse().expect("must be valid"), |
435 | | ); |
436 | | |
437 | | req |
438 | | } |
439 | | |
440 | | fn test_get_request_with_query() -> http::Request<&'static str> { |
441 | | let mut req = http::Request::new(""); |
442 | | *req.method_mut() = http::Method::GET; |
443 | | *req.uri_mut() = "http://127.0.0.1:9000/hello?list-type=2&max-keys=3&prefix=CI/&start-after=ExampleGuide.pdf" |
444 | | .parse() |
445 | | .expect("url must be valid"); |
446 | | |
447 | | req |
448 | | } |
449 | | |
450 | | fn test_get_request_virtual_host() -> http::Request<&'static str> { |
451 | | let mut req = http::Request::new(""); |
452 | | *req.method_mut() = http::Method::GET; |
453 | | *req.uri_mut() = "http://hello.s3.test.example.com" |
454 | | .parse() |
455 | | .expect("url must be valid"); |
456 | | |
457 | | req |
458 | | } |
459 | | |
460 | | fn test_get_request_with_query_virtual_host() -> http::Request<&'static str> { |
461 | | let mut req = http::Request::new(""); |
462 | | *req.method_mut() = http::Method::GET; |
463 | | *req.uri_mut() = "http://hello.s3.test.example.com?list-type=2&max-keys=3&prefix=CI/&start-after=ExampleGuide.pdf" |
464 | | .parse() |
465 | | .expect("url must be valid"); |
466 | | |
467 | | req |
468 | | } |
469 | | |
470 | | fn test_put_request() -> http::Request<&'static str> { |
471 | | let content = "Hello,World!"; |
472 | | let mut req = http::Request::new(content); |
473 | | *req.method_mut() = http::Method::PUT; |
474 | | *req.uri_mut() = "http://127.0.0.1:9000/hello" |
475 | | .parse() |
476 | | .expect("url must be valid"); |
477 | | |
478 | | req.headers_mut().insert( |
479 | | http::header::CONTENT_LENGTH, |
480 | | HeaderValue::from_str(&content.len().to_string()).expect("must be valid"), |
481 | | ); |
482 | | |
483 | | req |
484 | | } |
485 | | |
486 | | fn test_put_request_with_body_digest() -> http::Request<&'static str> { |
487 | | let content = "Hello,World!"; |
488 | | let mut req = http::Request::new(content); |
489 | | *req.method_mut() = http::Method::PUT; |
490 | | *req.uri_mut() = "http://127.0.0.1:9000/hello" |
491 | | .parse() |
492 | | .expect("url must be valid"); |
493 | | |
494 | | req.headers_mut().insert( |
495 | | header::CONTENT_LENGTH, |
496 | | HeaderValue::from_str(&content.len().to_string()).expect("must be valid"), |
497 | | ); |
498 | | |
499 | | let body = hex_sha256(content.as_bytes()); |
500 | | req.headers_mut().insert( |
501 | | "x-amz-content-sha256", |
502 | | HeaderValue::from_str(&body).expect("must be valid"), |
503 | | ); |
504 | | |
505 | | req |
506 | | } |
507 | | |
508 | | fn test_put_request_virtual_host() -> http::Request<&'static str> { |
509 | | let content = "Hello,World!"; |
510 | | let mut req = http::Request::new(content); |
511 | | *req.method_mut() = http::Method::PUT; |
512 | | *req.uri_mut() = "http://hello.s3.test.example.com" |
513 | | .parse() |
514 | | .expect("url must be valid"); |
515 | | |
516 | | req.headers_mut().insert( |
517 | | header::CONTENT_LENGTH, |
518 | | HeaderValue::from_str(&content.len().to_string()).expect("must be valid"), |
519 | | ); |
520 | | |
521 | | req |
522 | | } |
523 | | |
524 | | macro_rules! test_cases { |
525 | | ($($tt:tt)*) => { |
526 | | #[test_case::test_case(test_get_request)] |
527 | | #[test_case::test_case(test_get_request_with_sse)] |
528 | | #[test_case::test_case(test_get_request_with_query)] |
529 | | #[test_case::test_case(test_get_request_virtual_host)] |
530 | | #[test_case::test_case(test_get_request_with_query_virtual_host)] |
531 | | #[test_case::test_case(test_put_request)] |
532 | | #[test_case::test_case(test_put_request_virtual_host)] |
533 | | #[test_case::test_case(test_put_request_with_body_digest)] |
534 | | $($tt)* |
535 | | }; |
536 | | } |
537 | | |
538 | | fn compare_request(name: &str, l: &http::Request<&str>, r: &http::Request<&str>) { |
539 | | fn format_headers(req: &http::Request<&str>) -> Vec<String> { |
540 | | let mut hs = req |
541 | | .headers() |
542 | | .iter() |
543 | | .map(|(k, v)| format!("{}:{}", k, v.to_str().expect("must be valid"))) |
544 | | .collect::<Vec<_>>(); |
545 | | |
546 | | // Insert host if original request doesn't have it. |
547 | | if !hs.contains(&format!("host:{}", req.uri().authority().unwrap())) { |
548 | | hs.push(format!("host:{}", req.uri().authority().unwrap())) |
549 | | } |
550 | | |
551 | | hs.sort(); |
552 | | hs |
553 | | } |
554 | | |
555 | | assert_eq!( |
556 | | format_headers(l), |
557 | | format_headers(r), |
558 | | "{name} header mismatch" |
559 | | ); |
560 | | |
561 | | fn format_query(req: &http::Request<&str>) -> Vec<String> { |
562 | | let query = req.uri().query().unwrap_or_default(); |
563 | | let mut query = form_urlencoded::parse(query.as_bytes()) |
564 | | .map(|(k, v)| format!("{}={}", &k, &v)) |
565 | | .collect::<Vec<_>>(); |
566 | | query.sort(); |
567 | | query |
568 | | } |
569 | | |
570 | | assert_eq!(format_query(l), format_query(r), "{name} query mismatch"); |
571 | | } |
572 | | |
573 | | #[apply(test_cases)] |
574 | | #[tokio::test] |
575 | | async fn test_calculate(req_fn: fn() -> http::Request<&'static str>) -> Result<()> { |
576 | | let _ = env_logger::builder().is_test(true).try_init(); |
577 | | |
578 | | let mut req = req_fn(); |
579 | | let name = format!( |
580 | | "{} {} {:?}", |
581 | | req.method(), |
582 | | req.uri().path(), |
583 | | req.uri().query(), |
584 | | ); |
585 | | let now = now(); |
586 | | |
587 | | let mut ss = SigningSettings::default(); |
588 | | ss.percent_encoding_mode = PercentEncodingMode::Double; |
589 | | ss.payload_checksum_kind = PayloadChecksumKind::XAmzSha256; |
590 | | let id = Credentials::new( |
591 | | "access_key_id", |
592 | | "secret_access_key", |
593 | | None, |
594 | | None, |
595 | | "hardcoded-credentials", |
596 | | ) |
597 | | .into(); |
598 | | let sp = v4::SigningParams::builder() |
599 | | .identity(&id) |
600 | | .region("test") |
601 | | .name("s3") |
602 | | .time(SystemTime::from(now)) |
603 | | .settings(ss) |
604 | | .build() |
605 | | .expect("signing params must be valid"); |
606 | | |
607 | | let mut body = SignableBody::UnsignedPayload; |
608 | | if req.headers().get(X_AMZ_CONTENT_SHA_256).is_some() { |
609 | | body = SignableBody::Bytes(req.body().as_bytes()); |
610 | | } |
611 | | |
612 | | let output = aws_sigv4::http_request::sign( |
613 | | SignableRequest::new( |
614 | | req.method().as_str(), |
615 | | req.uri().to_string(), |
616 | | req.headers() |
617 | | .iter() |
618 | | .map(|(k, v)| (k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap())), |
619 | | body, |
620 | | ) |
621 | | .unwrap(), |
622 | | &sp.into(), |
623 | | ) |
624 | | .expect("signing must succeed"); |
625 | | let (aws_sig, _) = output.into_parts(); |
626 | | aws_sig.apply_to_request_http1x(&mut req); |
627 | | let expected_req = req; |
628 | | |
629 | | let mut req = req_fn(); |
630 | | |
631 | | let loader = AwsDefaultLoader::new( |
632 | | Client::new(), |
633 | | AwsConfig { |
634 | | access_key_id: Some("access_key_id".to_string()), |
635 | | secret_access_key: Some("secret_access_key".to_string()), |
636 | | ..Default::default() |
637 | | }, |
638 | | ); |
639 | | let cred = loader.load().await?.unwrap(); |
640 | | |
641 | | let signer = Signer::new("s3", "test").time(now); |
642 | | signer.sign(&mut req, &cred).expect("must apply success"); |
643 | | |
644 | | let actual_req = req; |
645 | | |
646 | | compare_request(&name, &expected_req, &actual_req); |
647 | | |
648 | | Ok(()) |
649 | | } |
650 | | |
651 | | #[apply(test_cases)] |
652 | | #[tokio::test] |
653 | | async fn test_calculate_in_query(req_fn: fn() -> http::Request<&'static str>) -> Result<()> { |
654 | | let _ = env_logger::builder().is_test(true).try_init(); |
655 | | |
656 | | let mut req = req_fn(); |
657 | | let name = format!( |
658 | | "{} {} {:?}", |
659 | | req.method(), |
660 | | req.uri().path(), |
661 | | req.uri().query(), |
662 | | ); |
663 | | let now = now(); |
664 | | |
665 | | let mut ss = SigningSettings::default(); |
666 | | ss.percent_encoding_mode = PercentEncodingMode::Double; |
667 | | ss.payload_checksum_kind = PayloadChecksumKind::XAmzSha256; |
668 | | ss.signature_location = SignatureLocation::QueryParams; |
669 | | ss.expires_in = Some(std::time::Duration::from_secs(3600)); |
670 | | let id = Credentials::new( |
671 | | "access_key_id", |
672 | | "secret_access_key", |
673 | | None, |
674 | | None, |
675 | | "hardcoded-credentials", |
676 | | ) |
677 | | .into(); |
678 | | let sp = v4::SigningParams::builder() |
679 | | .identity(&id) |
680 | | .region("test") |
681 | | .name("s3") |
682 | | .time(SystemTime::from(now)) |
683 | | .settings(ss) |
684 | | .build() |
685 | | .expect("signing params must be valid"); |
686 | | |
687 | | let mut body = SignableBody::UnsignedPayload; |
688 | | if req.headers().get(X_AMZ_CONTENT_SHA_256).is_some() { |
689 | | body = SignableBody::Bytes(req.body().as_bytes()); |
690 | | } |
691 | | |
692 | | let output = aws_sigv4::http_request::sign( |
693 | | SignableRequest::new( |
694 | | req.method().as_str(), |
695 | | req.uri().to_string(), |
696 | | req.headers() |
697 | | .iter() |
698 | | .map(|(k, v)| (k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap())), |
699 | | body, |
700 | | ) |
701 | | .unwrap(), |
702 | | &sp.into(), |
703 | | ) |
704 | | .expect("signing must succeed"); |
705 | | let (aws_sig, _) = output.into_parts(); |
706 | | aws_sig.apply_to_request_http1x(&mut req); |
707 | | let expected_req = req; |
708 | | |
709 | | let mut req = req_fn(); |
710 | | |
711 | | let loader = AwsDefaultLoader::new( |
712 | | Client::new(), |
713 | | AwsConfig { |
714 | | access_key_id: Some("access_key_id".to_string()), |
715 | | secret_access_key: Some("secret_access_key".to_string()), |
716 | | ..Default::default() |
717 | | }, |
718 | | ); |
719 | | let cred = loader.load().await?.unwrap(); |
720 | | |
721 | | let signer = Signer::new("s3", "test").time(now); |
722 | | |
723 | | signer.sign_query(&mut req, Duration::from_secs(3600), &cred)?; |
724 | | let actual_req = req; |
725 | | |
726 | | compare_request(&name, &expected_req, &actual_req); |
727 | | |
728 | | Ok(()) |
729 | | } |
730 | | |
731 | | #[apply(test_cases)] |
732 | | #[tokio::test] |
733 | | async fn test_calculate_with_token(req_fn: fn() -> http::Request<&'static str>) -> Result<()> { |
734 | | let _ = env_logger::builder().is_test(true).try_init(); |
735 | | |
736 | | let mut req = req_fn(); |
737 | | let name = format!( |
738 | | "{} {} {:?}", |
739 | | req.method(), |
740 | | req.uri().path(), |
741 | | req.uri().query(), |
742 | | ); |
743 | | let now = now(); |
744 | | |
745 | | let mut ss = SigningSettings::default(); |
746 | | ss.percent_encoding_mode = PercentEncodingMode::Double; |
747 | | ss.payload_checksum_kind = PayloadChecksumKind::XAmzSha256; |
748 | | let id = Credentials::new( |
749 | | "access_key_id", |
750 | | "secret_access_key", |
751 | | Some("security_token".to_string()), |
752 | | None, |
753 | | "hardcoded-credentials", |
754 | | ) |
755 | | .into(); |
756 | | let sp = v4::SigningParams::builder() |
757 | | .identity(&id) |
758 | | .region("test") |
759 | | .name("s3") |
760 | | .time(SystemTime::from(now)) |
761 | | .settings(ss) |
762 | | .build() |
763 | | .expect("signing params must be valid"); |
764 | | |
765 | | let mut body = SignableBody::UnsignedPayload; |
766 | | if req.headers().get(X_AMZ_CONTENT_SHA_256).is_some() { |
767 | | body = SignableBody::Bytes(req.body().as_bytes()); |
768 | | } |
769 | | |
770 | | let output = aws_sigv4::http_request::sign( |
771 | | SignableRequest::new( |
772 | | req.method().as_str(), |
773 | | req.uri().to_string(), |
774 | | req.headers() |
775 | | .iter() |
776 | | .map(|(k, v)| (k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap())), |
777 | | body, |
778 | | ) |
779 | | .unwrap(), |
780 | | &sp.into(), |
781 | | ) |
782 | | .expect("signing must succeed"); |
783 | | let (aws_sig, _) = output.into_parts(); |
784 | | aws_sig.apply_to_request_http1x(&mut req); |
785 | | let expected_req = req; |
786 | | |
787 | | let mut req = req_fn(); |
788 | | |
789 | | let loader = AwsDefaultLoader::new( |
790 | | Client::new(), |
791 | | AwsConfig { |
792 | | access_key_id: Some("access_key_id".to_string()), |
793 | | secret_access_key: Some("secret_access_key".to_string()), |
794 | | session_token: Some("security_token".to_string()), |
795 | | ..Default::default() |
796 | | }, |
797 | | ); |
798 | | let cred = loader.load().await?.unwrap(); |
799 | | |
800 | | let signer = Signer::new("s3", "test").time(now); |
801 | | |
802 | | signer.sign(&mut req, &cred).expect("must apply success"); |
803 | | let actual_req = req; |
804 | | |
805 | | compare_request(&name, &expected_req, &actual_req); |
806 | | |
807 | | Ok(()) |
808 | | } |
809 | | |
810 | | #[apply(test_cases)] |
811 | | #[tokio::test] |
812 | | async fn test_calculate_with_token_in_query( |
813 | | req_fn: fn() -> http::Request<&'static str>, |
814 | | ) -> Result<()> { |
815 | | let _ = env_logger::builder().is_test(true).try_init(); |
816 | | |
817 | | let mut req = req_fn(); |
818 | | let name = format!( |
819 | | "{} {} {:?}", |
820 | | req.method(), |
821 | | req.uri().path(), |
822 | | req.uri().query(), |
823 | | ); |
824 | | let now = now(); |
825 | | |
826 | | let mut ss = SigningSettings::default(); |
827 | | ss.percent_encoding_mode = PercentEncodingMode::Double; |
828 | | ss.payload_checksum_kind = PayloadChecksumKind::XAmzSha256; |
829 | | ss.signature_location = SignatureLocation::QueryParams; |
830 | | ss.expires_in = Some(std::time::Duration::from_secs(3600)); |
831 | | let id = Credentials::new( |
832 | | "access_key_id", |
833 | | "secret_access_key", |
834 | | Some("security_token".to_string()), |
835 | | None, |
836 | | "hardcoded-credentials", |
837 | | ) |
838 | | .into(); |
839 | | let sp = v4::SigningParams::builder() |
840 | | .identity(&id) |
841 | | .region("test") |
842 | | // .security_token("security_token") |
843 | | .name("s3") |
844 | | .time(SystemTime::from(now)) |
845 | | .settings(ss) |
846 | | .build() |
847 | | .expect("signing params must be valid"); |
848 | | |
849 | | let mut body = SignableBody::UnsignedPayload; |
850 | | if req.headers().get(X_AMZ_CONTENT_SHA_256).is_some() { |
851 | | body = SignableBody::Bytes(req.body().as_bytes()); |
852 | | } |
853 | | |
854 | | let output = aws_sigv4::http_request::sign( |
855 | | SignableRequest::new( |
856 | | req.method().as_str(), |
857 | | req.uri().to_string(), |
858 | | req.headers() |
859 | | .iter() |
860 | | .map(|(k, v)| (k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap())), |
861 | | body, |
862 | | ) |
863 | | .unwrap(), |
864 | | &sp.into(), |
865 | | ) |
866 | | .expect("signing must succeed"); |
867 | | let (aws_sig, _) = output.into_parts(); |
868 | | aws_sig.apply_to_request_http1x(&mut req); |
869 | | let expected_req = req; |
870 | | |
871 | | let mut req = req_fn(); |
872 | | |
873 | | let loader = AwsDefaultLoader::new( |
874 | | Client::new(), |
875 | | AwsConfig { |
876 | | access_key_id: Some("access_key_id".to_string()), |
877 | | secret_access_key: Some("secret_access_key".to_string()), |
878 | | session_token: Some("security_token".to_string()), |
879 | | ..Default::default() |
880 | | }, |
881 | | ); |
882 | | let cred = loader.load().await?.unwrap(); |
883 | | |
884 | | let signer = Signer::new("s3", "test").time(now); |
885 | | signer |
886 | | .sign_query(&mut req, Duration::from_secs(3600), &cred) |
887 | | .expect("must apply success"); |
888 | | let actual_req = req; |
889 | | |
890 | | compare_request(&name, &expected_req, &actual_req); |
891 | | |
892 | | Ok(()) |
893 | | } |
894 | | } |