aws_sigv4/http_request/
canonical_request.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use crate::date_time::{format_date, format_date_time};
7use crate::http_request::error::CanonicalRequestError;
8use crate::http_request::settings::SessionTokenMode;
9use crate::http_request::settings::UriPathNormalizationMode;
10use crate::http_request::sign::SignableRequest;
11use crate::http_request::uri_path_normalization::normalize_uri_path;
12use crate::http_request::url_escape::percent_encode_path;
13use crate::http_request::{PayloadChecksumKind, SignableBody, SignatureLocation, SigningParams};
14use crate::http_request::{PercentEncodingMode, SigningSettings};
15use crate::sign::v4::sha256_hex_string;
16use crate::SignatureVersion;
17use aws_smithy_http::query_writer::QueryWriter;
18use http0::header::{AsHeaderName, HeaderName, HOST};
19use http0::{HeaderMap, HeaderValue, Uri};
20use std::borrow::Cow;
21use std::cmp::Ordering;
22use std::convert::TryFrom;
23use std::fmt;
24use std::str::FromStr;
25use std::time::SystemTime;
26
27#[cfg(feature = "sigv4a")]
28pub(crate) mod sigv4a;
29
30pub(crate) mod header {
31    pub(crate) const X_AMZ_CONTENT_SHA_256: &str = "x-amz-content-sha256";
32    pub(crate) const X_AMZ_DATE: &str = "x-amz-date";
33    pub(crate) const X_AMZ_SECURITY_TOKEN: &str = "x-amz-security-token";
34    pub(crate) const X_AMZ_USER_AGENT: &str = "x-amz-user-agent";
35}
36
37pub(crate) mod param {
38    pub(crate) const X_AMZ_ALGORITHM: &str = "X-Amz-Algorithm";
39    pub(crate) const X_AMZ_CREDENTIAL: &str = "X-Amz-Credential";
40    pub(crate) const X_AMZ_DATE: &str = "X-Amz-Date";
41    pub(crate) const X_AMZ_EXPIRES: &str = "X-Amz-Expires";
42    pub(crate) const X_AMZ_SECURITY_TOKEN: &str = "X-Amz-Security-Token";
43    pub(crate) const X_AMZ_SIGNED_HEADERS: &str = "X-Amz-SignedHeaders";
44    pub(crate) const X_AMZ_SIGNATURE: &str = "X-Amz-Signature";
45}
46
47pub(crate) const HMAC_256: &str = "AWS4-HMAC-SHA256";
48
49const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
50const STREAMING_UNSIGNED_PAYLOAD_TRAILER: &str = "STREAMING-UNSIGNED-PAYLOAD-TRAILER";
51
52#[derive(Debug, PartialEq)]
53pub(crate) struct HeaderValues<'a> {
54    pub(crate) content_sha256: Cow<'a, str>,
55    pub(crate) date_time: String,
56    pub(crate) security_token: Option<&'a str>,
57    pub(crate) signed_headers: SignedHeaders,
58    #[cfg(feature = "sigv4a")]
59    pub(crate) region_set: Option<&'a str>,
60}
61
62#[derive(Debug, PartialEq)]
63pub(crate) struct QueryParamValues<'a> {
64    pub(crate) algorithm: &'static str,
65    pub(crate) content_sha256: Cow<'a, str>,
66    pub(crate) credential: String,
67    pub(crate) date_time: String,
68    pub(crate) expires: String,
69    pub(crate) security_token: Option<&'a str>,
70    pub(crate) signed_headers: SignedHeaders,
71    #[cfg(feature = "sigv4a")]
72    pub(crate) region_set: Option<&'a str>,
73}
74
75#[derive(Debug, PartialEq)]
76pub(crate) enum SignatureValues<'a> {
77    Headers(HeaderValues<'a>),
78    QueryParams(QueryParamValues<'a>),
79}
80
81impl<'a> SignatureValues<'a> {
82    pub(crate) fn signed_headers(&self) -> &SignedHeaders {
83        match self {
84            SignatureValues::Headers(values) => &values.signed_headers,
85            SignatureValues::QueryParams(values) => &values.signed_headers,
86        }
87    }
88
89    fn content_sha256(&self) -> &str {
90        match self {
91            SignatureValues::Headers(values) => &values.content_sha256,
92            SignatureValues::QueryParams(values) => &values.content_sha256,
93        }
94    }
95
96    pub(crate) fn as_headers(&self) -> Option<&HeaderValues<'_>> {
97        match self {
98            SignatureValues::Headers(values) => Some(values),
99            _ => None,
100        }
101    }
102
103    pub(crate) fn into_query_params(self) -> Result<QueryParamValues<'a>, Self> {
104        match self {
105            SignatureValues::QueryParams(values) => Ok(values),
106            _ => Err(self),
107        }
108    }
109}
110
111#[derive(Debug, PartialEq)]
112pub(crate) struct CanonicalRequest<'a> {
113    pub(crate) method: &'a str,
114    pub(crate) path: Cow<'a, str>,
115    pub(crate) params: Option<String>,
116    pub(crate) headers: HeaderMap,
117    pub(crate) values: SignatureValues<'a>,
118}
119
120impl<'a> CanonicalRequest<'a> {
121    /// Construct a CanonicalRequest from a [`SignableRequest`] and [`SigningParams`].
122    ///
123    /// The returned canonical request includes information required for signing as well
124    /// as query parameters or header values that go along with the signature in a request.
125    ///
126    /// ## Behavior
127    ///
128    /// There are several settings which alter signing behavior:
129    /// - If a `security_token` is provided as part of the credentials it will be included in the signed headers
130    /// - If `settings.percent_encoding_mode` specifies double encoding, `%` in the URL will be re-encoded as `%25`
131    /// - If `settings.payload_checksum_kind` is XAmzSha256, add a x-amz-content-sha256 with the body
132    ///   checksum. This is the same checksum used as the "payload_hash" in the canonical request
133    /// - If `settings.session_token_mode` specifies X-Amz-Security-Token to be
134    ///   included before calculating the signature, add it, otherwise omit it.
135    /// - `settings.signature_location` determines where the signature will be placed in a request,
136    ///   and also alters the kinds of signing values that go along with it in the request.
137    pub(crate) fn from<'b>(
138        req: &'b SignableRequest<'b>,
139        params: &'b SigningParams<'b>,
140    ) -> Result<CanonicalRequest<'b>, CanonicalRequestError> {
141        let creds = params
142            .credentials()
143            .map_err(|_| CanonicalRequestError::unsupported_identity_type())?;
144        // Path encoding: if specified, re-encode % as %25
145        // Set method and path into CanonicalRequest
146        let path = req.uri().path();
147        let path = match params.settings().uri_path_normalization_mode {
148            UriPathNormalizationMode::Enabled => normalize_uri_path(path),
149            UriPathNormalizationMode::Disabled => Cow::Borrowed(path),
150        };
151        let path = match params.settings().percent_encoding_mode {
152            // The string is already URI encoded, we don't need to encode everything again, just `%`
153            PercentEncodingMode::Double => Cow::Owned(percent_encode_path(&path)),
154            PercentEncodingMode::Single => path,
155        };
156        let payload_hash = Self::payload_hash(req.body());
157
158        let date_time = format_date_time(*params.time());
159        let (signed_headers, canonical_headers) =
160            Self::headers(req, params, &payload_hash, &date_time)?;
161        let signed_headers = SignedHeaders::new(signed_headers);
162
163        let security_token = match params.settings().session_token_mode {
164            SessionTokenMode::Include => creds.session_token(),
165            SessionTokenMode::Exclude => None,
166        };
167
168        let values = match params.settings().signature_location {
169            SignatureLocation::Headers => SignatureValues::Headers(HeaderValues {
170                content_sha256: payload_hash,
171                date_time,
172                security_token,
173                signed_headers,
174                #[cfg(feature = "sigv4a")]
175                region_set: params.region_set(),
176            }),
177            SignatureLocation::QueryParams => {
178                let credential = match params {
179                    SigningParams::V4(params) => {
180                        format!(
181                            "{}/{}/{}/{}/aws4_request",
182                            creds.access_key_id(),
183                            format_date(params.time),
184                            params.region,
185                            params.name,
186                        )
187                    }
188                    #[cfg(feature = "sigv4a")]
189                    SigningParams::V4a(params) => {
190                        format!(
191                            "{}/{}/{}/aws4_request",
192                            creds.access_key_id(),
193                            format_date(params.time),
194                            params.name,
195                        )
196                    }
197                };
198
199                SignatureValues::QueryParams(QueryParamValues {
200                    algorithm: params.algorithm(),
201                    content_sha256: payload_hash,
202                    credential,
203                    date_time,
204                    expires: params
205                        .settings()
206                        .expires_in
207                        .expect("presigning requires expires_in")
208                        .as_secs()
209                        .to_string(),
210                    security_token,
211                    signed_headers,
212                    #[cfg(feature = "sigv4a")]
213                    region_set: params.region_set(),
214                })
215            }
216        };
217
218        let creq = CanonicalRequest {
219            method: req.method(),
220            path,
221            params: Self::params(req.uri(), &values, params.settings()),
222            headers: canonical_headers,
223            values,
224        };
225        Ok(creq)
226    }
227
228    fn headers(
229        req: &SignableRequest<'_>,
230        params: &SigningParams<'_>,
231        payload_hash: &str,
232        date_time: &str,
233    ) -> Result<(Vec<CanonicalHeaderName>, HeaderMap), CanonicalRequestError> {
234        // Header computation:
235        // The canonical request will include headers not present in the input. We need to clone and
236        // normalize the headers from the original request and add:
237        // - host
238        // - x-amz-date
239        // - x-amz-security-token (if provided)
240        // - x-amz-content-sha256 (if requested by signing settings)
241        let mut canonical_headers = HeaderMap::with_capacity(req.headers().len());
242        for (name, value) in req.headers().iter() {
243            // Header names and values need to be normalized according to Step 4 of https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
244            // Using append instead of insert means this will not clobber headers that have the same lowercased name
245            canonical_headers.append(
246                HeaderName::from_str(&name.to_lowercase())?,
247                normalize_header_value(value)?,
248            );
249        }
250
251        Self::insert_host_header(&mut canonical_headers, req.uri());
252
253        let token_header_name = params
254            .settings()
255            .session_token_name_override
256            .unwrap_or(header::X_AMZ_SECURITY_TOKEN);
257
258        if params.settings().signature_location == SignatureLocation::Headers {
259            let creds = params
260                .credentials()
261                .map_err(|_| CanonicalRequestError::unsupported_identity_type())?;
262            Self::insert_date_header(&mut canonical_headers, date_time);
263
264            if let Some(security_token) = creds.session_token() {
265                let mut sec_header = HeaderValue::from_str(security_token)?;
266                sec_header.set_sensitive(true);
267                canonical_headers.insert(token_header_name, sec_header);
268            }
269
270            if params.settings().payload_checksum_kind == PayloadChecksumKind::XAmzSha256 {
271                let header = HeaderValue::from_str(payload_hash)?;
272                canonical_headers.insert(header::X_AMZ_CONTENT_SHA_256, header);
273            }
274
275            #[cfg(feature = "sigv4a")]
276            if let Some(region_set) = params.region_set() {
277                let header = HeaderValue::from_str(region_set)?;
278                canonical_headers.insert(sigv4a::header::X_AMZ_REGION_SET, header);
279            }
280        }
281
282        let mut signed_headers = Vec::with_capacity(canonical_headers.len());
283        for name in canonical_headers.keys() {
284            if let Some(excluded_headers) = params.settings().excluded_headers.as_ref() {
285                if excluded_headers.iter().any(|it| name.as_str() == it) {
286                    continue;
287                }
288            }
289
290            if params.settings().session_token_mode == SessionTokenMode::Exclude
291                && name == HeaderName::from_static(token_header_name)
292            {
293                continue;
294            }
295
296            if params.settings().signature_location == SignatureLocation::QueryParams {
297                // The X-Amz-User-Agent header should not be signed if this is for a presigned URL
298                if name == HeaderName::from_static(header::X_AMZ_USER_AGENT) {
299                    continue;
300                }
301            }
302            signed_headers.push(CanonicalHeaderName(name.clone()));
303        }
304
305        Ok((signed_headers, canonical_headers))
306    }
307
308    fn payload_hash<'b>(body: &'b SignableBody<'b>) -> Cow<'b, str> {
309        // Payload hash computation
310        //
311        // Based on the input body, set the payload_hash of the canonical request:
312        // Either:
313        // - compute a hash
314        // - use the precomputed hash
315        // - use `UnsignedPayload`
316        // - use `UnsignedPayload` for streaming requests
317        // - use `StreamingUnsignedPayloadTrailer` for streaming requests with trailers
318        match body {
319            SignableBody::Bytes(data) => Cow::Owned(sha256_hex_string(data)),
320            SignableBody::Precomputed(digest) => Cow::Borrowed(digest.as_str()),
321            SignableBody::UnsignedPayload => Cow::Borrowed(UNSIGNED_PAYLOAD),
322            SignableBody::StreamingUnsignedPayloadTrailer => {
323                Cow::Borrowed(STREAMING_UNSIGNED_PAYLOAD_TRAILER)
324            }
325        }
326    }
327
328    fn params(
329        uri: &Uri,
330        values: &SignatureValues<'_>,
331        settings: &SigningSettings,
332    ) -> Option<String> {
333        let mut params: Vec<(Cow<'_, str>, Cow<'_, str>)> =
334            form_urlencoded::parse(uri.query().unwrap_or_default().as_bytes()).collect();
335        fn add_param<'a>(params: &mut Vec<(Cow<'a, str>, Cow<'a, str>)>, k: &'a str, v: &'a str) {
336            params.push((Cow::Borrowed(k), Cow::Borrowed(v)));
337        }
338
339        if let SignatureValues::QueryParams(values) = values {
340            add_param(&mut params, param::X_AMZ_DATE, &values.date_time);
341            add_param(&mut params, param::X_AMZ_EXPIRES, &values.expires);
342
343            #[cfg(feature = "sigv4a")]
344            if let Some(regions) = values.region_set {
345                add_param(&mut params, sigv4a::param::X_AMZ_REGION_SET, regions);
346            }
347
348            add_param(&mut params, param::X_AMZ_ALGORITHM, values.algorithm);
349            add_param(&mut params, param::X_AMZ_CREDENTIAL, &values.credential);
350            add_param(
351                &mut params,
352                param::X_AMZ_SIGNED_HEADERS,
353                values.signed_headers.as_str(),
354            );
355
356            if let Some(security_token) = values.security_token {
357                add_param(
358                    &mut params,
359                    settings
360                        .session_token_name_override
361                        .unwrap_or(param::X_AMZ_SECURITY_TOKEN),
362                    security_token,
363                );
364            }
365        }
366        // Sort by param name, and then by param value
367        params.sort();
368
369        let mut query = QueryWriter::new(uri);
370        query.clear_params();
371        for (key, value) in params {
372            query.insert(&key, &value);
373        }
374
375        let query = query.build_query();
376        if query.is_empty() {
377            None
378        } else {
379            Some(query)
380        }
381    }
382
383    fn insert_host_header(
384        canonical_headers: &mut HeaderMap<HeaderValue>,
385        uri: &Uri,
386    ) -> HeaderValue {
387        match canonical_headers.get(&HOST) {
388            Some(header) => header.clone(),
389            None => {
390                let authority = uri
391                    .authority()
392                    .expect("request uri authority must be set for signing");
393                let header = HeaderValue::try_from(authority.as_str())
394                    .expect("endpoint must contain valid header characters");
395                canonical_headers.insert(HOST, header.clone());
396                header
397            }
398        }
399    }
400
401    fn insert_date_header(
402        canonical_headers: &mut HeaderMap<HeaderValue>,
403        date_time: &str,
404    ) -> HeaderValue {
405        let x_amz_date = HeaderName::from_static(header::X_AMZ_DATE);
406        let date_header = HeaderValue::try_from(date_time).expect("date is valid header value");
407        canonical_headers.insert(x_amz_date, date_header.clone());
408        date_header
409    }
410
411    fn header_values_for(&self, key: impl AsHeaderName) -> String {
412        let values: Vec<&str> = self
413            .headers
414            .get_all(key)
415            .into_iter()
416            .map(|value| {
417                std::str::from_utf8(value.as_bytes())
418                    .expect("SDK request header values are valid UTF-8")
419            })
420            .collect();
421        values.join(",")
422    }
423}
424
425impl<'a> fmt::Display for CanonicalRequest<'a> {
426    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427        writeln!(f, "{}", self.method)?;
428        writeln!(f, "{}", self.path)?;
429        writeln!(f, "{}", self.params.as_deref().unwrap_or(""))?;
430        // write out _all_ the headers
431        for header in &self.values.signed_headers().headers {
432            write!(f, "{}:", header.0.as_str())?;
433            writeln!(f, "{}", self.header_values_for(&header.0))?;
434        }
435        writeln!(f)?;
436        // write out the signed headers
437        writeln!(f, "{}", self.values.signed_headers().as_str())?;
438        write!(f, "{}", self.values.content_sha256())?;
439        Ok(())
440    }
441}
442
443/// Removes excess spaces before and after a given byte string, and converts multiple sequential
444/// spaces to a single space e.g. "  Some  example   text  " -> "Some example text".
445///
446/// This function ONLY affects spaces and not other kinds of whitespace.
447fn trim_all(text: &str) -> Cow<'_, str> {
448    let text = text.trim_matches(' ');
449    let requires_filter = text
450        .chars()
451        .zip(text.chars().skip(1))
452        .any(|(a, b)| a == ' ' && b == ' ');
453    if !requires_filter {
454        Cow::Borrowed(text)
455    } else {
456        // The normal trim function will trim non-breaking spaces and other various whitespace chars.
457        // S3 ONLY trims spaces so we use trim_matches to trim spaces only
458        Cow::Owned(
459            text.chars()
460                // Filter out consecutive spaces
461                .zip(text.chars().skip(1).chain(std::iter::once('!')))
462                .filter(|(a, b)| *a != ' ' || *b != ' ')
463                .map(|(a, _)| a)
464                .collect(),
465        )
466    }
467}
468
469/// Works just like [trim_all] but acts on HeaderValues instead of bytes.
470/// Will ensure that the underlying bytes are valid UTF-8.
471fn normalize_header_value(header_value: &str) -> Result<HeaderValue, CanonicalRequestError> {
472    let trimmed_value = trim_all(header_value);
473    HeaderValue::from_str(&trimmed_value).map_err(CanonicalRequestError::from)
474}
475
476#[derive(Debug, PartialEq, Default)]
477pub(crate) struct SignedHeaders {
478    headers: Vec<CanonicalHeaderName>,
479    formatted: String,
480}
481
482impl SignedHeaders {
483    fn new(mut headers: Vec<CanonicalHeaderName>) -> Self {
484        headers.sort();
485        let formatted = Self::fmt(&headers);
486        SignedHeaders { headers, formatted }
487    }
488
489    fn fmt(headers: &[CanonicalHeaderName]) -> String {
490        let mut value = String::new();
491        let mut iter = headers.iter().peekable();
492        while let Some(next) = iter.next() {
493            value += next.0.as_str();
494            if iter.peek().is_some() {
495                value.push(';');
496            }
497        }
498        value
499    }
500
501    pub(crate) fn as_str(&self) -> &str {
502        &self.formatted
503    }
504}
505
506impl fmt::Display for SignedHeaders {
507    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
508        write!(f, "{}", self.formatted)
509    }
510}
511
512#[derive(Debug, PartialEq, Eq, Clone)]
513struct CanonicalHeaderName(HeaderName);
514
515impl PartialOrd for CanonicalHeaderName {
516    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
517        Some(self.cmp(other))
518    }
519}
520
521impl Ord for CanonicalHeaderName {
522    fn cmp(&self, other: &Self) -> Ordering {
523        self.0.as_str().cmp(other.0.as_str())
524    }
525}
526
527#[derive(PartialEq, Debug, Clone)]
528pub(crate) struct SigningScope<'a> {
529    pub(crate) time: SystemTime,
530    pub(crate) region: &'a str,
531    pub(crate) service: &'a str,
532}
533
534impl<'a> SigningScope<'a> {
535    pub(crate) fn v4a_display(&self) -> String {
536        format!("{}/{}/aws4_request", format_date(self.time), self.service)
537    }
538}
539
540impl<'a> fmt::Display for SigningScope<'a> {
541    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
542        write!(
543            f,
544            "{}/{}/{}/aws4_request",
545            format_date(self.time),
546            self.region,
547            self.service
548        )
549    }
550}
551
552#[derive(PartialEq, Debug, Clone)]
553pub(crate) struct StringToSign<'a> {
554    pub(crate) algorithm: &'static str,
555    pub(crate) scope: SigningScope<'a>,
556    pub(crate) time: SystemTime,
557    pub(crate) region: &'a str,
558    pub(crate) service: &'a str,
559    pub(crate) hashed_creq: &'a str,
560    signature_version: SignatureVersion,
561}
562
563impl<'a> StringToSign<'a> {
564    pub(crate) fn new_v4(
565        time: SystemTime,
566        region: &'a str,
567        service: &'a str,
568        hashed_creq: &'a str,
569    ) -> Self {
570        let scope = SigningScope {
571            time,
572            region,
573            service,
574        };
575        Self {
576            algorithm: HMAC_256,
577            scope,
578            time,
579            region,
580            service,
581            hashed_creq,
582            signature_version: SignatureVersion::V4,
583        }
584    }
585
586    #[cfg(feature = "sigv4a")]
587    pub(crate) fn new_v4a(
588        time: SystemTime,
589        region_set: &'a str,
590        service: &'a str,
591        hashed_creq: &'a str,
592    ) -> Self {
593        use crate::sign::v4a::ECDSA_256;
594
595        let scope = SigningScope {
596            time,
597            region: region_set,
598            service,
599        };
600        Self {
601            algorithm: ECDSA_256,
602            scope,
603            time,
604            region: region_set,
605            service,
606            hashed_creq,
607            signature_version: SignatureVersion::V4a,
608        }
609    }
610}
611
612impl<'a> fmt::Display for StringToSign<'a> {
613    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
614        write!(
615            f,
616            "{}\n{}\n{}\n{}",
617            self.algorithm,
618            format_date_time(self.time),
619            match self.signature_version {
620                SignatureVersion::V4 => self.scope.to_string(),
621                SignatureVersion::V4a => self.scope.v4a_display(),
622            },
623            self.hashed_creq
624        )
625    }
626}
627
628#[cfg(test)]
629mod tests {
630    use crate::date_time::test_parsers::parse_date_time;
631    use crate::http_request::canonical_request::{
632        normalize_header_value, trim_all, CanonicalRequest, SigningScope, StringToSign,
633    };
634    use crate::http_request::test;
635    use crate::http_request::{
636        PayloadChecksumKind, SessionTokenMode, SignableBody, SignableRequest, SignatureLocation,
637        SigningParams, SigningSettings,
638    };
639    use crate::sign::v4;
640    use crate::sign::v4::sha256_hex_string;
641    use aws_credential_types::Credentials;
642    use aws_smithy_http::query_writer::QueryWriter;
643    use aws_smithy_runtime_api::client::identity::Identity;
644    use http0::{HeaderValue, Uri};
645    use pretty_assertions::assert_eq;
646    use proptest::{prelude::*, proptest};
647    use std::borrow::Cow;
648    use std::time::Duration;
649
650    fn signing_params(identity: &Identity, settings: SigningSettings) -> SigningParams<'_> {
651        v4::signing_params::Builder::default()
652            .identity(identity)
653            .region("test-region")
654            .name("testservicename")
655            .time(parse_date_time("20210511T154045Z").unwrap())
656            .settings(settings)
657            .build()
658            .unwrap()
659            .into()
660    }
661
662    #[test]
663    fn test_repeated_header() {
664        let mut req = test::v4::test_request("get-vanilla-query-order-key-case");
665        req.headers.push((
666            "x-amz-object-attributes".to_string(),
667            "Checksum".to_string(),
668        ));
669        req.headers.push((
670            "x-amz-object-attributes".to_string(),
671            "ObjectSize".to_string(),
672        ));
673        let req = SignableRequest::from(&req);
674        let settings = SigningSettings {
675            payload_checksum_kind: PayloadChecksumKind::XAmzSha256,
676            session_token_mode: SessionTokenMode::Exclude,
677            ..Default::default()
678        };
679        let identity = Credentials::for_tests().into();
680        let signing_params = signing_params(&identity, settings);
681        let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
682
683        assert_eq!(
684            creq.values.signed_headers().to_string(),
685            "host;x-amz-content-sha256;x-amz-date;x-amz-object-attributes"
686        );
687        assert_eq!(
688            creq.header_values_for("x-amz-object-attributes"),
689            "Checksum,ObjectSize",
690        );
691    }
692
693    #[test]
694    fn test_set_xamz_sha_256() {
695        let req = test::v4::test_request("get-vanilla-query-order-key-case");
696        let req = SignableRequest::from(&req);
697        let settings = SigningSettings {
698            payload_checksum_kind: PayloadChecksumKind::XAmzSha256,
699            session_token_mode: SessionTokenMode::Exclude,
700            ..Default::default()
701        };
702        let identity = Credentials::for_tests().into();
703        let mut signing_params = signing_params(&identity, settings);
704        let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
705        assert_eq!(
706            creq.values.content_sha256(),
707            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
708        );
709        // assert that the sha256 header was added
710        assert_eq!(
711            creq.values.signed_headers().as_str(),
712            "host;x-amz-content-sha256;x-amz-date"
713        );
714
715        signing_params.set_payload_checksum_kind(PayloadChecksumKind::NoHeader);
716        let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
717        assert_eq!(creq.values.signed_headers().as_str(), "host;x-amz-date");
718    }
719
720    #[test]
721    fn test_unsigned_payload() {
722        let mut req = test::v4::test_request("get-vanilla-query-order-key-case");
723        req.set_body(SignableBody::UnsignedPayload);
724        let req: SignableRequest<'_> = SignableRequest::from(&req);
725
726        let settings = SigningSettings {
727            payload_checksum_kind: PayloadChecksumKind::XAmzSha256,
728            ..Default::default()
729        };
730        let identity = Credentials::for_tests().into();
731        let signing_params = signing_params(&identity, settings);
732        let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
733        assert_eq!(creq.values.content_sha256(), "UNSIGNED-PAYLOAD");
734        assert!(creq.to_string().ends_with("UNSIGNED-PAYLOAD"));
735    }
736
737    #[test]
738    fn test_precomputed_payload() {
739        let payload_hash = "44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072";
740        let mut req = test::v4::test_request("get-vanilla-query-order-key-case");
741        req.set_body(SignableBody::Precomputed(String::from(payload_hash)));
742        let req = SignableRequest::from(&req);
743        let settings = SigningSettings {
744            payload_checksum_kind: PayloadChecksumKind::XAmzSha256,
745            ..Default::default()
746        };
747        let identity = Credentials::for_tests().into();
748        let signing_params = signing_params(&identity, settings);
749        let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
750        assert_eq!(creq.values.content_sha256(), payload_hash);
751        assert!(creq.to_string().ends_with(payload_hash));
752    }
753
754    #[test]
755    fn test_generate_scope() {
756        let expected = "20150830/us-east-1/iam/aws4_request\n";
757        let scope = SigningScope {
758            time: parse_date_time("20150830T123600Z").unwrap(),
759            region: "us-east-1",
760            service: "iam",
761        };
762        assert_eq!(format!("{}\n", scope), expected);
763    }
764
765    #[test]
766    fn test_string_to_sign() {
767        let time = parse_date_time("20150830T123600Z").unwrap();
768        let creq = test::v4::test_canonical_request("get-vanilla-query-order-key-case");
769        let expected_sts = test::v4::test_sts("get-vanilla-query-order-key-case");
770        let encoded = sha256_hex_string(creq.as_bytes());
771
772        let actual = StringToSign::new_v4(time, "us-east-1", "service", &encoded);
773        assert_eq!(expected_sts, actual.to_string());
774    }
775
776    #[test]
777    fn test_digest_of_canonical_request() {
778        let creq = test::v4::test_canonical_request("get-vanilla-query-order-key-case");
779        let expected = "816cd5b414d056048ba4f7c5386d6e0533120fb1fcfa93762cf0fc39e2cf19e0";
780        let actual = sha256_hex_string(creq.as_bytes());
781        assert_eq!(expected, actual);
782    }
783
784    #[test]
785    fn test_double_url_encode_path() {
786        let req = test::v4::test_request("double-encode-path");
787        let req = SignableRequest::from(&req);
788        let identity = Credentials::for_tests().into();
789        let signing_params = signing_params(&identity, SigningSettings::default());
790        let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
791
792        let expected = test::v4::test_canonical_request("double-encode-path");
793        let actual = format!("{}", creq);
794        assert_eq!(actual, expected);
795    }
796
797    #[test]
798    fn test_double_url_encode() {
799        let req = test::v4::test_request("double-url-encode");
800        let req = SignableRequest::from(&req);
801        let identity = Credentials::for_tests().into();
802        let signing_params = signing_params(&identity, SigningSettings::default());
803        let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
804
805        let expected = test::v4::test_canonical_request("double-url-encode");
806        let actual = format!("{}", creq);
807        assert_eq!(actual, expected);
808    }
809
810    #[test]
811    fn test_tilde_in_uri() {
812        let req = http0::Request::builder()
813            .uri("https://s3.us-east-1.amazonaws.com/my-bucket?list-type=2&prefix=~objprefix&single&k=&unreserved=-_.~").body("").unwrap().into();
814        let req = SignableRequest::from(&req);
815        let identity = Credentials::for_tests().into();
816        let signing_params = signing_params(&identity, SigningSettings::default());
817        let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
818        assert_eq!(
819            Some("k=&list-type=2&prefix=~objprefix&single=&unreserved=-_.~"),
820            creq.params.as_deref(),
821        );
822    }
823
824    #[test]
825    fn test_signing_urls_with_percent_encoded_query_strings() {
826        let all_printable_ascii_chars: String = (32u8..127).map(char::from).collect();
827        let uri = Uri::from_static("https://s3.us-east-1.amazonaws.com/my-bucket");
828
829        let mut query_writer = QueryWriter::new(&uri);
830        query_writer.insert("list-type", "2");
831        query_writer.insert("prefix", &all_printable_ascii_chars);
832
833        let req = http0::Request::builder()
834            .uri(query_writer.build_uri())
835            .body("")
836            .unwrap()
837            .into();
838        let req = SignableRequest::from(&req);
839        let identity = Credentials::for_tests().into();
840        let signing_params = signing_params(&identity, SigningSettings::default());
841        let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
842
843        let expected = "list-type=2&prefix=%20%21%22%23%24%25%26%27%28%29%2A%2B%2C-.%2F0123456789%3A%3B%3C%3D%3E%3F%40ABCDEFGHIJKLMNOPQRSTUVWXYZ%5B%5C%5D%5E_%60abcdefghijklmnopqrstuvwxyz%7B%7C%7D~";
844        let actual = creq.params.unwrap();
845        assert_eq!(expected, actual);
846    }
847
848    #[test]
849    fn test_omit_session_token() {
850        let req = test::v4::test_request("get-vanilla-query-order-key-case");
851        let req = SignableRequest::from(&req);
852        let settings = SigningSettings {
853            session_token_mode: SessionTokenMode::Include,
854            ..Default::default()
855        };
856        let identity = Credentials::for_tests_with_session_token().into();
857        let mut signing_params = signing_params(&identity, settings);
858
859        let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
860        assert_eq!(
861            creq.values.signed_headers().as_str(),
862            "host;x-amz-date;x-amz-security-token"
863        );
864        assert_eq!(
865            creq.headers.get("x-amz-security-token").unwrap(),
866            "notarealsessiontoken"
867        );
868
869        signing_params.set_session_token_mode(SessionTokenMode::Exclude);
870        let creq = CanonicalRequest::from(&req, &signing_params).unwrap();
871        assert_eq!(
872            creq.headers.get("x-amz-security-token").unwrap(),
873            "notarealsessiontoken"
874        );
875        assert_eq!(creq.values.signed_headers().as_str(), "host;x-amz-date");
876    }
877
878    // It should exclude authorization, user-agent, x-amzn-trace-id headers from presigning
879    #[test]
880    fn non_presigning_header_exclusion() {
881        let request = http0::Request::builder()
882            .uri("https://some-endpoint.some-region.amazonaws.com")
883            .header("authorization", "test-authorization")
884            .header("content-type", "application/xml")
885            .header("content-length", "0")
886            .header("user-agent", "test-user-agent")
887            .header("x-amzn-trace-id", "test-trace-id")
888            .header("x-amz-user-agent", "test-user-agent")
889            .body("")
890            .unwrap()
891            .into();
892        let request = SignableRequest::from(&request);
893
894        let settings = SigningSettings {
895            signature_location: SignatureLocation::Headers,
896            ..Default::default()
897        };
898
899        let identity = Credentials::for_tests().into();
900        let signing_params = signing_params(&identity, settings);
901        let canonical = CanonicalRequest::from(&request, &signing_params).unwrap();
902
903        let values = canonical.values.as_headers().unwrap();
904        assert_eq!(
905            "content-length;content-type;host;x-amz-date;x-amz-user-agent",
906            values.signed_headers.as_str()
907        );
908    }
909
910    // It should exclude authorization, user-agent, x-amz-user-agent, x-amzn-trace-id headers from presigning
911    #[test]
912    fn presigning_header_exclusion() {
913        let request = http0::Request::builder()
914            .uri("https://some-endpoint.some-region.amazonaws.com")
915            .header("authorization", "test-authorization")
916            .header("content-type", "application/xml")
917            .header("content-length", "0")
918            .header("user-agent", "test-user-agent")
919            .header("x-amzn-trace-id", "test-trace-id")
920            .header("x-amz-user-agent", "test-user-agent")
921            .body("")
922            .unwrap()
923            .into();
924        let request = SignableRequest::from(&request);
925
926        let settings = SigningSettings {
927            signature_location: SignatureLocation::QueryParams,
928            expires_in: Some(Duration::from_secs(30)),
929            ..Default::default()
930        };
931
932        let identity = Credentials::for_tests().into();
933        let signing_params = signing_params(&identity, settings);
934        let canonical = CanonicalRequest::from(&request, &signing_params).unwrap();
935
936        let values = canonical.values.into_query_params().unwrap();
937        assert_eq!(
938            "content-length;content-type;host",
939            values.signed_headers.as_str()
940        );
941    }
942
943    #[allow(clippy::ptr_arg)] // The proptest macro requires this arg to be a Vec instead of a slice.
944    fn valid_input(input: &Vec<String>) -> bool {
945        [
946            "content-length".to_owned(),
947            "content-type".to_owned(),
948            "host".to_owned(),
949        ]
950        .iter()
951        .all(|element| !input.contains(element))
952    }
953
954    proptest! {
955        #[test]
956        fn presigning_header_exclusion_with_explicit_exclusion_list_specified(
957            excluded_headers in prop::collection::vec("[a-z]{1,20}", 1..10).prop_filter(
958                "`excluded_headers` should pass the `valid_input` check",
959                valid_input,
960            )
961        ) {
962            let mut request_builder = http0::Request::builder()
963                .uri("https://some-endpoint.some-region.amazonaws.com")
964                .header("content-type", "application/xml")
965                .header("content-length", "0");
966            for key in &excluded_headers {
967                request_builder = request_builder.header(key, "value");
968            }
969            let request = request_builder.body("").unwrap().into();
970
971            let request = SignableRequest::from(&request);
972
973            let settings = SigningSettings {
974                signature_location: SignatureLocation::QueryParams,
975                expires_in: Some(Duration::from_secs(30)),
976                excluded_headers: Some(
977                    excluded_headers
978                        .into_iter()
979                        .map(std::borrow::Cow::Owned)
980                        .collect(),
981                ),
982                ..Default::default()
983            };
984
985        let identity = Credentials::for_tests().into();
986        let signing_params = signing_params(&identity, settings);
987            let canonical = CanonicalRequest::from(&request, &signing_params).unwrap();
988
989            let values = canonical.values.into_query_params().unwrap();
990            assert_eq!(
991                "content-length;content-type;host",
992                values.signed_headers.as_str()
993            );
994        }
995    }
996
997    #[test]
998    fn test_trim_all_handles_spaces_correctly() {
999        assert_eq!(Cow::Borrowed("don't touch me"), trim_all("don't touch me"));
1000        assert_eq!("trim left", trim_all("   trim left"));
1001        assert_eq!("trim right", trim_all("trim right "));
1002        assert_eq!("trim both", trim_all("   trim both  "));
1003        assert_eq!("", trim_all(" "));
1004        assert_eq!("", trim_all("  "));
1005        assert_eq!("a b", trim_all(" a   b "));
1006        assert_eq!("Some example text", trim_all("  Some  example   text  "));
1007    }
1008
1009    #[test]
1010    fn test_trim_all_ignores_other_forms_of_whitespace() {
1011        // \xA0 is a non-breaking space character
1012        assert_eq!(
1013            "\t\u{A0}Some\u{A0} example \u{A0}text\u{A0}\n",
1014            trim_all("\t\u{A0}Some\u{A0}     example   \u{A0}text\u{A0}\n")
1015        );
1016    }
1017
1018    #[test]
1019    fn trim_spaces_works_on_single_characters() {
1020        assert_eq!(trim_all("2").as_ref(), "2");
1021    }
1022
1023    proptest! {
1024        #[test]
1025        fn test_trim_all_doesnt_elongate_strings(s in ".*") {
1026            assert!(trim_all(&s).len() <= s.len())
1027        }
1028
1029        #[test]
1030        fn test_normalize_header_value_works_on_valid_header_value(v in (".*")) {
1031            prop_assume!(HeaderValue::from_str(&v).is_ok());
1032            assert!(normalize_header_value(&v).is_ok());
1033        }
1034
1035        #[test]
1036        fn test_trim_all_does_nothing_when_there_are_no_spaces(s in "[^ ]*") {
1037            assert_eq!(trim_all(&s).as_ref(), s);
1038        }
1039    }
1040}