Skip to main content

reqsign_aws_v4/
sign_request.rs

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