reqsign/aws/
v4.rs

1//! AWS service sigv4 signer
2
3use 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/// 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)]
34pub struct Signer {
35    service: String,
36    region: String,
37
38    time: Option<DateTime>,
39}
40
41impl Signer {
42    /// Create a builder.
43    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    /// 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    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 context
73        canonicalize_header(&mut ctx, method, cred, now)?;
74        canonicalize_query(&mut ctx, method, cred, now, &self.service, &self.region)?;
75
76        // build canonical request and string to sign.
77        let creq = canonical_request_string(&mut ctx)?;
78        let encoded_req = hex_sha256(creq.as_bytes());
79
80        // Scope: "20220313/<region>/<service>/aws4_request"
81        let scope = format!(
82            "{}/{}/{}/aws4_request",
83            format_date(now),
84            self.region,
85            self.service
86        );
87        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        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    /// Get the region of this signer.
132    pub fn region(&self) -> &str {
133        &self.region
134    }
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    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    /// 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    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    // 256 is specially chosen to avoid reallocation for most requests.
218    let mut f = String::with_capacity(256);
219
220    // Insert method
221    writeln!(f, "{}", ctx.method)?;
222    // Insert encoded path
223    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    // Insert query
230    writeln!(
231        f,
232        "{}",
233        ctx.query
234            .iter()
235            .map(|(k, v)| { format!("{k}={v}") })
236            .collect::<Vec<_>>()
237            .join("&")
238    )?;
239    // Insert signed headers
240    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    // 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    for (_, value) in ctx.headers.iter_mut() {
270        SigningContext::header_value_normalize(value)
271    }
272
273    // Insert HOST header if not present.
274    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        // Insert DATE header if not present.
281        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        // Insert X_AMZ_CONTENT_SHA_256 header if not present.
287        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        // Insert X_AMZ_SECURITY_TOKEN header if security token exists.
295        if let Some(token) = &cred.session_token {
296            let mut value = HeaderValue::from_str(token)?;
297            // Set token value sensitive to valid leaking.
298            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    // Return if query is empty.
343    if ctx.query.is_empty() {
344        return Ok(());
345    }
346
347    // Sort by param name
348    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    // Sign secret
366    let secret = format!("AWS4{secret}");
367    // Sign date
368    let sign_date = hmac_sha256(secret.as_bytes(), format_date(time).as_bytes());
369    // Sign region
370    let sign_region = hmac_sha256(sign_date.as_slice(), region.as_bytes());
371    // Sign service
372    let sign_service = hmac_sha256(sign_region.as_slice(), service.as_bytes());
373    // Sign request
374    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            // 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}