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,
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        // Rule 13: Credential scope must be correct for the region/service/date.
192        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    /// Return the signing key (`kSigning` from the [AWS documentation](https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html))
237    /// for the request.
238    #[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    /// Return the string to sign for the request.
278    #[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        // Remove the access key from the credential to get the credential scope. This requires that prevalidate() has
288        // been called.
289        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    /// Validate the request signature.
302    #[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    /// Retrieve the credential passed into the request.
347    pub fn get_credential(&self) -> Option<&str> {
348        self.credential.as_deref()
349    }
350
351    /// Retrieve the signature passed into the request.
352    pub fn get_signature(&self) -> Option<&str> {
353        self.signature.as_deref()
354    }
355
356    /// Retrieve the session token passed into the request.
357    pub fn get_session_token(&self) -> Option<&str> {
358        self.session_token.as_ref()?.as_deref()
359    }
360}
361
362/// Upon successful authentication of a signature, this is returned to convey the principal, session data, and possibly
363/// policies associated with the request.
364///
365/// SigV4AuthenticatorResponse structs are immutable. Use [SigV4AuthenticatorResponseBuilder] to construct a new
366/// response.
367#[derive(Builder, Clone, Debug)]
368pub struct SigV4AuthenticatorResponse {
369    /// The principal actors of the request.
370    #[builder(setter(into), default)]
371    principal: Principal,
372
373    /// The session data associated with the principal.
374    #[builder(setter(into), default)]
375    session_data: SessionData,
376}
377
378impl SigV4AuthenticatorResponse {
379    /// Create a [SigV4AuthenticatorResponseBuilder] to construct a [SigV4AuthenticatorResponse].
380    #[inline]
381    pub fn builder() -> SigV4AuthenticatorResponseBuilder {
382        SigV4AuthenticatorResponseBuilder::default()
383    }
384
385    /// Retrieve the principal actors of the request.
386    #[inline]
387    pub fn principal(&self) -> &Principal {
388        &self.principal
389    }
390
391    /// Retrieve the session data associated with the principal.
392    #[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        // Test that the error ordering is correct.
532        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}