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#[derive(Clone, Copy, Debug, Default)]
16pub struct SignatureOptions {
17 pub s3: bool,
19
20 pub url_encode_form: bool,
22}
23
24impl SignatureOptions {
25 pub const fn url_encode_form() -> Self {
34 Self {
35 s3: false,
36 url_encode_form: true,
37 }
38 }
39
40 pub const S3: Self = Self {
45 s3: true,
46 url_encode_form: false,
47 };
48}
49
50const ALLOWED_MISMATCH_MINUTES: i64 = 15;
52
53pub 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
115pub trait IntoRequestBytes {
119 fn into_request_bytes(self) -> impl Future<Output = Result<Bytes, BoxError>> + Send + Sync;
121}
122
123impl IntoRequestBytes for () {
125 async fn into_request_bytes(self) -> Result<Bytes, BoxError> {
129 Ok(Bytes::new())
130 }
131}
132
133impl IntoRequestBytes for Vec<u8> {
135 async fn into_request_bytes(self) -> Result<Bytes, BoxError> {
139 Ok(Bytes::from(self))
140 }
141}
142
143impl IntoRequestBytes for Bytes {
145 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 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 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 let req = Request::builder()
721 .method(Method::GET)
722 .uri("/a/path/../to//something") .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 let req = Request::builder()
743 .method(Method::GET)
744 .uri("/a/path/../to//something") .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}