scratchstack_aws_signature/
auth.rs

1//! AWS API request signatures verification routines.
2//!
3//! This implements the AWS [SigV4](http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html)
4//! and [SigV4S3](https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html)
5//! server-side validation algorithms.
6//!
7//! **Stability of this module is not guaranteed except for items exposed at the crate root**.
8//! The functions and types are subject to change in minor/patch versions. This is exposed for
9//! testing purposes only.
10
11use {
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
29/// Algorithm for AWS SigV4
30const AWS4_HMAC_SHA256: &str = "AWS4-HMAC-SHA256";
31
32/// String included at the end of the AWS SigV4 credential scope
33const AWS4_REQUEST: &str = "aws4_request";
34
35/// Compact ISO8601 format used for the string to sign.
36const ISO8601_COMPACT_FORMAT: &str = "%Y%m%dT%H%M%SZ";
37
38/// Length of an ISO8601 date string in the UTC time zone.
39const ISO8601_UTC_LENGTH: usize = 16;
40
41/// Error message: `"Credential must have exactly 5 slash-delimited elements, e.g. keyid/date/region/service/term,"`
42const MSG_CREDENTIAL_MUST_HAVE_FIVE_PARTS: &str =
43    "Credential must have exactly 5 slash-delimited elements, e.g. keyid/date/region/service/term,";
44
45/// Error message: `"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."`
46const 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
48/// SHA-256 of an empty string.
49const SHA256_EMPTY: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
50
51/// Length of a SHA-256 hex string.
52const SHA256_HEX_LENGTH: usize = SHA256_EMPTY.len();
53
54/// Low-level structure for performing AWS SigV4 authentication after a canonical request has been generated.
55#[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    /// The SHA-256 hash of the canonical request.
62    canonical_request_sha256: [u8; SHA256_OUTPUT_LEN],
63
64    /// The credential passed into the request, in the form of `keyid/date/region/service/aws4_request`.
65    /// The date must reflect that of the request timestamp in `YYYYMMDD` format, not the server's
66    /// date. Timestamp validation is performed separately.
67    credential: String,
68
69    /// The optional session token.
70    #[builder(setter(into, strip_option), default)]
71    session_token: Option<String>,
72
73    /// The signature passed into the request.
74    signature: String,
75
76    /// The timestamp of the request, from either `X-Amz-Date` query string/header or the `Date` header.
77    request_timestamp: DateTime<Utc>,
78}
79
80impl SigV4Authenticator {
81    /// Create a builder for `SigV4Authenticator`.
82    #[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    /// Retrieve the SHA-256 hash of the canonical request.
91    #[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    /// Retrieve the credential passed into the request, in the form of `keyid/date/region/service/aws4_request`.
100    #[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    /// Retrieve the optional session token.
109    #[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    /// Retrieve the signature passed into the request.
118    #[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    /// Retrieve the timestamp of the request.
127    #[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    /// Verify the request parameters make sense for the region, service, and specified timestamp.
136    /// This must be called successfully before calling [validate_signature][Self::validate_signature].
137    #[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        // Rule 10: Make sure date isn't expired...
152        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        // Rule 11: ... or too far into the future.
164        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        // Rule 12: Credential scope must have exactly five elements.
176        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        // Rule 13: Credential scope must be correct for the region/service/date.
191        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    /// Return the signing key (`kSigning` from the [AWS documentation](https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html))
236    /// for the request.
237    #[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    /// Return the string to sign for the request.
277    #[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        // Remove the access key from the credential to get the credential scope. This requires that prevalidate() has
287        // been called.
288        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    /// Validate the request signature.
301    #[cfg_attr(doc, doc(cfg(feature = "unstable")))]
302    #[cfg_attr(any(doc, feature = "unstable"), qualifiers(pub))]
303    #[cfg_attr(not(any(doc, feature = "unstable")), qualifiers(pub(crate)))]
304    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    /// Retrieve the credential passed into the request.
346    pub fn get_credential(&self) -> Option<&str> {
347        self.credential.as_deref()
348    }
349
350    /// Retrieve the signature passed into the request.
351    pub fn get_signature(&self) -> Option<&str> {
352        self.signature.as_deref()
353    }
354
355    /// Retrieve the session token passed into the request.
356    pub fn get_session_token(&self) -> Option<&str> {
357        self.session_token.as_ref()?.as_deref()
358    }
359}
360
361/// Upon successful authentication of a signature, this is returned to convey the principal, session data, and possibly
362/// policies associated with the request.
363///
364/// SigV4AuthenticatorResponse structs are immutable. Use [SigV4AuthenticatorResponseBuilder] to construct a new
365/// response.
366#[derive(Builder, Clone, Debug)]
367pub struct SigV4AuthenticatorResponse {
368    /// The principal actors of the request.
369    #[builder(setter(into), default)]
370    principal: Principal,
371
372    /// The session data associated with the principal.
373    #[builder(setter(into), default)]
374    session_data: SessionData,
375}
376
377impl SigV4AuthenticatorResponse {
378    /// Create a [SigV4AuthenticatorResponseBuilder] to construct a [SigV4AuthenticatorResponse].
379    #[inline]
380    pub fn builder() -> SigV4AuthenticatorResponseBuilder {
381        SigV4AuthenticatorResponseBuilder::default()
382    }
383
384    /// Retrieve the principal actors of the request.
385    #[inline]
386    pub fn principal(&self) -> &Principal {
387        &self.principal
388    }
389
390    /// Retrieve the session data associated with the principal.
391    #[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        // Test that the error ordering is correct.
531        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}