1use super::{Hmac, Mac, Route53Credentials, Sha256};
10
11#[derive(Clone, Copy)]
13pub(super) struct Scope {
14 region: &'static str,
15 service: &'static str,
16}
17
18pub(super) const ROUTE53: Scope = Scope {
21 region: "us-east-1",
22 service: "route53",
23};
24
25impl Scope {
26 fn at(self, amz_date: &str) -> String {
28 let date_stamp = &amz_date[..8];
29 format!("{date_stamp}/{}/{}/aws4_request", self.region, self.service)
30 }
31}
32
33pub(super) struct Request<'a> {
35 pub(super) method: &'a str,
36 pub(super) host: &'a str,
39 pub(super) path: &'a str,
40 pub(super) query: &'a [(String, String)],
41 pub(super) body: &'a str,
42}
43
44pub(super) struct Signed {
45 pub(super) amz_date: String,
46 pub(super) authorization: String,
47}
48
49pub(super) fn host_header(endpoint: &str) -> String {
50 endpoint
51 .trim_start_matches("https://")
52 .trim_start_matches("http://")
53 .trim_end_matches('/')
54 .to_string()
55}
56
57pub(super) fn sign(
60 credentials: &Route53Credentials,
61 scope: Scope,
62 request: &Request,
63 now: time::OffsetDateTime,
64) -> Signed {
65 let amz_date = format!(
66 "{:04}{:02}{:02}T{:02}{:02}{:02}Z",
67 now.year(),
68 u8::from(now.month()),
69 now.day(),
70 now.hour(),
71 now.minute(),
72 now.second()
73 );
74 let mut headers = vec![("host", request.host), ("x-amz-date", amz_date.as_str())];
75 if let Some(token) = &credentials.session_token {
76 headers.push(("x-amz-security-token", token.as_str()));
77 }
78
79 let canonical_request = canonical_request(
80 request.method,
81 request.path,
82 request.query,
83 &headers,
84 request.body,
85 );
86 let string_to_sign = string_to_sign(scope, &amz_date, &canonical_request);
87 let signature = signature(
88 &credentials.secret_access_key,
89 scope,
90 &amz_date,
91 &string_to_sign,
92 );
93 let authorization = format!(
94 "AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={signature}",
95 credentials.access_key_id,
96 scope.at(&amz_date),
97 signed_headers(&headers),
98 );
99
100 Signed {
101 amz_date,
102 authorization,
103 }
104}
105
106fn canonical_request(
109 method: &str,
110 path: &str,
111 query: &[(String, String)],
112 headers: &[(&str, &str)],
113 body: &str,
114) -> String {
115 let mut sorted = headers.to_vec();
116 sorted.sort_unstable();
117 let canonical_headers: String = sorted
118 .iter()
119 .map(|(name, value)| format!("{name}:{value}\n"))
120 .collect();
121 format!(
122 "{method}\n{}\n{}\n{canonical_headers}\n{}\n{}",
123 encode_path(path),
124 canonical_query(query),
125 signed_headers(headers),
126 hex_sha256(body.as_bytes()),
127 )
128}
129
130fn string_to_sign(scope: Scope, amz_date: &str, canonical_request: &str) -> String {
133 format!(
134 "AWS4-HMAC-SHA256\n{amz_date}\n{}\n{}",
135 scope.at(amz_date),
136 hex_sha256(canonical_request.as_bytes())
137 )
138}
139
140fn signature(
143 secret_access_key: &str,
144 scope: Scope,
145 amz_date: &str,
146 string_to_sign: &str,
147) -> String {
148 let date_stamp = &amz_date[..8];
149 let k_date = hmac_bytes(
150 format!("AWS4{secret_access_key}").as_bytes(),
151 date_stamp.as_bytes(),
152 );
153 let k_region = hmac_bytes(&k_date, scope.region.as_bytes());
154 let k_service = hmac_bytes(&k_region, scope.service.as_bytes());
155 let k_signing = hmac_bytes(&k_service, b"aws4_request");
156 hex::encode(hmac_bytes(&k_signing, string_to_sign.as_bytes()))
157}
158
159fn signed_headers(headers: &[(&str, &str)]) -> String {
161 let mut names: Vec<&str> = headers.iter().map(|(name, _)| *name).collect();
162 names.sort_unstable();
163 names.join(";")
164}
165
166fn hmac_bytes(key: &[u8], data: &[u8]) -> Vec<u8> {
167 let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("hmac accepts any key length");
168 mac.update(data);
169 mac.finalize().into_bytes().to_vec()
170}
171
172fn hex_sha256(data: &[u8]) -> String {
173 use sha2::Digest as _;
174 hex::encode(Sha256::digest(data))
175}
176
177pub(super) fn uri_encode(input: &str, encode_slash: bool) -> String {
181 let mut out = String::new();
182 for byte in input.bytes() {
183 let c = byte as char;
184 let unreserved = c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.' | '~');
185 let literal_slash = c == '/' && !encode_slash;
186 if unreserved || literal_slash {
187 out.push(c);
188 } else {
189 out.push_str(&format!("%{byte:02X}"));
190 }
191 }
192 out
193}
194
195fn encode_path(path: &str) -> String {
196 path.split('/')
197 .map(|segment| uri_encode(segment, true))
198 .collect::<Vec<_>>()
199 .join("/")
200}
201
202pub(super) fn canonical_query(query: &[(String, String)]) -> String {
203 let mut pairs: Vec<(String, String)> = query
204 .iter()
205 .map(|(k, v)| (uri_encode(k, true), uri_encode(v, true)))
206 .collect();
207 pairs.sort();
208 pairs
209 .into_iter()
210 .map(|(k, v)| format!("{k}={v}"))
211 .collect::<Vec<_>>()
212 .join("&")
213}
214
215#[cfg(test)]
216mod tests {
217 use super::*;
218
219 #[test]
220 fn sigv4_canonical_query_is_sorted_and_encoded() {
221 let query = vec![
222 ("name".to_string(), "a b.example".to_string()),
223 ("type".to_string(), "TXT".to_string()),
224 ];
225 assert_eq!(canonical_query(&query), "name=a%20b.example&type=TXT");
226 }
227
228 #[test]
229 fn sigv4_uri_encode_leaves_unreserved_characters_alone() {
230 assert_eq!(uri_encode("abc-DEF_123.~", false), "abc-DEF_123.~");
231 assert_eq!(uri_encode("a/b", false), "a/b");
232 assert_eq!(uri_encode("a/b", true), "a%2Fb");
233 assert_eq!(uri_encode(" ", false), "%20");
234 }
235
236 const SUITE_SCOPE: Scope = Scope {
244 region: "us-east-1",
245 service: "service",
246 };
247 const SUITE_SECRET: &str = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
248 const SUITE_HOST: &str = "example.amazonaws.com";
249 const SUITE_AMZ_DATE: &str = "20150830T123600Z";
250 const SUITE_MOMENT: time::OffsetDateTime = time::macros::datetime!(2015-08-30 12:36:00 UTC);
251
252 fn suite_credentials(session_token: Option<&str>) -> Route53Credentials {
253 Route53Credentials {
254 access_key_id: "AKIDEXAMPLE".to_string(),
255 secret_access_key: SUITE_SECRET.to_string(),
256 session_token: session_token.map(str::to_string),
257 }
258 }
259
260 const SUITE_GET: Request<'static> = Request {
262 method: "GET",
263 host: SUITE_HOST,
264 path: "/",
265 query: &[],
266 body: "",
267 };
268
269 fn check_steps(
272 method: &str,
273 headers: &[(&str, &str)],
274 body: &str,
275 expected_canonical_request: &str,
276 expected_string_to_sign: &str,
277 expected_signature: &str,
278 ) {
279 let canonical_request = canonical_request(method, "/", &[], headers, body);
280 assert_eq!(canonical_request, expected_canonical_request);
281 let string_to_sign = string_to_sign(SUITE_SCOPE, SUITE_AMZ_DATE, &canonical_request);
282 assert_eq!(string_to_sign, expected_string_to_sign);
283 let signature = signature(SUITE_SECRET, SUITE_SCOPE, SUITE_AMZ_DATE, &string_to_sign);
284 assert_eq!(signature, expected_signature);
285 }
286
287 #[test]
289 fn signs_the_suites_get_vanilla() {
290 check_steps(
291 "GET",
292 &[("host", SUITE_HOST), ("x-amz-date", SUITE_AMZ_DATE)],
293 "",
294 concat!(
295 "GET\n",
296 "/\n",
297 "\n",
298 "host:example.amazonaws.com\n",
299 "x-amz-date:20150830T123600Z\n",
300 "\n",
301 "host;x-amz-date\n",
302 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
303 ),
304 concat!(
305 "AWS4-HMAC-SHA256\n",
306 "20150830T123600Z\n",
307 "20150830/us-east-1/service/aws4_request\n",
308 "bb579772317eb040ac9ed261061d46c1f17a8133879d6129b6e1c25292927e63",
309 ),
310 "5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31",
311 );
312
313 let signed = sign(
315 &suite_credentials(None),
316 SUITE_SCOPE,
317 &SUITE_GET,
318 SUITE_MOMENT,
319 );
320 assert_eq!(signed.amz_date, SUITE_AMZ_DATE);
321 assert_eq!(
322 signed.authorization,
323 concat!(
324 "AWS4-HMAC-SHA256 ",
325 "Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, ",
326 "SignedHeaders=host;x-amz-date, ",
327 "Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31",
328 )
329 );
330 }
331
332 #[test]
335 fn signs_the_suites_get_vanilla_with_session_token() {
336 let token = "6e86291e8372ff2a2260956d9b8aae1d763fbf315fa00fa31553b73ebf194267";
337 check_steps(
338 "GET",
339 &[
340 ("host", SUITE_HOST),
341 ("x-amz-date", SUITE_AMZ_DATE),
342 ("x-amz-security-token", token),
343 ],
344 "",
345 concat!(
346 "GET\n",
347 "/\n",
348 "\n",
349 "host:example.amazonaws.com\n",
350 "x-amz-date:20150830T123600Z\n",
351 "x-amz-security-token:",
352 "6e86291e8372ff2a2260956d9b8aae1d763fbf315fa00fa31553b73ebf194267\n",
353 "\n",
354 "host;x-amz-date;x-amz-security-token\n",
355 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
356 ),
357 concat!(
358 "AWS4-HMAC-SHA256\n",
359 "20150830T123600Z\n",
360 "20150830/us-east-1/service/aws4_request\n",
361 "067b36aa60031588cea4a4cde1f21215227a047690c72247f1d70b32fbbfad2b",
362 ),
363 "07ec1639c89043aa0e3e2de82b96708f198cceab042d4a97044c66dd9f74e7f8",
364 );
365
366 let signed = sign(
367 &suite_credentials(Some(token)),
368 SUITE_SCOPE,
369 &SUITE_GET,
370 SUITE_MOMENT,
371 );
372 assert_eq!(
373 signed.authorization,
374 concat!(
375 "AWS4-HMAC-SHA256 ",
376 "Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, ",
377 "SignedHeaders=host;x-amz-date;x-amz-security-token, ",
378 "Signature=07ec1639c89043aa0e3e2de82b96708f198cceab042d4a97044c66dd9f74e7f8",
379 )
380 );
381 }
382
383 #[test]
387 fn signs_the_suites_post_with_a_body() {
388 check_steps(
389 "POST",
390 &[
391 ("content-type", "application/x-www-form-urlencoded"),
392 ("host", SUITE_HOST),
393 ("content-length", "13"),
394 ("x-amz-date", SUITE_AMZ_DATE),
395 (
396 "x-amz-content-sha256",
397 "9095672bbd1f56dfc5b65f3e153adc8731a4a654192329106275f4c7b24d0b6e",
398 ),
399 ],
400 "Param1=value1",
401 concat!(
402 "POST\n",
403 "/\n",
404 "\n",
405 "content-length:13\n",
406 "content-type:application/x-www-form-urlencoded\n",
407 "host:example.amazonaws.com\n",
408 "x-amz-content-sha256:",
409 "9095672bbd1f56dfc5b65f3e153adc8731a4a654192329106275f4c7b24d0b6e\n",
410 "x-amz-date:20150830T123600Z\n",
411 "\n",
412 "content-length;content-type;host;x-amz-content-sha256;x-amz-date\n",
413 "9095672bbd1f56dfc5b65f3e153adc8731a4a654192329106275f4c7b24d0b6e",
414 ),
415 concat!(
416 "AWS4-HMAC-SHA256\n",
417 "20150830T123600Z\n",
418 "20150830/us-east-1/service/aws4_request\n",
419 "b1edd1d03544c25390e32085d55b57acc9a3961bb59415ff86c45c3d89d16cfb",
420 ),
421 "d3875051da38690788ef43de4db0d8f280229d82040bfac253562e56c3f20e0b",
422 );
423 }
424}