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