aws_sigv4/http_request/
sign.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use super::error::SigningError;
7use super::{PayloadChecksumKind, SignatureLocation};
8use crate::http_request::canonical_request::header;
9use crate::http_request::canonical_request::param;
10use crate::http_request::canonical_request::{CanonicalRequest, StringToSign};
11use crate::http_request::error::CanonicalRequestError;
12use crate::http_request::SigningParams;
13use crate::sign::v4;
14#[cfg(feature = "sigv4a")]
15use crate::sign::v4a;
16use crate::{SignatureVersion, SigningOutput};
17use http0::Uri;
18use std::borrow::Cow;
19use std::fmt::{Debug, Formatter};
20use std::str;
21
22const LOG_SIGNABLE_BODY: &str = "LOG_SIGNABLE_BODY";
23
24/// Represents all of the information necessary to sign an HTTP request.
25#[derive(Debug)]
26#[non_exhaustive]
27pub struct SignableRequest<'a> {
28    method: &'a str,
29    uri: Uri,
30    headers: Vec<(&'a str, &'a str)>,
31    body: SignableBody<'a>,
32}
33
34impl<'a> SignableRequest<'a> {
35    /// Creates a new `SignableRequest`.
36    ///
37    /// NOTE: The `uri` is expected to already in encoded form.
38    pub fn new(
39        method: &'a str,
40        uri: impl Into<Cow<'a, str>>,
41        headers: impl Iterator<Item = (&'a str, &'a str)>,
42        body: SignableBody<'a>,
43    ) -> Result<Self, SigningError> {
44        let uri = uri
45            .into()
46            .parse()
47            .map_err(|e| SigningError::from(CanonicalRequestError::from(e)))?;
48        let headers = headers.collect();
49        Ok(Self {
50            method,
51            uri,
52            headers,
53            body,
54        })
55    }
56
57    /// Returns the signable URI
58    pub(crate) fn uri(&self) -> &Uri {
59        &self.uri
60    }
61
62    /// Returns the signable HTTP method
63    pub(crate) fn method(&self) -> &str {
64        self.method
65    }
66
67    /// Returns the request headers
68    pub(crate) fn headers(&self) -> &[(&str, &str)] {
69        self.headers.as_slice()
70    }
71
72    /// Returns the signable body
73    pub fn body(&self) -> &SignableBody<'_> {
74        &self.body
75    }
76}
77
78/// A signable HTTP request body
79#[derive(Clone, Eq, PartialEq)]
80#[non_exhaustive]
81pub enum SignableBody<'a> {
82    /// A body composed of a slice of bytes
83    Bytes(&'a [u8]),
84
85    /// An unsigned payload
86    ///
87    /// UnsignedPayload is used for streaming requests where the contents of the body cannot be
88    /// known prior to signing
89    UnsignedPayload,
90
91    /// A precomputed body checksum. The checksum should be a SHA256 checksum of the body,
92    /// lowercase hex encoded. Eg:
93    /// `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
94    Precomputed(String),
95
96    /// Set when a streaming body has checksum trailers.
97    StreamingUnsignedPayloadTrailer,
98}
99
100/// Formats the value using the given formatter. To print the body data, set the environment variable `LOG_SIGNABLE_BODY=true`.
101impl Debug for SignableBody<'_> {
102    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
103        let should_log_signable_body = std::env::var(LOG_SIGNABLE_BODY)
104            .map(|v| v.eq_ignore_ascii_case("true"))
105            .unwrap_or_default();
106        match self {
107            Self::Bytes(arg0) => {
108                if should_log_signable_body {
109                    f.debug_tuple("Bytes").field(arg0).finish()
110                } else {
111                    let redacted = format!("** REDACTED **. To print {body_size} bytes of raw data, set environment variable `LOG_SIGNABLE_BODY=true`", body_size = arg0.len());
112                    f.debug_tuple("Bytes").field(&redacted).finish()
113                }
114            }
115            Self::UnsignedPayload => write!(f, "UnsignedPayload"),
116            Self::Precomputed(arg0) => f.debug_tuple("Precomputed").field(arg0).finish(),
117            Self::StreamingUnsignedPayloadTrailer => {
118                write!(f, "StreamingUnsignedPayloadTrailer")
119            }
120        }
121    }
122}
123
124impl SignableBody<'_> {
125    /// Create a new empty signable body
126    pub fn empty() -> SignableBody<'static> {
127        SignableBody::Bytes(&[])
128    }
129}
130
131/// Instructions for applying a signature to an HTTP request.
132#[derive(Debug)]
133pub struct SigningInstructions {
134    headers: Vec<Header>,
135    params: Vec<(&'static str, Cow<'static, str>)>,
136}
137
138/// Header representation for use in [`SigningInstructions`]
139pub struct Header {
140    key: &'static str,
141    value: String,
142    sensitive: bool,
143}
144
145impl Debug for Header {
146    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
147        let mut fmt = f.debug_struct("Header");
148        fmt.field("key", &self.key);
149        let value = if self.sensitive {
150            "** REDACTED **"
151        } else {
152            &self.value
153        };
154        fmt.field("value", &value);
155        fmt.finish()
156    }
157}
158
159impl Header {
160    /// The name of this header
161    pub fn name(&self) -> &'static str {
162        self.key
163    }
164
165    /// The value of this header
166    pub fn value(&self) -> &str {
167        &self.value
168    }
169
170    /// Whether this header has a sensitive value
171    pub fn sensitive(&self) -> bool {
172        self.sensitive
173    }
174}
175
176impl SigningInstructions {
177    fn new(headers: Vec<Header>, params: Vec<(&'static str, Cow<'static, str>)>) -> Self {
178        Self { headers, params }
179    }
180
181    /// Returns the headers and query params that should be applied to this request
182    pub fn into_parts(self) -> (Vec<Header>, Vec<(&'static str, Cow<'static, str>)>) {
183        (self.headers, self.params)
184    }
185
186    /// Returns a reference to the headers that should be added to the request.
187    pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
188        self.headers
189            .iter()
190            .map(|header| (header.key, header.value.as_str()))
191    }
192
193    /// Returns a reference to the query parameters that should be added to the request.
194    pub fn params(&self) -> &[(&str, Cow<'static, str>)] {
195        self.params.as_slice()
196    }
197
198    #[cfg(any(feature = "http0-compat", test))]
199    /// Applies the instructions to the given `request`.
200    pub fn apply_to_request_http0x<B>(self, request: &mut http0::Request<B>) {
201        let (new_headers, new_query) = self.into_parts();
202        for header in new_headers.into_iter() {
203            let mut value = http0::HeaderValue::from_str(&header.value).unwrap();
204            value.set_sensitive(header.sensitive);
205            request.headers_mut().insert(header.key, value);
206        }
207
208        if !new_query.is_empty() {
209            let mut query = aws_smithy_http::query_writer::QueryWriter::new(request.uri());
210            for (name, value) in new_query {
211                query.insert(name, &value);
212            }
213            *request.uri_mut() = query.build_uri();
214        }
215    }
216
217    #[cfg(any(feature = "http1", test))]
218    /// Applies the instructions to the given `request`.
219    pub fn apply_to_request_http1x<B>(self, request: &mut http::Request<B>) {
220        // TODO(https://github.com/smithy-lang/smithy-rs/issues/3367): Update query writer to reduce
221        // allocations
222        let (new_headers, new_query) = self.into_parts();
223        for header in new_headers.into_iter() {
224            let mut value = http::HeaderValue::from_str(&header.value).unwrap();
225            value.set_sensitive(header.sensitive);
226            request.headers_mut().insert(header.key, value);
227        }
228
229        if !new_query.is_empty() {
230            let mut query = aws_smithy_http::query_writer::QueryWriter::new_from_string(
231                &request.uri().to_string(),
232            )
233            .expect("unreachable: URI is valid");
234            for (name, value) in new_query {
235                query.insert(name, &value);
236            }
237            *request.uri_mut() = query
238                .build_uri()
239                .to_string()
240                .parse()
241                .expect("unreachable: URI is valid");
242        }
243    }
244}
245
246/// Produces a signature for the given `request` and returns instructions
247/// that can be used to apply that signature to an HTTP request.
248pub fn sign<'a>(
249    request: SignableRequest<'a>,
250    params: &'a SigningParams<'a>,
251) -> Result<SigningOutput<SigningInstructions>, SigningError> {
252    tracing::trace!(request = ?request, params = ?params, "signing request");
253    match params.settings().signature_location {
254        SignatureLocation::Headers => {
255            let (signing_headers, signature) =
256                calculate_signing_headers(&request, params)?.into_parts();
257            Ok(SigningOutput::new(
258                SigningInstructions::new(signing_headers, vec![]),
259                signature,
260            ))
261        }
262        SignatureLocation::QueryParams => {
263            let (params, signature) = calculate_signing_params(&request, params)?;
264            Ok(SigningOutput::new(
265                SigningInstructions::new(vec![], params),
266                signature,
267            ))
268        }
269    }
270}
271
272type CalculatedParams = Vec<(&'static str, Cow<'static, str>)>;
273
274fn calculate_signing_params<'a>(
275    request: &'a SignableRequest<'a>,
276    params: &'a SigningParams<'a>,
277) -> Result<(CalculatedParams, String), SigningError> {
278    let creds = params.credentials()?;
279    let creq = CanonicalRequest::from(request, params)?;
280    let encoded_creq = &v4::sha256_hex_string(creq.to_string().as_bytes());
281
282    let (signature, string_to_sign) = match params {
283        SigningParams::V4(params) => {
284            let string_to_sign =
285                StringToSign::new_v4(params.time, params.region, params.name, encoded_creq)
286                    .to_string();
287            let signing_key = v4::generate_signing_key(
288                creds.secret_access_key(),
289                params.time,
290                params.region,
291                params.name,
292            );
293            let signature = v4::calculate_signature(signing_key, string_to_sign.as_bytes());
294            (signature, string_to_sign)
295        }
296        #[cfg(feature = "sigv4a")]
297        SigningParams::V4a(params) => {
298            let string_to_sign =
299                StringToSign::new_v4a(params.time, params.region_set, params.name, encoded_creq)
300                    .to_string();
301
302            let secret_key =
303                v4a::generate_signing_key(creds.access_key_id(), creds.secret_access_key());
304            let signature = v4a::calculate_signature(&secret_key, string_to_sign.as_bytes());
305            (signature, string_to_sign)
306        }
307    };
308    tracing::trace!(canonical_request = %creq, string_to_sign = %string_to_sign, "calculated signing parameters");
309
310    let values = creq.values.into_query_params().expect("signing with query");
311    let mut signing_params = vec![
312        (param::X_AMZ_ALGORITHM, Cow::Borrowed(values.algorithm)),
313        (param::X_AMZ_CREDENTIAL, Cow::Owned(values.credential)),
314        (param::X_AMZ_DATE, Cow::Owned(values.date_time)),
315        (param::X_AMZ_EXPIRES, Cow::Owned(values.expires)),
316        (
317            param::X_AMZ_SIGNED_HEADERS,
318            Cow::Owned(values.signed_headers.as_str().into()),
319        ),
320        (param::X_AMZ_SIGNATURE, Cow::Owned(signature.clone())),
321    ];
322
323    #[cfg(feature = "sigv4a")]
324    if let Some(region_set) = params.region_set() {
325        if params.signature_version() == SignatureVersion::V4a {
326            signing_params.push((
327                crate::http_request::canonical_request::sigv4a::param::X_AMZ_REGION_SET,
328                Cow::Owned(region_set.to_owned()),
329            ));
330        }
331    }
332
333    if let Some(security_token) = creds.session_token() {
334        signing_params.push((
335            params
336                .settings()
337                .session_token_name_override
338                .unwrap_or(param::X_AMZ_SECURITY_TOKEN),
339            Cow::Owned(security_token.to_string()),
340        ));
341    }
342
343    Ok((signing_params, signature))
344}
345
346/// Calculates the signature headers that need to get added to the given `request`.
347///
348/// `request` MUST NOT contain any of the following headers:
349/// - x-amz-date
350/// - x-amz-content-sha-256
351/// - x-amz-security-token
352fn calculate_signing_headers<'a>(
353    request: &'a SignableRequest<'a>,
354    params: &'a SigningParams<'a>,
355) -> Result<SigningOutput<Vec<Header>>, SigningError> {
356    let creds = params.credentials()?;
357
358    // Step 1: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-create-canonical-request.html.
359    let creq = CanonicalRequest::from(request, params)?;
360    // Step 2: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-create-string-to-sign.html.
361    let encoded_creq = v4::sha256_hex_string(creq.to_string().as_bytes());
362    tracing::trace!(canonical_request = %creq);
363    let mut headers = vec![];
364
365    let signature = match params {
366        SigningParams::V4(params) => {
367            let sts = StringToSign::new_v4(
368                params.time,
369                params.region,
370                params.name,
371                encoded_creq.as_str(),
372            );
373
374            // Step 3: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-calculate-signature.html
375            let signing_key = v4::generate_signing_key(
376                creds.secret_access_key(),
377                params.time,
378                params.region,
379                params.name,
380            );
381            let signature = v4::calculate_signature(signing_key, sts.to_string().as_bytes());
382
383            // Step 4: https://docs.aws.amazon.com/en_pv/general/latest/gr/sigv4-add-signature-to-request.html
384            let values = creq.values.as_headers().expect("signing with headers");
385            add_header(&mut headers, header::X_AMZ_DATE, &values.date_time, false);
386            headers.push(Header {
387                key: "authorization",
388                value: build_authorization_header(
389                    creds.access_key_id(),
390                    &creq,
391                    sts,
392                    &signature,
393                    SignatureVersion::V4,
394                ),
395                sensitive: false,
396            });
397            if params.settings.payload_checksum_kind == PayloadChecksumKind::XAmzSha256 {
398                add_header(
399                    &mut headers,
400                    header::X_AMZ_CONTENT_SHA_256,
401                    &values.content_sha256,
402                    false,
403                );
404            }
405
406            if let Some(security_token) = creds.session_token() {
407                add_header(
408                    &mut headers,
409                    params
410                        .settings
411                        .session_token_name_override
412                        .unwrap_or(header::X_AMZ_SECURITY_TOKEN),
413                    security_token,
414                    true,
415                );
416            }
417            signature
418        }
419        #[cfg(feature = "sigv4a")]
420        SigningParams::V4a(params) => {
421            let sts = StringToSign::new_v4a(
422                params.time,
423                params.region_set,
424                params.name,
425                encoded_creq.as_str(),
426            );
427
428            let signing_key =
429                v4a::generate_signing_key(creds.access_key_id(), creds.secret_access_key());
430            let signature = v4a::calculate_signature(&signing_key, sts.to_string().as_bytes());
431
432            let values = creq.values.as_headers().expect("signing with headers");
433            add_header(&mut headers, header::X_AMZ_DATE, &values.date_time, false);
434            add_header(
435                &mut headers,
436                crate::http_request::canonical_request::sigv4a::header::X_AMZ_REGION_SET,
437                params.region_set,
438                false,
439            );
440
441            headers.push(Header {
442                key: "authorization",
443                value: build_authorization_header(
444                    creds.access_key_id(),
445                    &creq,
446                    sts,
447                    &signature,
448                    SignatureVersion::V4a,
449                ),
450                sensitive: false,
451            });
452            if params.settings.payload_checksum_kind == PayloadChecksumKind::XAmzSha256 {
453                add_header(
454                    &mut headers,
455                    header::X_AMZ_CONTENT_SHA_256,
456                    &values.content_sha256,
457                    false,
458                );
459            }
460
461            if let Some(security_token) = creds.session_token() {
462                add_header(
463                    &mut headers,
464                    header::X_AMZ_SECURITY_TOKEN,
465                    security_token,
466                    true,
467                );
468            }
469            signature
470        }
471    };
472
473    Ok(SigningOutput::new(headers, signature))
474}
475
476fn add_header(map: &mut Vec<Header>, key: &'static str, value: &str, sensitive: bool) {
477    map.push(Header {
478        key,
479        value: value.to_string(),
480        sensitive,
481    });
482}
483
484// add signature to authorization header
485// Authorization: algorithm Credential=access key ID/credential scope, SignedHeaders=SignedHeaders, Signature=signature
486fn build_authorization_header(
487    access_key: &str,
488    creq: &CanonicalRequest<'_>,
489    sts: StringToSign<'_>,
490    signature: &str,
491    signature_version: SignatureVersion,
492) -> String {
493    let scope = match signature_version {
494        SignatureVersion::V4 => sts.scope.to_string(),
495        SignatureVersion::V4a => sts.scope.v4a_display(),
496    };
497    format!(
498        "{} Credential={}/{}, SignedHeaders={}, Signature={}",
499        sts.algorithm,
500        access_key,
501        scope,
502        creq.values.signed_headers().as_str(),
503        signature
504    )
505}
506#[cfg(test)]
507mod tests {
508    use crate::date_time::test_parsers::parse_date_time;
509    use crate::http_request::sign::{add_header, SignableRequest};
510    use crate::http_request::test::SigningSuiteTest;
511    use crate::http_request::{
512        sign, SessionTokenMode, SignableBody, SignatureLocation, SigningInstructions,
513        SigningSettings,
514    };
515    use crate::sign::v4;
516    use aws_credential_types::Credentials;
517    use http0::{HeaderValue, Request};
518    use pretty_assertions::assert_eq;
519    use proptest::proptest;
520    use std::borrow::Cow;
521    use std::iter;
522
523    macro_rules! assert_req_eq {
524        (http: $expected:expr, $actual:expr) => {
525            let mut expected = ($expected).map(|_b|"body");
526            let mut actual = ($actual).map(|_b|"body");
527            make_headers_comparable(&mut expected);
528            make_headers_comparable(&mut actual);
529            assert_eq!(format!("{:?}", expected), format!("{:?}", actual));
530        };
531        ($expected:tt, $actual:tt) => {
532            assert_req_eq!(http: ($expected).as_http_request(), $actual);
533        };
534    }
535
536    pub(crate) fn make_headers_comparable<B>(request: &mut Request<B>) {
537        for (_name, value) in request.headers_mut() {
538            value.set_sensitive(false);
539        }
540    }
541
542    // Sigv4A suite tests
543    #[cfg(feature = "sigv4a")]
544    mod v4a_suite {
545        use crate::http_request::test::v4a::run_test_suite_v4a;
546
547        #[test]
548        fn test_get_header_key_duplicate() {
549            run_test_suite_v4a("get-header-key-duplicate")
550        }
551
552        #[test]
553        #[ignore = "httpparse doesn't support parsing multiline headers since they are deprecated in RFC7230"]
554        fn test_get_header_value_multiline() {
555            run_test_suite_v4a("get-header-value-multiline")
556        }
557
558        #[test]
559        fn test_get_header_value_order() {
560            run_test_suite_v4a("get-header-value-order")
561        }
562
563        #[test]
564        fn test_get_header_value_trim() {
565            run_test_suite_v4a("get-header-value-trim");
566        }
567
568        #[test]
569        fn test_get_relative_normalized() {
570            run_test_suite_v4a("get-relative-normalized");
571        }
572
573        #[test]
574        fn test_get_relative_relative_normalized() {
575            run_test_suite_v4a("get-relative-relative-normalized");
576        }
577
578        #[test]
579        fn test_get_relative_relative_unnormalized() {
580            run_test_suite_v4a("get-relative-relative-unnormalized");
581        }
582
583        #[test]
584        fn test_get_relative_unnormalized() {
585            run_test_suite_v4a("get-relative-unnormalized");
586        }
587
588        #[test]
589        fn test_get_slash_dot_slash_normalized() {
590            run_test_suite_v4a("get-slash-dot-slash-normalized");
591        }
592
593        #[test]
594        fn test_get_slash_dot_slash_unnormalized() {
595            run_test_suite_v4a("get-slash-dot-slash-unnormalized");
596        }
597
598        #[test]
599        fn test_get_slash_normalized() {
600            run_test_suite_v4a("get-slash-normalized");
601        }
602
603        #[test]
604        fn test_get_slash_pointless_dot_normalized() {
605            run_test_suite_v4a("get-slash-pointless-dot-normalized");
606        }
607
608        #[test]
609        fn test_get_slash_pointless_dot_unnormalized() {
610            run_test_suite_v4a("get-slash-pointless-dot-unnormalized");
611        }
612
613        #[test]
614        fn test_get_slash_unnormalized() {
615            run_test_suite_v4a("get-slash-unnormalized");
616        }
617
618        #[test]
619        fn test_get_slashes_normalized() {
620            run_test_suite_v4a("get-slashes-normalized");
621        }
622
623        #[test]
624        fn test_get_slashes_unnormalized() {
625            run_test_suite_v4a("get-slashes-unnormalized");
626        }
627
628        #[test]
629        #[ignore = "relies on single encode of path segments"]
630        // rely on single encoding of path segments, i.e. string-to-sign contains %20 for spaces rather than %25%20 as it should.
631        // skipped until we add control over double_uri_encode in context.json
632        fn test_get_space_normalized() {
633            run_test_suite_v4a("get-space-normalized");
634        }
635
636        #[test]
637        #[ignore = "httpparse fails on unencoded spaces in path"]
638        // the input request has unencoded space ' ' in the path which fails to parse
639        fn test_get_space_unnormalized() {
640            run_test_suite_v4a("get-space-unnormalized");
641        }
642
643        #[test]
644        fn test_get_unreserved() {
645            run_test_suite_v4a("get-unreserved");
646        }
647
648        #[test]
649        #[ignore = "httparse fails on invalid uri character"]
650        // relies on /ሴ canonicalized as /%E1%88%B4 when it should be /%25%E1%25%88%25%B4
651        fn test_get_utf8() {
652            run_test_suite_v4a("get-utf8");
653        }
654
655        #[test]
656        fn test_get_vanilla() {
657            run_test_suite_v4a("get-vanilla");
658        }
659
660        #[test]
661        fn test_get_vanilla_empty_query_key() {
662            run_test_suite_v4a("get-vanilla-empty-query-key");
663        }
664
665        #[test]
666        fn test_get_vanilla_query() {
667            run_test_suite_v4a("get-vanilla-query");
668        }
669
670        #[test]
671        fn test_get_vanilla_query_order_encoded() {
672            run_test_suite_v4a("get-vanilla-query-order-encoded");
673        }
674
675        #[test]
676        fn test_get_vanilla_query_order_key_case() {
677            run_test_suite_v4a("get-vanilla-query-order-key-case");
678        }
679
680        #[test]
681        fn test_get_vanilla_query_unreserved() {
682            run_test_suite_v4a("get-vanilla-query-unreserved");
683        }
684
685        #[test]
686        #[ignore = "httparse fails on invalid uri character"]
687        // relies on /ሴ canonicalized as /%E1%88%B4 when it should be /%25%E1%25%88%25%B4
688        fn test_get_vanilla_utf8_query() {
689            run_test_suite_v4a("get-vanilla-utf8-query");
690        }
691
692        #[test]
693        fn test_get_vanilla_with_session_token() {
694            run_test_suite_v4a("get-vanilla-with-session-token")
695        }
696
697        #[test]
698        fn test_post_header_key_case() {
699            run_test_suite_v4a("post-header-key-case");
700        }
701
702        #[test]
703        fn test_post_header_key_sort() {
704            run_test_suite_v4a("post-header-key-sort");
705        }
706
707        #[test]
708        fn test_post_header_value_case() {
709            run_test_suite_v4a("post-header-value-case");
710        }
711
712        #[test]
713        fn test_post_sts_header_after() {
714            run_test_suite_v4a("post-sts-header-after");
715        }
716
717        #[test]
718        fn test_post_sts_header_before() {
719            run_test_suite_v4a("post-sts-header-before");
720        }
721
722        #[test]
723        fn test_post_vanilla() {
724            run_test_suite_v4a("post-vanilla");
725        }
726
727        #[test]
728        fn test_post_vanilla_empty_query_value() {
729            run_test_suite_v4a("post-vanilla-empty-query-value");
730        }
731
732        #[test]
733        fn test_post_vanilla_query() {
734            run_test_suite_v4a("post-vanilla-query");
735        }
736
737        #[test]
738        fn test_post_x_www_form_urlencoded() {
739            run_test_suite_v4a("post-x-www-form-urlencoded");
740        }
741
742        #[test]
743        fn test_post_x_www_form_urlencoded_parameters() {
744            run_test_suite_v4a("post-x-www-form-urlencoded-parameters");
745        }
746    }
747
748    #[test]
749    fn test_sign_url_escape() {
750        let test = SigningSuiteTest::v4("double-encode-path");
751        let settings = SigningSettings::default();
752        let identity = &Credentials::for_tests().into();
753        let params = v4::SigningParams {
754            identity,
755            region: "us-east-1",
756            name: "service",
757            time: parse_date_time("20150830T123600Z").unwrap(),
758            settings,
759        }
760        .into();
761
762        let original = test.request();
763        let signable = SignableRequest::from(&original);
764        let out = sign(signable, &params).unwrap();
765        assert_eq!(
766            "57d157672191bac40bae387e48bbe14b15303c001fdbb01f4abf295dccb09705",
767            out.signature
768        );
769
770        let mut signed = original.as_http_request();
771        out.output.apply_to_request_http0x(&mut signed);
772
773        let expected = test.signed_request(SignatureLocation::Headers);
774        assert_req_eq!(expected, signed);
775    }
776
777    #[test]
778    fn test_sign_headers_utf8() {
779        let settings = SigningSettings::default();
780        let identity = &Credentials::for_tests().into();
781        let params = v4::SigningParams {
782            identity,
783            region: "us-east-1",
784            name: "service",
785            time: parse_date_time("20150830T123600Z").unwrap(),
786            settings,
787        }
788        .into();
789
790        let original = http0::Request::builder()
791            .uri("https://some-endpoint.some-region.amazonaws.com")
792            .header("some-header", HeaderValue::from_str("テスト").unwrap())
793            .body("")
794            .unwrap()
795            .into();
796        let signable = SignableRequest::from(&original);
797        let out = sign(signable, &params).unwrap();
798        assert_eq!(
799            "55e16b31f9bde5fd04f9d3b780dd2b5e5f11a5219001f91a8ca9ec83eaf1618f",
800            out.signature
801        );
802
803        let mut signed = original.as_http_request();
804        out.output.apply_to_request_http0x(&mut signed);
805
806        let expected = http0::Request::builder()
807            .uri("https://some-endpoint.some-region.amazonaws.com")
808            .header("some-header", HeaderValue::from_str("テスト").unwrap())
809            .header(
810                "x-amz-date",
811                HeaderValue::from_str("20150830T123600Z").unwrap(),
812            )
813            .header(
814                "authorization",
815                HeaderValue::from_str(
816                    "AWS4-HMAC-SHA256 \
817                        Credential=ANOTREAL/20150830/us-east-1/service/aws4_request, \
818                        SignedHeaders=host;some-header;x-amz-date, \
819                        Signature=55e16b31f9bde5fd04f9d3b780dd2b5e5f11a5219001f91a8ca9ec83eaf1618f",
820                )
821                .unwrap(),
822            )
823            .body("")
824            .unwrap();
825        assert_req_eq!(http: expected, signed);
826    }
827
828    #[test]
829    fn test_sign_headers_excluding_session_token() {
830        let settings = SigningSettings {
831            session_token_mode: SessionTokenMode::Exclude,
832            ..Default::default()
833        };
834        let identity = &Credentials::for_tests_with_session_token().into();
835        let params = v4::SigningParams {
836            identity,
837            region: "us-east-1",
838            name: "service",
839            time: parse_date_time("20150830T123600Z").unwrap(),
840            settings,
841        }
842        .into();
843
844        let original = http0::Request::builder()
845            .uri("https://some-endpoint.some-region.amazonaws.com")
846            .body("")
847            .unwrap()
848            .into();
849        let out_without_session_token = sign(SignableRequest::from(&original), &params).unwrap();
850
851        let out_with_session_token_but_excluded =
852            sign(SignableRequest::from(&original), &params).unwrap();
853        assert_eq!(
854            "ab32de057edf094958d178b3c91f3c8d5c296d526b11da991cd5773d09cea560",
855            out_with_session_token_but_excluded.signature
856        );
857        assert_eq!(
858            out_with_session_token_but_excluded.signature,
859            out_without_session_token.signature
860        );
861
862        let mut signed = original.as_http_request();
863        out_with_session_token_but_excluded
864            .output
865            .apply_to_request_http0x(&mut signed);
866
867        let expected = http0::Request::builder()
868            .uri("https://some-endpoint.some-region.amazonaws.com")
869            .header(
870                "x-amz-date",
871                HeaderValue::from_str("20150830T123600Z").unwrap(),
872            )
873            .header(
874                "authorization",
875                HeaderValue::from_str(
876                    "AWS4-HMAC-SHA256 \
877                        Credential=ANOTREAL/20150830/us-east-1/service/aws4_request, \
878                        SignedHeaders=host;x-amz-date, \
879                        Signature=ab32de057edf094958d178b3c91f3c8d5c296d526b11da991cd5773d09cea560",
880                )
881                .unwrap(),
882            )
883            .header(
884                "x-amz-security-token",
885                HeaderValue::from_str("notarealsessiontoken").unwrap(),
886            )
887            .body(b"")
888            .unwrap();
889        assert_req_eq!(http: expected, signed);
890    }
891
892    #[test]
893    fn test_sign_headers_space_trimming() {
894        let settings = SigningSettings::default();
895        let identity = &Credentials::for_tests().into();
896        let params = v4::SigningParams {
897            identity,
898            region: "us-east-1",
899            name: "service",
900            time: parse_date_time("20150830T123600Z").unwrap(),
901            settings,
902        }
903        .into();
904
905        let original = http0::Request::builder()
906            .uri("https://some-endpoint.some-region.amazonaws.com")
907            .header(
908                "some-header",
909                HeaderValue::from_str("  test  test   ").unwrap(),
910            )
911            .body("")
912            .unwrap()
913            .into();
914        let signable = SignableRequest::from(&original);
915        let out = sign(signable, &params).unwrap();
916        assert_eq!(
917            "244f2a0db34c97a528f22715fe01b2417b7750c8a95c7fc104a3c48d81d84c08",
918            out.signature
919        );
920
921        let mut signed = original.as_http_request();
922        out.output.apply_to_request_http0x(&mut signed);
923
924        let expected = http0::Request::builder()
925            .uri("https://some-endpoint.some-region.amazonaws.com")
926            .header(
927                "some-header",
928                HeaderValue::from_str("  test  test   ").unwrap(),
929            )
930            .header(
931                "x-amz-date",
932                HeaderValue::from_str("20150830T123600Z").unwrap(),
933            )
934            .header(
935                "authorization",
936                HeaderValue::from_str(
937                    "AWS4-HMAC-SHA256 \
938                        Credential=ANOTREAL/20150830/us-east-1/service/aws4_request, \
939                        SignedHeaders=host;some-header;x-amz-date, \
940                        Signature=244f2a0db34c97a528f22715fe01b2417b7750c8a95c7fc104a3c48d81d84c08",
941                )
942                .unwrap(),
943            )
944            .body("")
945            .unwrap();
946        assert_req_eq!(http: expected, signed);
947    }
948
949    proptest! {
950        #[test]
951        // Only byte values between 32 and 255 (inclusive) are permitted, excluding byte 127, for
952        // [HeaderValue](https://docs.rs/http/latest/http/header/struct.HeaderValue.html#method.from_bytes).
953        fn test_sign_headers_no_panic(
954            header in ".*"
955        ) {
956            let settings = SigningSettings::default();
957        let identity = &Credentials::for_tests().into();
958        let params = v4::SigningParams {
959            identity,
960                region: "us-east-1",
961                name: "foo",
962                time: std::time::SystemTime::UNIX_EPOCH,
963                settings,
964            }.into();
965
966            let req = SignableRequest::new(
967                "GET",
968                "https://foo.com",
969                iter::once(("x-sign-me", header.as_str())),
970                SignableBody::Bytes(&[])
971            );
972
973            if let Ok(req) = req {
974                // The test considered a pass if the creation of `creq` does not panic.
975                let _creq = crate::http_request::sign(req, &params);
976            }
977        }
978    }
979
980    #[test]
981    fn apply_signing_instructions_headers() {
982        let mut headers = vec![];
983        add_header(&mut headers, "some-header", "foo", false);
984        add_header(&mut headers, "some-other-header", "bar", false);
985        let instructions = SigningInstructions::new(headers, vec![]);
986
987        let mut request = http0::Request::builder()
988            .uri("https://some-endpoint.some-region.amazonaws.com")
989            .body("")
990            .unwrap();
991
992        instructions.apply_to_request_http0x(&mut request);
993
994        let get_header = |n: &str| request.headers().get(n).unwrap().to_str().unwrap();
995        assert_eq!("foo", get_header("some-header"));
996        assert_eq!("bar", get_header("some-other-header"));
997    }
998
999    #[test]
1000    fn apply_signing_instructions_query_params() {
1001        let params = vec![
1002            ("some-param", Cow::Borrowed("f&o?o")),
1003            ("some-other-param?", Cow::Borrowed("bar")),
1004        ];
1005        let instructions = SigningInstructions::new(vec![], params);
1006
1007        let mut request = http0::Request::builder()
1008            .uri("https://some-endpoint.some-region.amazonaws.com/some/path")
1009            .body("")
1010            .unwrap();
1011
1012        instructions.apply_to_request_http0x(&mut request);
1013
1014        assert_eq!(
1015            "/some/path?some-param=f%26o%3Fo&some-other-param%3F=bar",
1016            request.uri().path_and_query().unwrap().to_string()
1017        );
1018    }
1019
1020    #[test]
1021    fn apply_signing_instructions_query_params_http_1x() {
1022        let params = vec![
1023            ("some-param", Cow::Borrowed("f&o?o")),
1024            ("some-other-param?", Cow::Borrowed("bar")),
1025        ];
1026        let instructions = SigningInstructions::new(vec![], params);
1027
1028        let mut request = http::Request::builder()
1029            .uri("https://some-endpoint.some-region.amazonaws.com/some/path")
1030            .body("")
1031            .unwrap();
1032
1033        instructions.apply_to_request_http1x(&mut request);
1034
1035        assert_eq!(
1036            "/some/path?some-param=f%26o%3Fo&some-other-param%3F=bar",
1037            request.uri().path_and_query().unwrap().to_string()
1038        );
1039    }
1040
1041    #[test]
1042    fn test_debug_signable_body() {
1043        let sut = SignableBody::Bytes(b"hello signable body");
1044        assert_eq!(
1045            "Bytes(\"** REDACTED **. To print 19 bytes of raw data, set environment variable `LOG_SIGNABLE_BODY=true`\")",
1046            format!("{sut:?}")
1047        );
1048
1049        let sut = SignableBody::UnsignedPayload;
1050        assert_eq!("UnsignedPayload", format!("{sut:?}"));
1051
1052        let sut = SignableBody::Precomputed("precomputed".to_owned());
1053        assert_eq!("Precomputed(\"precomputed\")", format!("{sut:?}"));
1054
1055        let sut = SignableBody::StreamingUnsignedPayloadTrailer;
1056        assert_eq!("StreamingUnsignedPayloadTrailer", format!("{sut:?}"));
1057    }
1058
1059    // v4 test suite
1060    mod v4_suite {
1061        use crate::http_request::test::run_test_suite_v4;
1062
1063        #[test]
1064        fn test_get_header_key_duplicate() {
1065            run_test_suite_v4("get-header-key-duplicate");
1066        }
1067
1068        #[test]
1069        #[ignore = "httpparse doesn't support parsing multiline headers since they are deprecated in RFC7230"]
1070        fn test_get_header_value_multiline() {
1071            run_test_suite_v4("get-header-value-multiline");
1072        }
1073
1074        #[test]
1075        fn test_get_header_value_order() {
1076            run_test_suite_v4("get-header-value-order");
1077        }
1078
1079        #[test]
1080        fn test_get_header_value_trim() {
1081            run_test_suite_v4("get-header-value-trim");
1082        }
1083
1084        #[test]
1085        fn test_get_relative_normalized() {
1086            run_test_suite_v4("get-relative-normalized");
1087        }
1088
1089        #[test]
1090        fn test_get_relative_relative_normalized() {
1091            run_test_suite_v4("get-relative-relative-normalized");
1092        }
1093
1094        #[test]
1095        fn test_get_relative_relative_unnormalized() {
1096            run_test_suite_v4("get-relative-relative-unnormalized");
1097        }
1098
1099        #[test]
1100        fn test_get_relative_unnormalized() {
1101            run_test_suite_v4("get-relative-unnormalized");
1102        }
1103
1104        #[test]
1105        fn test_get_slash_dot_slash_normalized() {
1106            run_test_suite_v4("get-slash-dot-slash-normalized");
1107        }
1108
1109        #[test]
1110        fn test_get_slash_dot_slash_unnormalized() {
1111            run_test_suite_v4("get-slash-dot-slash-unnormalized");
1112        }
1113
1114        #[test]
1115        fn test_get_slash_normalized() {
1116            run_test_suite_v4("get-slash-normalized");
1117        }
1118
1119        #[test]
1120        fn test_get_slash_pointless_dot_normalized() {
1121            run_test_suite_v4("get-slash-pointless-dot-normalized");
1122        }
1123
1124        #[test]
1125        fn test_get_slash_pointless_dot_unnormalized() {
1126            run_test_suite_v4("get-slash-pointless-dot-unnormalized");
1127        }
1128
1129        #[test]
1130        fn test_get_slash_unnormalized() {
1131            run_test_suite_v4("get-slash-unnormalized");
1132        }
1133
1134        #[test]
1135        fn test_get_slashes_normalized() {
1136            run_test_suite_v4("get-slashes-normalized");
1137        }
1138
1139        #[test]
1140        fn test_get_slashes_unnormalized() {
1141            run_test_suite_v4("get-slashes-unnormalized");
1142        }
1143
1144        #[test]
1145        #[ignore = "relies on single encode of path segments"]
1146        // rely on single encoding of path segments, i.e. string-to-sign contains %20 for spaces rather than %25%20 as it should.
1147        // skipped until we add control over double_uri_encode in context.json
1148        fn test_get_space_normalized() {
1149            run_test_suite_v4("get-space-normalized");
1150        }
1151
1152        #[test]
1153        #[ignore = "httpparse fails on unencoded spaces in path"]
1154        // the input request has unencoded space ' ' in the path which fails to parse
1155        fn test_get_space_unnormalized() {
1156            run_test_suite_v4("get-space-unnormalized");
1157        }
1158
1159        #[test]
1160        fn test_get_unreserved() {
1161            run_test_suite_v4("get-unreserved");
1162        }
1163
1164        #[test]
1165        #[ignore = "httparse fails on invalid uri character"]
1166        // relies on /ሴ canonicalized as /%E1%88%B4 when it should be /%25%E1%25%88%25%B4
1167        fn test_get_utf8() {
1168            run_test_suite_v4("get-utf8");
1169        }
1170
1171        #[test]
1172        fn test_get_vanilla() {
1173            run_test_suite_v4("get-vanilla");
1174        }
1175
1176        #[test]
1177        fn test_get_vanilla_empty_query_key() {
1178            run_test_suite_v4("get-vanilla-empty-query-key");
1179        }
1180
1181        #[test]
1182        fn test_get_vanilla_query() {
1183            run_test_suite_v4("get-vanilla-query");
1184        }
1185
1186        #[test]
1187        fn test_get_vanilla_query_order_encoded() {
1188            run_test_suite_v4("get-vanilla-query-order-encoded");
1189        }
1190
1191        #[test]
1192        fn test_get_vanilla_query_order_key_case() {
1193            run_test_suite_v4("get-vanilla-query-order-key-case");
1194        }
1195
1196        #[test]
1197        fn test_get_vanilla_query_unreserved() {
1198            run_test_suite_v4("get-vanilla-query-unreserved");
1199        }
1200
1201        #[test]
1202        #[ignore = "httparse fails on invalid uri character"]
1203        // relies on /ሴ canonicalized as /%E1%88%B4 when it should be /%25%E1%25%88%25%B4
1204        fn test_get_vanilla_utf8_query() {
1205            run_test_suite_v4("get-vanilla-utf8-query");
1206        }
1207
1208        #[test]
1209        fn test_get_vanilla_with_session_token() {
1210            run_test_suite_v4("get-vanilla-with-session-token");
1211        }
1212
1213        #[test]
1214        fn test_post_header_key_case() {
1215            run_test_suite_v4("post-header-key-case");
1216        }
1217
1218        #[test]
1219        fn test_post_header_key_sort() {
1220            run_test_suite_v4("post-header-key-sort");
1221        }
1222
1223        #[test]
1224        fn test_post_header_value_case() {
1225            run_test_suite_v4("post-header-value-case");
1226        }
1227
1228        #[test]
1229        fn test_post_sts_header_after() {
1230            run_test_suite_v4("post-sts-header-after");
1231        }
1232
1233        #[test]
1234        fn test_post_sts_header_before() {
1235            run_test_suite_v4("post-sts-header-before");
1236        }
1237
1238        #[test]
1239        fn test_post_vanilla() {
1240            run_test_suite_v4("post-vanilla");
1241        }
1242
1243        #[test]
1244        fn test_post_vanilla_empty_query_value() {
1245            run_test_suite_v4("post-vanilla-empty-query-value");
1246        }
1247
1248        #[test]
1249        fn test_post_vanilla_query() {
1250            run_test_suite_v4("post-vanilla-query");
1251        }
1252
1253        #[test]
1254        fn test_post_x_www_form_urlencoded() {
1255            run_test_suite_v4("post-x-www-form-urlencoded");
1256        }
1257
1258        #[test]
1259        fn test_post_x_www_form_urlencoded_parameters() {
1260            run_test_suite_v4("post-x-www-form-urlencoded-parameters");
1261        }
1262    }
1263}