Skip to main content

scratchstack_aws_signature/
canonical.rs

1//! Canonicalization functionality for signature generation and validation.
2//!
3//! This includes various URL and header canonicalization functions, as well as the ability to
4//! create an AWS SigV4 canonical request.
5//!
6//! **Stability of this module is not guaranteed except for items exposed at the crate root**.
7//! The functions and types are subject to change in minor/patch versions. This is exposed for
8//! testing purposes only.
9
10use {
11    crate::{
12        auth::{SigV4Authenticator, SigV4AuthenticatorBuilder},
13        chronoutil::ParseISO8601,
14        crypto::{sha256, sha256_hex, SHA256_OUTPUT_LEN},
15        SignatureError, SignatureOptions,
16    },
17    bytes::Bytes,
18    chrono::{offset::FixedOffset, DateTime, Utc},
19    encoding_rs::UTF_8,
20    http::{
21        header::{HeaderMap, HeaderValue},
22        request::Parts,
23        uri::Uri,
24    },
25    lazy_static::lazy_static,
26    log::trace,
27    qualifier_attr::qualifiers,
28    regex::Regex,
29    std::{
30        borrow::Cow,
31        collections::HashMap,
32        fmt::{Debug, Formatter, Result as FmtResult},
33        str::from_utf8,
34    },
35};
36
37/// Content-Type string for HTML forms
38const APPLICATION_X_WWW_FORM_URLENCODED: &str = "application/x-www-form-urlencoded";
39
40/// Header parameter for the authorization
41const AUTHORIZATION: &str = "authorization";
42
43/// Algorithm for AWS SigV4
44const AWS4_HMAC_SHA256: &str = "AWS4-HMAC-SHA256";
45
46/// Algorithm for AWS SigV4 (bytes)
47const AWS4_HMAC_SHA256_BYTES: &[u8] = b"AWS4-HMAC-SHA256";
48
49/// Content-Type parameter for specifying the character set
50const CHARSET: &str = "charset";
51
52/// Header field for the content type
53const CONTENT_TYPE: &str = "content-type";
54
55/// Signature field for the access key
56const CREDENTIAL: &[u8] = b"Credential";
57
58/// Header parameter for the date.
59const DATE: &str = "date";
60
61/// Uppercase hex digits.
62const HEX_DIGITS_UPPER: [u8; 16] =
63    [b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'A', b'B', b'C', b'D', b'E', b'F'];
64
65/// Error message: `"Authorization header requires 'Credential' parameter."`
66const MSG_AUTH_HEADER_REQ_CREDENTIAL: &str = "Authorization header requires 'Credential' parameter.";
67
68/// Error message: `"Authorization header requires existence of either a 'X-Amz-Date' or a 'Date' header."`
69const MSG_AUTH_HEADER_REQ_DATE: &str =
70    "Authorization header requires existence of either a 'X-Amz-Date' or a 'Date' header.";
71
72/// Error message: `"Authorization header requires 'Signature' parameter."`
73const MSG_AUTH_HEADER_REQ_SIGNATURE: &str = "Authorization header requires 'Signature' parameter.";
74
75/// Error message: `"Authorization header requires 'SignedHeaders' parameter."`
76const MSG_AUTH_HEADER_REQ_SIGNED_HEADERS: &str = "Authorization header requires 'SignedHeaders' parameter.";
77
78/// Error message: `"'Host' or ':authority' must be a 'SignedHeader' in the AWS Authorization."`
79const MSG_HOST_AUTHORITY_MUST_BE_SIGNED: &str =
80    "'Host' or ':authority' must be a 'SignedHeader' in the AWS Authorization.";
81
82/// Error message: `"Illegal hex character in escape % pattern: %"`
83const MSG_ILLEGAL_HEX_CHAR: &str = "Illegal hex character in escape % pattern: %";
84
85/// Error message: `"Incomplete trailing escape % sequence"`
86const MSG_INCOMPLETE_TRAILING_ESCAPE: &str = "Incomplete trailing escape % sequence";
87
88/// Error message: `"AWS query-string parameters must include 'X-Amz-Credential'"`
89const MSG_QUERY_STRING_MUST_INCLUDE_CREDENTIAL: &str = "AWS query-string parameters must include 'X-Amz-Credential'.";
90
91/// Error message: `"AWS query-string parameters must include 'X-Amz-Sigature'"`
92const MSG_QUERY_STRING_MUST_INCLUDE_SIGNATURE: &str = "AWS query-string parameters must include 'X-Amz-Signature'.";
93
94/// Error message: `"AWS query-string parameters must include 'X-Amz-SignedHeaders'"`
95const MSG_QUERY_STRING_MUST_INCLUDE_SIGNED_HEADERS: &str =
96    "AWS query-string parameters must include 'X-Amz-SignedHeaders'.";
97
98/// Error message: `"AWS query-string parameters must include 'X-Amz-Date'"`
99const MSG_QUERY_STRING_MUST_INCLUDE_DATE: &str = "AWS query-string parameters must include 'X-Amz-Date'.";
100
101/// Error message: `"Re-examine the query-string parameters."`
102const MSG_REEXAMINE_QUERY_STRING_PARAMS: &str = "Re-examine the query-string parameters.";
103
104/// Error message: `"Request is missing Authentication Token"`
105const MSG_REQUEST_MISSING_AUTH_TOKEN: &str = "Request is missing Authentication Token";
106
107/// Error message: `"Unsupported AWS 'algorithm': "`
108const MSG_UNSUPPORTED_ALGORITHM: &str = "Unsupported AWS 'algorithm': ";
109
110/// Signature field for the signature itself
111const SIGNATURE: &[u8] = b"Signature";
112
113/// Authorization header parameter specifying the signed headers
114const SIGNED_HEADERS: &[u8] = b"SignedHeaders";
115
116/// Query parameter for the signature algorithm
117const X_AMZ_ALGORITHM: &str = "X-Amz-Algorithm";
118
119/// Query parameter for delivering the access key
120const X_AMZ_CREDENTIAL: &str = "X-Amz-Credential";
121
122/// Query parameter for delivering the date
123const X_AMZ_DATE: &str = "X-Amz-Date";
124
125/// Header for delivering the alternate date
126const X_AMZ_DATE_LOWER: &str = "x-amz-date";
127
128/// Query parameter for delivering the session token
129const X_AMZ_SECURITY_TOKEN: &str = "X-Amz-Security-Token";
130
131/// Header for delivering the session token
132const X_AMZ_SECURITY_TOKEN_LOWER: &str = "x-amz-security-token";
133
134/// Query parameter for delivering the signature
135const X_AMZ_SIGNATURE: &str = "X-Amz-Signature";
136
137/// Query parameter specifying the signed headers
138const X_AMZ_SIGNED_HEADERS: &str = "X-Amz-SignedHeaders";
139
140lazy_static! {
141    /// Multiple slash pattern for condensing URIs
142    static ref MULTISLASH: Regex = Regex::new("//+").unwrap();
143
144    /// Multiple space pattern for condensing header values
145    static ref MULTISPACE: Regex = Regex::new("  +").unwrap();
146
147    /// Pattern for the start of an AWS4 signature Authorization header.
148    static ref AWS4_HMAC_SHA256_RE: Regex = Regex::new(r"\s*AWS4-HMAC-SHA256(?:\s+|$)").unwrap();
149}
150
151/// Authentication parameters extracted from the header or query string.
152#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
153#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
154#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
155#[derive(Debug)]
156struct AuthParams {
157    /// Builder for creating an authenticator.
158    pub builder: SigV4AuthenticatorBuilder,
159
160    /// The headers that are required to be signed in the request.
161    pub signed_headers: Vec<String>,
162
163    /// The timestamp string for the request in YYYYMMDD'T'HHMMSS'Z' format.
164    pub timestamp_str: String,
165}
166
167/// A canonicalized request for AWS SigV4.
168///
169/// This is mainly used internally for generating the canonical request for signing, but is
170/// exposed for testing and debugging purposes.
171///
172/// **The stability of this struct is not guaranteed.** The fields and methods are subject to
173/// change in minor/patch versions.
174#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
175#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
176#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
177#[derive(Clone)]
178struct CanonicalRequest {
179    /// The HTTP method for the request (e.g., "GET", "POST", etc.)
180    request_method: String,
181
182    /// The canonicalized path from the HTTP request. This is guaranteed to be ASCII.
183    canonical_path: String,
184
185    /// Query parameters from the HTTP request. Values are ordered as they appear in the URL. If a
186    /// request body is present and of type `application/x-www-form-urlencoded` and
187    /// [`SignatureOptions`] includes `url_encode_form`, the request body is parsed and added as
188    /// query parameters.
189    query_parameters: HashMap<String, Vec<String>>,
190
191    /// Headers from the HTTP request. Values are ordered as they appear in the HTTP request.
192    ///
193    /// The encoding of header values is Latin 1 (ISO 8859-1), apart from a few oddities like Content-Disposition.
194    headers: HashMap<String, Vec<Vec<u8>>>,
195
196    /// The SHA-256 hash of the body.
197    body_sha256: String,
198}
199
200impl CanonicalRequest {
201    /// Create a CanonicalRequest from an HTTP request [Parts] and a body of [Bytes].
202    #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
203    #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
204    #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
205    fn from_request_parts(
206        mut parts: Parts,
207        mut body: Bytes,
208        options: SignatureOptions,
209    ) -> Result<(Self, Parts, Bytes), SignatureError> {
210        let canonical_path = canonicalize_uri_path(parts.uri.path(), options.s3)?;
211        let content_type = get_content_type_and_charset(&parts.headers);
212        let mut query_parameters = query_string_to_normalized_map(parts.uri.query().unwrap_or(""))?;
213
214        if options.url_encode_form {
215            // Treat requests with application/x-www-form-urlencoded bodies as if they were passed into the query string.
216            if let Some(content_type) = content_type {
217                if content_type.content_type == APPLICATION_X_WWW_FORM_URLENCODED {
218                    trace!("Body is application/x-www-form-urlencoded; converting to query parameters");
219
220                    let encoding = match &content_type.charset {
221                        Some(charset) => match encoding_rs::Encoding::for_label(charset.as_bytes()) {
222                            Some(encoding) => encoding,
223                            None => {
224                                return Err(SignatureError::InvalidBodyEncoding(format!(
225                                    "application/x-www-form-urlencoded body uses unsupported charset '{}'",
226                                    charset
227                                )))
228                            }
229                        },
230                        None => {
231                            trace!("Falling back to UTF-8 for application/x-www-form-urlencoded body");
232                            UTF_8
233                        }
234                    };
235
236                    let (body_query, _, has_errors) = encoding.decode(&body);
237                    if has_errors {
238                        return Err(SignatureError::InvalidBodyEncoding(format!(
239                            "Invalid body data encountered parsing application/x-www-form-urlencoded with charset '{}'",
240                            encoding.name()
241                        )));
242                    }
243
244                    query_parameters.extend(query_string_to_normalized_map(&body_query)?);
245                    // Rebuild the parts URI with the new query string.
246                    let qs = canonicalize_query_to_string(&query_parameters);
247                    trace!("Rebuilding URI with new query string: {}", qs);
248
249                    let mut pq = canonical_path.clone();
250                    if !qs.is_empty() {
251                        pq.push('?');
252                        pq.push_str(&qs);
253                    }
254
255                    parts.uri =
256                        Uri::builder().path_and_query(pq).build().expect("failed to rebuild URI with new query string");
257                    body = Bytes::from("");
258                }
259            }
260        }
261
262        let headers = normalize_headers(&parts.headers);
263        let body_sha256 = sha256_hex(body.as_ref());
264
265        Ok((
266            CanonicalRequest {
267                request_method: parts.method.to_string(),
268                canonical_path,
269                query_parameters,
270                headers,
271                body_sha256,
272            },
273            parts,
274            body,
275        ))
276    }
277
278    /// Retrieve the HTTP request method.
279    #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
280    #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
281    #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
282    #[inline(always)]
283    fn request_method(&self) -> &str {
284        &self.request_method
285    }
286
287    /// Retrieve the canonicalized URI path from the request.
288    #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
289    #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
290    #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
291    #[inline(always)]
292    fn canonical_path(&self) -> &str {
293        &self.canonical_path
294    }
295
296    /// Retrieve the query parameters from the request. Values are ordered as they appear in the URL, followed by any
297    /// values in the request body if the request body is of type `application/x-www-form-urlencoded`. Values are
298    /// normalized to be percent-encoded.
299    #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
300    #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
301    #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
302    #[inline(always)]
303    fn query_parameters(&self) -> &HashMap<String, Vec<String>> {
304        &self.query_parameters
305    }
306
307    /// Retrieve the headers from the request. Values are ordered as they appear in the HTTP request.
308    #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
309    #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
310    #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
311    #[inline(always)]
312    fn headers(&self) -> &HashMap<String, Vec<Vec<u8>>> {
313        &self.headers
314    }
315
316    /// Retrieve the SHA-256 hash of the request body.
317    #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
318    #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
319    #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
320    #[inline(always)]
321    fn body_sha256(&self) -> &str {
322        &self.body_sha256
323    }
324
325    /// Get the canonical query string from the request.
326    #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
327    #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
328    #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
329    fn canonical_query_string(&self) -> String {
330        canonicalize_query_to_string(&self.query_parameters)
331    }
332
333    /// Get the [canonical request to hash](https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html)
334    /// for the request.
335    #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
336    #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
337    #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
338    fn canonical_request(&self, signed_headers: &Vec<String>) -> Vec<u8> {
339        let mut result = Vec::with_capacity(1024);
340        result.extend(self.request_method().as_bytes());
341        result.push(b'\n');
342        result.extend(self.canonical_path().as_bytes());
343        result.push(b'\n');
344        result.extend(self.canonical_query_string().as_bytes());
345        result.push(b'\n');
346
347        for header in signed_headers {
348            let values = self.headers.get(header);
349            if let Some(values) = values {
350                for (i, value) in values.iter().enumerate() {
351                    if i == 0 {
352                        result.extend(header.as_bytes());
353                        result.push(b':');
354                    } else {
355                        result.push(b',');
356                    }
357                    result.extend(value);
358                }
359                result.push(b'\n')
360            }
361        }
362
363        result.push(b'\n');
364        result.extend(signed_headers.join(";").as_bytes());
365        result.push(b'\n');
366        result.extend(self.body_sha256().as_bytes());
367
368        trace!("Canonical request:\n{}", String::from_utf8_lossy(&result));
369
370        result
371    }
372
373    /// Get the SHA-256 hash of the [canonical request](https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html).
374    #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
375    #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
376    #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
377    fn canonical_request_sha256(&self, signed_headers: &Vec<String>) -> [u8; SHA256_OUTPUT_LEN] {
378        let canonical_request = self.canonical_request(signed_headers);
379        let result_digest = sha256(&canonical_request);
380        let result_slice = result_digest.as_ref();
381        assert!(result_slice.len() == SHA256_OUTPUT_LEN);
382        let mut result: [u8; SHA256_OUTPUT_LEN] = [0; SHA256_OUTPUT_LEN];
383        result.as_mut_slice().clone_from_slice(result_slice);
384        result
385    }
386
387    /// Create a [SigV4Authenticator] for the request. This performs steps 1-8 from the AWS Auth Error Ordering
388    /// workflow.
389    #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
390    #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
391    #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
392    fn get_authenticator<S>(&self, signed_header_requirements: &S) -> Result<SigV4Authenticator, SignatureError>
393    where
394        S: SignedHeaderRequirements,
395    {
396        let auth_params = self.get_auth_parameters(signed_header_requirements)?;
397        self.get_authenticator_from_auth_parameters(auth_params)
398    }
399
400    /// Create an authenticator based on the provided [`AuthParams`].
401    #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
402    #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
403    #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
404    fn get_authenticator_from_auth_parameters(
405        &self,
406        auth_params: AuthParams,
407    ) -> Result<SigV4Authenticator, SignatureError> {
408        // Rule 9: The date must be in ISO 8601 format.
409        let timestamp_str = auth_params.timestamp_str.as_str();
410        let timestamp = DateTime::<FixedOffset>::parse_from_iso8601(timestamp_str)
411            .map_err(|_| {
412                SignatureError::IncompleteSignature(format!(
413                    "Date must be in ISO-8601 'basic format'. Got '{}'. See http://en.wikipedia.org/wiki/ISO_8601",
414                    auth_params.timestamp_str
415                ))
416            })?
417            .with_timezone(&Utc);
418        let mut builder = auth_params.builder;
419        builder.request_timestamp(timestamp);
420
421        let signed_headers = auth_params.signed_headers;
422
423        // Create the canonical request.
424        builder.canonical_request_sha256(self.canonical_request_sha256(&signed_headers));
425
426        Ok(builder.build().expect("all fields should be set"))
427    }
428
429    /// Create an [AuthParams] structure, either from the `Authorization` header or the query strings as appropriate.
430    /// This performs step 5 and either performs steps 6a-6d or 7a-7d from the AWS Auth Error Ordering workflow.
431    #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
432    #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
433    #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
434    fn get_auth_parameters<S>(&self, signed_header_requirements: &S) -> Result<AuthParams, SignatureError>
435    where
436        S: SignedHeaderRequirements,
437    {
438        let auth_header = self.headers().get(AUTHORIZATION);
439        let sig_algs = self.query_parameters().get(X_AMZ_ALGORITHM);
440
441        // Rule 5: Either the Authorization header or X-Amz-Algorithm query parameter must be present, not both.
442        let params = match (auth_header, sig_algs) {
443            // Use first header (per rule 6a).
444            (Some(auth_header), None) => self.get_auth_parameters_from_auth_header(&auth_header[0])?,
445            // Use first algorithm (per rule 7a).
446            (None, Some(sig_algs)) => self.get_auth_parameters_from_query_parameters(&sig_algs[0])?,
447            (Some(_), Some(_)) => return Err(SignatureError::SignatureDoesNotMatch(None)),
448            (None, None) => {
449                return Err(SignatureError::MissingAuthenticationToken(MSG_REQUEST_MISSING_AUTH_TOKEN.to_string()))
450            }
451        };
452
453        // Rule 8: SignedHeaders must include "Host" or ":authority".
454        let mut found_host = false;
455        for header in &params.signed_headers {
456            if header == "host" || header == ":authority" {
457                found_host = true;
458                break;
459            }
460        }
461        if !found_host {
462            return Err(SignatureError::SignatureDoesNotMatch(Some(MSG_HOST_AUTHORITY_MUST_BE_SIGNED.to_string())));
463        }
464
465        for header in signed_header_requirements.always_present() {
466            let header_lower = header.to_lowercase();
467            if !params.signed_headers.contains(&header_lower) {
468                return Err(SignatureError::SignatureDoesNotMatch(Some(format!(
469                    "'{}' must be a 'SignedHeader' in the AWS Authorization.",
470                    header
471                ))));
472            }
473        }
474
475        for header in signed_header_requirements.if_in_request() {
476            let header_lower = header.to_lowercase();
477            if self.headers.contains_key(&header_lower) && !params.signed_headers.contains(&header_lower) {
478                return Err(SignatureError::SignatureDoesNotMatch(Some(format!(
479                    "'{}' must be a 'SignedHeader' in the AWS Authorization.",
480                    header
481                ))));
482            }
483        }
484
485        for header in signed_header_requirements.prefixes() {
486            let header_lower = header.to_lowercase();
487            for http_header in self.headers.keys() {
488                if http_header.starts_with(&header_lower) && !params.signed_headers.contains(http_header) {
489                    return Err(SignatureError::SignatureDoesNotMatch(Some(format!(
490                        "'{}' must be a 'SignedHeader' in the AWS Authorization.",
491                        http_header
492                    ))));
493                }
494            }
495        }
496
497        Ok(params)
498    }
499
500    /// Create an [`AuthParams`] structure from the `Authorization` header. This performs steps 6a-6d of the AWS Auth
501    /// Error Ordering workflow.
502    #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
503    #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
504    #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
505    fn get_auth_parameters_from_auth_header<'a>(&'a self, auth_header: &'a [u8]) -> Result<AuthParams, SignatureError> {
506        // Interpret the header as Latin-1, trimmed.
507        let auth_header = trim_ascii(auth_header);
508
509        // Rule 6a: Make sure the Authorization header starts with "AWS4-HMAC-SHA256".
510        let parts = auth_header.splitn(2, |c| *c == b' ').collect::<Vec<&'a [u8]>>();
511        let algorithm = parts[0];
512        if algorithm != AWS4_HMAC_SHA256_BYTES {
513            return Err(SignatureError::IncompleteSignature(format!(
514                "{}'{}'.",
515                MSG_UNSUPPORTED_ALGORITHM,
516                String::from_utf8_lossy(algorithm)
517            )));
518        }
519
520        let parameters = if parts.len() > 1 {
521            parts[1]
522        } else {
523            b""
524        };
525
526        // Split the parameters by commas; trim each one; then split into key=value pairs.
527        let mut parameter_map = HashMap::new();
528        for parameter_untrimmed in parameters.split(|c| *c == b',') {
529            let parameter = trim_ascii(parameter_untrimmed);
530
531            // Needed if we have no parameters at all; this loop will always run at least once.
532            if parameter.is_empty() {
533                continue;
534            }
535
536            let parts = parameter.splitn(2, |c| *c == b'=').collect::<Vec<&'a [u8]>>();
537
538            // Rule 6b: All parameters must be in key=value format.
539            if parts.len() != 2 {
540                return Err(SignatureError::IncompleteSignature(format!(
541                    "'{}' not a valid key=value pair (missing equal-sign) in Authorization header: '{}'",
542                    latin1_to_string(parameter),
543                    latin1_to_string(auth_header)
544                )));
545            }
546
547            // Rule 6c: Use the last value for each key; overwriting is ok.
548            parameter_map.insert(parts[0], parts[1]);
549        }
550
551        // Rule 6d: ensure all authorization header parameters/headers are present.
552        let mut missing_messages = Vec::new();
553        let mut builder = SigV4Authenticator::builder();
554
555        if let Some(credential) = parameter_map.get(CREDENTIAL) {
556            builder.credential(latin1_to_string(credential));
557        } else {
558            missing_messages.push(MSG_AUTH_HEADER_REQ_CREDENTIAL);
559        }
560
561        if let Some(signature) = parameter_map.get(SIGNATURE) {
562            builder.signature(latin1_to_string(signature));
563        } else {
564            missing_messages.push(MSG_AUTH_HEADER_REQ_SIGNATURE);
565        }
566
567        let mut signed_headers = if let Some(signed_headers) = parameter_map.get(SIGNED_HEADERS) {
568            signed_headers.split(|c| *c == b';').map(latin1_to_string).collect()
569        } else {
570            missing_messages.push(MSG_AUTH_HEADER_REQ_SIGNED_HEADERS);
571            Vec::new()
572        };
573        signed_headers.sort();
574
575        let mut timestamp_str = None;
576
577        if let Some(date) = self.headers.get(X_AMZ_DATE_LOWER) {
578            // Rule 6e: Use the first X-Amz-Date header (per rule 6a).
579            timestamp_str = Some(latin1_to_string(&date[0]));
580        } else if let Some(date) = self.headers.get(DATE) {
581            // Rule 6e: Use the first Date header (per rule 6a).
582            timestamp_str = Some(latin1_to_string(&date[0]));
583        } else {
584            missing_messages.push(MSG_AUTH_HEADER_REQ_DATE);
585        }
586
587        if !missing_messages.is_empty() {
588            return Err(SignatureError::IncompleteSignature(format!(
589                "{} Authorization={}",
590                missing_messages.join(" "),
591                latin1_to_string(algorithm)
592            )));
593        }
594
595        // Get the session token if present.
596        if let Some(token) = self.headers.get(X_AMZ_SECURITY_TOKEN_LOWER) {
597            builder.session_token(latin1_to_string(&token[0]));
598        }
599
600        // Return the builder and the date.
601        let timestamp_str = timestamp_str.expect("date_str should be set");
602        Ok(AuthParams {
603            builder,
604            signed_headers,
605            timestamp_str,
606        })
607    }
608
609    /// Create an [`AuthParams`] structure from the query parameters. This performs steps 7a-7d of the AWS Auth
610    /// Error Ordering workflow.
611    #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
612    #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
613    #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
614    fn get_auth_parameters_from_query_parameters(&self, query_alg: &str) -> Result<AuthParams, SignatureError> {
615        // Rule 7a: Make sure the X-Amz-Algorithm query parameter is "AWS4-HMAC-SHA256".
616        if query_alg != AWS4_HMAC_SHA256 {
617            return Err(SignatureError::MissingAuthenticationToken(MSG_REQUEST_MISSING_AUTH_TOKEN.to_string()));
618        }
619
620        let mut missing_messages = Vec::new();
621        let mut builder = SigV4Authenticator::builder();
622
623        // Rule 7c: Use the first value for each key.
624        if let Some(credential) = self.query_parameters.get(X_AMZ_CREDENTIAL) {
625            builder.credential(credential[0].clone());
626        } else {
627            missing_messages.push(MSG_QUERY_STRING_MUST_INCLUDE_CREDENTIAL);
628        }
629
630        if let Some(signature) = self.query_parameters.get(X_AMZ_SIGNATURE) {
631            builder.signature(signature[0].clone());
632        } else {
633            missing_messages.push(MSG_QUERY_STRING_MUST_INCLUDE_SIGNATURE);
634        }
635
636        let mut signed_headers = if let Some(signed_headers) = self.query_parameters.get(X_AMZ_SIGNED_HEADERS) {
637            let unescaped_signed_headers = unescape_uri_encoding(&signed_headers[0]);
638            unescaped_signed_headers.split(';').map(|s| s.to_string()).collect::<Vec<String>>()
639        } else {
640            missing_messages.push(MSG_QUERY_STRING_MUST_INCLUDE_SIGNED_HEADERS);
641            Vec::new()
642        };
643        signed_headers.sort();
644
645        let timestamp_str = self.query_parameters.get(X_AMZ_DATE);
646        if timestamp_str.is_none() {
647            missing_messages.push(MSG_QUERY_STRING_MUST_INCLUDE_DATE);
648        }
649
650        if !missing_messages.is_empty() {
651            return Err(SignatureError::IncompleteSignature(format!(
652                "{} {}",
653                missing_messages.join(" "),
654                MSG_REEXAMINE_QUERY_STRING_PARAMS
655            )));
656        }
657
658        // Get the session token if present.
659        if let Some(token) = self.query_parameters.get(X_AMZ_SECURITY_TOKEN) {
660            builder.session_token(token[0].clone());
661        }
662
663        let timestamp_str = timestamp_str.expect("date_str should be set")[0].clone();
664        Ok(AuthParams {
665            builder,
666            signed_headers,
667            timestamp_str,
668        })
669    }
670}
671
672impl Debug for CanonicalRequest {
673    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
674        let headers = debug_headers(&self.headers);
675
676        f.debug_struct("CanonicalRequest")
677            .field("request_method", &self.request_method)
678            .field("canonical_path", &self.canonical_path)
679            .field("query_parameters", &self.query_parameters)
680            .field("headers", &headers)
681            .field("body_sha256", &self.body_sha256)
682            .finish()
683    }
684}
685
686/// The Content-Type header value, along with the character set (if specified).
687#[derive(Debug, Clone, PartialEq, Eq)]
688#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
689#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
690#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
691struct ContentTypeCharset {
692    /// The content type of the body.
693    pub content_type: String,
694
695    /// The encoding (charset) of the body.
696    pub charset: Option<String>,
697}
698
699/// Trait for informing validation routines indicating which headers must be signed in addition to
700/// the standard AWS SigV4 headers.
701pub trait SignedHeaderRequirements {
702    /// Return the headers that must always be present in SignedHeaders.
703    fn always_present(&self) -> &[Cow<'_, str>];
704
705    /// Return the headers that must be present in SignedHeaders if they are present in the request.
706    fn if_in_request(&self) -> &[Cow<'_, str>];
707
708    /// Return the prefixes that must be present in SignedHeaders if any headers with that prefix.
709    fn prefixes(&self) -> &[Cow<'_, str>];
710}
711
712/// Static implementation of [SignedHeaderRequirements] that uses slices of string slices.
713#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
714pub struct SliceSignedHeaderRequirements<'a, 'b, 'c> {
715    /// Headers that must always be present in SignedHeaders.
716    always_present: &'a [Cow<'a, str>],
717
718    /// Headers that must be present in SignedHeaders if they are present in the request.
719    if_in_request: &'b [Cow<'b, str>],
720
721    /// Prefixes that must be present in SignedHeaders if any headers with that prefix are present in the request.
722    prefixes: &'c [Cow<'c, str>],
723}
724
725impl<'a, 'b, 'c> SignedHeaderRequirements for SliceSignedHeaderRequirements<'a, 'b, 'c> {
726    #[inline(always)]
727    fn always_present(&self) -> &[Cow<'_, str>] {
728        self.always_present
729    }
730
731    #[inline(always)]
732    fn if_in_request(&self) -> &[Cow<'_, str>] {
733        self.if_in_request
734    }
735
736    #[inline(always)]
737    fn prefixes(&self) -> &[Cow<'_, str>] {
738        self.prefixes
739    }
740}
741
742impl<'a, 'b, 'c> SliceSignedHeaderRequirements<'a, 'b, 'c> {
743    /// Create a new `SliceSignedHeaderRequirements` structure from the provided data.
744    pub const fn new(
745        always_present: &'a [Cow<'a, str>],
746        if_in_request: &'b [Cow<'b, str>],
747        prefixes: &'c [Cow<'c, str>],
748    ) -> Self {
749        SliceSignedHeaderRequirements {
750            always_present,
751            if_in_request,
752            prefixes,
753        }
754    }
755}
756
757/// SignedHeaderRequirements from constant slices.
758pub type ConstSignedHeaderRequirements = SliceSignedHeaderRequirements<'static, 'static, 'static>;
759
760/// Constant [`SignedHeaderRequirements`] value to use when no additional signed headers are
761/// required.
762pub const NO_ADDITIONAL_SIGNED_HEADERS: ConstSignedHeaderRequirements =
763    ConstSignedHeaderRequirements::new(&[], &[], &[]);
764
765/// `SignedHeaderRequirements` that can be dynamically changed.
766#[derive(Clone, Debug, Default, PartialEq, Eq)]
767pub struct VecSignedHeaderRequirements {
768    /// Headers that must always be present in SignedHeaders.
769    always_present: Vec<Cow<'static, str>>,
770
771    /// Headers that must be present in SignedHeaders if they are present in the request.
772    if_in_request: Vec<Cow<'static, str>>,
773
774    /// Prefixes that must be present in SignedHeaders if any headers with that prefix are present in the request.
775    prefixes: Vec<Cow<'static, str>>,
776}
777
778impl SignedHeaderRequirements for VecSignedHeaderRequirements {
779    #[inline(always)]
780    fn always_present(&self) -> &[Cow<'_, str>] {
781        &self.always_present
782    }
783
784    #[inline(always)]
785    fn if_in_request(&self) -> &[Cow<'_, str>] {
786        &self.if_in_request
787    }
788
789    #[inline(always)]
790    fn prefixes(&self) -> &[Cow<'_, str>] {
791        &self.prefixes
792    }
793}
794
795impl VecSignedHeaderRequirements {
796    /// Create a new `VecSignedHeaderRequirements` structure from the provided data.
797    pub fn new<A, B, C>(always_present: &[&A], if_in_request: &[&B], prefixes: &[&C]) -> Self
798    where
799        for<'a> &'a A: Into<String>,
800        for<'b> &'b B: Into<String>,
801        for<'c> &'c C: Into<String>,
802        A: ?Sized,
803        B: ?Sized,
804        C: ?Sized,
805    {
806        let always_present = always_present.iter().map(|s| Cow::Owned((*s).into())).collect();
807        let if_in_request = if_in_request.iter().map(|s| Cow::Owned((*s).into())).collect();
808        let prefixes = prefixes.iter().map(|s| Cow::Owned((*s).into())).collect();
809
810        VecSignedHeaderRequirements {
811            always_present,
812            if_in_request,
813            prefixes,
814        }
815    }
816
817    /// Add a header that must always be present in `SignedHeaders`.
818    pub fn add_always_present(&mut self, header: &str) {
819        let header_lower = header.to_ascii_lowercase();
820
821        for h in self.always_present.iter() {
822            if h == &header_lower {
823                return;
824            }
825        }
826
827        self.always_present.push(Cow::Owned(header.to_string()));
828    }
829
830    /// Add a header that must be present in `SignedHeaders` if it is present in the request.
831    pub fn add_if_in_request(&mut self, header: &str) {
832        let header_lower = header.to_ascii_lowercase();
833
834        for h in self.if_in_request.iter() {
835            if h == &header_lower {
836                return;
837            }
838        }
839
840        self.if_in_request.push(Cow::Owned(header.to_string()));
841    }
842
843    /// Add a prefix that must be present in `SignedHeaders` if any headers with that prefix are
844    /// present in the request.
845    pub fn add_prefix(&mut self, prefix: &str) {
846        let prefix_lower = prefix.to_ascii_lowercase();
847
848        for h in self.prefixes.iter() {
849            if h == &prefix_lower {
850                return;
851            }
852        }
853
854        self.prefixes.push(Cow::Owned(prefix.to_string()));
855    }
856
857    /// Remove a header that must always be present in `SignedHeaders`.
858    pub fn remove_always_present(&mut self, header: &str) {
859        let header = header.to_ascii_lowercase();
860        self.always_present.retain(|h| h.to_ascii_lowercase() != header);
861    }
862
863    /// Remove a header that must be present in `SignedHeaders` if it is present in the request.
864    pub fn remove_if_in_request(&mut self, header: &str) {
865        let header = header.to_ascii_lowercase();
866        self.if_in_request.retain(|h| h.to_ascii_lowercase() != header);
867    }
868
869    /// Remove a prefix that must be present in `SignedHeaders` if any headers with that prefix are
870    /// present in the request.
871    pub fn remove_prefix(&mut self, prefix: &str) {
872        let prefix = prefix.to_ascii_lowercase();
873        self.prefixes.retain(|h| h.to_ascii_lowercase() != prefix);
874    }
875}
876
877/// Indicates whether we are normalizing a URI path element or a query string element. This is used to create the
878/// correct error message.
879#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
880#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
881#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
882enum UriElement {
883    /// URI element represents a path
884    Path,
885
886    /// URI element represents a query string
887    Query,
888}
889
890/// Convert a [`HashMap`] of query parameters to a string for the canonical request.
891#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
892#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
893#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
894pub fn canonicalize_query_to_string(query_parameters: &HashMap<String, Vec<String>>) -> String {
895    let mut results = Vec::new();
896
897    for (key, values) in query_parameters.iter() {
898        // Don't include the signature itself.
899        if key != X_AMZ_SIGNATURE {
900            for value in values.iter() {
901                results.push(format!("{}={}", key, value));
902            }
903        }
904    }
905
906    results.sort_unstable();
907    results.join("&")
908}
909
910/// Normalizes the specified URI path, removing redundant slashes and relative path components (unless performing S3
911/// canonicalization).
912#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
913#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
914#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
915pub fn canonicalize_uri_path(uri_path: &str, s3: bool) -> Result<String, SignatureError> {
916    // Special case: empty path is converted to '/'; also short-circuit the usual '/' path here.
917    if uri_path.is_empty() || uri_path == "/" {
918        return Ok("/".to_string());
919    }
920
921    // All other paths must be abolute.
922    if !uri_path.starts_with('/') {
923        return Err(SignatureError::InvalidURIPath(format!("Path is not absolute: {}", uri_path)));
924    }
925
926    let uri_path = if s3 {
927        Cow::Borrowed(uri_path)
928    } else {
929        // Replace double slashes; this makes it easier to handle slashes at the end.
930        MULTISLASH.replace_all(uri_path, "/")
931    };
932
933    // Examine each path component for relative directories.
934    let mut components: Vec<String> = uri_path.split('/').map(|s| s.to_string()).collect();
935    let mut i = 1; // Ignore the leading "/"
936    while i < components.len() {
937        let component = normalize_uri_path_component(&components[i])?;
938
939        if component == "." && !s3 {
940            // Relative path: current directory; remove this.
941            components.remove(i);
942
943            // Don't increment i; with the deletion, we're now pointing to the next element in the path.
944        } else if component == ".." && !s3 {
945            // Relative path: parent directory.  Remove this and the previous component.
946
947            if i <= 1 {
948                // This isn't allowed at the beginning!
949                return Err(SignatureError::InvalidURIPath(format!(
950                    "Relative path entry '..' navigates above root: {}",
951                    uri_path
952                )));
953            }
954
955            components.remove(i - 1);
956            components.remove(i - 1);
957
958            // Since we've deleted two components, we need to back up one to examine what's now the next component.
959            i -= 1;
960        } else {
961            // Leave it alone; proceed to the next component.
962            components[i] = component;
963            i += 1;
964        }
965    }
966
967    assert!(!components.is_empty());
968    match components.len() {
969        1 => Ok("/".to_string()),
970        _ => Ok(components.join("/")),
971    }
972}
973
974/// Formats HTTP headers in a HashMap suitable for debugging.
975#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
976#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
977#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
978fn debug_headers(headers: &HashMap<String, Vec<Vec<u8>>>) -> String {
979    use std::io::Write;
980    let mut result = Vec::new();
981    for (key, values) in headers.iter() {
982        for value in values {
983            match String::from_utf8(value.clone()) {
984                Ok(s) => writeln!(result, "{}: {}", key, s).unwrap(),
985                Err(_) => writeln!(result, "{}: {:?}", key, value).unwrap(),
986            }
987        }
988    }
989
990    if result.is_empty() {
991        return String::new();
992    }
993
994    // Remove the last newline.
995    let result_except_last = &result[..result.len() - 1];
996    String::from_utf8_lossy(result_except_last).to_string()
997}
998
999/// Get the content type and character set used in the body
1000#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
1001#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
1002#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
1003fn get_content_type_and_charset(headers: &HeaderMap<HeaderValue>) -> Option<ContentTypeCharset> {
1004    let content_type_opts = match headers.get(CONTENT_TYPE) {
1005        Some(value) => value.as_ref(),
1006        None => return None,
1007    };
1008
1009    let mut parts = content_type_opts.split(|c| *c == b';').map(trim_ascii);
1010    let content_type = latin1_to_string(parts.next().expect("split always returns at least one element"));
1011
1012    for option in parts {
1013        let opt_trim = trim_ascii(option);
1014        let mut opt_parts = opt_trim.splitn(2, |c| *c == b'=');
1015
1016        let opt_name = opt_parts.next().unwrap();
1017        if latin1_to_string(opt_name).to_lowercase() == CHARSET {
1018            if let Some(opt_value) = opt_parts.next() {
1019                return Some(ContentTypeCharset {
1020                    content_type,
1021                    charset: Some(latin1_to_string(opt_value)),
1022                });
1023            }
1024        }
1025    }
1026
1027    Some(ContentTypeCharset {
1028        content_type,
1029        charset: None,
1030    })
1031}
1032
1033/// Indicates whether the specified byte is RFC3986 unreserved -- i.e., can be represented without being
1034/// percent-encoded, e.g. '?' -> '%3F'.
1035#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
1036#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
1037#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
1038#[inline(always)]
1039pub fn is_rfc3986_unreserved(c: u8) -> bool {
1040    c.is_ascii_alphanumeric() || c == b'-' || c == b'.' || c == b'_' || c == b'~'
1041}
1042
1043/// Convert a Latin-1 slice of bytes to a UTF-8 string.
1044#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
1045#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
1046#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
1047pub fn latin1_to_string(bytes: &[u8]) -> String {
1048    let mut result = String::new();
1049    for b in bytes {
1050        result.push(*b as char);
1051    }
1052    result
1053}
1054
1055/// Returns a sorted dictionary containing the header names and their values.
1056#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
1057#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
1058#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
1059pub fn normalize_headers(headers: &HeaderMap<HeaderValue>) -> HashMap<String, Vec<Vec<u8>>> {
1060    let mut result = HashMap::<String, Vec<Vec<u8>>>::new();
1061    for (key, value) in headers.iter() {
1062        let key = key.as_str().to_lowercase();
1063        let value = normalize_header_value(value.as_bytes());
1064        result.entry(key).or_default().push(value);
1065    }
1066
1067    result
1068}
1069
1070/// Normalizes a header value by trimming whitespace and converting multiple spaces to a single space.
1071#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
1072#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
1073#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
1074pub fn normalize_header_value(value: &[u8]) -> Vec<u8> {
1075    let mut result = Vec::with_capacity(value.len());
1076
1077    // Remove leading whitespace and reduce multiple spaces to a single space.
1078    let mut last_was_space = true;
1079
1080    for c in value {
1081        if *c == b' ' {
1082            if !last_was_space {
1083                result.push(b' ');
1084                last_was_space = true;
1085            }
1086        } else {
1087            result.push(*c);
1088            last_was_space = false;
1089        }
1090    }
1091
1092    if last_was_space {
1093        // Remove trailing spaces.
1094        while result.last() == Some(&b' ') {
1095            result.pop();
1096        }
1097    }
1098
1099    result
1100}
1101
1102/// Normalize a single element (key or value from key=value) of a query string.
1103#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
1104#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
1105#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
1106pub fn normalize_query_string_element(element: &str) -> Result<String, SignatureError> {
1107    normalize_uri_element(element, UriElement::Query)
1108}
1109
1110/// Normalizes a path element of a URI.
1111#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
1112#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
1113#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
1114pub fn normalize_uri_path_component(path: &str) -> Result<String, SignatureError> {
1115    normalize_uri_element(path, UriElement::Path)
1116}
1117
1118/// Normalize the URI or query string according to RFC 3986.  This performs the following operations:
1119/// * Alpha, digit, and the symbols `-`, `.`, `_`, and `~` (unreserved characters) are left alone.
1120/// * Characters outside this range are percent-encoded.
1121/// * Percent-encoded values are upper-cased (`%2a` becomes `%2A`)
1122/// * Percent-encoded values in the unreserved space (`%41`-`%5A`, `%61`-`%7A`, `%30`-`%39`, `%2D`, `%2E`, `%5F`,
1123///   `%7E`) are converted to normal characters.
1124///
1125/// If a percent encoding is incomplete, an error is returned.
1126#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
1127#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
1128#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
1129fn normalize_uri_element(uri_el: &str, uri_el_type: UriElement) -> Result<String, SignatureError> {
1130    let path_component = uri_el.as_bytes();
1131    let mut i = 0;
1132    let result = &mut Vec::<u8>::new();
1133
1134    while i < path_component.len() {
1135        let c = path_component[i];
1136
1137        if is_rfc3986_unreserved(c) {
1138            result.push(c);
1139            i += 1;
1140        } else if c == b'%' {
1141            if i + 2 >= path_component.len() {
1142                // % encoding would go beyond end of string.
1143                return Err(match uri_el_type {
1144                    UriElement::Path => {
1145                        // AWS Auth Error Ordering Rule 1.
1146                        SignatureError::InvalidURIPath(MSG_INCOMPLETE_TRAILING_ESCAPE.to_string())
1147                    }
1148                    UriElement::Query => {
1149                        // AWS Auth Error Ordering Rule 4.
1150                        SignatureError::MalformedQueryString(MSG_INCOMPLETE_TRAILING_ESCAPE.to_string())
1151                    }
1152                });
1153            }
1154
1155            let hex_digits = &path_component[i + 1..i + 3];
1156            match hex::decode(hex_digits) {
1157                Ok(value) => {
1158                    assert_eq!(value.len(), 1);
1159                    let c = value[0];
1160
1161                    if is_rfc3986_unreserved(c) {
1162                        result.push(c);
1163                    } else {
1164                        // Rewrite the hex-escape so it's always upper-cased.
1165                        result.push(b'%');
1166                        result.extend(u8_to_upper_hex(c));
1167                    }
1168                    i += 3;
1169                }
1170                Err(_) => {
1171                    let message = format!("{}{}{}", MSG_ILLEGAL_HEX_CHAR, hex_digits[0] as char, hex_digits[1] as char);
1172                    return Err(match uri_el_type {
1173                        // AWS Auth Error Ordering Rule 1.
1174                        UriElement::Path => SignatureError::InvalidURIPath(message),
1175                        // AWS Auth Error Ordering Rule 4.
1176                        UriElement::Query => SignatureError::MalformedQueryString(message),
1177                    });
1178                }
1179            }
1180        } else if c == b'+' {
1181            // Plus-encoded space. Convert this to %20.
1182            result.extend_from_slice(b"%20");
1183            i += 1;
1184        } else {
1185            // Character should have been encoded.
1186            result.push(b'%');
1187            result.extend(u8_to_upper_hex(c));
1188            i += 1;
1189        }
1190    }
1191
1192    Ok(from_utf8(result.as_slice()).unwrap().to_string())
1193}
1194
1195/// Normalize the query parameters by normalizing the keys and values of each parameter and return a `HashMap` mapping
1196/// each key to a *vector* of values (since it is valid for a query parameters to appear multiple times).
1197///
1198/// The order of the values matches the order that they appeared in the query string -- this is important for SigV4
1199/// validation.
1200#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
1201#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
1202#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
1203pub fn query_string_to_normalized_map(query_string: &str) -> Result<HashMap<String, Vec<String>>, SignatureError> {
1204    if query_string.is_empty() {
1205        return Ok(HashMap::new());
1206    }
1207
1208    // Split the query string into parameters on '&' boundaries.
1209    let components = query_string.split('&');
1210    let mut result = HashMap::<String, Vec<String>>::new();
1211
1212    for component in components {
1213        if component.is_empty() {
1214            // Empty component; skip it.
1215            continue;
1216        }
1217
1218        // Split the parameter into key and value portions on the '='
1219        let parts: Vec<&str> = component.splitn(2, '=').collect();
1220        let key = parts[0];
1221        let value = if parts.len() > 1 {
1222            parts[1]
1223        } else {
1224            ""
1225        };
1226
1227        // Normalize the key and value.
1228        let norm_key = normalize_query_string_element(key)?;
1229        let norm_value = normalize_query_string_element(value)?;
1230
1231        // If we already have a value for this key, append to it; otherwise, create a new vector containing the value.
1232        if let Some(result_value) = result.get_mut(&norm_key) {
1233            result_value.push(norm_value);
1234        } else {
1235            result.insert(norm_key, vec![norm_value]);
1236        }
1237    }
1238
1239    Ok(result)
1240}
1241
1242/// Returns a byte slice with leading ASCII whitespace bytes removed.
1243///
1244/// ‘Whitespace’ refers to the definition used by u8::is_ascii_whitespace.
1245///
1246/// This is copied from the Rust standard library source until the
1247/// [`byte_slice_trim_ascii` feature](https://github.com/rust-lang/rust/issues/94035) is stabilized.
1248#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
1249#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
1250#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
1251pub const fn trim_ascii_start(bytes: &[u8]) -> &[u8] {
1252    let mut bytes = bytes;
1253    // Note: A pattern matching based approach (instead of indexing) allows
1254    // making the function const.
1255    while let [first, rest @ ..] = bytes {
1256        if first.is_ascii_whitespace() {
1257            bytes = rest;
1258        } else {
1259            break;
1260        }
1261    }
1262    bytes
1263}
1264
1265/// Returns a byte slice with trailing ASCII whitespace bytes removed.
1266///
1267/// ‘Whitespace’ refers to the definition used by u8::is_ascii_whitespace.
1268///
1269/// This is copied from the Rust standard library source until the
1270/// [`byte_slice_trim_ascii` feature](https://github.com/rust-lang/rust/issues/94035) is stabilized.
1271#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
1272#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
1273#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
1274pub const fn trim_ascii_end(bytes: &[u8]) -> &[u8] {
1275    let mut bytes = bytes;
1276    // Note: A pattern matching based approach (instead of indexing) allows
1277    // making the function const.
1278    while let [rest @ .., last] = bytes {
1279        if last.is_ascii_whitespace() {
1280            bytes = rest;
1281        } else {
1282            break;
1283        }
1284    }
1285    bytes
1286}
1287
1288/// Returns a byte slice with leading and trailing ASCII whitespace bytes removed.
1289///
1290/// ‘Whitespace’ refers to the definition used by u8::is_ascii_whitespace.
1291///
1292/// This is copied from the Rust standard library source until the
1293/// [`byte_slice_trim_ascii` feature](https://github.com/rust-lang/rust/issues/94035) is stabilized.
1294#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
1295#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
1296#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
1297pub const fn trim_ascii(bytes: &[u8]) -> &[u8] {
1298    trim_ascii_end(trim_ascii_start(bytes))
1299}
1300
1301/// Convert a byte to uppercase hex representation.
1302#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
1303#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
1304#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
1305#[inline(always)]
1306pub const fn u8_to_upper_hex(b: u8) -> [u8; 2] {
1307    let result: [u8; 2] = [HEX_DIGITS_UPPER[((b >> 4) & 0xf) as usize], HEX_DIGITS_UPPER[(b & 0xf) as usize]];
1308    result
1309}
1310
1311/// Unescapes a URI percent-encoded string.
1312///
1313/// This function panics if the input string contains invalid percent encodings.
1314#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
1315#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
1316#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
1317pub fn unescape_uri_encoding(s: &str) -> String {
1318    let mut result = String::with_capacity(s.len());
1319    let mut chars = s.bytes();
1320
1321    while let Some(c) = chars.next() {
1322        if c == b'%' {
1323            let mut hex_digits = [0u8; 2];
1324            hex_digits[0] = chars.next().expect(MSG_INCOMPLETE_TRAILING_ESCAPE);
1325            hex_digits[1] = chars.next().expect(MSG_INCOMPLETE_TRAILING_ESCAPE);
1326            match u8::from_str_radix(from_utf8(&hex_digits).unwrap(), 16) {
1327                Ok(c) => result.push(c as char),
1328                Err(_) => panic!("{}{}{}", MSG_ILLEGAL_HEX_CHAR, hex_digits[0] as char, hex_digits[1] as char),
1329            }
1330        } else {
1331            result.push(c as char);
1332        }
1333    }
1334
1335    result
1336}
1337
1338#[cfg(test)]
1339mod tests {
1340    use {
1341        super::{debug_headers, u8_to_upper_hex},
1342        crate::{
1343            canonical::{
1344                canonicalize_query_to_string, canonicalize_uri_path, normalize_uri_path_component,
1345                query_string_to_normalized_map, unescape_uri_encoding, CanonicalRequest,
1346            },
1347            SignatureError, SignatureOptions, NO_ADDITIONAL_SIGNED_HEADERS,
1348        },
1349        bytes::Bytes,
1350        http::{
1351            method::Method,
1352            request::Request,
1353            uri::{PathAndQuery, Uri},
1354        },
1355        scratchstack_errors::ServiceError,
1356        std::collections::HashMap,
1357    };
1358
1359    macro_rules! expect_err {
1360        ($test:expr, $expected:ident) => {
1361            match $test {
1362                Ok(ref v) => panic!("Expected Err({}); got Ok({:?})", stringify!($expected), v),
1363                Err(ref e) => match e {
1364                    SignatureError::$expected(_) => e.to_string(),
1365                    _ => panic!("Expected {}; got {:#?}: {}", stringify!($expected), &e, &e),
1366                },
1367            }
1368        };
1369    }
1370
1371    #[test_log::test]
1372    fn canonicalize_uri_path_empty() {
1373        assert_eq!(canonicalize_uri_path("", false).unwrap(), "/".to_string());
1374        assert_eq!(canonicalize_uri_path("/", false).unwrap(), "/".to_string());
1375    }
1376
1377    #[test_log::test]
1378    fn canonicalize_valid() {
1379        assert_eq!(canonicalize_uri_path("/hello/world", false).unwrap(), "/hello/world".to_string());
1380        assert_eq!(canonicalize_uri_path("/hello///world", false).unwrap(), "/hello/world".to_string());
1381        assert_eq!(canonicalize_uri_path("/hello/./world", false).unwrap(), "/hello/world".to_string());
1382        assert_eq!(canonicalize_uri_path("/hello/foo/../world", false).unwrap(), "/hello/world".to_string());
1383        assert_eq!(canonicalize_uri_path("/hello/foo/%2E%2E/world", false).unwrap(), "/hello/world".to_string());
1384        assert_eq!(canonicalize_uri_path("/hello/%77%6F%72%6C%64", false).unwrap(), "/hello/world".to_string());
1385        assert_eq!(canonicalize_uri_path("/hello/w*rld", false).unwrap(), "/hello/w%2Arld".to_string());
1386        assert_eq!(canonicalize_uri_path("/hello/w%2arld", false).unwrap(), "/hello/w%2Arld".to_string());
1387        assert_eq!(canonicalize_uri_path("/hello/w+rld", false).unwrap(), "/hello/w%20rld".to_string());
1388
1389        assert_eq!(canonicalize_uri_path("/hello/world", true).unwrap(), "/hello/world".to_string());
1390        assert_eq!(canonicalize_uri_path("/hello///world", true).unwrap(), "/hello///world".to_string());
1391        assert_eq!(canonicalize_uri_path("/hello/./world", true).unwrap(), "/hello/./world".to_string());
1392        assert_eq!(canonicalize_uri_path("/hello/foo/../world", true).unwrap(), "/hello/foo/../world".to_string());
1393        assert_eq!(canonicalize_uri_path("/hello/%77%6F%72%6C%64", true).unwrap(), "/hello/world".to_string());
1394        assert_eq!(canonicalize_uri_path("/hello/w*rld", true).unwrap(), "/hello/w%2Arld".to_string());
1395        assert_eq!(canonicalize_uri_path("/hello/w%2arld", true).unwrap(), "/hello/w%2Arld".to_string());
1396        assert_eq!(canonicalize_uri_path("/hello/w+rld", true).unwrap(), "/hello/w%20rld".to_string());
1397        assert_eq!(canonicalize_uri_path("/hello/../../world", true).unwrap(), "/hello/../../world".to_string());
1398        assert_eq!(
1399            canonicalize_uri_path("/hello/%2e%2e/%2e%2e/world", true).unwrap(),
1400            "/hello/../../world".to_string()
1401        );
1402    }
1403
1404    #[test_log::test]
1405    fn canonicalize_invalid() {
1406        let e = expect_err!(canonicalize_uri_path("hello/world", false), InvalidURIPath);
1407        assert_eq!(e.to_string(), "Path is not absolute: hello/world");
1408        let e = canonicalize_uri_path("/hello/../../world", false).unwrap_err();
1409        if let SignatureError::InvalidURIPath(_) = e {
1410            assert_eq!(e.to_string(), "Relative path entry '..' navigates above root: /hello/../../world");
1411            assert_eq!(e.error_code(), "InvalidURIPath");
1412            assert_eq!(e.http_status(), 400);
1413        } else {
1414            panic!("Expected InvalidURIPath; got {:#?}", &e);
1415        }
1416
1417        let e = canonicalize_uri_path("/hello/%2E%2E/%2E%2E/world", false).unwrap_err();
1418        if let SignatureError::InvalidURIPath(_) = e {
1419            assert_eq!(e.to_string(), "Relative path entry '..' navigates above root: /hello/%2E%2E/%2E%2E/world");
1420            assert_eq!(e.error_code(), "InvalidURIPath");
1421            assert_eq!(e.http_status(), 400);
1422        } else {
1423            panic!("Expected InvalidURIPath; got {:#?}", &e);
1424        }
1425    }
1426
1427    #[test_log::test]
1428    fn canonicalize_query_excludes_signature() {
1429        let query = HashMap::from([
1430            ("X-Amz-Signature".to_string(), vec!["abcdef".to_string()]),
1431            ("b".to_string(), vec!["B".to_string()]),
1432            ("c".to_string(), vec!["C".to_string()]),
1433            ("a".to_string(), vec!["A".to_string()]),
1434            ("e".to_string(), vec!["E".to_string()]),
1435            ("d".to_string(), vec!["d".to_string()]),
1436        ]);
1437
1438        let query = canonicalize_query_to_string(&query);
1439        assert_eq!(query, "a=A&b=B&c=C&d=d&e=E");
1440    }
1441
1442    #[test_log::test]
1443    fn normalize_valid1() {
1444        let result = query_string_to_normalized_map("Hello=World&foo=bar&baz=bomb&foo=2&name").unwrap();
1445        let hello = result.get("Hello").unwrap();
1446        assert_eq!(hello.len(), 1);
1447        assert_eq!(hello[0], "World");
1448
1449        let foo = result.get("foo").unwrap();
1450        assert_eq!(foo.len(), 2);
1451        assert_eq!(foo[0], "bar");
1452        assert_eq!(foo[1], "2");
1453
1454        let baz = result.get("baz").unwrap();
1455        assert_eq!(baz.len(), 1);
1456        assert_eq!(baz[0], "bomb");
1457
1458        let name = result.get("name").unwrap();
1459        assert_eq!(name.len(), 1);
1460        assert_eq!(name[0], "");
1461    }
1462
1463    #[test_log::test]
1464    fn normalize_empty() {
1465        let result = query_string_to_normalized_map("Hello=World&&foo=bar");
1466        let v = result.unwrap();
1467        let hello = v.get("Hello").unwrap();
1468
1469        assert_eq!(hello.len(), 1);
1470        assert_eq!(hello[0], "World");
1471
1472        let foo = v.get("foo").unwrap();
1473        assert_eq!(foo.len(), 1);
1474        assert_eq!(foo[0], "bar");
1475
1476        assert!(!v.contains_key(""));
1477    }
1478
1479    #[test_log::test]
1480    fn normalize_invalid_hex() {
1481        let e = expect_err!(normalize_uri_path_component("abcd%yy"), InvalidURIPath);
1482        assert_eq!(e.as_str(), "Illegal hex character in escape % pattern: %yy");
1483        expect_err!(normalize_uri_path_component("abcd%yy"), InvalidURIPath);
1484        expect_err!(normalize_uri_path_component("abcd%0"), InvalidURIPath);
1485        expect_err!(normalize_uri_path_component("abcd%"), InvalidURIPath);
1486        assert_eq!(normalize_uri_path_component("abcd%65").unwrap(), "abcde");
1487    }
1488
1489    struct PathAndQuerySimulate {
1490        data: Bytes,
1491        _query: u16,
1492    }
1493
1494    #[test_log::test]
1495    fn normalize_invalid_hex_path_cr() {
1496        // The HTTP crate does its own validation; we need to hack into it to force invalid URI elements in there.
1497        for (path, error_message) in [
1498            ("/abcd%yy", "Illegal hex character in escape % pattern: %yy"),
1499            ("/abcd%0", "Incomplete trailing escape % sequence"),
1500            ("/abcd%", "Incomplete trailing escape % sequence"),
1501        ] {
1502            let mut fake_path = "/".to_string();
1503            while fake_path.len() < path.len() {
1504                fake_path.push('a');
1505            }
1506
1507            let mut pq = PathAndQuery::from_maybe_shared(fake_path.clone()).unwrap();
1508            let pq_path = Bytes::from_static(path.as_bytes());
1509
1510            unsafe {
1511                // Rewrite the path to be invalid. This can't be done with the normal PathAndQuery
1512                // API.
1513                let pq_ptr: *mut PathAndQuerySimulate = &mut pq as *mut PathAndQuery as *mut PathAndQuerySimulate;
1514                (*pq_ptr).data = pq_path;
1515            }
1516
1517            let uri = Uri::builder().path_and_query(pq).build().unwrap();
1518            let request = Request::builder()
1519                .method(Method::GET)
1520                .uri(uri)
1521                .header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
1522                .header("authorization", "Basic foobar")
1523                .header("x-amz-date", "20150830T123600Z")
1524                .body(Bytes::new())
1525                .unwrap();
1526            let (parts, body) = request.into_parts();
1527
1528            let e = CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap_err();
1529            if let SignatureError::InvalidURIPath(msg) = e {
1530                assert_eq!(msg.as_str(), error_message);
1531            }
1532        }
1533    }
1534
1535    #[test_log::test]
1536    fn normalize_invalid_hex_query_cr() {
1537        // The HTTP crate does its own validation; we need to hack into it to force invalid URI elements in there.
1538        for (path, error_message) in [
1539            ("/?x=abcd%yy", "Illegal hex character in escape % pattern: %yy"),
1540            ("/?x=abcd%0", "Incomplete trailing escape % sequence"),
1541            ("/?x=abcd%", "Incomplete trailing escape % sequence"),
1542        ] {
1543            let mut fake_path = "/?x=".to_string();
1544            while fake_path.len() < path.len() {
1545                fake_path.push('a');
1546            }
1547
1548            let mut pq = PathAndQuery::from_maybe_shared(fake_path.clone()).unwrap();
1549            let pq_path = Bytes::from_static(path.as_bytes());
1550
1551            unsafe {
1552                // Rewrite the path to be invalid.
1553                let pq_ptr: *mut PathAndQuerySimulate = &mut pq as *mut PathAndQuery as *mut PathAndQuerySimulate;
1554                (*pq_ptr).data = pq_path;
1555            }
1556
1557            let uri = Uri::builder().path_and_query(pq).build().unwrap();
1558            let request = Request::builder()
1559                .method(Method::GET)
1560                .uri(uri)
1561                .header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
1562                .header("authorization", "Basic foobar")
1563                .header("x-amz-date", "20150830T123600Z")
1564                .body(Bytes::new())
1565                .unwrap();
1566            let (parts, body) = request.into_parts();
1567
1568            let e = CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap_err();
1569            if let SignatureError::MalformedQueryString(msg) = e {
1570                assert_eq!(msg.as_str(), error_message);
1571            }
1572        }
1573    }
1574
1575    /// Check for query parameters without a value, e.g. ?Key2&
1576    /// https://github.com/dacut/scratchstack-aws-signature/issues/2
1577    #[test_log::test]
1578    fn normalize_query_parameters_missing_value() {
1579        let result = query_string_to_normalized_map("Key1=Value1&Key2&Key3=Value3");
1580        assert!(result.is_ok());
1581        let result = result.unwrap();
1582        assert_eq!(result["Key1"], vec!["Value1"]);
1583        assert_eq!(result["Key2"], vec![""]);
1584        assert_eq!(result["Key3"], vec!["Value3"]);
1585    }
1586
1587    #[test_log::test]
1588    fn test_multiple_algorithms() {
1589        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
1590        let request = Request::builder()
1591            .method(Method::GET)
1592            .uri(uri)
1593            .header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
1594            .header("authorization", "Basic foobar")
1595            .header("x-amz-date", "20150830T123600Z")
1596            .body(Bytes::new())
1597            .unwrap();
1598        let (parts, body) = request.into_parts();
1599
1600        let (cr, _, _) =
1601            CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
1602
1603        // Ensure we can debug print the canonical request.
1604        let _ = format!("{:?}", cr);
1605
1606        assert_eq!(cr.request_method(), "GET");
1607        assert_eq!(cr.canonical_path(), "/");
1608        assert!(cr.query_parameters().is_empty());
1609        assert_eq!(cr.headers().len(), 2);
1610        assert_eq!(cr.headers().get("authorization").unwrap().len(), 2);
1611        assert_eq!(
1612            cr.headers().get("authorization").unwrap()[0],
1613            b"AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678"
1614        );
1615        assert_eq!(cr.body_sha256(), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
1616
1617        let params = cr.get_auth_parameters(&NO_ADDITIONAL_SIGNED_HEADERS).unwrap();
1618        // Ensure we can debug print the auth parameters.
1619        let _ = format!("{:?}", params);
1620        assert_eq!(params.signed_headers, vec!["date", "host"]);
1621    }
1622
1623    #[test_log::test]
1624    fn test_bad_form_urlencoded_charset() {
1625        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
1626        let request = Request::builder()
1627            .method(Method::POST)
1628            .uri(uri)
1629            .header("content-type", "application/x-www-form-urlencoded; hello=world; charset=foobar")
1630            .header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
1631            .header("x-amz-date", "20150830T123600Z")
1632            .body(Bytes::from_static(b"foo=ba\x80r"))
1633            .unwrap();
1634        let (parts, body) = request.into_parts();
1635
1636        let e = CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap_err();
1637        if let SignatureError::InvalidBodyEncoding(_) = e {
1638            assert_eq!(e.to_string(), "application/x-www-form-urlencoded body uses unsupported charset 'foobar'");
1639            assert_eq!(e.error_code(), "InvalidBodyEncoding");
1640            assert_eq!(e.http_status(), 400);
1641        } else {
1642            panic!("Unexpected error: {:?}", e);
1643        }
1644    }
1645
1646    #[test_log::test]
1647    fn test_empty_form() {
1648        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
1649        let request = Request::builder()
1650            .method(Method::POST)
1651            .uri(uri)
1652            .header("content-type", "application/x-www-form-urlencoded; charset=utf-8")
1653            .header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
1654            .header("x-amz-date", "20150830T123600Z")
1655            .body(Bytes::from_static(b""))
1656            .unwrap();
1657        let (parts, body) = request.into_parts();
1658
1659        let (cr, _, _) =
1660            CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
1661        assert!(cr.query_parameters().is_empty());
1662    }
1663
1664    #[test_log::test]
1665    fn test_default_form_encoding() {
1666        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
1667        let request = Request::builder()
1668            .method(Method::POST)
1669            .uri(uri)
1670            .header("content-type", "application/x-www-form-urlencoded")
1671            .header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
1672            .header("x-amz-date", "20150830T123600Z")
1673            .body(Bytes::from(b"foo=bar\xc3\xbf".to_vec()))
1674            .unwrap();
1675        let (parts, body) = request.into_parts();
1676
1677        let (cr, _, _) =
1678            CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
1679        assert_eq!(cr.query_parameters().get("foo").unwrap(), &vec!["bar%C3%BF".to_string()]);
1680
1681        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
1682        let request = Request::builder()
1683            .method(Method::POST)
1684            .uri(uri)
1685            .header("content-type", "application/x-www-form-urlencoded; hello=world")
1686            .header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
1687            .header("x-amz-date", "20150830T123600Z")
1688            .body(Bytes::from(b"foo=bar\xc3\xbf".to_vec()))
1689            .unwrap();
1690        let (parts, body) = request.into_parts();
1691
1692        let (cr, _, _) =
1693            CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
1694        assert_eq!(cr.query_parameters().get("foo").unwrap(), &vec!["bar%C3%BF".to_string()]);
1695    }
1696
1697    #[test_log::test]
1698    fn test_no_map_form() {
1699        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
1700        let request = Request::builder()
1701            .method(Method::POST)
1702            .uri(uri)
1703            .header("content-type", "application/x-www-form-urlencoded")
1704            .header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
1705            .header("x-amz-date", "20150830T123600Z")
1706            .body(Bytes::from(b"foo=bar\xc3\xbf".to_vec()))
1707            .unwrap();
1708        let (parts, body) = request.into_parts();
1709
1710        let (cr, _, _) = CanonicalRequest::from_request_parts(parts, body, SignatureOptions::default()).unwrap();
1711        assert!(!cr.query_parameters().contains_key("foo"));
1712    }
1713
1714    #[test_log::test]
1715    fn test_bad_debug_headers() {
1716        let mut headers = HashMap::new();
1717        headers.insert("Host".to_string(), vec![vec![0xffu8]]);
1718        let debug = debug_headers(&headers);
1719        assert_eq!(debug, "Host: [255]");
1720
1721        assert_eq!(debug_headers(&HashMap::new()), "");
1722    }
1723
1724    #[test_log::test]
1725    fn test_bad_form_encoding() {
1726        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
1727        let request = Request::builder()
1728            .method(Method::POST)
1729            .uri(uri)
1730            .header("content-type", "application/x-www-form-urlencoded; charset=utf-8")
1731            .header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
1732            .header("x-amz-date", "20150830T123600Z")
1733            .body(Bytes::from(b"foo=ba\x80r".to_vec()))
1734            .unwrap();
1735        let (parts, body) = request.into_parts();
1736
1737        let e = CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap_err();
1738        if let SignatureError::InvalidBodyEncoding(msg) = e {
1739            assert_eq!(
1740                msg.as_str(),
1741                "Invalid body data encountered parsing application/x-www-form-urlencoded with charset 'UTF-8'"
1742            )
1743        } else {
1744            panic!("Unexpected error: {:?}", e);
1745        }
1746    }
1747
1748    #[test_log::test]
1749    fn test_bad_form_charset_param() {
1750        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
1751        let request = Request::builder()
1752            .method(Method::POST)
1753            .uri(uri)
1754            .header("content-type", "application/x-www-form-urlencoded; charset")
1755            .header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
1756            .header("x-amz-date", "20150830T123600Z")
1757            .body(Bytes::from(b"foo=bar".to_vec()))
1758            .unwrap();
1759        let (parts, body) = request.into_parts();
1760
1761        let (_, _, body) =
1762            CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
1763        assert_eq!(body.as_ref(), b"");
1764    }
1765
1766    #[test_log::test]
1767    fn test_bad_form_urlencoding() {
1768        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
1769        let request = Request::builder()
1770            .method(Method::POST)
1771            .uri(uri)
1772            .header("content-type", "application/x-www-form-urlencoded; charset=utf-8")
1773            .header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
1774            .header("x-amz-date", "20150830T123600Z")
1775            .body(Bytes::from(b"foo=bar%yy".to_vec()))
1776            .unwrap();
1777        let (parts, body) = request.into_parts();
1778
1779        let e = CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap_err();
1780        if let SignatureError::MalformedQueryString(msg) = e {
1781            assert_eq!(msg.as_str(), "Illegal hex character in escape % pattern: %yy")
1782        } else {
1783            panic!("Unexpected error: {:?}", e);
1784        }
1785
1786        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
1787        let request = Request::builder()
1788            .method(Method::POST)
1789            .uri(uri)
1790            .header("content-type", "application/x-www-form-urlencoded; charset=utf-8")
1791            .header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
1792            .header("x-amz-date", "20150830T123600Z")
1793            .body(Bytes::from(b"foo%tt=bar".to_vec()))
1794            .unwrap();
1795        let (parts, body) = request.into_parts();
1796
1797        let e = CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap_err();
1798        if let SignatureError::MalformedQueryString(msg) = e {
1799            assert_eq!(msg.as_str(), "Illegal hex character in escape % pattern: %tt")
1800        } else {
1801            panic!("Unexpected error: {:?}", e);
1802        }
1803
1804        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
1805        let request = Request::builder()
1806            .method(Method::POST)
1807            .uri(uri)
1808            .header("content-type", "application/x-www-form-urlencoded; charset=utf-8")
1809            .header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
1810            .header("x-amz-date", "20150830T123600Z")
1811            .body(Bytes::from(b"foo=bar%y".to_vec()))
1812            .unwrap();
1813        let (parts, body) = request.into_parts();
1814
1815        let e = CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap_err();
1816        if let SignatureError::MalformedQueryString(msg) = e {
1817            assert_eq!(msg.as_str(), "Incomplete trailing escape % sequence")
1818        } else {
1819            panic!("Unexpected error: {:?}", e);
1820        }
1821    }
1822
1823    #[test_log::test]
1824    fn test_u8_to_upper_hex() {
1825        for i in 0..=255 {
1826            let result = u8_to_upper_hex(i);
1827            assert_eq!(String::from_utf8_lossy(result.as_slice()), format!("{:02X}", i));
1828        }
1829    }
1830
1831    #[test_log::test]
1832    fn test_missing_auth_header_components() {
1833        for i in 0..15 {
1834            let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
1835            let mut error_messages = Vec::with_capacity(4);
1836            let mut auth_header = Vec::with_capacity(3);
1837
1838            if i & 1 != 0 {
1839                auth_header.push(" Credential=1234  ");
1840            } else {
1841                error_messages.push("Authorization header requires 'Credential' parameter.");
1842            }
1843
1844            if i & 2 != 0 {
1845                auth_header.push(" Signature=5678  ");
1846            } else {
1847                error_messages.push("Authorization header requires 'Signature' parameter.");
1848            }
1849
1850            if i & 4 != 0 {
1851                auth_header.push(" SignedHeaders=host;x-amz-date");
1852            } else {
1853                error_messages.push("Authorization header requires 'SignedHeaders' parameter.");
1854            }
1855
1856            let auth_header = format!("AWS4-HMAC-SHA256 {}", auth_header.join(", "));
1857            let builder = Request::builder().method(Method::GET).uri(uri).header("authorization", auth_header);
1858
1859            let builder = if i & 8 != 0 {
1860                builder.header("x-amz-date", "20150830T123600Z")
1861            } else {
1862                error_messages
1863                    .push("Authorization header requires existence of either a 'X-Amz-Date' or a 'Date' header.");
1864                builder
1865            };
1866
1867            let request = builder.body(Bytes::new()).unwrap();
1868            let (parts, body) = request.into_parts();
1869
1870            let (cr, _, _) =
1871                CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
1872            let e = cr.get_auth_parameters(&NO_ADDITIONAL_SIGNED_HEADERS).unwrap_err();
1873            if let SignatureError::IncompleteSignature(msg) = e {
1874                let error_message = format!("{} Authorization=AWS4-HMAC-SHA256", error_messages.join(" "));
1875                assert_eq!(msg.as_str(), error_message.as_str());
1876            } else {
1877                panic!("Unexpected error: {:?}", e);
1878            }
1879        }
1880    }
1881
1882    #[test_log::test]
1883    fn test_malformed_auth_header() {
1884        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
1885        let request = Request::builder()
1886            .method(Method::GET)
1887            .uri(uri)
1888            .header("x-amz-date", "20150830T123600Z")
1889            .header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeadersdate;host")
1890            .body(Bytes::new())
1891            .unwrap();
1892
1893        let (parts, body) = request.into_parts();
1894
1895        let (cr, _, _) =
1896            CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
1897        let e = cr.get_auth_parameters(&NO_ADDITIONAL_SIGNED_HEADERS).unwrap_err();
1898        if let SignatureError::IncompleteSignature(msg) = e {
1899            assert_eq!(msg.as_str(), "'SignedHeadersdate;host' not a valid key=value pair (missing equal-sign) in Authorization header: 'AWS4-HMAC-SHA256 Credential=1234, SignedHeadersdate;host'");
1900        } else {
1901            panic!("Unexpected error: {:?}", e);
1902        }
1903    }
1904
1905    #[test_log::test]
1906    fn test_missing_auth_query_components() {
1907        for i in 0..15 {
1908            let mut error_messages = Vec::with_capacity(4);
1909            let mut auth_query = Vec::with_capacity(5);
1910
1911            auth_query.push("X-Amz-Algorithm=AWS4-HMAC-SHA256");
1912
1913            if i & 1 != 0 {
1914                auth_query.push("X-Amz-Credential=1234");
1915            } else {
1916                error_messages.push("AWS query-string parameters must include 'X-Amz-Credential'.");
1917            }
1918
1919            if i & 2 != 0 {
1920                auth_query.push("X-Amz-Signature=5678");
1921            } else {
1922                error_messages.push("AWS query-string parameters must include 'X-Amz-Signature'.");
1923            }
1924
1925            if i & 4 != 0 {
1926                auth_query.push("X-Amz-SignedHeaders=host;x-amz-date");
1927            } else {
1928                error_messages.push("AWS query-string parameters must include 'X-Amz-SignedHeaders'.");
1929            }
1930
1931            if i & 8 != 0 {
1932                auth_query.push("X-Amz-Date=20150830T123600Z")
1933            } else {
1934                error_messages.push("AWS query-string parameters must include 'X-Amz-Date'.");
1935            };
1936
1937            let query_string = auth_query.join("&");
1938
1939            let pq = PathAndQuery::from_maybe_shared(format!("/?{}", query_string)).unwrap();
1940            let uri = Uri::builder().path_and_query(pq).build().unwrap();
1941            let builder = Request::builder().method(Method::GET).uri(uri);
1942
1943            let request = builder.body(Bytes::new()).unwrap();
1944            let (parts, body) = request.into_parts();
1945
1946            let (cr, _, _) =
1947                CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
1948            let e = cr.get_auth_parameters(&NO_ADDITIONAL_SIGNED_HEADERS).unwrap_err();
1949            if let SignatureError::IncompleteSignature(msg) = e {
1950                let error_message = format!("{} Re-examine the query-string parameters.", error_messages.join(" "));
1951                assert_eq!(msg.as_str(), error_message.as_str());
1952            } else {
1953                panic!("Unexpected error: {:?}", e);
1954            }
1955        }
1956    }
1957
1958    #[test_log::test]
1959    fn test_auth_component_ordering() {
1960        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
1961        let request = Request::builder()
1962            .method(Method::GET)
1963            .uri(uri)
1964            .header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678, Credential=ABCD, SignedHeaders=foo;bar;host, Signature=DEFG")
1965            .header("authorization", "AWS3 Credential=1234, SignedHeaders=date;host, Signature=5678, Credential=ABCD, SignedHeaders=foo;bar;host, Signature=DEFG")
1966            .header("host", "example.amazonaws.com")
1967            .header("x-amz-date", "20150830T123600Z")
1968            .header("x-amz-date", "20161231T235959Z")
1969            .header("x-amz-security-token", "Test1")
1970            .header("x-amz-security-token", "Test2")
1971            .body(Bytes::new())
1972            .unwrap();
1973        let (parts, body) = request.into_parts();
1974
1975        let (cr, _, _) =
1976            CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
1977        let auth = cr.get_auth_parameters(&NO_ADDITIONAL_SIGNED_HEADERS).unwrap();
1978        // Expect last component found
1979        assert_eq!(auth.builder.get_credential(), Some("ABCD"));
1980        assert_eq!(auth.builder.get_signature(), Some("DEFG"));
1981        assert_eq!(auth.signed_headers, vec!["bar", "foo", "host"]);
1982        // Expect first header found.
1983        assert_eq!(auth.builder.get_session_token(), Some("Test1"));
1984        assert_eq!(auth.timestamp_str, "20150830T123600Z");
1985
1986        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Algorithm=AWS3&X-Amz-Credential=1234&X-Amz-SignedHeaders=date%3Bhost&X-Amz-Signature=5678&X-Amz-Security-Token=Test1&X-Amz-Date=20150830T123600Z&X-Amz-Credential=ABCD&X-Amz-SignedHeaders=foo%3Bbar%3Bhost&X-Amz-Signature=DEFG&X-Amz-SecurityToken=Test2&X-Amz-Date=20161231T235959Z")).build().unwrap();
1987        let request = Request::builder()
1988            .method(Method::GET)
1989            .uri(uri)
1990            .header("host", "example.amazonaws.com")
1991            .body(Bytes::new())
1992            .unwrap();
1993        let (parts, body) = request.into_parts();
1994
1995        let (cr, _, _) =
1996            CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
1997        let auth = cr.get_auth_parameters(&NO_ADDITIONAL_SIGNED_HEADERS).unwrap();
1998        // Expect first component found
1999        assert_eq!(auth.builder.get_credential(), Some("1234"));
2000        assert_eq!(auth.builder.get_signature(), Some("5678"));
2001        assert_eq!(auth.builder.get_session_token(), Some("Test1"));
2002        assert_eq!(auth.timestamp_str, "20150830T123600Z");
2003        assert_eq!(auth.signed_headers, vec!["date", "host"]);
2004
2005        let auth = cr.get_authenticator(&NO_ADDITIONAL_SIGNED_HEADERS);
2006        assert!(auth.is_ok());
2007    }
2008
2009    #[test_log::test]
2010    fn test_signed_headers_missing_host() {
2011        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
2012        let request = Request::builder()
2013            .method(Method::GET)
2014            .uri(uri)
2015            .header("x-amz-date", "20150830T123600Z")
2016            .header("host", "example.amazonaws.com")
2017            .header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=x-amz-date, Signature=5678")
2018            .body(Bytes::new())
2019            .unwrap();
2020
2021        let (parts, body) = request.into_parts();
2022
2023        let (cr, _, _) =
2024            CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
2025        let required_headers = NO_ADDITIONAL_SIGNED_HEADERS;
2026        let required_headers2 = required_headers;
2027        assert_eq!(&required_headers, &required_headers2);
2028        assert_eq!(format!("{:?}", required_headers), format!("{:?}", required_headers2));
2029        let e = cr.get_auth_parameters(&required_headers).unwrap_err();
2030        if let SignatureError::SignatureDoesNotMatch(msg) = e {
2031            let msg = msg.expect("Expected error message");
2032            assert_eq!(msg.as_str(), "'Host' or ':authority' must be a 'SignedHeader' in the AWS Authorization.");
2033        } else {
2034            panic!("Unexpected error: {:?}", e);
2035        }
2036
2037        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=1234&X-Amz-SignedHeaders=&X-Amz-Signature=5678&X-Amz-Date=20150830T123600Z&X-Amz-SecurityToken=Foo")).build().unwrap();
2038        let request = Request::builder()
2039            .method(Method::GET)
2040            .uri(uri)
2041            .header("host", "example.amazonaws.com")
2042            .body(Bytes::new())
2043            .unwrap();
2044
2045        let (parts, body) = request.into_parts();
2046
2047        let (cr, _, _) =
2048            CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
2049        let e = cr.get_auth_parameters(&NO_ADDITIONAL_SIGNED_HEADERS).unwrap_err();
2050        if let SignatureError::SignatureDoesNotMatch(msg) = e {
2051            let msg = msg.expect("Expected error message");
2052            assert_eq!(msg.as_str(), "'Host' or ':authority' must be a 'SignedHeader' in the AWS Authorization.");
2053        } else {
2054            panic!("Unexpected error: {:?}", e);
2055        }
2056    }
2057
2058    #[test_log::test]
2059    fn test_missing_signed_header() {
2060        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
2061        let request = Request::builder()
2062            .method(Method::GET)
2063            .uri(uri)
2064            .header("x-amz-date", "20150830T123600Z")
2065            .header("host", "example.amazonaws.com")
2066            .header(
2067                "authorization",
2068                "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=a;host;x-amz-date, Signature=5678",
2069            )
2070            .body(Bytes::new())
2071            .unwrap();
2072
2073        let (parts, body) = request.into_parts();
2074
2075        let (cr, _, _) =
2076            CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
2077        let a = cr.get_auth_parameters(&NO_ADDITIONAL_SIGNED_HEADERS).unwrap();
2078        assert_eq!(a.signed_headers, vec!["a", "host", "x-amz-date"]);
2079        let cr_bytes = cr.canonical_request(&a.signed_headers);
2080        assert!(!cr_bytes.is_empty());
2081    }
2082
2083    #[test_log::test]
2084    fn test_bad_algorithms() {
2085        // No algorithm present (rule 5)
2086        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
2087        let request = Request::builder()
2088            .method(Method::POST)
2089            .uri(uri)
2090            .header("content-type", "application/json")
2091            .header("x-amz-date", "20150830T123600Z")
2092            .header("host", "example.amazonaws.com")
2093            .body(Bytes::from_static(b"{}"))
2094            .unwrap();
2095
2096        let (parts, body) = request.into_parts();
2097
2098        let (cr, _, _) =
2099            CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
2100        let e = cr.get_auth_parameters(&NO_ADDITIONAL_SIGNED_HEADERS).unwrap_err();
2101        if let SignatureError::MissingAuthenticationToken(msg) = e {
2102            assert_eq!(msg.as_str(), "Request is missing Authentication Token");
2103        } else {
2104            panic!("Unexpected error: {:?}", e);
2105        }
2106
2107        // Both header and query string signatures present (rule 5)
2108        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=1234&X-Amz-SignedHeaders=&X-Amz-Signature=5678&X-Amz-Date=20150830T123600Z&X-Amz-SecurityToken=Foo")).build().unwrap();
2109        let request = Request::builder()
2110            .method(Method::GET)
2111            .uri(uri)
2112            .header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=x-amz-date, Signature=5678")
2113            .header("x-amz-date", "20150830T123600Z")
2114            .header("host", "example.amazonaws.com")
2115            .body(Bytes::new())
2116            .unwrap();
2117
2118        let (parts, body) = request.into_parts();
2119
2120        let (cr, _, _) =
2121            CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
2122        let e = cr.get_auth_parameters(&NO_ADDITIONAL_SIGNED_HEADERS).unwrap_err();
2123        if let SignatureError::SignatureDoesNotMatch(ref msg) = e {
2124            assert!(msg.is_none());
2125            assert_eq!(e.to_string(), "");
2126        } else {
2127            panic!("Unexpected error: {:?}", e);
2128        }
2129
2130        // Wrong algorithm header (rule 6a)
2131        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
2132        let request = Request::builder()
2133            .method(Method::GET)
2134            .uri(uri)
2135            .header("authorization", "AWS3-HMAC-SHA256 Credential=1234, SignedHeaders=x-amz-date, Signature=5678")
2136            .header("x-amz-date", "20150830T123600Z")
2137            .header("host", "example.amazonaws.com")
2138            .body(Bytes::new())
2139            .unwrap();
2140
2141        let (parts, body) = request.into_parts();
2142
2143        let (cr, _, _) =
2144            CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
2145        let e = cr.get_auth_parameters(&NO_ADDITIONAL_SIGNED_HEADERS).unwrap_err();
2146        if let SignatureError::IncompleteSignature(msg) = e {
2147            assert_eq!(msg.as_str(), "Unsupported AWS 'algorithm': 'AWS3-HMAC-SHA256'.");
2148        } else {
2149            panic!("Unexpected error: {:?}", e);
2150        }
2151
2152        // Wrong algorithm query string (rule 7a)
2153        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/?X-Amz-Algorithm=AWS3-HMAC-SHA256&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=1234&X-Amz-SignedHeaders=date%3Bhost&X-Amz-Signature=5678&X-Amz-Security-Token=Test1&X-Amz-Date=20150830T123600Z&X-Amz-Credential=ABCD&X-Amz-SignedHeaders=foo%3Bbar%3Bhost&X-Amz-Signature=DEFG&X-Amz-SecurityToken=Test2&X-Amz-Date=20161231T235959Z")).build().unwrap();
2154        let request = Request::builder()
2155            .method(Method::GET)
2156            .uri(uri)
2157            .header("x-amz-date", "20150830T123600Z")
2158            .header("host", "example.amazonaws.com")
2159            .body(Bytes::new())
2160            .unwrap();
2161
2162        let (parts, body) = request.into_parts();
2163
2164        let (cr, _, _) =
2165            CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
2166        let e = cr.get_auth_parameters(&NO_ADDITIONAL_SIGNED_HEADERS).unwrap_err();
2167        if let SignatureError::MissingAuthenticationToken(msg) = e {
2168            assert_eq!(msg.as_str(), "Request is missing Authentication Token");
2169        } else {
2170            panic!("Unexpected error: {:?}", e);
2171        }
2172    }
2173
2174    #[test_log::test]
2175    #[should_panic]
2176    fn unescape_uri_encoding_invalid_panics() {
2177        unescape_uri_encoding("%YY");
2178    }
2179}