aws_sigv4/http_request/
sign.rs

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