scratchstack_aws_signature/
error.rs

1use {
2    http::status::StatusCode,
3    scratchstack_errors::ServiceError,
4    std::{
5        error::Error,
6        fmt::{Display, Formatter, Result as FmtResult},
7        io::Error as IOError,
8    },
9};
10
11/// Error code: ExpiredToken
12const ERR_CODE_EXPIRED_TOKEN: &str = "ExpiredToken";
13
14/// Error code: InternalFailure
15const ERR_CODE_INTERNAL_FAILURE: &str = "InternalFailure";
16
17/// Error code: InvalidContentType (non-AWS standard)
18const ERR_CODE_INVALID_CONTENT_TYPE: &str = "InvalidContentType";
19
20/// Error code: InvalidBodyEncoding
21const ERR_CODE_INVALID_BODY_ENCODING: &str = "InvalidBodyEncoding";
22
23/// Error code: InvalidClientTokenId
24const ERR_CODE_INVALID_CLIENT_TOKEN_ID: &str = "InvalidClientTokenId";
25
26/// Error code: IncompleteSignature
27const ERR_CODE_INCOMPLETE_SIGNATURE: &str = "IncompleteSignature";
28
29/// Error code: InvalidRequestMethod (non-AWS standard)
30const ERR_CODE_INVALID_REQUEST_METHOD: &str = "InvalidRequestMethod";
31
32/// Error code: InvalidURIPath
33const ERR_CODE_INVALID_URI_PATH: &str = "InvalidURIPath";
34
35/// Error code: MalformedQueryString
36const ERR_CODE_MALFORMED_QUERY_STRING: &str = "MalformedQueryString";
37
38/// Error code: MissingAuthenticationToken
39const ERR_CODE_MISSING_AUTHENTICATION_TOKEN: &str = "MissingAuthenticationToken";
40
41/// Error code: SignatureDoesNotMatch
42const ERR_CODE_SIGNATURE_DOES_NOT_MATCH: &str = "SignatureDoesNotMatch";
43
44/// Error returned when an attempt at validating an AWS SigV4 signature fails.
45#[derive(Debug)]
46#[non_exhaustive]
47pub enum SignatureError {
48    /// The security token included with the request is expired.
49    ExpiredToken(/* message */ String),
50
51    /// Validation failed due to an underlying I/O error.
52    IO(IOError),
53
54    /// Validation failed due to an internal service error.
55    InternalServiceError(Box<dyn Error + Send + Sync>),
56
57    /// The request body used an unsupported character set encoding. Currently only UTF-8 is supported.
58    InvalidBodyEncoding(/* message */ String),
59
60    /// The AWS access key provided does not exist in our records.
61    InvalidClientTokenId(/* message */ String),
62
63    /// The content-type of the request is unsupported.
64    InvalidContentType(/* message */ String),
65
66    /// Invalid request method.
67    InvalidRequestMethod(/* message */ String),
68
69    /// The request signature does not conform to AWS standards. Sample messages:  
70    /// `Authorization header requires 'Credential' parameter. Authorization=...`  
71    /// `Authorization header requires existence of either a 'X-Amz-Date' or a 'Date' header.`  
72    /// `Date must be in ISO-8601 'basic format'. Got '...'. See http://en.wikipedia.org/wiki/ISO_8601`  
73    /// `Unsupported AWS 'algorithm': 'AWS4-HMAC-SHA512'`
74    IncompleteSignature(/* message */ String),
75
76    /// The URI path includes invalid components. This can be a malformed hex encoding (e.g. `%0J`), a non-absolute
77    /// URI path (`foo/bar`), or a URI path that attempts to navigate above the root (`/x/../../../y`).
78    InvalidURIPath(/* message */ String),
79
80    /// A query parameter was malformed -- the value could not be decoded as UTF-8, or the parameter was empty and
81    /// this is not allowed (e.g. a signature parameter), or the parameter could not be parsed (e.g., the `X-Amz-Date`
82    /// parameter is not a valid date).
83    ///
84    /// `Incomplete trailing escape % sequence`
85    MalformedQueryString(/* message */ String),
86
87    /// The request must contain either a valid (registered) AWS access key ID or X.509 certificate. Sample messages:  
88    /// `Request is missing Authentication Token`  
89    MissingAuthenticationToken(/* message */ String),
90
91    /// Signature did not match the calculated signature value.
92    /// Example messages:  
93    /// `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.`  
94    /// `Signature expired: 20210502T144040Z is now earlier than 20210502T173143Z (20210502T174643Z - 15 min.)`  
95    /// `Signature not yet current: 20210502T183640Z is still later than 20210502T175140Z (20210502T173640Z + 15 min.)`
96    SignatureDoesNotMatch(Option</* message */ String>),
97}
98
99impl SignatureError {
100    fn error_code(&self) -> &'static str {
101        match self {
102            Self::ExpiredToken(_) => ERR_CODE_EXPIRED_TOKEN,
103            Self::IO(_) | Self::InternalServiceError(_) => ERR_CODE_INTERNAL_FAILURE,
104            Self::InvalidBodyEncoding(_) => ERR_CODE_INVALID_BODY_ENCODING,
105            Self::InvalidClientTokenId(_) => ERR_CODE_INVALID_CLIENT_TOKEN_ID,
106            Self::InvalidContentType(_) => ERR_CODE_INVALID_CONTENT_TYPE,
107            Self::InvalidRequestMethod(_) => ERR_CODE_INVALID_REQUEST_METHOD,
108            Self::IncompleteSignature(_) => ERR_CODE_INCOMPLETE_SIGNATURE,
109            Self::InvalidURIPath(_) => ERR_CODE_INVALID_URI_PATH,
110            Self::MalformedQueryString(_) => ERR_CODE_MALFORMED_QUERY_STRING,
111            Self::MissingAuthenticationToken(_) => ERR_CODE_MISSING_AUTHENTICATION_TOKEN,
112            Self::SignatureDoesNotMatch(_) => ERR_CODE_SIGNATURE_DOES_NOT_MATCH,
113        }
114    }
115
116    fn http_status(&self) -> StatusCode {
117        match self {
118            Self::IncompleteSignature(_)
119            | Self::InvalidBodyEncoding(_)
120            | Self::InvalidRequestMethod(_)
121            | Self::InvalidURIPath(_)
122            | Self::MalformedQueryString(_)
123            | Self::MissingAuthenticationToken(_) => StatusCode::BAD_REQUEST,
124            Self::IO(_) | Self::InternalServiceError(_) => StatusCode::INTERNAL_SERVER_ERROR,
125            _ => StatusCode::FORBIDDEN,
126        }
127    }
128}
129
130impl ServiceError for SignatureError {
131    fn error_code(&self) -> &'static str {
132        SignatureError::error_code(self)
133    }
134
135    fn http_status(&self) -> StatusCode {
136        SignatureError::http_status(self)
137    }
138}
139
140impl Display for SignatureError {
141    fn fmt(&self, f: &mut Formatter) -> FmtResult {
142        match self {
143            Self::ExpiredToken(msg) => f.write_str(msg),
144            Self::IO(ref e) => Display::fmt(e, f),
145            Self::InternalServiceError(ref e) => Display::fmt(e, f),
146            Self::InvalidBodyEncoding(msg) => f.write_str(msg),
147            Self::InvalidClientTokenId(msg) => f.write_str(msg),
148            Self::InvalidContentType(msg) => f.write_str(msg),
149            Self::InvalidRequestMethod(msg) => f.write_str(msg),
150            Self::IncompleteSignature(msg) => f.write_str(msg),
151            Self::InvalidURIPath(msg) => f.write_str(msg),
152            Self::MalformedQueryString(msg) => f.write_str(msg),
153            Self::MissingAuthenticationToken(msg) => f.write_str(msg),
154            Self::SignatureDoesNotMatch(msg) => {
155                if let Some(msg) = msg {
156                    f.write_str(msg)
157                } else {
158                    Ok(())
159                }
160            }
161        }
162    }
163}
164
165impl Error for SignatureError {
166    fn source(&self) -> Option<&(dyn Error + 'static)> {
167        match self {
168            Self::IO(ref e) => Some(e),
169            _ => None,
170        }
171    }
172}
173
174impl From<IOError> for SignatureError {
175    fn from(e: IOError) -> SignatureError {
176        SignatureError::IO(e)
177    }
178}
179
180impl From<Box<dyn Error + Send + Sync>> for SignatureError {
181    fn from(e: Box<dyn Error + Send + Sync>) -> SignatureError {
182        match e.downcast::<SignatureError>() {
183            Ok(sig_err) => *sig_err,
184            Err(e) => SignatureError::InternalServiceError(e),
185        }
186    }
187}
188
189/// Error returned by `KSecretKey::from_str` when the secret key cannot fit in the expected size.
190#[derive(Clone, Copy, Debug, Eq, PartialEq)]
191pub struct KeyTooLongError;
192
193impl Display for KeyTooLongError {
194    fn fmt(&self, f: &mut Formatter) -> FmtResult {
195        f.write_str("Key too long")
196    }
197}
198
199impl Error for KeyTooLongError {}
200
201#[cfg(test)]
202mod tests {
203    use {crate::SignatureError, std::error::Error};
204
205    #[test_log::test]
206    fn test_from() {
207        // This just exercises a few codepaths that aren't usually exercised.
208        let utf8_error = Box::new(String::from_utf8(b"\x80".to_vec()).unwrap_err());
209        let e: SignatureError = (utf8_error as Box<dyn Error + Send + Sync + 'static>).into();
210        assert_eq!(e.error_code(), "InternalFailure");
211        assert_eq!(e.http_status(), 500);
212
213        let e = SignatureError::MalformedQueryString("foo".to_string());
214        let e2 = SignatureError::from(Box::new(e) as Box<dyn Error + Send + Sync + 'static>);
215        assert_eq!(e2.to_string(), "foo");
216        assert_eq!(e2.error_code(), "MalformedQueryString");
217
218        let e = SignatureError::InvalidContentType("Invalid content type: image/jpeg".to_string());
219        assert_eq!(e.error_code(), "InvalidContentType");
220        assert_eq!(e.http_status(), 403); // Should be 400, but AWS returns 403.
221        assert_eq!(format!("{}", e), "Invalid content type: image/jpeg");
222
223        let e = SignatureError::InvalidRequestMethod("Invalid request method: DELETE".to_string());
224        assert_eq!(e.error_code(), "InvalidRequestMethod");
225        assert_eq!(e.http_status(), 400);
226        assert_eq!(format!("{}", e), "Invalid request method: DELETE");
227    }
228}