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