aws_sigv4/http_request/
sign.rs

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