scratchstack_aws_signature/
signature.rs

1use {
2    crate::{
3        auth::SigV4AuthenticatorResponse, canonical::CanonicalRequest, GetSigningKeyRequest, GetSigningKeyResponse,
4        SignedHeaderRequirements,
5    },
6    bytes::Bytes,
7    chrono::{DateTime, Duration, Utc},
8    http::request::{Parts, Request},
9    log::trace,
10    std::future::Future,
11    tower::{BoxError, Service},
12};
13
14/// Options that can be used to configure the signature service.
15#[derive(Clone, Copy, Debug, Default)]
16pub struct SignatureOptions {
17    /// Canonicalize requests according to S3 rules.
18    pub s3: bool,
19
20    /// Fold `application/x-www-form-urlencoded` bodies into the query string.
21    pub url_encode_form: bool,
22}
23
24impl SignatureOptions {
25    /// Create a `SignatureOptions` suitable for use with services that treat
26    /// `application/x-www-form-urlencoded` bodies as part of the query string.
27    ///
28    /// Some AWS services require this behavior. This typically happens when a query string is too
29    /// long to fit in the URL, so a `GET` request is transformed into a `POST` request with the
30    /// query string passed as an HTML form.
31    ///
32    /// This sets `s3` to `false` and `url_encode_form` to `true`.
33    pub const fn url_encode_form() -> Self {
34        Self {
35            s3: false,
36            url_encode_form: true,
37        }
38    }
39
40    /// Create a `SignatureOptions` suitable for use with S3-type authentication.
41    ///
42    /// This sets `s3` to `true` and `url_encode_form` to `false`, resulting in AWS SigV4S3-style
43    /// canonicalization.
44    pub const S3: Self = Self {
45        s3: true,
46        url_encode_form: false,
47    };
48}
49
50/// Default allowed timestamp mismatch in minutes.
51const ALLOWED_MISMATCH_MINUTES: i64 = 15;
52
53/// Validate an AWS SigV4 request.
54///
55/// This takes in an HTTP [`Request`] along with other service-specific paramters. If the
56/// validation is successful (i.e. the request is properly signed with a known access key), this
57/// returns:
58/// * The request headers (as HTTP [`Parts`]).
59/// * The request body (as a [`Bytes`] object, which is empty if no body was provided).
60/// * The [response from the authenticator][SigV4AuthenticatorResponse], which contains the
61///   principal and other session data.
62///
63/// # Parameters
64/// * `request` - The HTTP [`Request`] to validate.
65/// * `region` - The AWS region in which the request is being made.
66/// * `service` - The AWS service to which the request is being made.
67/// * `get_signing_key` - A service that can provide the signing key for the request.
68/// * `server_timestamp` - The timestamp of the server when the request was received. Usually this
69///   is the current time, `Utc::now()`.
70/// * `required_headers` - The headers that are required to be signed in the request in addition to
71///   the default SigV4 headers. If none, use
72///   [`NO_ADDITIONAL_SIGNED_HEADERS`][crate::NO_ADDITIONAL_SIGNED_HEADERS].
73/// * `options` - [`SignatureOptions`]` that affect the behavior of the signature validation. For
74///   most services, use `SignatureOptions::default()`.
75///
76/// # Errors
77/// This function returns a [`SignatureError`][crate::SignatureError] if the HTTP request is
78/// malformed or the request was not properly signed. The validation follows the
79/// [AWS Auth Error Ordering](https://github.com/dacut/scratchstack-aws-signature/blob/main/docs/AWS%20Auth%20Error%20Ordering.pdf)
80/// document.
81pub async fn sigv4_validate_request<B, G, F, S>(
82    request: Request<B>,
83    region: &str,
84    service: &str,
85    get_signing_key: &mut G,
86    server_timestamp: DateTime<Utc>,
87    required_headers: &S,
88    options: SignatureOptions,
89) -> Result<(Parts, Bytes, SigV4AuthenticatorResponse), BoxError>
90where
91    B: IntoRequestBytes,
92    G: Service<GetSigningKeyRequest, Response = GetSigningKeyResponse, Error = BoxError, Future = F> + Send,
93    F: Future<Output = Result<GetSigningKeyResponse, BoxError>> + Send,
94    S: SignedHeaderRequirements,
95{
96    let (parts, body) = request.into_parts();
97    let body = body.into_request_bytes().await?;
98    let (canonical_request, parts, body) = CanonicalRequest::from_request_parts(parts, body, options)?;
99    trace!("Created canonical request: {:?}", canonical_request);
100    let auth = canonical_request.get_authenticator(required_headers)?;
101    trace!("Created authenticator: {:?}", auth);
102    let sigv4_response = auth
103        .validate_signature(
104            region,
105            service,
106            server_timestamp,
107            Duration::minutes(ALLOWED_MISMATCH_MINUTES),
108            get_signing_key,
109        )
110        .await?;
111
112    Ok((parts, body, sigv4_response))
113}
114
115/// A trait for converting various body types into a [`Bytes`] object.
116///
117/// This requires reading the entire body into memory.
118pub trait IntoRequestBytes {
119    /// Convert this object into a [`Bytes`] object.
120    fn into_request_bytes(self) -> impl Future<Output = Result<Bytes, BoxError>> + Send + Sync;
121}
122
123/// Convert the unit type `()` into an empty [`Bytes`] object.
124impl IntoRequestBytes for () {
125    /// Convert the unit type `()` into an empty [`Bytes`] object.
126    ///
127    /// This is infalliable.
128    async fn into_request_bytes(self) -> Result<Bytes, BoxError> {
129        Ok(Bytes::new())
130    }
131}
132
133/// Convert a `Vec<u8>` into a [`Bytes`] object.
134impl IntoRequestBytes for Vec<u8> {
135    /// Convert a `Vec<u8>` into a [`Bytes`] object.
136    ///
137    /// This is infalliable.
138    async fn into_request_bytes(self) -> Result<Bytes, BoxError> {
139        Ok(Bytes::from(self))
140    }
141}
142
143/// Identity transformation: return the [`Bytes`] object as-is.
144impl IntoRequestBytes for Bytes {
145    /// Identity transformation: return the [`Bytes`] object as-is.
146    ///
147    /// This is infalliable.
148    async fn into_request_bytes(self) -> Result<Bytes, BoxError> {
149        Ok(self)
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use {
156        crate::{
157            auth::SigV4AuthenticatorResponse, service_for_signing_key_fn, sigv4_validate_request, GetSigningKeyRequest,
158            GetSigningKeyResponse, KSecretKey, SignatureError, SignatureOptions, SignedHeaderRequirements,
159            VecSignedHeaderRequirements, NO_ADDITIONAL_SIGNED_HEADERS,
160        },
161        bytes::Bytes,
162        chrono::{DateTime, NaiveDate, Utc},
163        http::{
164            method::Method,
165            request::{Parts, Request},
166            uri::{PathAndQuery, Uri},
167        },
168        lazy_static::lazy_static,
169        scratchstack_aws_principal::{Principal, User},
170        scratchstack_errors::ServiceError,
171        std::{borrow::Cow, str::FromStr},
172        tower::BoxError,
173    };
174
175    const TEST_REGION: &str = "us-east-1";
176    const TEST_SERVICE: &str = "service";
177
178    lazy_static! {
179        static ref TEST_TIMESTAMP: DateTime<Utc> = DateTime::from_naive_utc_and_offset(
180            NaiveDate::from_ymd_opt(2015, 8, 30).unwrap().and_hms_opt(12, 36, 0).unwrap(),
181            Utc
182        );
183    }
184
185    macro_rules! expect_err {
186        ($test:expr, $expected:ident) => {
187            match $test {
188                Ok(ref v) => panic!("Expected Err({}); got Ok({:?})", stringify!($expected), v),
189                Err(e) => match e.downcast::<SignatureError>() {
190                    Ok(e) => {
191                        let e_string = e.to_string();
192                        let e_debug = format!("{:?}", e);
193                        match *e {
194                            SignatureError::$expected(_) => e_string,
195                            _ => panic!("Expected {}; got {}: {}", stringify!($expected), e_debug, e_string),
196                        }
197                    }
198                    Err(ref other) => panic!("Expected {}; got {:#?}: {}", stringify!($expected), &other, &other),
199                },
200            }
201        };
202    }
203
204    macro_rules! run_auth_test_expect_kind {
205        ($auth_str:expr, $expected:ident) => {
206            expect_err!(run_auth_test($auth_str).await, $expected)
207        };
208    }
209
210    const VALID_AUTH_HEADER: &str = "AWS4-HMAC-SHA256 \
211    Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, \
212    SignedHeaders=host;x-amz-date, \
213    Signature=c9d5ea9f3f72853aea855b47ea873832890dbdd183b4468f858259531a5138ea";
214
215    async fn get_signing_key(req: GetSigningKeyRequest) -> Result<GetSigningKeyResponse, BoxError> {
216        let k_secret = KSecretKey::from_str("wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY").unwrap();
217        let k_signing = k_secret.to_ksigning(req.request_date(), req.region(), req.service());
218
219        let principal = Principal::from(vec![User::new("aws", "123456789012", "/", "test").unwrap().into()]);
220        Ok(GetSigningKeyResponse::builder().principal(principal).signing_key(k_signing).build().unwrap())
221    }
222
223    async fn run_auth_test(auth_str: &str) -> Result<(Parts, Bytes, SigV4AuthenticatorResponse), BoxError> {
224        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
225        let request = Request::builder()
226            .method(Method::GET)
227            .uri(uri)
228            .header("authorization", auth_str)
229            .header("host", "example.amazonaws.com")
230            .header("x-amz-date", "20150830T123600Z")
231            .body(())
232            .unwrap();
233        let mut get_signing_key_svc = service_for_signing_key_fn(get_signing_key);
234        sigv4_validate_request(
235            request,
236            TEST_REGION,
237            TEST_SERVICE,
238            &mut get_signing_key_svc,
239            *TEST_TIMESTAMP,
240            &NO_ADDITIONAL_SIGNED_HEADERS,
241            SignatureOptions::url_encode_form(),
242        )
243        .await
244    }
245
246    #[test_log::test(tokio::test)]
247    async fn test_wrong_auth_algorithm() {
248        assert_eq!(
249            run_auth_test_expect_kind!("AWS3-ZZZ Credential=12345", IncompleteSignature),
250            "Unsupported AWS 'algorithm': 'AWS3-ZZZ'."
251        );
252    }
253
254    #[test_log::test(tokio::test)]
255    async fn missing_date() {
256        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
257        let mut gsk_service = service_for_signing_key_fn(get_signing_key);
258        let request = Request::builder()
259            .method(Method::GET)
260            .uri(uri)
261            .header("authorization", VALID_AUTH_HEADER)
262            .header("host", "localhost")
263            .body(())
264            .unwrap();
265        let e = expect_err!(
266            sigv4_validate_request(
267                request,
268                TEST_REGION,
269                TEST_SERVICE,
270                &mut gsk_service,
271                *TEST_TIMESTAMP,
272                &NO_ADDITIONAL_SIGNED_HEADERS,
273                SignatureOptions::url_encode_form()
274            )
275            .await,
276            IncompleteSignature
277        );
278        assert_eq!(
279            e.as_str(),
280            r#"Authorization header requires existence of either a 'X-Amz-Date' or a 'Date' header. Authorization=AWS4-HMAC-SHA256"#
281        );
282    }
283
284    #[test_log::test(tokio::test)]
285    async fn invalid_date() {
286        let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
287        let mut gsk_service = service_for_signing_key_fn(get_signing_key);
288        let request = Request::builder()
289            .method(Method::GET)
290            .uri(uri)
291            .header("authorization", VALID_AUTH_HEADER)
292            .header("date", "zzzzzzzzz")
293            .body(())
294            .unwrap();
295        let e = expect_err!(
296            sigv4_validate_request(
297                request,
298                TEST_REGION,
299                TEST_SERVICE,
300                &mut gsk_service,
301                *TEST_TIMESTAMP,
302                &NO_ADDITIONAL_SIGNED_HEADERS,
303                SignatureOptions::url_encode_form()
304            )
305            .await,
306            IncompleteSignature
307        );
308        assert_eq!(
309            e.as_str(),
310            r#"Date must be in ISO-8601 'basic format'. Got 'zzzzzzzzz'. See http://en.wikipedia.org/wiki/ISO_8601"#
311        );
312    }
313
314    struct PathAndQuerySimulate {
315        data: Bytes,
316        _query: u16,
317    }
318
319    #[test_log::test(tokio::test)]
320    async fn error_ordering_auth_header() {
321        for i in 0..22 {
322            let fake_path = "/aaa?aaa".to_string();
323            let mut pq = PathAndQuery::from_maybe_shared(fake_path).unwrap();
324            let pq_path = Bytes::from_static("/aaa?a%yy".as_bytes());
325            let get_signing_key_svc = service_for_signing_key_fn(get_signing_key);
326
327            if i == 0 {
328                unsafe {
329                    // Rewrite the path to be invalid. This can't be done with the normal PathAndQuery API.
330                    let pq_ptr: *mut PathAndQuerySimulate = &mut pq as *mut PathAndQuery as *mut PathAndQuerySimulate;
331                    (*pq_ptr).data = pq_path;
332                }
333            }
334
335            let uri = Uri::builder().path_and_query(pq).build().unwrap();
336            let mut builder = Request::builder()
337                .method(Method::GET)
338                .uri(uri)
339                .header("x-amz-request-id", "12345")
340                .header("ETag", "ABCD");
341
342            if i > 1 {
343                builder = builder.header(
344                    "authorization",
345                    match i {
346                        2 => "AWS5-HMAC-SHA256 FooBar, BazBurp",
347                        3 => "AWS4-HMAC-SHA256 FooBar, BazBurp",
348                        4 => "AWS4-HMAC-SHA256 Foo=Bar, Baz=Burp",
349                        5 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE",
350                        6 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE, Signature=ABCDEF",
351                        7..=8 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE, Signature=ABCDEF, SignedHeaders=bar",
352                        9 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE, Signature=ABCDEF, SignedHeaders=host;x-amz-date",
353                        10 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE, Signature=ABCDEF, SignedHeaders=content-type;host;x-amz-date",
354                        11 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE, Signature=ABCDEF, SignedHeaders=content-type;etag;host;x-amz-date",
355                        12..=15 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE, Signature=ABCDEF, SignedHeaders=content-type;etag;host;x-amz-date;x-amz-request-id",
356                        16 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/foobar/wrong-region/wrong-service/aws5_request, Signature=ABCDEF, SignedHeaders=content-type;etag;host;x-amz-date;x-amz-request-id",
357                        17 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/wrong-region/wrong-service/aws5_request, Signature=ABCDEF, SignedHeaders=content-type;etag;host;x-amz-date;x-amz-request-id",
358                        18 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/wrong-service/aws5_request, Signature=ABCDEF, SignedHeaders=content-type;etag;host;x-amz-date;x-amz-request-id",
359                        19 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws5_request, Signature=ABCDEF, SignedHeaders=content-type;etag;host;x-amz-date;x-amz-request-id",
360                        20 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, Signature=ABCDEF, SignedHeaders=content-type;etag;host;x-amz-date;x-amz-request-id",
361                        _ => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, Signature=0e669f2a32894c33e1214831b3605dbc6e14c1708872c55d4b04a6c10a20de40, SignedHeaders=content-type;etag;host;x-amz-date;x-amz-request-id",
362                    },
363                );
364            }
365
366            match i {
367                0..=7 => (),
368                8..=12 => builder = builder.header("x-amz-date", "2015/08/30T12/36/00Z"),
369                13 => builder = builder.header("x-amz-date", "20150830T122059Z"),
370                14 => builder = builder.header("x-amz-date", "20150830T125101Z"),
371                _ => builder = builder.header("x-amz-date", "20150830T122100Z"),
372            }
373
374            let request = builder.body(()).unwrap();
375            let mut required_headers = VecSignedHeaderRequirements::default();
376            required_headers.add_always_present("Content-Type");
377            required_headers.add_always_present("Qwerty");
378            required_headers.add_if_in_request("Foo");
379            required_headers.add_if_in_request("Bar");
380            required_headers.add_if_in_request("ETag");
381            required_headers.add_prefix("x-amz");
382            required_headers.add_prefix("a-am2");
383            required_headers.remove_always_present("QWERTY");
384            required_headers.remove_if_in_request("BAR");
385            required_headers.remove_prefix("A-am2");
386
387            let result = sigv4_validate_request(
388                request,
389                TEST_REGION,
390                TEST_SERVICE,
391                &mut get_signing_key_svc.clone(),
392                *TEST_TIMESTAMP,
393                &required_headers,
394                SignatureOptions::url_encode_form(),
395            )
396            .await;
397
398            if i >= 21 {
399                assert!(result.is_ok());
400            } else {
401                let e = result.unwrap_err();
402                assert!(e.source().is_none());
403                let e = e.downcast_ref::<SignatureError>().expect("Expected SignatureError");
404                match (i, e) {
405                    (0, SignatureError::MalformedQueryString(_)) => {
406                        assert_eq!(e.to_string().as_str(), "Illegal hex character in escape % pattern: %yy")
407                    }
408                    (1, SignatureError::MissingAuthenticationToken(_)) => {
409                        assert_eq!(e.to_string().as_str(), "Request is missing Authentication Token")
410                    }
411                    (2, SignatureError::IncompleteSignature(_)) => {
412                        assert_eq!(e.to_string().as_str(), "Unsupported AWS 'algorithm': 'AWS5-HMAC-SHA256'.")
413                    }
414                    (3, SignatureError::IncompleteSignature(_)) => {
415                        assert_eq!(e.to_string().as_str(), "'FooBar' not a valid key=value pair (missing equal-sign) in Authorization header: 'AWS4-HMAC-SHA256 FooBar, BazBurp'")
416                    }
417                    (4, SignatureError::IncompleteSignature(_)) => {
418                        assert_eq!(e.to_string().as_str(), "Authorization header requires 'Credential' parameter. Authorization header requires 'Signature' parameter. Authorization header requires 'SignedHeaders' parameter. Authorization header requires existence of either a 'X-Amz-Date' or a 'Date' header. Authorization=AWS4-HMAC-SHA256")
419                    }
420                    (5, SignatureError::IncompleteSignature(_)) => {
421                        assert_eq!(e.to_string().as_str(), "Authorization header requires 'Signature' parameter. Authorization header requires 'SignedHeaders' parameter. Authorization header requires existence of either a 'X-Amz-Date' or a 'Date' header. Authorization=AWS4-HMAC-SHA256")
422                    }
423                    (6, SignatureError::IncompleteSignature(_)) => {
424                        assert_eq!(e.to_string().as_str(), "Authorization header requires 'SignedHeaders' parameter. Authorization header requires existence of either a 'X-Amz-Date' or a 'Date' header. Authorization=AWS4-HMAC-SHA256")
425                    }
426                    (7, SignatureError::IncompleteSignature(_)) => {
427                        assert_eq!(e.to_string().as_str(), "Authorization header requires existence of either a 'X-Amz-Date' or a 'Date' header. Authorization=AWS4-HMAC-SHA256")
428                    }
429                    (8, SignatureError::SignatureDoesNotMatch(_)) => {
430                        assert_eq!(
431                            e.to_string().as_str(),
432                            "'Host' or ':authority' must be a 'SignedHeader' in the AWS Authorization."
433                        )
434                    }
435                    (9, SignatureError::SignatureDoesNotMatch(_)) => {
436                        assert_eq!(
437                            e.to_string().as_str(),
438                            "'Content-Type' must be a 'SignedHeader' in the AWS Authorization."
439                        )
440                    }
441                    (10, SignatureError::SignatureDoesNotMatch(_)) => {
442                        assert_eq!(e.to_string().as_str(), "'ETag' must be a 'SignedHeader' in the AWS Authorization.")
443                    }
444                    (11, SignatureError::SignatureDoesNotMatch(_)) => {
445                        assert_eq!(
446                            e.to_string().as_str(),
447                            "'x-amz-request-id' must be a 'SignedHeader' in the AWS Authorization."
448                        )
449                    }
450                    (12, SignatureError::IncompleteSignature(_)) => {
451                        assert_eq!(e.to_string().as_str(), "Date must be in ISO-8601 'basic format'. Got '2015/08/30T12/36/00Z'. See http://en.wikipedia.org/wiki/ISO_8601")
452                    }
453                    (13, SignatureError::SignatureDoesNotMatch(_)) => {
454                        assert_eq!(e.to_string().as_str(), "Signature expired: 20150830T122059Z is now earlier than 20150830T122100Z (20150830T123600Z - 15 min.)")
455                    }
456                    (14, SignatureError::SignatureDoesNotMatch(_)) => {
457                        assert_eq!(e.to_string().as_str(), "Signature not yet current: 20150830T125101Z is still later than 20150830T125100Z (20150830T123600Z + 15 min.)")
458                    }
459                    (15, SignatureError::IncompleteSignature(_)) => {
460                        assert_eq!(e.to_string().as_str(), "Credential must have exactly 5 slash-delimited elements, e.g. keyid/date/region/service/term, got 'AKIDEXAMPLE'")
461                    }
462                    (16, SignatureError::SignatureDoesNotMatch(_)) => {
463                        assert_eq!(e.to_string().as_str(), "Credential should be scoped to a valid region, not 'wrong-region'. Credential should be scoped to correct service: 'service'. Credential should be scoped with a valid terminator: 'aws4_request', not 'aws5_request'. Date in Credential scope does not match YYYYMMDD from ISO-8601 version of date from HTTP: 'foobar' != '20150830', from '20150830T122100Z'.")
464                    }
465                    (17, SignatureError::SignatureDoesNotMatch(_)) => {
466                        assert_eq!(e.to_string().as_str(), "Credential should be scoped to a valid region, not 'wrong-region'. Credential should be scoped to correct service: 'service'. Credential should be scoped with a valid terminator: 'aws4_request', not 'aws5_request'.")
467                    }
468                    (18, SignatureError::SignatureDoesNotMatch(_)) => {
469                        assert_eq!(e.to_string().as_str(), "Credential should be scoped to correct service: 'service'. Credential should be scoped with a valid terminator: 'aws4_request', not 'aws5_request'.")
470                    }
471                    (19, SignatureError::SignatureDoesNotMatch(_)) => {
472                        assert_eq!(
473                            e.to_string().as_str(),
474                            "Credential should be scoped with a valid terminator: 'aws4_request', not 'aws5_request'."
475                        )
476                    }
477                    (20, SignatureError::SignatureDoesNotMatch(_)) => {
478                        assert_eq!(e.to_string().as_str(), "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.")
479                    }
480                    _ => panic!("Incorrect error returned on run {}: {:?}", i, e),
481                }
482            }
483        }
484    }
485
486    #[test_log::test(tokio::test)]
487    async fn error_ordering_auth_header_streaming_body() {
488        for i in 0..22 {
489            let fake_path = "/aaa?aaa".to_string();
490            let mut pq = PathAndQuery::from_maybe_shared(fake_path).unwrap();
491            let pq_path = Bytes::from_static("/aaa?a%yy".as_bytes());
492            let get_signing_key_svc = service_for_signing_key_fn(get_signing_key);
493
494            if i == 0 {
495                unsafe {
496                    // Rewrite the path to be invalid. This cannot be done with the normal PathAndQuery API.
497                    let pq_ptr: *mut PathAndQuerySimulate = &mut pq as *mut PathAndQuery as *mut PathAndQuerySimulate;
498                    (*pq_ptr).data = pq_path;
499                }
500            }
501
502            let uri = Uri::builder().path_and_query(pq).build().unwrap();
503            let mut builder = Request::builder()
504                .method(Method::GET)
505                .uri(uri)
506                .header("x-amz-request-id", "12345")
507                .header("ETag", "ABCD");
508
509            if i > 1 {
510                builder = builder.header(
511                    "authorization",
512                    match i {
513                        2 => "AWS5-HMAC-SHA256 FooBar, BazBurp",
514                        3 => "AWS4-HMAC-SHA256 FooBar, BazBurp",
515                        4 => "AWS4-HMAC-SHA256 Foo=Bar, Baz=Burp",
516                        5 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE",
517                        6 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE, Signature=ABCDEF",
518                        7..=8 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE, Signature=ABCDEF, SignedHeaders=bar",
519                        9 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE, Signature=ABCDEF, SignedHeaders=host;x-amz-date",
520                        10 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE, Signature=ABCDEF, SignedHeaders=content-type;host;x-amz-date",
521                        11 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE, Signature=ABCDEF, SignedHeaders=content-type;etag;host;x-amz-date",
522                        12..=15 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE, Signature=ABCDEF, SignedHeaders=content-type;etag;host;x-amz-date;x-amz-request-id",
523                        16 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/foobar/wrong-region/wrong-service/aws5_request, Signature=ABCDEF, SignedHeaders=content-type;etag;host;x-amz-date;x-amz-request-id",
524                        17 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/wrong-region/wrong-service/aws5_request, Signature=ABCDEF, SignedHeaders=content-type;etag;host;x-amz-date;x-amz-request-id",
525                        18 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/wrong-service/aws5_request, Signature=ABCDEF, SignedHeaders=content-type;etag;host;x-amz-date;x-amz-request-id",
526                        19 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws5_request, Signature=ABCDEF, SignedHeaders=content-type;etag;host;x-amz-date;x-amz-request-id",
527                        20 => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, Signature=ABCDEF, SignedHeaders=content-type;etag;host;x-amz-date;x-amz-request-id",
528                        _ => "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, Signature=07758ff72d5726780290f484e5f7d1c026f36067d3656435e99e2391e1818c54, SignedHeaders=content-type;etag;host;x-amz-date;x-amz-request-id",
529                    },
530                );
531            }
532
533            match i {
534                0..=7 => (),
535                8..=12 => builder = builder.header("x-amz-date", "2015/08/30T12/36/00Z"),
536                13 => builder = builder.header("x-amz-date", "20150830T122059Z"),
537                14 => builder = builder.header("x-amz-date", "20150830T125101Z"),
538                _ => builder = builder.header("x-amz-date", "20150830T122100Z"),
539            }
540
541            let body = Bytes::from_static(b"{}");
542
543            let request = builder.body(body).unwrap();
544            let mut required_headers =
545                VecSignedHeaderRequirements::new(&["Content-Type", "Qwerty"], &["Foo", "Bar", "ETag"], &["x-amz"]);
546            required_headers.remove_always_present("QWERTY");
547            assert!(!required_headers.always_present().contains(&Cow::Borrowed("Qwerty")));
548            required_headers.remove_if_in_request("BAR");
549            required_headers.remove_prefix("A-am2");
550            let result = sigv4_validate_request(
551                request,
552                TEST_REGION,
553                TEST_SERVICE,
554                &mut get_signing_key_svc.clone(),
555                *TEST_TIMESTAMP,
556                &required_headers,
557                SignatureOptions::url_encode_form(),
558            )
559            .await;
560
561            if i >= 21 {
562                assert!(result.is_ok());
563            } else {
564                let e = result.unwrap_err();
565                assert!(e.source().is_none());
566                let e = e.downcast::<SignatureError>().unwrap();
567                match (i, &*e) {
568                    (0, SignatureError::MalformedQueryString(_)) => {
569                        assert_eq!(e.to_string().as_str(), "Illegal hex character in escape % pattern: %yy");
570                        assert_eq!(e.error_code(), "MalformedQueryString");
571                        assert_eq!(e.http_status(), 400);
572                    }
573                    (1, SignatureError::MissingAuthenticationToken(_)) => {
574                        assert_eq!(e.to_string().as_str(), "Request is missing Authentication Token");
575                        assert_eq!(e.error_code(), "MissingAuthenticationToken");
576                        assert_eq!(e.http_status(), 400);
577                    }
578                    (2, SignatureError::IncompleteSignature(_)) => {
579                        assert_eq!(e.to_string().as_str(), "Unsupported AWS 'algorithm': 'AWS5-HMAC-SHA256'.");
580                        assert_eq!(e.error_code(), "IncompleteSignature");
581                        assert_eq!(e.http_status(), 400);
582                    }
583                    (3, SignatureError::IncompleteSignature(_)) => {
584                        assert_eq!(e.to_string().as_str(), "'FooBar' not a valid key=value pair (missing equal-sign) in Authorization header: 'AWS4-HMAC-SHA256 FooBar, BazBurp'");
585                        assert_eq!(e.error_code(), "IncompleteSignature");
586                        assert_eq!(e.http_status(), 400);
587                    }
588                    (4, SignatureError::IncompleteSignature(_)) => {
589                        assert_eq!(e.to_string().as_str(), "Authorization header requires 'Credential' parameter. Authorization header requires 'Signature' parameter. Authorization header requires 'SignedHeaders' parameter. Authorization header requires existence of either a 'X-Amz-Date' or a 'Date' header. Authorization=AWS4-HMAC-SHA256");
590                        assert_eq!(e.error_code(), "IncompleteSignature");
591                        assert_eq!(e.http_status(), 400);
592                    }
593                    (5, SignatureError::IncompleteSignature(_)) => {
594                        assert_eq!(e.to_string().as_str(), "Authorization header requires 'Signature' parameter. Authorization header requires 'SignedHeaders' parameter. Authorization header requires existence of either a 'X-Amz-Date' or a 'Date' header. Authorization=AWS4-HMAC-SHA256");
595                        assert_eq!(e.error_code(), "IncompleteSignature");
596                        assert_eq!(e.http_status(), 400);
597                    }
598                    (6, SignatureError::IncompleteSignature(_)) => {
599                        assert_eq!(e.to_string().as_str(), "Authorization header requires 'SignedHeaders' parameter. Authorization header requires existence of either a 'X-Amz-Date' or a 'Date' header. Authorization=AWS4-HMAC-SHA256");
600                        assert_eq!(e.error_code(), "IncompleteSignature");
601                        assert_eq!(e.http_status(), 400);
602                    }
603                    (7, SignatureError::IncompleteSignature(_)) => {
604                        assert_eq!(e.to_string().as_str(), "Authorization header requires existence of either a 'X-Amz-Date' or a 'Date' header. Authorization=AWS4-HMAC-SHA256");
605                        assert_eq!(e.error_code(), "IncompleteSignature");
606                        assert_eq!(e.http_status(), 400);
607                    }
608                    (8, SignatureError::SignatureDoesNotMatch(_)) => {
609                        assert_eq!(
610                            e.to_string().as_str(),
611                            "'Host' or ':authority' must be a 'SignedHeader' in the AWS Authorization."
612                        );
613                        assert_eq!(e.error_code(), "SignatureDoesNotMatch");
614                        assert_eq!(e.http_status(), 403);
615                    }
616                    (9, SignatureError::SignatureDoesNotMatch(_)) => {
617                        assert_eq!(
618                            e.to_string().as_str(),
619                            "'Content-Type' must be a 'SignedHeader' in the AWS Authorization."
620                        );
621                        assert_eq!(e.error_code(), "SignatureDoesNotMatch");
622                        assert_eq!(e.http_status(), 403);
623                    }
624                    (10, SignatureError::SignatureDoesNotMatch(_)) => {
625                        assert_eq!(e.to_string().as_str(), "'ETag' must be a 'SignedHeader' in the AWS Authorization.");
626                        assert_eq!(e.error_code(), "SignatureDoesNotMatch");
627                        assert_eq!(e.http_status(), 403);
628                    }
629                    (11, SignatureError::SignatureDoesNotMatch(_)) => {
630                        assert_eq!(
631                            e.to_string().as_str(),
632                            "'x-amz-request-id' must be a 'SignedHeader' in the AWS Authorization."
633                        );
634                        assert_eq!(e.error_code(), "SignatureDoesNotMatch");
635                        assert_eq!(e.http_status(), 403);
636                    }
637                    (12, SignatureError::IncompleteSignature(_)) => {
638                        assert_eq!(e.to_string().as_str(), "Date must be in ISO-8601 'basic format'. Got '2015/08/30T12/36/00Z'. See http://en.wikipedia.org/wiki/ISO_8601");
639                        assert_eq!(e.error_code(), "IncompleteSignature");
640                        assert_eq!(e.http_status(), 400);
641                    }
642                    (13, SignatureError::SignatureDoesNotMatch(_)) => {
643                        assert_eq!(e.to_string().as_str(), "Signature expired: 20150830T122059Z is now earlier than 20150830T122100Z (20150830T123600Z - 15 min.)");
644                        assert_eq!(e.error_code(), "SignatureDoesNotMatch");
645                        assert_eq!(e.http_status(), 403);
646                    }
647                    (14, SignatureError::SignatureDoesNotMatch(_)) => {
648                        assert_eq!(e.to_string().as_str(), "Signature not yet current: 20150830T125101Z is still later than 20150830T125100Z (20150830T123600Z + 15 min.)");
649                        assert_eq!(e.error_code(), "SignatureDoesNotMatch");
650                        assert_eq!(e.http_status(), 403);
651                    }
652                    (15, SignatureError::IncompleteSignature(_)) => {
653                        assert_eq!(e.to_string().as_str(), "Credential must have exactly 5 slash-delimited elements, e.g. keyid/date/region/service/term, got 'AKIDEXAMPLE'");
654                        assert_eq!(e.error_code(), "IncompleteSignature");
655                        assert_eq!(e.http_status(), 400);
656                    }
657                    (16, SignatureError::SignatureDoesNotMatch(_)) => {
658                        assert_eq!(e.to_string().as_str(), "Credential should be scoped to a valid region, not 'wrong-region'. Credential should be scoped to correct service: 'service'. Credential should be scoped with a valid terminator: 'aws4_request', not 'aws5_request'. Date in Credential scope does not match YYYYMMDD from ISO-8601 version of date from HTTP: 'foobar' != '20150830', from '20150830T122100Z'.");
659                        assert_eq!(e.error_code(), "SignatureDoesNotMatch");
660                        assert_eq!(e.http_status(), 403);
661                    }
662                    (17, SignatureError::SignatureDoesNotMatch(_)) => {
663                        assert_eq!(e.to_string().as_str(), "Credential should be scoped to a valid region, not 'wrong-region'. Credential should be scoped to correct service: 'service'. Credential should be scoped with a valid terminator: 'aws4_request', not 'aws5_request'.");
664                        assert_eq!(e.error_code(), "SignatureDoesNotMatch");
665                        assert_eq!(e.http_status(), 403);
666                    }
667                    (18, SignatureError::SignatureDoesNotMatch(_)) => {
668                        assert_eq!(e.to_string().as_str(), "Credential should be scoped to correct service: 'service'. Credential should be scoped with a valid terminator: 'aws4_request', not 'aws5_request'.");
669                        assert_eq!(e.error_code(), "SignatureDoesNotMatch");
670                        assert_eq!(e.http_status(), 403);
671                    }
672                    (19, SignatureError::SignatureDoesNotMatch(_)) => {
673                        assert_eq!(
674                            e.to_string().as_str(),
675                            "Credential should be scoped with a valid terminator: 'aws4_request', not 'aws5_request'."
676                        );
677                        assert_eq!(e.error_code(), "SignatureDoesNotMatch");
678                        assert_eq!(e.http_status(), 403);
679                    }
680                    (20, SignatureError::SignatureDoesNotMatch(_)) => {
681                        assert_eq!(e.to_string().as_str(), "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.");
682                        assert_eq!(e.error_code(), "SignatureDoesNotMatch");
683                        assert_eq!(e.http_status(), 403);
684                    }
685                    _ => panic!("Incorrect error returned on run {}: {:?}", i, e),
686                }
687            }
688        }
689    }
690
691    #[test_log::test]
692    fn test_signature_options() {
693        assert!(!SignatureOptions::default().s3);
694        assert!(!SignatureOptions::default().url_encode_form);
695
696        let opt1 = SignatureOptions::S3;
697        let opt2 = SignatureOptions {
698            s3: true,
699            ..Default::default()
700        };
701        let opt3 = opt1;
702        let opt4 = opt1;
703        assert_eq!(opt1.s3, opt2.s3);
704        assert_eq!(opt1.s3, opt3.s3);
705        assert_eq!(opt1.s3, opt4.s3);
706        assert_eq!(opt1.url_encode_form, opt2.url_encode_form);
707        assert_eq!(opt1.url_encode_form, opt3.url_encode_form);
708        assert_eq!(opt1.url_encode_form, opt4.url_encode_form);
709        assert!(opt1.s3);
710        assert!(!opt1.url_encode_form);
711
712        assert_eq!(format!("{:?}", opt1), "SignatureOptions { s3: true, url_encode_form: false }");
713    }
714
715    #[test_log::test(tokio::test)]
716    async fn test_canonicalization_forms() {
717        let mut get_signing_key_svc = service_for_signing_key_fn(get_signing_key);
718
719        // Regular, non-S3 request.
720        let req = Request::builder()
721            .method(Method::GET)
722            .uri("/a/path/../to//something") // Becomes /a/to/something.
723            .header("Host", "example.amazonaws.com")
724            .header("X-Amz-Date", "20150830T123600Z")
725            .header("Authorization", "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, Signature=444cab3690e122afc941d086f06cfbc82c1b4f5c553e32ac81e7629a82ff3831, SignedHeaders=host;x-amz-date")
726            .body(())
727            .unwrap();
728
729        assert!(sigv4_validate_request(
730            req,
731            "us-east-1",
732            "service",
733            &mut get_signing_key_svc,
734            *TEST_TIMESTAMP,
735            &NO_ADDITIONAL_SIGNED_HEADERS,
736            SignatureOptions::default()
737        )
738        .await
739        .is_ok());
740
741        // S3 request.
742        let req = Request::builder()
743            .method(Method::GET)
744            .uri("/a/path/../to//something") // Becomes /a/to/something.
745            .header("Host", "example.amazonaws.com")
746            .header("X-Amz-Date", "20150830T123600Z")
747            .header("Authorization", "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, Signature=b475de2c96e7bfdfe03bd784d948218730ef62f48ac8bb9f2922af9a44f8657c, SignedHeaders=host;x-amz-date")
748            .body(())
749            .unwrap();
750
751        assert!(sigv4_validate_request(
752            req,
753            "us-east-1",
754            "service",
755            &mut get_signing_key_svc,
756            *TEST_TIMESTAMP,
757            &NO_ADDITIONAL_SIGNED_HEADERS,
758            SignatureOptions::S3,
759        )
760        .await
761        .is_ok());
762    }
763}