1use std::fmt::Debug;
4use std::fmt::Write;
5use std::time::Duration;
6
7use anyhow::Result;
8use http::header;
9use http::HeaderValue;
10use log::debug;
11use percent_encoding::percent_decode_str;
12use percent_encoding::utf8_percent_encode;
13
14use super::constants::AWS_QUERY_ENCODE_SET;
15use super::constants::X_AMZ_CONTENT_SHA_256;
16use super::constants::X_AMZ_DATE;
17use super::constants::X_AMZ_SECURITY_TOKEN;
18use super::credential::Credential;
19use crate::ctx::SigningContext;
20use crate::ctx::SigningMethod;
21use crate::hash::hex_hmac_sha256;
22use crate::hash::hex_sha256;
23use crate::hash::hmac_sha256;
24use crate::request::SignableRequest;
25use crate::time::format_date;
26use crate::time::format_iso8601;
27use crate::time::now;
28use crate::time::DateTime;
29
30#[derive(Debug)]
34pub struct Signer {
35 service: String,
36 region: String,
37
38 time: Option<DateTime>,
39}
40
41impl Signer {
42 pub fn new(service: &str, region: &str) -> Self {
44 Self {
45 service: service.to_string(),
46 region: region.to_string(),
47 time: None,
48 }
49 }
50
51 #[cfg(test)]
58 pub fn time(mut self, time: DateTime) -> Self {
59 self.time = Some(time);
60 self
61 }
62
63 fn build(
64 &self,
65 req: &mut impl SignableRequest,
66 method: SigningMethod,
67 cred: &Credential,
68 ) -> Result<SigningContext> {
69 let now = self.time.unwrap_or_else(now);
70 let mut ctx = req.build()?;
71
72 canonicalize_header(&mut ctx, method, cred, now)?;
74 canonicalize_query(&mut ctx, method, cred, now, &self.service, &self.region)?;
75
76 let creq = canonical_request_string(&mut ctx)?;
78 let encoded_req = hex_sha256(creq.as_bytes());
79
80 let scope = format!(
82 "{}/{}/{}/aws4_request",
83 format_date(now),
84 self.region,
85 self.service
86 );
87 debug!("calculated scope: {scope}");
88
89 let string_to_sign = {
96 let mut f = String::new();
97 writeln!(f, "AWS4-HMAC-SHA256")?;
98 writeln!(f, "{}", format_iso8601(now))?;
99 writeln!(f, "{}", &scope)?;
100 write!(f, "{}", &encoded_req)?;
101 f
102 };
103 debug!("calculated string to sign: {string_to_sign}");
104
105 let signing_key =
106 generate_signing_key(&cred.secret_access_key, now, &self.region, &self.service);
107 let signature = hex_hmac_sha256(&signing_key, string_to_sign.as_bytes());
108
109 match method {
110 SigningMethod::Header => {
111 let mut authorization = HeaderValue::from_str(&format!(
112 "AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}",
113 cred.access_key_id,
114 scope,
115 ctx.header_name_to_vec_sorted().join(";"),
116 signature
117 ))?;
118 authorization.set_sensitive(true);
119
120 ctx.headers
121 .insert(http::header::AUTHORIZATION, authorization);
122 }
123 SigningMethod::Query(_) => {
124 ctx.query.push(("X-Amz-Signature".into(), signature));
125 }
126 }
127
128 Ok(ctx)
129 }
130
131 pub fn region(&self) -> &str {
133 &self.region
134 }
135
136 pub fn sign(&self, req: &mut impl SignableRequest, cred: &Credential) -> Result<()> {
168 let ctx = self.build(req, SigningMethod::Header, cred)?;
169 req.apply(ctx)
170 }
171
172 pub fn sign_query(
206 &self,
207 req: &mut impl SignableRequest,
208 expire: Duration,
209 cred: &Credential,
210 ) -> Result<()> {
211 let ctx = self.build(req, SigningMethod::Query(expire), cred)?;
212 req.apply(ctx)
213 }
214}
215
216fn canonical_request_string(ctx: &mut SigningContext) -> Result<String> {
217 let mut f = String::with_capacity(256);
219
220 writeln!(f, "{}", ctx.method)?;
222 let path = percent_decode_str(&ctx.path).decode_utf8()?;
224 writeln!(
225 f,
226 "{}",
227 utf8_percent_encode(&path, &super::constants::AWS_URI_ENCODE_SET)
228 )?;
229 writeln!(
231 f,
232 "{}",
233 ctx.query
234 .iter()
235 .map(|(k, v)| { format!("{k}={v}") })
236 .collect::<Vec<_>>()
237 .join("&")
238 )?;
239 let signed_headers = ctx.header_name_to_vec_sorted();
241 for header in signed_headers.iter() {
242 let value = &ctx.headers[*header];
243 writeln!(
244 f,
245 "{}:{}",
246 header,
247 value.to_str().expect("header value must be valid")
248 )?;
249 }
250 writeln!(f)?;
251 writeln!(f, "{}", signed_headers.join(";"))?;
252
253 if ctx.headers.get(X_AMZ_CONTENT_SHA_256).is_none() {
254 write!(f, "UNSIGNED-PAYLOAD")?;
255 } else {
256 write!(f, "{}", ctx.headers[X_AMZ_CONTENT_SHA_256].to_str()?)?;
257 }
258
259 Ok(f)
260}
261
262fn canonicalize_header(
263 ctx: &mut SigningContext,
264 method: SigningMethod,
265 cred: &Credential,
266 now: DateTime,
267) -> Result<()> {
268 for (_, value) in ctx.headers.iter_mut() {
270 SigningContext::header_value_normalize(value)
271 }
272
273 if ctx.headers.get(header::HOST).is_none() {
275 ctx.headers
276 .insert(header::HOST, ctx.authority.as_str().parse()?);
277 }
278
279 if method == SigningMethod::Header {
280 if ctx.headers.get(X_AMZ_DATE).is_none() {
282 let date_header = HeaderValue::try_from(format_iso8601(now))?;
283 ctx.headers.insert(X_AMZ_DATE, date_header);
284 }
285
286 if ctx.headers.get(X_AMZ_CONTENT_SHA_256).is_none() {
288 ctx.headers.insert(
289 X_AMZ_CONTENT_SHA_256,
290 HeaderValue::from_static("UNSIGNED-PAYLOAD"),
291 );
292 }
293
294 if let Some(token) = &cred.session_token {
296 let mut value = HeaderValue::from_str(token)?;
297 value.set_sensitive(true);
299
300 ctx.headers.insert(X_AMZ_SECURITY_TOKEN, value);
301 }
302 }
303
304 Ok(())
305}
306
307fn canonicalize_query(
308 ctx: &mut SigningContext,
309 method: SigningMethod,
310 cred: &Credential,
311 now: DateTime,
312 service: &str,
313 region: &str,
314) -> Result<()> {
315 if let SigningMethod::Query(expire) = method {
316 ctx.query
317 .push(("X-Amz-Algorithm".into(), "AWS4-HMAC-SHA256".into()));
318 ctx.query.push((
319 "X-Amz-Credential".into(),
320 format!(
321 "{}/{}/{}/{}/aws4_request",
322 cred.access_key_id,
323 format_date(now),
324 region,
325 service
326 ),
327 ));
328 ctx.query.push(("X-Amz-Date".into(), format_iso8601(now)));
329 ctx.query
330 .push(("X-Amz-Expires".into(), expire.as_secs().to_string()));
331 ctx.query.push((
332 "X-Amz-SignedHeaders".into(),
333 ctx.header_name_to_vec_sorted().join(";"),
334 ));
335
336 if let Some(token) = &cred.session_token {
337 ctx.query
338 .push(("X-Amz-Security-Token".into(), token.into()));
339 }
340 }
341
342 if ctx.query.is_empty() {
344 return Ok(());
345 }
346
347 ctx.query.sort();
349
350 ctx.query = ctx
351 .query
352 .iter()
353 .map(|(k, v)| {
354 (
355 utf8_percent_encode(k, &AWS_QUERY_ENCODE_SET).to_string(),
356 utf8_percent_encode(v, &AWS_QUERY_ENCODE_SET).to_string(),
357 )
358 })
359 .collect();
360
361 Ok(())
362}
363
364fn generate_signing_key(secret: &str, time: DateTime, region: &str, service: &str) -> Vec<u8> {
365 let secret = format!("AWS4{secret}");
367 let sign_date = hmac_sha256(secret.as_bytes(), format_date(time).as_bytes());
369 let sign_region = hmac_sha256(sign_date.as_slice(), region.as_bytes());
371 let sign_service = hmac_sha256(sign_region.as_slice(), service.as_bytes());
373 let sign_request = hmac_sha256(sign_service.as_slice(), "aws4_request".as_bytes());
375
376 sign_request
377}
378
379#[cfg(test)]
380mod 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 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 .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}