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