1use {
12 crate::{
13 crypto::{hmac_sha256, SHA256_OUTPUT_LEN},
14 GetSigningKeyRequest, GetSigningKeyResponse, SignatureError,
15 },
16 chrono::{DateTime, Duration, Utc},
17 derive_builder::Builder,
18 log::{debug, trace},
19 qualifier_attr::qualifiers,
20 scratchstack_aws_principal::{Principal, SessionData},
21 std::{
22 fmt::{Debug, Formatter, Result as FmtResult},
23 future::Future,
24 },
25 subtle::ConstantTimeEq,
26 tower::{BoxError, Service, ServiceExt},
27};
28
29const AWS4_HMAC_SHA256: &str = "AWS4-HMAC-SHA256";
31
32const AWS4_REQUEST: &str = "aws4_request";
34
35const ISO8601_COMPACT_FORMAT: &str = "%Y%m%dT%H%M%SZ";
37
38const ISO8601_UTC_LENGTH: usize = 16;
40
41const MSG_CREDENTIAL_MUST_HAVE_FIVE_PARTS: &str =
43 "Credential must have exactly 5 slash-delimited elements, e.g. keyid/date/region/service/term,";
44
45const MSG_REQUEST_SIGNATURE_MISMATCH: &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.";
47
48const SHA256_EMPTY: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
50
51const SHA256_HEX_LENGTH: usize = SHA256_EMPTY.len();
53
54#[derive(Builder, Clone, Default)]
56#[cfg_attr(doc, doc(cfg(feature = "unstable")))]
57#[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
58#[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
59#[builder(derive(Debug))]
60pub struct SigV4Authenticator {
61 canonical_request_sha256: [u8; SHA256_OUTPUT_LEN],
63
64 credential: String,
68
69 #[builder(setter(into, strip_option), default)]
71 session_token: Option<String>,
72
73 signature: String,
75
76 request_timestamp: DateTime<Utc>,
78}
79
80impl SigV4Authenticator {
81 #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
83 #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
84 #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
85 #[inline(always)]
86 fn builder() -> SigV4AuthenticatorBuilder {
87 SigV4AuthenticatorBuilder::default()
88 }
89
90 #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
92 #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
93 #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
94 #[inline(always)]
95 fn canonical_request_sha256(&self) -> [u8; SHA256_OUTPUT_LEN] {
96 self.canonical_request_sha256
97 }
98
99 #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
101 #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
102 #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
103 #[inline(always)]
104 fn credential(&self) -> &str {
105 &self.credential
106 }
107
108 #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
110 #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
111 #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
112 #[inline(always)]
113 fn session_token(&self) -> Option<&str> {
114 self.session_token.as_deref()
115 }
116
117 #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
119 #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
120 #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
121 #[inline(always)]
122 fn signature(&self) -> &str {
123 &self.signature
124 }
125
126 #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
128 #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
129 #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
130 #[inline(always)]
131 fn request_timestamp(&self) -> DateTime<Utc> {
132 self.request_timestamp
133 }
134
135 #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
138 #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
139 #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
140 pub fn prevalidate(
141 &self,
142 region: &str,
143 service: &str,
144 server_timestamp: DateTime<Utc>,
145 allowed_mismatch: Duration,
146 ) -> Result<(), SignatureError> {
147 let req_ts = self.request_timestamp();
148 let min_ts = server_timestamp.checked_sub_signed(allowed_mismatch).unwrap_or(server_timestamp);
149 let max_ts = server_timestamp.checked_add_signed(allowed_mismatch).unwrap_or(server_timestamp);
150
151 if req_ts < min_ts {
153 trace!("prevalidate: request timestamp {} is before minimum timestamp {}", req_ts, min_ts);
154 return Err(SignatureError::SignatureDoesNotMatch(Some(format!(
155 "Signature expired: {} is now earlier than {} ({} - {}.)",
156 req_ts.format(ISO8601_COMPACT_FORMAT),
157 min_ts.format(ISO8601_COMPACT_FORMAT),
158 server_timestamp.format(ISO8601_COMPACT_FORMAT),
159 duration_to_string(allowed_mismatch)
160 ))));
161 }
162
163 if req_ts > max_ts {
165 trace!("prevalidate: request timestamp {} is after maximum timestamp {}", req_ts, max_ts);
166 return Err(SignatureError::SignatureDoesNotMatch(Some(format!(
167 "Signature not yet current: {} is still later than {} ({} + {}.)",
168 req_ts.format(ISO8601_COMPACT_FORMAT),
169 max_ts.format(ISO8601_COMPACT_FORMAT),
170 server_timestamp.format(ISO8601_COMPACT_FORMAT),
171 duration_to_string(allowed_mismatch)
172 ))));
173 }
174
175 let credential_parts = self.credential().split('/').collect::<Vec<&str>>();
177 if credential_parts.len() != 5 {
178 trace!("prevalidate: credential has {} parts, expected 5", credential_parts.len());
179 return Err(SignatureError::IncompleteSignature(format!(
180 "{} got '{}'",
181 MSG_CREDENTIAL_MUST_HAVE_FIVE_PARTS,
182 self.credential()
183 )));
184 }
185
186 let cscope_date = credential_parts[1];
187 let cscope_region = credential_parts[2];
188 let cscope_service = credential_parts[3];
189 let cscope_term = credential_parts[4];
190
191 let mut cscope_errors = Vec::new();
193 if cscope_region != region {
194 trace!("prevalidate: credential region '{}' does not match expected region '{}'", cscope_region, region);
195 cscope_errors.push(format!("Credential should be scoped to a valid region, not '{}'.", cscope_region));
196 }
197
198 if cscope_service != service {
199 trace!(
200 "prevalidate: credential service '{}' does not match expected service '{}'",
201 cscope_service,
202 service
203 );
204 cscope_errors.push(format!("Credential should be scoped to correct service: '{}'.", service));
205 }
206
207 if cscope_term != AWS4_REQUEST {
208 trace!(
209 "prevalidate: credential terminator '{}' does not match expected terminator '{}'",
210 cscope_term,
211 AWS4_REQUEST
212 );
213 cscope_errors.push(format!(
214 "Credential should be scoped with a valid terminator: 'aws4_request', not '{}'.",
215 cscope_term
216 ));
217 }
218
219 let expected_cscope_date = req_ts.format("%Y%m%d").to_string();
220 if cscope_date != expected_cscope_date {
221 trace!(
222 "prevalidate: credential date '{}' does not match expected date '{}'",
223 cscope_date,
224 expected_cscope_date
225 );
226 cscope_errors.push(format!("Date in Credential scope does not match YYYYMMDD from ISO-8601 version of date from HTTP: '{}' != '{}', from '{}'.", cscope_date, expected_cscope_date, req_ts.format(ISO8601_COMPACT_FORMAT)));
227 }
228
229 if !cscope_errors.is_empty() {
230 return Err(SignatureError::SignatureDoesNotMatch(Some(cscope_errors.join(" "))));
231 }
232
233 Ok(())
234 }
235
236 #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
239 #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
240 #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
241 async fn get_signing_key<S, F>(
242 &self,
243 region: &str,
244 service: &str,
245 get_signing_key: &mut S,
246 ) -> Result<GetSigningKeyResponse, SignatureError>
247 where
248 S: Service<GetSigningKeyRequest, Response = GetSigningKeyResponse, Error = BoxError, Future = F> + Send,
249 F: Future<Output = Result<GetSigningKeyResponse, BoxError>> + Send,
250 {
251 let access_key = self.credential().split('/').next().expect("prevalidate must been called first").to_string();
252
253 let req = GetSigningKeyRequest::builder()
254 .access_key(access_key)
255 .session_token(self.session_token().map(|x| x.to_string()))
256 .request_date(self.request_timestamp().date_naive())
257 .region(region)
258 .service(service)
259 .build()
260 .expect("All fields set");
261
262 match get_signing_key.oneshot(req).await {
263 Ok(key) => {
264 trace!("get_signing_key: got signing key");
265 Ok(key)
266 }
267 Err(e) => {
268 debug!("get_signing_key: error getting signing key: {}", e);
269 match e.downcast::<SignatureError>() {
270 Ok(sig_err) => Err(*sig_err),
271 Err(e) => Err(SignatureError::InternalServiceError(e)),
272 }
273 }
274 }
275 }
276
277 #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
279 #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
280 #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
281 fn get_string_to_sign(&self) -> Vec<u8> {
282 let mut result = Vec::with_capacity(
283 AWS4_HMAC_SHA256.len() + 1 + ISO8601_UTC_LENGTH + 1 + self.credential().len() + 1 + SHA256_HEX_LENGTH,
284 );
285 let hashed_canonical_request = hex::encode(self.canonical_request_sha256());
286
287 let cscope = self.credential().split_once('/').map(|x| x.1).expect("prevalidate should have been called first");
290
291 result.extend(AWS4_HMAC_SHA256.as_bytes());
292 result.push(b'\n');
293 result.extend(self.request_timestamp().format(ISO8601_COMPACT_FORMAT).to_string().as_bytes());
294 result.push(b'\n');
295 result.extend(cscope.as_bytes());
296 result.push(b'\n');
297 result.extend(hashed_canonical_request.as_bytes());
298 result
299 }
300
301 #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
303 #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
304 #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
305 pub async fn validate_signature<S, F>(
306 &self,
307 region: &str,
308 service: &str,
309 server_timestamp: DateTime<Utc>,
310 allowed_mismatch: Duration,
311 get_signing_key: &mut S,
312 ) -> Result<SigV4AuthenticatorResponse, SignatureError>
313 where
314 S: Service<GetSigningKeyRequest, Response = GetSigningKeyResponse, Error = BoxError, Future = F> + Send,
315 F: Future<Output = Result<GetSigningKeyResponse, BoxError>> + Send,
316 {
317 self.prevalidate(region, service, server_timestamp, allowed_mismatch)?;
318 let string_to_sign = self.get_string_to_sign();
319 trace!("String to sign:\n{}", String::from_utf8_lossy(string_to_sign.as_ref()));
320 let response = self.get_signing_key(region, service, get_signing_key).await?;
321 let expected_signature = hex::encode(hmac_sha256(response.signing_key().as_ref(), string_to_sign.as_ref()));
322 let expected_signature_bytes = expected_signature.as_bytes();
323 let signature_bytes = self.signature().as_bytes();
324 let is_equal: bool = signature_bytes.ct_eq(expected_signature_bytes).into();
325 if !is_equal {
326 trace!("Signature mismatch: expected '{}', got '{}'", expected_signature, self.signature());
327 Err(SignatureError::SignatureDoesNotMatch(Some(MSG_REQUEST_SIGNATURE_MISMATCH.to_string())))
328 } else {
329 Ok(response.into())
330 }
331 }
332}
333
334impl Debug for SigV4Authenticator {
335 fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
336 f.debug_struct("SigV4Authenticator")
337 .field("canonical_request_sha256", &hex::encode(self.canonical_request_sha256()))
338 .field("session_token", &self.session_token())
339 .field("signature", &self.signature())
340 .field("request_timestamp", &self.request_timestamp())
341 .finish()
342 }
343}
344
345impl SigV4AuthenticatorBuilder {
346 pub fn get_credential(&self) -> Option<&str> {
348 self.credential.as_deref()
349 }
350
351 pub fn get_signature(&self) -> Option<&str> {
353 self.signature.as_deref()
354 }
355
356 pub fn get_session_token(&self) -> Option<&str> {
358 self.session_token.as_ref()?.as_deref()
359 }
360}
361
362#[derive(Builder, Clone, Debug)]
368pub struct SigV4AuthenticatorResponse {
369 #[builder(setter(into), default)]
371 principal: Principal,
372
373 #[builder(setter(into), default)]
375 session_data: SessionData,
376}
377
378impl SigV4AuthenticatorResponse {
379 #[inline]
381 pub fn builder() -> SigV4AuthenticatorResponseBuilder {
382 SigV4AuthenticatorResponseBuilder::default()
383 }
384
385 #[inline]
387 pub fn principal(&self) -> &Principal {
388 &self.principal
389 }
390
391 #[inline]
393 pub fn session_data(&self) -> &SessionData {
394 &self.session_data
395 }
396}
397
398impl From<GetSigningKeyResponse> for SigV4AuthenticatorResponse {
399 fn from(request: GetSigningKeyResponse) -> Self {
400 SigV4AuthenticatorResponse {
401 principal: request.principal,
402 session_data: request.session_data,
403 }
404 }
405}
406
407fn duration_to_string(duration: Duration) -> String {
408 let secs = duration.num_seconds();
409 if secs % 60 == 0 {
410 format!("{} min", duration.num_minutes())
411 } else {
412 format!("{} sec", secs)
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use {
419 super::duration_to_string,
420 crate::{
421 auth::{SigV4Authenticator, SigV4AuthenticatorBuilder, SigV4AuthenticatorResponse},
422 crypto::SHA256_OUTPUT_LEN,
423 service_for_signing_key_fn, GetSigningKeyRequest, GetSigningKeyResponse, KSecretKey, SignatureError,
424 },
425 chrono::{DateTime, Duration, NaiveDate, NaiveDateTime, NaiveTime, Utc},
426 log::LevelFilter,
427 scratchstack_aws_principal::{Principal, User},
428 scratchstack_errors::ServiceError,
429 std::{error::Error, fs::File, str::FromStr},
430 tower::BoxError,
431 };
432
433 fn init() {
434 let _ = env_logger::builder().is_test(true).filter_level(LevelFilter::Trace).try_init();
435 }
436
437 #[test]
438 fn test_derived() {
439 init();
440 let epoch = DateTime::<Utc>::from_timestamp(0, 0).unwrap();
441 let test_time = DateTime::<Utc>::from_naive_utc_and_offset(
442 NaiveDateTime::new(
443 NaiveDate::from_ymd_opt(2015, 8, 30).unwrap(),
444 NaiveTime::from_hms_opt(12, 36, 0).unwrap(),
445 ),
446 Utc,
447 );
448 let auth1: SigV4Authenticator = Default::default();
449 assert_eq!(
450 auth1.canonical_request_sha256().as_slice(),
451 b"\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"
452 );
453 assert!(auth1.credential().is_empty());
454 assert!(auth1.session_token().is_none());
455 assert!(auth1.signature().is_empty());
456 assert_eq!(auth1.request_timestamp(), epoch);
457
458 let sha256: [u8; 32] = [
459 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28,
460 29, 30, 31,
461 ];
462 let auth2 = SigV4AuthenticatorBuilder::default()
463 .canonical_request_sha256(sha256)
464 .credential("AKIA1/20151231/us-east-1/example/aws4_request".to_string())
465 .session_token("token".to_string())
466 .signature("1234".to_string())
467 .request_timestamp(test_time)
468 .build()
469 .unwrap();
470
471 assert_eq!(
472 auth2.canonical_request_sha256().as_slice(),
473 &[
474 0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
475 28, 29, 30, 31,
476 ]
477 );
478 assert_eq!(auth2.credential(), "AKIA1/20151231/us-east-1/example/aws4_request");
479 assert_eq!(auth2.session_token(), Some("token"));
480 assert_eq!(auth2.signature(), "1234");
481 assert_eq!(auth2.request_timestamp(), test_time);
482
483 assert_eq!(auth2.credential(), auth2.clone().credential());
484 let _ = format!("{:?}", auth2);
485 }
486
487 async fn get_signing_key(request: GetSigningKeyRequest) -> Result<GetSigningKeyResponse, BoxError> {
488 if let Some(token) = request.session_token() {
489 match token {
490 "internal-service-error" => {
491 return Err("internal service error".into());
492 }
493 "invalid" => {
494 return Err(Box::new(SignatureError::InvalidClientTokenId(
495 "The security token included in the request is invalid".to_string(),
496 )))
497 }
498 "io-error" => {
499 let e = File::open("/00Hi1i6V4qad5nF/6KPlcyW4H9miTOD02meLgTaV09O2UToMPTE9j6sNmHZ/08EzM4qOs8bYOINWJ9RheQVadpgixRTh0VjcwpVPoo1Rh4gNAJhS4cj/this-path/does//not/exist").unwrap_err();
500 return Err(Box::new(SignatureError::from(e)));
501 }
502 "expired" => {
503 return Err(Box::new(SignatureError::ExpiredToken(
504 "The security token included in the request is expired".to_string(),
505 )))
506 }
507 _ => (),
508 }
509 }
510
511 match request.access_key() {
512 "AKIDEXAMPLE" => {
513 let principal = Principal::from(vec![User::new("aws", "123456789012", "/", "test").unwrap().into()]);
514 let k_secret = KSecretKey::from_str("wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY").unwrap();
515 let k_signing = k_secret.to_ksigning(request.request_date(), request.region(), request.service());
516
517 let response =
518 GetSigningKeyResponse::builder().principal(principal).signing_key(k_signing).build().unwrap();
519 Ok(response)
520 }
521 _ => Err(Box::new(SignatureError::InvalidClientTokenId(
522 "The AWS access key provided does not exist in our records".to_string(),
523 ))),
524 }
525 }
526
527 #[tokio::test]
528 async fn test_error_ordering() {
529 init();
530
531 let creq_sha256: [u8; SHA256_OUTPUT_LEN] = [0; SHA256_OUTPUT_LEN];
533 let test_timestamp = DateTime::<Utc>::from_naive_utc_and_offset(
534 NaiveDateTime::new(
535 NaiveDate::from_ymd_opt(2015, 8, 30).unwrap(),
536 NaiveTime::from_hms_opt(12, 36, 0).unwrap(),
537 ),
538 Utc,
539 );
540 let outdated_timestamp = DateTime::<Utc>::from_naive_utc_and_offset(
541 NaiveDateTime::new(
542 NaiveDate::from_ymd_opt(2015, 8, 30).unwrap(),
543 NaiveTime::from_hms_opt(12, 20, 59).unwrap(),
544 ),
545 Utc,
546 );
547 let future_timestamp = DateTime::<Utc>::from_naive_utc_and_offset(
548 NaiveDateTime::new(
549 NaiveDate::from_ymd_opt(2015, 8, 30).unwrap(),
550 NaiveTime::from_hms_opt(12, 51, 1).unwrap(),
551 ),
552 Utc,
553 );
554 let get_signing_key_svc = service_for_signing_key_fn(get_signing_key);
555 let mismatch = Duration::minutes(15);
556
557 let auth = SigV4Authenticator::builder()
558 .canonical_request_sha256(creq_sha256)
559 .credential("AKIDFOO/20130101/wrong-region/wrong-service".to_string())
560 .session_token("expired")
561 .signature("invalid".to_string())
562 .request_timestamp(outdated_timestamp)
563 .build()
564 .unwrap();
565
566 let e = auth
567 .validate_signature("us-east-1", "example", test_timestamp, mismatch, &mut get_signing_key_svc.clone())
568 .await
569 .unwrap_err();
570
571 if let SignatureError::SignatureDoesNotMatch(ref msg) = e {
572 assert_eq!(
573 msg.as_ref().unwrap(),
574 "Signature expired: 20150830T122059Z is now earlier than 20150830T122100Z (20150830T123600Z - 15 min.)"
575 );
576 assert_eq!(e.error_code(), "SignatureDoesNotMatch");
577 assert_eq!(e.http_status(), 403);
578 } else {
579 panic!("Unexpected error: {:?}", e);
580 }
581
582 let auth = SigV4Authenticator::builder()
583 .canonical_request_sha256(creq_sha256)
584 .credential("AKIDFOO/20130101/wrong-region/wrong-service".to_string())
585 .session_token("expired")
586 .signature("invalid".to_string())
587 .request_timestamp(future_timestamp)
588 .build()
589 .unwrap();
590
591 let e = auth
592 .validate_signature("us-east-1", "example", test_timestamp, mismatch, &mut get_signing_key_svc.clone())
593 .await
594 .unwrap_err();
595
596 if let SignatureError::SignatureDoesNotMatch(ref msg) = e {
597 assert_eq!(
598 msg.as_ref().unwrap(),
599 "Signature not yet current: 20150830T125101Z is still later than 20150830T125100Z (20150830T123600Z + 15 min.)"
600 );
601 assert_eq!(e.error_code(), "SignatureDoesNotMatch");
602 assert_eq!(e.http_status(), 403);
603 } else {
604 panic!("Unexpected error: {:?}", e);
605 }
606
607 let auth = SigV4Authenticator::builder()
608 .canonical_request_sha256(creq_sha256)
609 .credential("AKIDFOO/20130101/wrong-region/wrong-service".to_string())
610 .session_token("expired")
611 .signature("invalid".to_string())
612 .request_timestamp(test_timestamp)
613 .build()
614 .unwrap();
615
616 let e = auth
617 .validate_signature("us-east-1", "example", test_timestamp, mismatch, &mut get_signing_key_svc.clone())
618 .await
619 .unwrap_err();
620
621 if let SignatureError::IncompleteSignature(_) = e {
622 assert_eq!(
623 e.to_string(),
624 "Credential must have exactly 5 slash-delimited elements, e.g. keyid/date/region/service/term, got 'AKIDFOO/20130101/wrong-region/wrong-service'"
625 );
626 assert_eq!(e.error_code(), "IncompleteSignature");
627 assert_eq!(e.http_status(), 400);
628 } else {
629 panic!("Unexpected error: {:?}", e);
630 }
631
632 let auth = SigV4Authenticator::builder()
633 .canonical_request_sha256(creq_sha256)
634 .credential("AKIDFOO/20130101/wrong-region/wrong-service/aws5_request".to_string())
635 .session_token("expired")
636 .signature("invalid".to_string())
637 .request_timestamp(test_timestamp)
638 .build()
639 .unwrap();
640
641 let e = auth
642 .validate_signature("us-east-1", "example", test_timestamp, mismatch, &mut get_signing_key_svc.clone())
643 .await
644 .unwrap_err();
645
646 if let SignatureError::SignatureDoesNotMatch(_) = e {
647 assert_eq!(
648 e.to_string(),
649 "Credential should be scoped to a valid region, not 'wrong-region'. Credential should be scoped to correct service: 'example'. 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: '20130101' != '20150830', from '20150830T123600Z'."
650 );
651 assert_eq!(e.error_code(), "SignatureDoesNotMatch");
652 assert_eq!(e.http_status(), 403);
653 } else {
654 panic!("Unexpected error: {:?}", e);
655 }
656
657 let auth = SigV4Authenticator::builder()
658 .canonical_request_sha256(creq_sha256)
659 .credential("AKIDFOO/20150830/us-east-1/example/aws4_request".to_string())
660 .session_token("invalid")
661 .signature("invalid".to_string())
662 .request_timestamp(test_timestamp)
663 .build()
664 .unwrap();
665
666 let e = auth
667 .validate_signature("us-east-1", "example", test_timestamp, mismatch, &mut get_signing_key_svc.clone())
668 .await
669 .unwrap_err();
670
671 if let SignatureError::InvalidClientTokenId(_) = e {
672 assert_eq!(e.to_string(), "The security token included in the request is invalid");
673 assert_eq!(e.error_code(), "InvalidClientTokenId");
674 assert_eq!(e.http_status(), 403);
675 } else {
676 panic!("Unexpected error: {:?}", e);
677 }
678
679 let auth = SigV4Authenticator::builder()
680 .canonical_request_sha256(creq_sha256)
681 .credential("AKIDFOO/20150830/us-east-1/example/aws4_request".to_string())
682 .session_token("expired")
683 .signature("invalid".to_string())
684 .request_timestamp(test_timestamp)
685 .build()
686 .unwrap();
687
688 let e = auth
689 .validate_signature("us-east-1", "example", test_timestamp, mismatch, &mut get_signing_key_svc.clone())
690 .await
691 .unwrap_err();
692
693 if let SignatureError::ExpiredToken(_) = e {
694 assert_eq!(e.to_string(), "The security token included in the request is expired");
695 assert_eq!(e.error_code(), "ExpiredToken");
696 assert_eq!(e.http_status(), 403);
697 } else {
698 panic!("Unexpected error: {:?}", e);
699 }
700
701 let auth = SigV4Authenticator::builder()
702 .canonical_request_sha256(creq_sha256)
703 .credential("AKIDFOO/20150830/us-east-1/example/aws4_request".to_string())
704 .session_token("internal-service-error")
705 .signature("invalid".to_string())
706 .request_timestamp(test_timestamp)
707 .build()
708 .unwrap();
709
710 let e = auth
711 .validate_signature("us-east-1", "example", test_timestamp, mismatch, &mut get_signing_key_svc.clone())
712 .await
713 .unwrap_err();
714
715 if let SignatureError::InternalServiceError(ref err) = e {
716 assert_eq!(format!("{:?}", err), r#""internal service error""#);
717 assert_eq!(e.to_string(), "internal service error");
718 assert_eq!(e.error_code(), "InternalFailure");
719 assert_eq!(e.http_status(), 500);
720 } else {
721 panic!("Unexpected error: {:?}", e);
722 }
723
724 let auth = SigV4Authenticator::builder()
725 .canonical_request_sha256(creq_sha256)
726 .credential("AKIDFOO/20150830/us-east-1/example/aws4_request".to_string())
727 .session_token("io-error")
728 .signature("invalid".to_string())
729 .request_timestamp(test_timestamp)
730 .build()
731 .unwrap();
732
733 let e = auth
734 .validate_signature("us-east-1", "example", test_timestamp, mismatch, &mut get_signing_key_svc.clone())
735 .await
736 .unwrap_err();
737
738 if let SignatureError::IO(_) = e {
739 let e_string = e.to_string();
740 assert!(
741 e_string.contains("No such file or directory")
742 || e_string.contains("The system cannot find the file specified"),
743 "Error message: {:#?}",
744 e_string
745 );
746 assert_eq!(e.error_code(), "InternalFailure");
747 assert_eq!(e.http_status(), 500);
748 assert!(e.source().is_some());
749 } else {
750 panic!("Unexpected error: {:?}", e);
751 }
752
753 let auth = SigV4Authenticator::builder()
754 .canonical_request_sha256(creq_sha256)
755 .credential("AKIDFOO/20150830/us-east-1/example/aws4_request".to_string())
756 .session_token("ok")
757 .signature("invalid".to_string())
758 .request_timestamp(test_timestamp)
759 .build()
760 .unwrap();
761
762 let e = auth
763 .validate_signature("us-east-1", "example", test_timestamp, mismatch, &mut get_signing_key_svc.clone())
764 .await
765 .unwrap_err();
766
767 if let SignatureError::InvalidClientTokenId(_) = e {
768 assert_eq!(e.to_string(), "The AWS access key provided does not exist in our records");
769 assert_eq!(e.error_code(), "InvalidClientTokenId");
770 assert_eq!(e.http_status(), 403);
771 } else {
772 panic!("Unexpected error: {:?}", e);
773 }
774
775 let auth = SigV4Authenticator::builder()
776 .canonical_request_sha256(creq_sha256)
777 .credential("AKIDEXAMPLE/20150830/us-east-1/example/aws4_request".to_string())
778 .session_token("ok")
779 .signature("invalid".to_string())
780 .request_timestamp(test_timestamp)
781 .build()
782 .unwrap();
783
784 let e = auth
785 .validate_signature("us-east-1", "example", test_timestamp, mismatch, &mut get_signing_key_svc.clone())
786 .await
787 .unwrap_err();
788
789 if let SignatureError::SignatureDoesNotMatch(_) = e {
790 assert_eq!(e.to_string(), "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.");
791 assert_eq!(e.error_code(), "SignatureDoesNotMatch");
792 assert_eq!(e.http_status(), 403);
793 } else {
794 panic!("Unexpected error: {:?}", e);
795 }
796
797 let auth = SigV4Authenticator::builder()
798 .canonical_request_sha256(creq_sha256)
799 .credential("AKIDEXAMPLE/20150830/us-east-1/example/aws4_request".to_string())
800 .session_token("ok")
801 .signature("88bf1ccb1e3e4df7bb2ed6d89bcd8558d6770845007e1a5c392ac9edce0d5deb".to_string())
802 .request_timestamp(test_timestamp)
803 .build()
804 .unwrap();
805
806 let _ = auth
807 .validate_signature("us-east-1", "example", test_timestamp, mismatch, &mut get_signing_key_svc.clone())
808 .await
809 .unwrap();
810 }
811
812 #[test]
813 fn test_duration_formatting() {
814 init();
815 assert_eq!(duration_to_string(Duration::seconds(32)).as_str(), "32 sec");
816 assert_eq!(duration_to_string(Duration::seconds(60)).as_str(), "1 min");
817 assert_eq!(duration_to_string(Duration::seconds(61)).as_str(), "61 sec");
818 assert_eq!(duration_to_string(Duration::seconds(600)).as_str(), "10 min");
819 }
820
821 #[test_log::test]
822 fn test_response_builder() {
823 let response = SigV4AuthenticatorResponse::builder().build().unwrap();
824 assert!(response.principal().is_empty());
825 assert!(response.session_data().is_empty());
826
827 let response2 = response.clone();
828 assert_eq!(format!("{:?}", response), format!("{:?}", response2));
829 }
830}