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 http::request::Parts;
20use http::{HeaderValue, header};
21use log::debug;
22use reqsign_aws_core::signing::{
23    append_query_fragment, append_query_pairs, canonical_request_string, canonicalize_headers,
24    canonicalize_headers_with_standard_session_token, canonicalize_query,
25};
26use reqsign_core::hash::{hex_hmac_sha256, hex_sha256, hmac_sha256};
27use reqsign_core::time::Timestamp;
28use reqsign_core::{Context, Result, SignRequest, SigningCredential, SigningRequest};
29use std::fmt::Write;
30use std::time::Duration;
31
32const CREDENTIAL_OPERATION_HEADROOM: Duration = Duration::from_secs(10);
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    use_standard_session_token: bool,
42
43    time: Option<Timestamp>,
44}
45
46impl RequestSigner {
47    /// Create a new builder for AWS V4 signer.
48    pub fn new(service: &str, region: &str) -> Self {
49        Self {
50            service: service.into(),
51            region: region.into(),
52            use_standard_session_token: false,
53
54            time: None,
55        }
56    }
57
58    /// Use the standard AWS session-token representation.
59    ///
60    /// This mode is required when an S3 Express request is authenticated with
61    /// temporary IAM or STS credentials. Header authentication uses
62    /// `x-amz-security-token`, and query authentication uses
63    /// `X-Amz-Security-Token`.
64    ///
65    /// By default, S3 Express endpoints use the [`CreateSession`] credential
66    /// representation: `x-amz-s3session-token` for header authentication and
67    /// `X-Amz-S3session-Token` for query authentication. The S3 Express
68    /// [`CopyObject`], [`HeadBucket`], and [`UploadPartCopy`] operations require
69    /// IAM credentials instead of CreateSession credentials.
70    ///
71    /// This setting only selects how an existing [`Credential`] session token
72    /// is represented. It does not modify the credential or load a different
73    /// credential source.
74    ///
75    /// # Example
76    ///
77    /// ```
78    /// use reqsign_aws_v4::RequestSigner;
79    ///
80    /// let signer = RequestSigner::new("s3express", "us-west-2")
81    ///     .with_standard_session_token();
82    /// ```
83    ///
84    /// [`CopyObject`]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_CopyObject.html
85    /// [`CreateSession`]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_CreateSession.html
86    /// [`HeadBucket`]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadBucket.html
87    /// [`UploadPartCopy`]: https://docs.aws.amazon.com/AmazonS3/latest/API/API_UploadPartCopy.html
88    pub fn with_standard_session_token(mut self) -> Self {
89        self.use_standard_session_token = true;
90        self
91    }
92
93    /// Specify the signing time.
94    ///
95    /// # Note
96    ///
97    /// We should always take current time to sign requests.
98    /// Only use this function for testing.
99    #[cfg(test)]
100    pub fn with_time(mut self, time: Timestamp) -> Self {
101        self.time = Some(time);
102        self
103    }
104
105    fn get_time(&self) -> Timestamp {
106        self.time.unwrap_or_else(Timestamp::now)
107    }
108
109    fn required_valid_until_at(
110        &self,
111        signing_time: Timestamp,
112        expires_in: Option<Duration>,
113    ) -> Timestamp {
114        signing_time + expires_in.unwrap_or(CREDENTIAL_OPERATION_HEADROOM)
115    }
116}
117impl SignRequest for RequestSigner {
118    type Credential = Credential;
119
120    fn required_valid_until(
121        &self,
122        _credential: &Self::Credential,
123        expires_in: Option<Duration>,
124    ) -> Timestamp {
125        self.required_valid_until_at(self.get_time(), expires_in)
126    }
127
128    async fn sign_request(
129        &self,
130        _: &Context,
131        req: &mut Parts,
132        credential: Option<&Self::Credential>,
133        expires_in: Option<Duration>,
134    ) -> Result<()> {
135        let Some(cred) = credential else {
136            return Ok(());
137        };
138
139        let now = self.get_time();
140        let required_until = self.required_valid_until_at(now, expires_in);
141        if !cred.is_valid_at(required_until) {
142            return Err(reqsign_core::Error::credential_invalid(
143                "credential expires before the requested signing operation deadline",
144            ));
145        }
146
147        let original_uri = req.uri.clone();
148        let mut signed_req = SigningRequest::build(req)?;
149
150        // canonicalize context
151        if self.use_standard_session_token {
152            canonicalize_headers_with_standard_session_token(
153                &mut signed_req,
154                cred,
155                expires_in,
156                now,
157            )?;
158        } else {
159            canonicalize_headers(&mut signed_req, cred, expires_in, now)?;
160        }
161        let authentication_query = authentication_query(
162            &signed_req,
163            cred,
164            expires_in,
165            now,
166            &self.service,
167            &self.region,
168            self.use_standard_session_token,
169        );
170        let canonical_query = canonicalize_query(&signed_req, &authentication_query);
171
172        // build canonical request and string to sign.
173        let creq = canonical_request_string(&signed_req, &canonical_query)?;
174        let encoded_req = hex_sha256(creq.as_bytes());
175
176        // Scope: "20220313/<region>/<service>/aws4_request"
177        let scope = format!(
178            "{}/{}/{}/aws4_request",
179            now.format_date(),
180            self.region,
181            self.service
182        );
183        debug!("calculated scope: {scope}");
184
185        // StringToSign:
186        //
187        // AWS4-HMAC-SHA256
188        // 20220313T072004Z
189        // 20220313/<region>/<service>/aws4_request
190        // <hashed_canonical_request>
191        let string_to_sign = {
192            let mut f = String::new();
193            writeln!(f, "AWS4-HMAC-SHA256").map_err(|e| {
194                reqsign_core::Error::unexpected(format!("failed to write algorithm: {e}"))
195            })?;
196            writeln!(f, "{}", now.format_iso8601()).map_err(|e| {
197                reqsign_core::Error::unexpected(format!("failed to write timestamp: {e}"))
198            })?;
199            writeln!(f, "{}", scope).map_err(|e| {
200                reqsign_core::Error::unexpected(format!("failed to write scope: {e}"))
201            })?;
202            write!(f, "{}", encoded_req).map_err(|e| {
203                reqsign_core::Error::unexpected(format!("failed to write encoded request: {e}"))
204            })?;
205            f
206        };
207        debug!("calculated string to sign: {string_to_sign}");
208
209        let signing_key =
210            generate_signing_key(&cred.secret_access_key, now, &self.region, &self.service);
211        let signature = hex_hmac_sha256(&signing_key, string_to_sign.as_bytes());
212
213        let final_uri = if expires_in.is_some() {
214            let unsigned_uri = append_query_pairs(&original_uri, &authentication_query)?;
215            Some(append_query_fragment(
216                &unsigned_uri,
217                &format!("X-Amz-Signature={signature}"),
218            )?)
219        } else {
220            let mut authorization = HeaderValue::from_str(&format!(
221                "AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}",
222                cred.access_key_id,
223                scope,
224                signed_req.header_name_to_vec_sorted().join(";"),
225                signature
226            ))
227            .map_err(|e| {
228                reqsign_core::Error::unexpected(format!(
229                    "failed to create authorization header: {e}"
230                ))
231            })?;
232            authorization.set_sensitive(true);
233
234            signed_req
235                .headers
236                .insert(header::AUTHORIZATION, authorization);
237            None
238        };
239
240        // Apply to the request.
241        signed_req.apply(req)?;
242        if let Some(uri) = final_uri {
243            req.uri = uri;
244        }
245        Ok(())
246    }
247}
248
249fn authentication_query(
250    ctx: &SigningRequest,
251    cred: &Credential,
252    expires_in: Option<Duration>,
253    now: Timestamp,
254    service: &str,
255    region: &str,
256    use_standard_session_token: bool,
257) -> Vec<(String, String)> {
258    let mut query = Vec::new();
259    if let Some(expire) = expires_in {
260        query.push(("X-Amz-Algorithm".into(), "AWS4-HMAC-SHA256".into()));
261        query.push((
262            "X-Amz-Credential".into(),
263            format!(
264                "{}/{}/{}/{}/aws4_request",
265                cred.access_key_id,
266                now.format_date(),
267                region,
268                service
269            ),
270        ));
271        query.push(("X-Amz-Date".into(), now.format_iso8601()));
272        query.push(("X-Amz-Expires".into(), expire.as_secs().to_string()));
273        query.push((
274            "X-Amz-SignedHeaders".into(),
275            ctx.header_name_to_vec_sorted().join(";"),
276        ));
277
278        if let Some(token) = &cred.session_token {
279            let is_s3_express = !use_standard_session_token
280                && (ctx.authority.as_str().contains("s3express")
281                    || ctx.authority.as_str().contains("--x-s3"));
282            let name = if is_s3_express {
283                "X-Amz-S3session-Token"
284            } else {
285                "X-Amz-Security-Token"
286            };
287            query.push((name.into(), token.into()));
288        }
289    }
290    query
291}
292
293fn generate_signing_key(secret: &str, time: Timestamp, region: &str, service: &str) -> Vec<u8> {
294    // Sign secret
295    let secret = format!("AWS4{secret}");
296    // Sign date
297    let sign_date = hmac_sha256(secret.as_bytes(), time.format_date().as_bytes());
298    // Sign region
299    let sign_region = hmac_sha256(sign_date.as_slice(), region.as_bytes());
300    // Sign service
301    let sign_service = hmac_sha256(sign_region.as_slice(), service.as_bytes());
302    // Sign request
303    hmac_sha256(sign_service.as_slice(), "aws4_request".as_bytes())
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309    use crate::StaticCredentialProvider;
310    use anyhow::Result;
311    use aws_credential_types::Credentials;
312    use aws_sigv4::http_request::PayloadChecksumKind;
313    use aws_sigv4::http_request::PercentEncodingMode;
314    use aws_sigv4::http_request::SignableBody;
315    use aws_sigv4::http_request::SignableRequest;
316    use aws_sigv4::http_request::SignatureLocation;
317    use aws_sigv4::http_request::SigningSettings;
318    use aws_sigv4::sign::v4;
319    use http::Request;
320    use http::header;
321    use reqsign_aws_core::constants::X_AMZ_CONTENT_SHA_256;
322    use reqsign_aws_core::signing::canonical_uri;
323    use reqsign_core::{ErrorKind, ProvideCredential, Signer};
324    use reqsign_file_read_tokio::TokioFileRead;
325    use reqsign_http_send_reqwest::ReqwestHttpSend;
326    use std::collections::VecDeque;
327    use std::sync::atomic::{AtomicUsize, Ordering};
328    use std::sync::{Arc, Mutex};
329
330    const RAW_QUERY: &str = "slash=%2F&hash=%23&amp=%26&equals=%3D&space=%20&encoded-plus=%2B&literal-plus=+&double=%252F&dup=first&dup=second&=empty-key&empty=&flag&flag=&";
331    const S3_EXPRESS_URI: &str =
332        "https://bucket--use2-az1--x-s3.s3express-use2-az1.us-east-2.amazonaws.com/object";
333    const S3_EXPRESS_REGION: &str = "us-east-2";
334    const TEST_ACCESS_KEY: &str = "AKIDEXAMPLE";
335    const TEST_SECRET_KEY: &str = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
336    const TEST_SESSION_TOKEN: &str = "test-session-token";
337
338    fn s3_express_credential(now: Timestamp) -> Credential {
339        Credential {
340            access_key_id: TEST_ACCESS_KEY.to_string(),
341            secret_access_key: TEST_SECRET_KEY.to_string(),
342            session_token: Some(TEST_SESSION_TOKEN.to_string()),
343            expires_in: Some(now + Duration::from_secs(3600)),
344        }
345    }
346
347    fn aws_signed_s3_express_request(
348        mut request: Request<&'static str>,
349        now: Timestamp,
350        presigned: bool,
351        session_token_name_override: Option<&'static str>,
352    ) -> Result<Request<&'static str>> {
353        let mut settings = SigningSettings::default();
354        settings.percent_encoding_mode = PercentEncodingMode::Double;
355        settings.payload_checksum_kind = PayloadChecksumKind::XAmzSha256;
356        settings.session_token_name_override = session_token_name_override;
357        if presigned {
358            settings.signature_location = SignatureLocation::QueryParams;
359            settings.expires_in = Some(Duration::from_secs(60));
360        }
361
362        let identity = Credentials::new(
363            TEST_ACCESS_KEY,
364            TEST_SECRET_KEY,
365            Some(TEST_SESSION_TOKEN.to_string()),
366            None,
367            "hardcoded-credentials",
368        )
369        .into();
370        let params = v4::SigningParams::builder()
371            .identity(&identity)
372            .region(S3_EXPRESS_REGION)
373            .name("s3express")
374            .time(now.as_system_time())
375            .settings(settings)
376            .build()
377            .expect("signing params must be valid");
378        let output = aws_sigv4::http_request::sign(
379            SignableRequest::new(
380                request.method().as_str(),
381                request.uri().to_string(),
382                request
383                    .headers()
384                    .iter()
385                    .map(|(name, value)| (name.as_str(), value.to_str().unwrap())),
386                SignableBody::UnsignedPayload,
387            )?,
388            &params.into(),
389        )?;
390        let (instructions, _) = output.into_parts();
391        instructions.apply_to_request_http1x(&mut request);
392        Ok(request)
393    }
394
395    fn reqsign_s3_express_canonical_request(
396        now: Timestamp,
397        presigned: bool,
398        use_standard_session_token: bool,
399    ) -> Result<String> {
400        let credential = s3_express_credential(now);
401        let mut parts = Request::get(S3_EXPRESS_URI).body(())?.into_parts().0;
402        let mut signing_request = SigningRequest::build(&mut parts)?;
403        let expires_in = presigned.then_some(Duration::from_secs(60));
404        if use_standard_session_token {
405            canonicalize_headers_with_standard_session_token(
406                &mut signing_request,
407                &credential,
408                expires_in,
409                now,
410            )?;
411        } else {
412            canonicalize_headers(&mut signing_request, &credential, expires_in, now)?;
413        }
414        let authentication_query = authentication_query(
415            &signing_request,
416            &credential,
417            expires_in,
418            now,
419            "s3express",
420            S3_EXPRESS_REGION,
421            use_standard_session_token,
422        );
423        let canonical_query = canonicalize_query(&signing_request, &authentication_query);
424        canonical_request_string(&signing_request, &canonical_query).map_err(Into::into)
425    }
426
427    #[derive(Debug)]
428    struct SequenceProvider {
429        credentials: Mutex<VecDeque<Credential>>,
430        calls: Arc<AtomicUsize>,
431    }
432
433    impl ProvideCredential for SequenceProvider {
434        type Credential = Credential;
435
436        async fn provide_credential(
437            &self,
438            _ctx: &Context,
439        ) -> reqsign_core::Result<Option<Self::Credential>> {
440            self.calls.fetch_add(1, Ordering::SeqCst);
441            Ok(self.credentials.lock().unwrap().pop_front())
442        }
443    }
444
445    #[test]
446    fn presign_deadline_uses_signing_time_and_requested_expiry() {
447        let now: Timestamp = "2026-07-22T00:00:00Z"
448            .parse()
449            .expect("timestamp must parse");
450        let signer = RequestSigner::new("s3", "test").with_time(now);
451        let credential = Credential::default();
452
453        assert_eq!(
454            signer.required_valid_until(&credential, Some(Duration::from_secs(3600))),
455            now + Duration::from_secs(3600)
456        );
457        assert_eq!(
458            signer.required_valid_until(&credential, None),
459            now + CREDENTIAL_OPERATION_HEADROOM
460        );
461    }
462
463    #[tokio::test]
464    async fn s3_express_header_token_modes_match_aws_sigv4() -> Result<()> {
465        let now: Timestamp = "2026-07-22T00:00:00Z".parse()?;
466        let credential = s3_express_credential(now);
467
468        let expected_session = aws_signed_s3_express_request(
469            Request::get(S3_EXPRESS_URI).body("")?,
470            now,
471            false,
472            Some("x-amz-s3session-token"),
473        )?;
474        let (mut session_parts, body) = Request::get(S3_EXPRESS_URI).body("")?.into_parts();
475        RequestSigner::new("s3express", S3_EXPRESS_REGION)
476            .with_time(now)
477            .sign_request(&Context::new(), &mut session_parts, Some(&credential), None)
478            .await?;
479        let actual_session = Request::from_parts(session_parts, body);
480        compare_request(
481            "S3 Express CreateSession header mode",
482            &expected_session,
483            &actual_session,
484        );
485        assert_eq!(
486            actual_session.headers()["x-amz-s3session-token"],
487            TEST_SESSION_TOKEN
488        );
489        assert!(actual_session.headers()["x-amz-s3session-token"].is_sensitive());
490        assert!(
491            !actual_session
492                .headers()
493                .contains_key("x-amz-security-token")
494        );
495
496        let expected_iam = aws_signed_s3_express_request(
497            Request::get(S3_EXPRESS_URI).body("")?,
498            now,
499            false,
500            None,
501        )?;
502        let (mut iam_parts, body) = Request::get(S3_EXPRESS_URI).body("")?.into_parts();
503        RequestSigner::new("s3express", S3_EXPRESS_REGION)
504            .with_standard_session_token()
505            .with_time(now)
506            .sign_request(&Context::new(), &mut iam_parts, Some(&credential), None)
507            .await?;
508        let actual_iam = Request::from_parts(iam_parts, body);
509        compare_request("S3 Express IAM header mode", &expected_iam, &actual_iam);
510        assert_eq!(
511            actual_iam.headers()["x-amz-security-token"],
512            TEST_SESSION_TOKEN
513        );
514        assert!(actual_iam.headers()["x-amz-security-token"].is_sensitive());
515        assert!(!actual_iam.headers().contains_key("x-amz-s3session-token"));
516
517        let session_authorization = actual_session.headers()[header::AUTHORIZATION].to_str()?;
518        let iam_authorization = actual_iam.headers()[header::AUTHORIZATION].to_str()?;
519        assert!(session_authorization.contains("x-amz-s3session-token"));
520        assert!(iam_authorization.contains("x-amz-security-token"));
521        assert_ne!(session_authorization, iam_authorization);
522        let session_canonical = reqsign_s3_express_canonical_request(now, false, false)?;
523        let iam_canonical = reqsign_s3_express_canonical_request(now, false, true)?;
524        assert_eq!(
525            session_canonical,
526            format!(
527                "GET\n/object\n\nhost:bucket--use2-az1--x-s3.s3express-use2-az1.us-east-2.amazonaws.com\nx-amz-content-sha256:UNSIGNED-PAYLOAD\nx-amz-date:20260722T000000Z\nx-amz-s3session-token:{TEST_SESSION_TOKEN}\n\nhost;x-amz-content-sha256;x-amz-date;x-amz-s3session-token\nUNSIGNED-PAYLOAD"
528            )
529        );
530        assert_eq!(
531            iam_canonical,
532            format!(
533                "GET\n/object\n\nhost:bucket--use2-az1--x-s3.s3express-use2-az1.us-east-2.amazonaws.com\nx-amz-content-sha256:UNSIGNED-PAYLOAD\nx-amz-date:20260722T000000Z\nx-amz-security-token:{TEST_SESSION_TOKEN}\n\nhost;x-amz-content-sha256;x-amz-date;x-amz-security-token\nUNSIGNED-PAYLOAD"
534            )
535        );
536        assert_ne!(session_canonical, iam_canonical);
537        assert_eq!(
538            credential.session_token.as_deref(),
539            Some(TEST_SESSION_TOKEN)
540        );
541
542        Ok(())
543    }
544
545    #[tokio::test]
546    async fn s3_express_query_token_modes_match_aws_sigv4() -> Result<()> {
547        let now: Timestamp = "2026-07-22T00:00:00Z".parse()?;
548        let credential = s3_express_credential(now);
549        let expires_in = Some(Duration::from_secs(60));
550
551        let expected_session = aws_signed_s3_express_request(
552            Request::get(S3_EXPRESS_URI).body("")?,
553            now,
554            true,
555            Some("X-Amz-S3session-Token"),
556        )?;
557        let (mut session_parts, body) = Request::get(S3_EXPRESS_URI).body("")?.into_parts();
558        RequestSigner::new("s3express", S3_EXPRESS_REGION)
559            .with_time(now)
560            .sign_request(
561                &Context::new(),
562                &mut session_parts,
563                Some(&credential),
564                expires_in,
565            )
566            .await?;
567        let actual_session = Request::from_parts(session_parts, body);
568        compare_request(
569            "S3 Express CreateSession query mode",
570            &expected_session,
571            &actual_session,
572        );
573
574        let expected_iam =
575            aws_signed_s3_express_request(Request::get(S3_EXPRESS_URI).body("")?, now, true, None)?;
576        let (mut iam_parts, body) = Request::get(S3_EXPRESS_URI).body("")?.into_parts();
577        RequestSigner::new("s3express", S3_EXPRESS_REGION)
578            .with_standard_session_token()
579            .with_time(now)
580            .sign_request(
581                &Context::new(),
582                &mut iam_parts,
583                Some(&credential),
584                expires_in,
585            )
586            .await?;
587        let actual_iam = Request::from_parts(iam_parts, body);
588        compare_request("S3 Express IAM query mode", &expected_iam, &actual_iam);
589
590        let session_query = actual_session.uri().query().expect("query must exist");
591        let iam_query = actual_iam.uri().query().expect("query must exist");
592        assert!(session_query.contains("X-Amz-S3session-Token="));
593        assert!(!session_query.contains("X-Amz-Security-Token="));
594        assert!(iam_query.contains("X-Amz-Security-Token="));
595        assert!(!iam_query.contains("X-Amz-S3session-Token="));
596        let session_signature = form_urlencoded::parse(session_query.as_bytes())
597            .find(|(name, _)| name == "X-Amz-Signature")
598            .expect("signature must exist")
599            .1;
600        let iam_signature = form_urlencoded::parse(iam_query.as_bytes())
601            .find(|(name, _)| name == "X-Amz-Signature")
602            .expect("signature must exist")
603            .1;
604        assert_ne!(session_signature, iam_signature);
605        let session_canonical = reqsign_s3_express_canonical_request(now, true, false)?;
606        let iam_canonical = reqsign_s3_express_canonical_request(now, true, true)?;
607        assert_eq!(
608            session_canonical,
609            format!(
610                "GET\n/object\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIDEXAMPLE%2F20260722%2Fus-east-2%2Fs3express%2Faws4_request&X-Amz-Date=20260722T000000Z&X-Amz-Expires=60&X-Amz-S3session-Token={TEST_SESSION_TOKEN}&X-Amz-SignedHeaders=host\nhost:bucket--use2-az1--x-s3.s3express-use2-az1.us-east-2.amazonaws.com\n\nhost\nUNSIGNED-PAYLOAD"
611            )
612        );
613        assert_eq!(
614            iam_canonical,
615            format!(
616                "GET\n/object\nX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIDEXAMPLE%2F20260722%2Fus-east-2%2Fs3express%2Faws4_request&X-Amz-Date=20260722T000000Z&X-Amz-Expires=60&X-Amz-Security-Token={TEST_SESSION_TOKEN}&X-Amz-SignedHeaders=host\nhost:bucket--use2-az1--x-s3.s3express-use2-az1.us-east-2.amazonaws.com\n\nhost\nUNSIGNED-PAYLOAD"
617            )
618        );
619        assert_ne!(session_canonical, iam_canonical);
620        assert_eq!(
621            credential.session_token.as_deref(),
622            Some(TEST_SESSION_TOKEN)
623        );
624
625        Ok(())
626    }
627
628    #[tokio::test]
629    async fn session_token_is_redacted_from_debug_and_signing_errors() -> Result<()> {
630        const TOKEN_MARKER: &str = "SENSITIVE_SESSION_TOKEN";
631
632        let now: Timestamp = "2026-07-22T00:00:00Z".parse()?;
633        let credential = Credential {
634            session_token: Some(format!("{TOKEN_MARKER}\n")),
635            ..s3_express_credential(now)
636        };
637        assert!(!format!("{credential:?}").contains(TOKEN_MARKER));
638
639        for use_standard_session_token in [false, true] {
640            let signer = RequestSigner::new("s3express", S3_EXPRESS_REGION);
641            let signer = if use_standard_session_token {
642                signer.with_standard_session_token()
643            } else {
644                signer
645            }
646            .with_time(now);
647            assert!(!format!("{signer:?}").contains(TOKEN_MARKER));
648
649            let mut parts = Request::get(S3_EXPRESS_URI).body(())?.into_parts().0;
650            let original = parts.clone();
651            let error = signer
652                .sign_request(&Context::new(), &mut parts, Some(&credential), None)
653                .await
654                .expect_err("invalid session token must be rejected");
655
656            assert!(!format!("{error}").contains(TOKEN_MARKER));
657            assert!(!format!("{error:?}").contains(TOKEN_MARKER));
658            assert_eq!(parts.uri, original.uri);
659            assert_eq!(parts.headers, original.headers);
660            assert_eq!(
661                credential.session_token.as_deref(),
662                Some("SENSITIVE_SESSION_TOKEN\n")
663            );
664        }
665
666        Ok(())
667    }
668
669    #[tokio::test]
670    async fn presign_refreshes_credential_that_cannot_cover_url_lifetime() -> Result<()> {
671        let now = Timestamp::now();
672        let short_lived = Credential {
673            access_key_id: "short-lived-ak".to_string(),
674            secret_access_key: "short-lived-sk".to_string(),
675            session_token: Some("short-lived-token".to_string()),
676            expires_in: Some(now + Duration::from_secs(600)),
677        };
678        let long_lived = Credential {
679            access_key_id: "long-lived-ak".to_string(),
680            secret_access_key: "long-lived-sk".to_string(),
681            session_token: Some("long-lived-token".to_string()),
682            expires_in: Some(now + Duration::from_secs(7200)),
683        };
684        let calls = Arc::new(AtomicUsize::new(0));
685        let provider = SequenceProvider {
686            credentials: Mutex::new(VecDeque::from([short_lived, long_lived])),
687            calls: calls.clone(),
688        };
689        let signer = Signer::new(
690            Context::new(),
691            provider,
692            RequestSigner::new("s3", "test").with_time(now),
693        );
694
695        let mut header_parts = Request::get("https://example.com/object")
696            .body(())?
697            .into_parts()
698            .0;
699        signer.sign(&mut header_parts, None).await?;
700
701        let mut query_parts = Request::get("https://example.com/object")
702            .body(())?
703            .into_parts()
704            .0;
705        signer
706            .sign(&mut query_parts, Some(Duration::from_secs(3600)))
707            .await?;
708
709        assert_eq!(calls.load(Ordering::SeqCst), 2);
710        assert!(
711            query_parts
712                .uri
713                .query()
714                .expect("presigned query must exist")
715                .contains("X-Amz-Credential=long-lived-ak%2F")
716        );
717        Ok(())
718    }
719
720    #[tokio::test]
721    async fn presign_rejects_refreshed_credential_that_cannot_cover_url_lifetime() -> Result<()> {
722        let now = Timestamp::now();
723        let short_lived = Credential {
724            access_key_id: "short-lived-ak".to_string(),
725            secret_access_key: "short-lived-sk".to_string(),
726            session_token: Some("short-lived-token".to_string()),
727            expires_in: Some(now + Duration::from_secs(600)),
728        };
729        let calls = Arc::new(AtomicUsize::new(0));
730        let provider = SequenceProvider {
731            credentials: Mutex::new(VecDeque::from([short_lived])),
732            calls: calls.clone(),
733        };
734        let signer = Signer::new(
735            Context::new(),
736            provider,
737            RequestSigner::new("s3", "test").with_time(now),
738        );
739        let mut parts = Request::get("https://example.com/object")
740            .body(())?
741            .into_parts()
742            .0;
743        let original = parts.clone();
744
745        let err = signer
746            .sign(&mut parts, Some(Duration::from_secs(3600)))
747            .await
748            .expect_err("credential must cover the entire presigned URL lifetime");
749
750        assert_eq!(err.kind(), ErrorKind::CredentialInvalid);
751        assert_eq!(calls.load(Ordering::SeqCst), 1);
752        assert_eq!(parts.uri, original.uri);
753        assert_eq!(parts.headers, original.headers);
754
755        let mut header_parts = Request::get("https://example.com/object")
756            .body(())?
757            .into_parts()
758            .0;
759        signer.sign(&mut header_parts, None).await?;
760        assert_eq!(calls.load(Ordering::SeqCst), 1);
761        assert!(
762            header_parts
763                .headers
764                .get(header::AUTHORIZATION)
765                .expect("authorization header must exist")
766                .to_str()?
767                .contains("Credential=short-lived-ak/")
768        );
769        Ok(())
770    }
771
772    /// (name, request_builder)
773    type TestCase = (&'static str, fn() -> Request<&'static str>);
774
775    fn test_cases() -> Vec<TestCase> {
776        vec![
777            ("get_request", test_get_request),
778            ("get_request_with_sse", test_get_request_with_sse),
779            ("get_request_with_query", test_get_request_with_query),
780            ("get_request_virtual_host", test_get_request_virtual_host),
781            (
782                "get_request_with_query_virtual_host",
783                test_get_request_with_query_virtual_host,
784            ),
785            ("put_request", test_put_request),
786            (
787                "put_request_with_body_digest",
788                test_put_request_with_body_digest,
789            ),
790            ("put_request_virtual_host", test_put_request_virtual_host),
791        ]
792    }
793
794    fn test_get_request() -> Request<&'static str> {
795        let mut req = Request::new("");
796        *req.method_mut() = http::Method::GET;
797        *req.uri_mut() = "http://127.0.0.1:9000/hello"
798            .parse()
799            .expect("url must be valid");
800
801        req
802    }
803
804    fn test_get_request_with_sse() -> Request<&'static str> {
805        let mut req = Request::new("");
806        *req.method_mut() = http::Method::GET;
807        *req.uri_mut() = "http://127.0.0.1:9000/hello"
808            .parse()
809            .expect("url must be valid");
810        req.headers_mut().insert(
811            "x-amz-server-side-encryption",
812            "a".parse().expect("must be valid"),
813        );
814        req.headers_mut().insert(
815            "x-amz-server-side-encryption-customer-algorithm",
816            "b".parse().expect("must be valid"),
817        );
818        req.headers_mut().insert(
819            "x-amz-server-side-encryption-customer-key",
820            "c".parse().expect("must be valid"),
821        );
822        req.headers_mut().insert(
823            "x-amz-server-side-encryption-customer-key-md5",
824            "d".parse().expect("must be valid"),
825        );
826        req.headers_mut().insert(
827            "x-amz-server-side-encryption-aws-kms-key-id",
828            "e".parse().expect("must be valid"),
829        );
830
831        req
832    }
833
834    fn test_get_request_with_query() -> Request<&'static str> {
835        let mut req = Request::new("");
836        *req.method_mut() = http::Method::GET;
837        *req.uri_mut() = "http://127.0.0.1:9000/hello?list-type=2&max-keys=3&prefix=CI/&start-after=ExampleGuide.pdf"
838            .parse()
839            .expect("url must be valid");
840
841        req
842    }
843
844    fn test_get_request_virtual_host() -> Request<&'static str> {
845        let mut req = Request::new("");
846        *req.method_mut() = http::Method::GET;
847        *req.uri_mut() = "http://hello.s3.test.example.com"
848            .parse()
849            .expect("url must be valid");
850
851        req
852    }
853
854    fn test_get_request_with_query_virtual_host() -> Request<&'static str> {
855        let mut req = Request::new("");
856        *req.method_mut() = http::Method::GET;
857        *req.uri_mut() = "http://hello.s3.test.example.com?list-type=2&max-keys=3&prefix=CI/&start-after=ExampleGuide.pdf"
858            .parse()
859            .expect("url must be valid");
860
861        req
862    }
863
864    fn test_put_request() -> Request<&'static str> {
865        let content = "Hello,World!";
866        let mut req = Request::new(content);
867        *req.method_mut() = http::Method::PUT;
868        *req.uri_mut() = "http://127.0.0.1:9000/hello"
869            .parse()
870            .expect("url must be valid");
871
872        req.headers_mut().insert(
873            header::CONTENT_LENGTH,
874            HeaderValue::from_str(&content.len().to_string()).expect("must be valid"),
875        );
876
877        req
878    }
879
880    fn test_put_request_with_body_digest() -> Request<&'static str> {
881        let content = "Hello,World!";
882        let mut req = Request::new(content);
883        *req.method_mut() = http::Method::PUT;
884        *req.uri_mut() = "http://127.0.0.1:9000/hello"
885            .parse()
886            .expect("url must be valid");
887
888        req.headers_mut().insert(
889            header::CONTENT_LENGTH,
890            HeaderValue::from_str(&content.len().to_string()).expect("must be valid"),
891        );
892
893        let body = hex_sha256(content.as_bytes());
894        req.headers_mut().insert(
895            "x-amz-content-sha256",
896            HeaderValue::from_str(&body).expect("must be valid"),
897        );
898
899        req
900    }
901
902    fn test_put_request_virtual_host() -> Request<&'static str> {
903        let content = "Hello,World!";
904        let mut req = Request::new(content);
905        *req.method_mut() = http::Method::PUT;
906        *req.uri_mut() = "http://hello.s3.test.example.com"
907            .parse()
908            .expect("url must be valid");
909
910        req.headers_mut().insert(
911            header::CONTENT_LENGTH,
912            HeaderValue::from_str(&content.len().to_string()).expect("must be valid"),
913        );
914
915        req
916    }
917
918    #[track_caller]
919    fn compare_request(name: &str, l: &Request<&str>, r: &Request<&str>) {
920        fn format_headers(req: &Request<&str>) -> Vec<String> {
921            let mut hs = req
922                .headers()
923                .iter()
924                .map(|(k, v)| format!("{}:{}", k, v.to_str().expect("must be valid")))
925                .collect::<Vec<_>>();
926
927            // Insert host if original request doesn't have it.
928            if !hs.contains(&format!("host:{}", req.uri().authority().unwrap())) {
929                hs.push(format!("host:{}", req.uri().authority().unwrap()))
930            }
931
932            hs.sort();
933            hs
934        }
935
936        assert_eq!(
937            format_headers(l),
938            format_headers(r),
939            "{name} header mismatch"
940        );
941
942        fn format_query(req: &Request<&str>) -> Vec<String> {
943            let query = req.uri().query().unwrap_or_default();
944            let mut query = form_urlencoded::parse(query.as_bytes())
945                .map(|(k, v)| format!("{k}={v}"))
946                .collect::<Vec<_>>();
947            query.sort();
948            query
949        }
950
951        assert_eq!(format_query(l), format_query(r), "{name} query mismatch");
952    }
953
954    #[tokio::test]
955    async fn canonicalization_preserves_wire_uri() -> Result<()> {
956        let credential = Credential {
957            access_key_id: "access_key_id".to_string(),
958            secret_access_key: "secret_access_key".to_string(),
959            ..Default::default()
960        };
961        let now: Timestamp = "2026-07-22T00:00:00Z".parse()?;
962        let signer = RequestSigner::new("s3", "test").with_time(now);
963        let original_uri = format!("https://example.com/object%2Fname?{RAW_QUERY}");
964
965        let mut canonical_parts = Request::get(&original_uri).body(())?.into_parts().0;
966        let mut signing_req = SigningRequest::build(&mut canonical_parts)?;
967        canonicalize_headers(
968            &mut signing_req,
969            &credential,
970            Some(Duration::from_secs(60)),
971            now,
972        )?;
973        let auth_query = authentication_query(
974            &signing_req,
975            &credential,
976            Some(Duration::from_secs(60)),
977            now,
978            "s3",
979            "test",
980            false,
981        );
982        let canonical_query = canonicalize_query(&signing_req, &auth_query);
983
984        assert_eq!(canonical_uri(&signing_req.path)?, "/object%2Fname");
985        assert!(canonical_query.contains(&("literal-plus".to_string(), "%2B".to_string())));
986        assert!(canonical_query.contains(&("double".to_string(), "%252F".to_string())));
987        assert!(canonical_query.contains(&(String::new(), "empty-key".to_string())));
988        assert!(canonical_query.contains(&("flag".to_string(), String::new())));
989
990        let mut header_parts = Request::get(&original_uri)
991            .header("x-custom", " value ")
992            .body(())?
993            .into_parts()
994            .0;
995        signer
996            .sign_request(&Context::new(), &mut header_parts, Some(&credential), None)
997            .await?;
998        assert_eq!(header_parts.uri.to_string(), original_uri);
999        assert_eq!(header_parts.headers["x-custom"], " value ");
1000
1001        let mut query_parts = Request::get(&original_uri).body(())?.into_parts().0;
1002        signer
1003            .sign_request(
1004                &Context::new(),
1005                &mut query_parts,
1006                Some(&credential),
1007                Some(Duration::from_secs(60)),
1008            )
1009            .await?;
1010        assert!(
1011            query_parts
1012                .uri
1013                .to_string()
1014                .starts_with(&format!("{original_uri}X-Amz-Algorithm="))
1015        );
1016        assert!(
1017            query_parts
1018                .uri
1019                .query()
1020                .unwrap()
1021                .contains("X-Amz-Signature=")
1022        );
1023
1024        Ok(())
1025    }
1026
1027    #[tokio::test]
1028    async fn test() -> Result<()> {
1029        for (name, req) in test_cases() {
1030            calculate(req)
1031                .await
1032                .unwrap_or_else(|err| panic!("calculate {name} should pass: {err:?}"));
1033            calculate_in_query(req)
1034                .await
1035                .unwrap_or_else(|err| panic!("calculate_in_query {name} should pass: {err:?}"));
1036            test_calculate_with_token(req).await.unwrap_or_else(|err| {
1037                panic!("test_calculate_with_token {name} should pass: {err:?}")
1038            });
1039            test_calculate_with_token_in_query(req)
1040                .await
1041                .unwrap_or_else(|err| {
1042                    panic!("test_calculate_with_token_in_query {name} should pass: {err:?}")
1043                });
1044        }
1045        Ok(())
1046    }
1047
1048    async fn calculate(req_fn: fn() -> Request<&'static str>) -> Result<()> {
1049        let _ = env_logger::builder().is_test(true).try_init();
1050
1051        let mut req = req_fn();
1052        let name = format!(
1053            "{} {} {:?}",
1054            req.method(),
1055            req.uri().path(),
1056            req.uri().query(),
1057        );
1058        let now = Timestamp::now();
1059
1060        let mut ss = SigningSettings::default();
1061        ss.percent_encoding_mode = PercentEncodingMode::Double;
1062        ss.payload_checksum_kind = PayloadChecksumKind::XAmzSha256;
1063        let id = Credentials::new(
1064            "access_key_id",
1065            "secret_access_key",
1066            None,
1067            None,
1068            "hardcoded-credentials",
1069        )
1070        .into();
1071        let sp = v4::SigningParams::builder()
1072            .identity(&id)
1073            .region("test")
1074            .name("s3")
1075            .time(now.as_system_time())
1076            .settings(ss)
1077            .build()
1078            .expect("signing params must be valid");
1079
1080        let mut body = SignableBody::UnsignedPayload;
1081        if req.headers().get(X_AMZ_CONTENT_SHA_256).is_some() {
1082            body = SignableBody::Bytes(req.body().as_bytes());
1083        }
1084
1085        let output = aws_sigv4::http_request::sign(
1086            SignableRequest::new(
1087                req.method().as_str(),
1088                req.uri().to_string(),
1089                req.headers()
1090                    .iter()
1091                    .map(|(k, v)| (k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap())),
1092                body,
1093            )
1094            .unwrap(),
1095            &sp.into(),
1096        )?;
1097        let (aws_sig, _) = output.into_parts();
1098        aws_sig.apply_to_request_http1x(&mut req);
1099        let expected_req = req;
1100
1101        let req = req_fn();
1102        let (mut parts, body) = req.into_parts();
1103
1104        let ctx = Context::new()
1105            .with_file_read(TokioFileRead)
1106            .with_http_send(ReqwestHttpSend::default());
1107        let loader = StaticCredentialProvider::new("access_key_id", "secret_access_key");
1108        let cred = loader.provide_credential(&ctx).await?.unwrap();
1109
1110        let builder = RequestSigner::new("s3", "test").with_time(now);
1111        builder
1112            .sign_request(&ctx, &mut parts, Some(&cred), None)
1113            .await
1114            .expect("must apply success");
1115
1116        let actual_req = Request::from_parts(parts, body);
1117
1118        compare_request(&name, &expected_req, &actual_req);
1119
1120        Ok(())
1121    }
1122
1123    async fn calculate_in_query(req_fn: fn() -> Request<&'static str>) -> Result<()> {
1124        let _ = env_logger::builder().is_test(true).try_init();
1125
1126        let mut req = req_fn();
1127        let name = format!(
1128            "{} {} {:?}",
1129            req.method(),
1130            req.uri().path(),
1131            req.uri().query(),
1132        );
1133        let now = Timestamp::now();
1134
1135        let mut ss = SigningSettings::default();
1136        ss.percent_encoding_mode = PercentEncodingMode::Double;
1137        ss.payload_checksum_kind = PayloadChecksumKind::XAmzSha256;
1138        ss.signature_location = SignatureLocation::QueryParams;
1139        ss.expires_in = Some(Duration::from_secs(3600));
1140        let id = Credentials::new(
1141            "access_key_id",
1142            "secret_access_key",
1143            None,
1144            None,
1145            "hardcoded-credentials",
1146        )
1147        .into();
1148        let sp = v4::SigningParams::builder()
1149            .identity(&id)
1150            .region("test")
1151            .name("s3")
1152            .time(now.as_system_time())
1153            .settings(ss)
1154            .build()
1155            .expect("signing params must be valid");
1156
1157        let mut body = SignableBody::UnsignedPayload;
1158        if req.headers().get(X_AMZ_CONTENT_SHA_256).is_some() {
1159            body = SignableBody::Bytes(req.body().as_bytes());
1160        }
1161
1162        let output = aws_sigv4::http_request::sign(
1163            SignableRequest::new(
1164                req.method().as_str(),
1165                req.uri().to_string(),
1166                req.headers()
1167                    .iter()
1168                    .map(|(k, v)| (k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap())),
1169                body,
1170            )
1171            .unwrap(),
1172            &sp.into(),
1173        )
1174        .expect("signing must succeed");
1175        let (aws_sig, _) = output.into_parts();
1176        aws_sig.apply_to_request_http1x(&mut req);
1177        let expected_req = req;
1178
1179        let req = req_fn();
1180        let (mut parts, body) = req.into_parts();
1181
1182        let ctx = Context::new()
1183            .with_file_read(TokioFileRead)
1184            .with_http_send(ReqwestHttpSend::default());
1185        let loader = StaticCredentialProvider::new("access_key_id", "secret_access_key");
1186        let cred = loader.provide_credential(&ctx).await?.unwrap();
1187
1188        let builder = RequestSigner::new("s3", "test").with_time(now);
1189
1190        builder
1191            .sign_request(
1192                &ctx,
1193                &mut parts,
1194                Some(&cred),
1195                Some(Duration::from_secs(3600)),
1196            )
1197            .await?;
1198        let actual_req = Request::from_parts(parts, body);
1199
1200        compare_request(&name, &expected_req, &actual_req);
1201
1202        Ok(())
1203    }
1204
1205    async fn test_calculate_with_token(req_fn: fn() -> Request<&'static str>) -> Result<()> {
1206        let _ = env_logger::builder().is_test(true).try_init();
1207
1208        let mut req = req_fn();
1209        let name = format!(
1210            "{} {} {:?}",
1211            req.method(),
1212            req.uri().path(),
1213            req.uri().query(),
1214        );
1215        let now = Timestamp::now();
1216
1217        let mut ss = SigningSettings::default();
1218        ss.percent_encoding_mode = PercentEncodingMode::Double;
1219        ss.payload_checksum_kind = PayloadChecksumKind::XAmzSha256;
1220        let id = Credentials::new(
1221            "access_key_id",
1222            "secret_access_key",
1223            Some("security_token".to_string()),
1224            None,
1225            "hardcoded-credentials",
1226        )
1227        .into();
1228        let sp = v4::SigningParams::builder()
1229            .identity(&id)
1230            .region("test")
1231            .name("s3")
1232            .time(now.as_system_time())
1233            .settings(ss)
1234            .build()
1235            .expect("signing params must be valid");
1236
1237        let mut body = SignableBody::UnsignedPayload;
1238        if req.headers().get(X_AMZ_CONTENT_SHA_256).is_some() {
1239            body = SignableBody::Bytes(req.body().as_bytes());
1240        }
1241
1242        let output = aws_sigv4::http_request::sign(
1243            SignableRequest::new(
1244                req.method().as_str(),
1245                req.uri().to_string(),
1246                req.headers()
1247                    .iter()
1248                    .map(|(k, v)| (k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap())),
1249                body,
1250            )
1251            .unwrap(),
1252            &sp.into(),
1253        )
1254        .expect("signing must succeed");
1255        let (aws_sig, _) = output.into_parts();
1256        aws_sig.apply_to_request_http1x(&mut req);
1257        let expected_req = req;
1258
1259        let req = req_fn();
1260        let (mut parts, body) = req.into_parts();
1261
1262        let ctx = Context::new()
1263            .with_file_read(TokioFileRead)
1264            .with_http_send(ReqwestHttpSend::default());
1265        let loader = StaticCredentialProvider::new("access_key_id", "secret_access_key")
1266            .with_session_token("security_token");
1267        let cred = loader.provide_credential(&ctx).await?.unwrap();
1268
1269        let builder = RequestSigner::new("s3", "test").with_time(now);
1270        builder
1271            .sign_request(&ctx, &mut parts, Some(&cred), None)
1272            .await
1273            .expect("must apply success");
1274        let actual_req = Request::from_parts(parts, body);
1275
1276        compare_request(&name, &expected_req, &actual_req);
1277
1278        Ok(())
1279    }
1280
1281    async fn test_calculate_with_token_in_query(
1282        req_fn: fn() -> Request<&'static str>,
1283    ) -> Result<()> {
1284        let _ = env_logger::builder().is_test(true).try_init();
1285
1286        let mut req = req_fn();
1287        let name = format!(
1288            "{} {} {:?}",
1289            req.method(),
1290            req.uri().path(),
1291            req.uri().query(),
1292        );
1293        let now = Timestamp::now();
1294
1295        let mut ss = SigningSettings::default();
1296        ss.percent_encoding_mode = PercentEncodingMode::Double;
1297        ss.payload_checksum_kind = PayloadChecksumKind::XAmzSha256;
1298        ss.signature_location = SignatureLocation::QueryParams;
1299        ss.expires_in = Some(Duration::from_secs(3600));
1300        let id = Credentials::new(
1301            "access_key_id",
1302            "secret_access_key",
1303            Some("security_token".to_string()),
1304            None,
1305            "hardcoded-credentials",
1306        )
1307        .into();
1308        let sp = v4::SigningParams::builder()
1309            .identity(&id)
1310            .region("test")
1311            // .security_token("security_token")
1312            .name("s3")
1313            .time(now.as_system_time())
1314            .settings(ss)
1315            .build()
1316            .expect("signing params must be valid");
1317
1318        let mut body = SignableBody::UnsignedPayload;
1319        if req.headers().get(X_AMZ_CONTENT_SHA_256).is_some() {
1320            body = SignableBody::Bytes(req.body().as_bytes());
1321        }
1322
1323        let output = aws_sigv4::http_request::sign(
1324            SignableRequest::new(
1325                req.method().as_str(),
1326                req.uri().to_string(),
1327                req.headers()
1328                    .iter()
1329                    .map(|(k, v)| (k.as_str(), std::str::from_utf8(v.as_bytes()).unwrap())),
1330                body,
1331            )
1332            .unwrap(),
1333            &sp.into(),
1334        )
1335        .expect("signing must succeed");
1336        let (aws_sig, _) = output.into_parts();
1337        aws_sig.apply_to_request_http1x(&mut req);
1338        let expected_req = req;
1339
1340        let req = req_fn();
1341        let (mut parts, body) = req.into_parts();
1342
1343        let ctx = Context::new()
1344            .with_file_read(TokioFileRead)
1345            .with_http_send(ReqwestHttpSend::default());
1346        let loader = StaticCredentialProvider::new("access_key_id", "secret_access_key")
1347            .with_session_token("security_token");
1348        let cred = loader.provide_credential(&ctx).await?.unwrap();
1349
1350        let builder = RequestSigner::new("s3", "test").with_time(now);
1351        builder
1352            .sign_request(
1353                &ctx,
1354                &mut parts,
1355                Some(&cred),
1356                Some(Duration::from_secs(3600)),
1357            )
1358            .await
1359            .expect("must apply success");
1360        let actual_req = Request::from_parts(parts, body);
1361
1362        compare_request(&name, &expected_req, &actual_req);
1363
1364        Ok(())
1365    }
1366}