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