dynoxide/
auth_material.rs1#[derive(Debug, Default, Clone, Copy)]
16pub struct AuthMaterial<'a> {
17 pub authorization: Option<&'a str>,
19 pub query: &'a str,
21 pub has_date_header: bool,
23}
24
25pub fn validate(material: AuthMaterial<'_>) -> Option<String> {
31 let AuthMaterial {
32 authorization,
33 query,
34 has_date_header,
35 } = material;
36
37 let has_algorithm_query = query.split('&').any(|p| {
38 let key = p.split('=').next().unwrap_or("");
39 key == "X-Amz-Algorithm"
40 });
41
42 if authorization.is_some() && has_algorithm_query {
44 return Some(envelope(
45 "InvalidSignatureException",
46 "Found both 'X-Amz-Algorithm' as a query-string param and 'Authorization' as HTTP header.",
47 ));
48 }
49
50 if has_algorithm_query {
51 return validate_query_auth(query);
52 }
53
54 validate_header_auth(authorization, has_date_header)
55}
56
57fn validate_query_auth(query: &str) -> Option<String> {
58 let mut missing = Vec::new();
59 let query_params: Vec<&str> = query
60 .split('&')
61 .map(|p| p.split('=').next().unwrap_or(""))
62 .collect();
63
64 let algo_has_value = query.split('&').any(|p| {
66 let mut parts = p.splitn(2, '=');
67 let key = parts.next().unwrap_or("");
68 let val = parts.next().unwrap_or("");
69 key == "X-Amz-Algorithm" && !val.is_empty()
70 });
71
72 if !algo_has_value {
73 missing.push("'X-Amz-Algorithm'");
74 }
75 for (param, label) in [
76 ("X-Amz-Credential", "'X-Amz-Credential'"),
77 ("X-Amz-Signature", "'X-Amz-Signature'"),
78 ("X-Amz-SignedHeaders", "'X-Amz-SignedHeaders'"),
79 ("X-Amz-Date", "'X-Amz-Date'"),
80 ] {
81 if !query_params.contains(¶m) {
82 missing.push(label);
83 }
84 }
85
86 if missing.is_empty() {
87 return None;
88 }
89
90 let parts: Vec<String> = missing
91 .iter()
92 .map(|p| format!("AWS query-string parameters must include {p}. "))
93 .collect();
94 Some(envelope(
95 "IncompleteSignatureException",
96 &format!("{}Re-examine the query-string parameters.", parts.join("")),
97 ))
98}
99
100fn validate_header_auth(authorization: Option<&str>, has_date_header: bool) -> Option<String> {
101 const MISSING_TOKEN: &str = "Request is missing Authentication Token";
102
103 let Some(auth) = authorization else {
104 return Some(envelope(
105 "MissingAuthenticationTokenException",
106 MISSING_TOKEN,
107 ));
108 };
109
110 if !auth.starts_with("AWS4-") {
111 return Some(envelope(
112 "MissingAuthenticationTokenException",
113 MISSING_TOKEN,
114 ));
115 }
116
117 let has_credential = auth.contains("Credential=") || auth.contains("credential=");
120 let has_signature = auth.contains("Signature=") || auth.contains("signature=");
121 let has_signed_headers = auth.contains("SignedHeaders=") || auth.contains("signedheaders=");
122
123 let mut missing = Vec::new();
124 if !has_credential {
125 missing.push("'Credential'");
126 }
127 if !has_signature {
128 missing.push("'Signature'");
129 }
130 if !has_signed_headers {
131 missing.push("'SignedHeaders'");
132 }
133 if !has_date_header {
134 missing.push("existence of either a 'X-Amz-Date' or a 'Date' header.");
135 }
136
137 if missing.is_empty() {
138 return None;
139 }
140
141 let mut parts: Vec<String> = missing
142 .iter()
143 .map(|p| {
144 if p.contains("existence of") {
145 format!("Authorization header requires {p}")
146 } else {
147 format!("Authorization header requires {p} parameter.")
148 }
149 })
150 .collect();
151 parts.push(format!("Authorization={auth}"));
152 Some(envelope("IncompleteSignatureException", &parts.join(" ")))
153}
154
155fn envelope(error_type: &str, message: &str) -> String {
156 serde_json::json!({
157 "__type": format!("com.amazon.coral.service#{error_type}"),
158 "message": message,
159 })
160 .to_string()
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 const SIGNED: &str = "AWS4-HMAC-SHA256 Credential=fake/20260724/eu-west-2/dynamodb/aws4_request, SignedHeaders=host;x-amz-date, Signature=abc";
168
169 fn header(auth: &str) -> AuthMaterial<'_> {
170 AuthMaterial {
171 authorization: Some(auth),
172 query: "",
173 has_date_header: true,
174 }
175 }
176
177 #[test]
178 fn a_well_formed_signed_request_passes() {
179 assert!(validate(header(SIGNED)).is_none());
180 }
181
182 #[test]
183 fn a_missing_authorization_header_is_a_missing_token() {
184 let out = validate(AuthMaterial::default()).expect("should reject");
185 assert!(out.contains("MissingAuthenticationTokenException"), "{out}");
186 }
187
188 #[test]
189 fn a_non_sigv4_authorization_header_is_a_missing_token() {
190 let out = validate(header("Basic abc123")).expect("should reject");
191 assert!(out.contains("MissingAuthenticationTokenException"), "{out}");
192 }
193
194 #[test]
195 fn an_incomplete_authorization_header_names_each_missing_part() {
196 let out = validate(header("AWS4-HMAC-SHA256 Credential=fake")).expect("should reject");
197 assert!(out.contains("IncompleteSignatureException"), "{out}");
198 assert!(out.contains("'Signature'"), "{out}");
199 assert!(out.contains("'SignedHeaders'"), "{out}");
200 }
201
202 #[test]
203 fn a_signed_request_without_a_date_header_is_incomplete() {
204 let material = AuthMaterial {
205 authorization: Some(SIGNED),
206 query: "",
207 has_date_header: false,
208 };
209 let out = validate(material).expect("should reject");
210 assert!(out.contains("X-Amz-Date"), "{out}");
211 }
212
213 #[test]
214 fn complete_query_string_auth_passes() {
215 let material = AuthMaterial {
216 authorization: None,
217 query: "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=c&X-Amz-Signature=s&X-Amz-SignedHeaders=host&X-Amz-Date=d",
218 has_date_header: false,
219 };
220 assert!(validate(material).is_none());
221 }
222
223 #[test]
224 fn incomplete_query_string_auth_names_the_missing_parameters() {
225 let material = AuthMaterial {
226 authorization: None,
227 query: "X-Amz-Algorithm=AWS4-HMAC-SHA256",
228 has_date_header: false,
229 };
230 let out = validate(material).expect("should reject");
231 assert!(out.contains("IncompleteSignatureException"), "{out}");
232 assert!(out.contains("'X-Amz-Signature'"), "{out}");
233 }
234
235 #[test]
236 fn mixing_header_and_query_auth_is_an_invalid_signature() {
237 let material = AuthMaterial {
238 authorization: Some(SIGNED),
239 query: "X-Amz-Algorithm=AWS4-HMAC-SHA256",
240 has_date_header: true,
241 };
242 let out = validate(material).expect("should reject");
243 assert!(out.contains("InvalidSignatureException"), "{out}");
244 }
245}