1use hmac::{Hmac, Mac};
9use sha2::{Digest, Sha256};
10
11type HmacSha256 = Hmac<Sha256>;
12
13pub const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
14
15pub struct Signing<'a> {
17 pub access_key: &'a str,
18 pub secret_key: &'a str,
19 pub region: &'a str,
20 pub service: &'a str,
21 pub timestamp: &'a str,
23}
24
25impl Signing<'_> {
26 fn date(&self) -> &str {
27 &self.timestamp[..8]
29 }
30
31 fn scope(&self) -> String {
32 format!("{}/{}/{}/aws4_request", self.date(), self.region, self.service)
33 }
34}
35
36pub fn sha256_hex(bytes: &[u8]) -> String {
37 hex(&Sha256::digest(bytes))
38}
39
40fn hex(bytes: &[u8]) -> String {
41 bytes.iter().map(|byte| format!("{byte:02x}")).collect()
42}
43
44fn hmac(key: &[u8], message: &[u8]) -> Vec<u8> {
45 let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts any key length");
46 mac.update(message);
47 mac.finalize().into_bytes().to_vec()
48}
49
50pub fn canonical_request(
52 method: &str,
53 path: &str,
54 query: &str,
55 headers: &[(String, String)],
56 payload_hash: &str,
57) -> (String, String) {
58 let mut sorted: Vec<(String, String)> = headers
59 .iter()
60 .map(|(name, value)| (name.to_ascii_lowercase(), value.trim().to_string()))
61 .collect();
62 sorted.sort_by(|a, b| a.0.cmp(&b.0));
63
64 let canonical_headers: String =
65 sorted.iter().map(|(name, value)| format!("{name}:{value}\n")).collect();
66 let signed_headers =
67 sorted.iter().map(|(name, _)| name.as_str()).collect::<Vec<_>>().join(";");
68
69 let request = format!(
70 "{method}\n{}\n{query}\n{canonical_headers}\n{signed_headers}\n{payload_hash}",
71 encode_path(path)
72 );
73
74 (request, signed_headers)
75}
76
77pub fn encode_path(path: &str) -> String {
82 path.split('/').map(encode_segment).collect::<Vec<_>>().join("/")
83}
84
85pub fn encode_segment(segment: &str) -> String {
87 let mut out = String::with_capacity(segment.len());
88 for byte in segment.bytes() {
89 match byte {
90 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
91 out.push(byte as char)
92 }
93 _ => out.push_str(&format!("%{byte:02X}")),
94 }
95 }
96 out
97}
98
99pub fn signature(signing: &Signing<'_>, canonical: &str) -> String {
101 let string_to_sign = format!(
102 "AWS4-HMAC-SHA256\n{}\n{}\n{}",
103 signing.timestamp,
104 signing.scope(),
105 sha256_hex(canonical.as_bytes())
106 );
107 hex(&hmac(&signing_key(signing), string_to_sign.as_bytes()))
108}
109
110pub fn presigned_query(
125 signing: &Signing<'_>,
126 method: &str,
127 host: &str,
128 path: &str,
129 expires_seconds: u64,
130 extra: &[(&str, &str)],
131) -> String {
132 let credential = format!("{}/{}", signing.access_key, signing.scope());
133
134 let mut pairs: Vec<(String, String)> = vec![
135 ("X-Amz-Algorithm".into(), "AWS4-HMAC-SHA256".into()),
136 ("X-Amz-Credential".into(), credential),
137 ("X-Amz-Date".into(), signing.timestamp.into()),
138 ("X-Amz-Expires".into(), expires_seconds.to_string()),
139 ("X-Amz-SignedHeaders".into(), "host".into()),
140 ];
141 for (name, value) in extra {
142 pairs.push(((*name).into(), (*value).into()));
143 }
144 pairs.sort();
147 let query: String = pairs
148 .iter()
149 .map(|(name, value)| format!("{}={}", encode_segment(name), encode_segment(value)))
150 .collect::<Vec<_>>()
151 .join("&");
152
153 let headers = vec![("host".to_string(), host.to_string())];
154 let (canonical, _) = canonical_request(method, path, &query, &headers, UNSIGNED_PAYLOAD);
155
156 format!("{query}&X-Amz-Signature={}", signature(signing, &canonical))
157}
158
159pub fn authorization(
161 signing: &Signing<'_>,
162 method: &str,
163 path: &str,
164 query: &str,
165 headers: &[(String, String)],
166 payload_hash: &str,
167) -> String {
168 let (canonical, signed_headers) =
169 canonical_request(method, path, query, headers, payload_hash);
170 let signature = signature(signing, &canonical);
171
172 format!(
173 "AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={signed_headers}, Signature={signature}",
174 signing.access_key,
175 signing.scope()
176 )
177}
178
179fn signing_key(signing: &Signing<'_>) -> Vec<u8> {
184 let initial = format!("AWS4{}", signing.secret_key);
185 let date = hmac(initial.as_bytes(), signing.date().as_bytes());
186 let region = hmac(&date, signing.region.as_bytes());
187 let service = hmac(®ion, signing.service.as_bytes());
188 hmac(&service, b"aws4_request")
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 fn example() -> Signing<'static> {
197 Signing {
198 access_key: "AKIAIOSFODNN7EXAMPLE",
199 secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
200 region: "us-east-1",
201 service: "s3",
202 timestamp: "20130524T000000Z",
203 }
204 }
205
206 #[test]
212 fn presigns_the_documented_example_to_the_documented_signature() {
213 let signing = Signing {
214 access_key: "AKIAIOSFODNN7EXAMPLE",
215 secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
216 region: "us-east-1",
217 service: "s3",
218 timestamp: "20130524T000000Z",
219 };
220
221 let query =
222 presigned_query(&signing, "GET", "examplebucket.s3.amazonaws.com", "/test.txt", 86400, &[]);
223
224 assert!(
225 query.ends_with(
226 "&X-Amz-Signature=aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404"
227 ),
228 "{query}"
229 );
230 assert!(query.starts_with("X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential="), "{query}");
231 assert!(query.contains("X-Amz-Expires=86400"), "{query}");
232 assert!(query.contains("X-Amz-SignedHeaders=host"), "{query}");
233 }
234
235 #[test]
236 fn hashes_an_empty_payload_to_the_documented_value() {
237 assert_eq!(
238 sha256_hex(b""),
239 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
240 );
241 }
242
243 #[test]
244 fn derives_the_documented_signing_key() {
245 assert_eq!(
246 hex(&signing_key(&example())),
247 "dbb893acc010964918f1fd433add87c70e8b0db6be30c1fbeafefa5ec6ba8378"
248 );
249 }
250
251 #[test]
252 fn builds_the_documented_canonical_request() {
253 let headers = vec![
254 ("host".to_string(), "examplebucket.s3.amazonaws.com".to_string()),
255 (
256 "x-amz-content-sha256".to_string(),
257 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string(),
258 ),
259 ("x-amz-date".to_string(), "20130524T000000Z".to_string()),
260 ];
261
262 let (canonical, signed) = canonical_request(
263 "GET",
264 "/test.txt",
265 "",
266 &headers,
267 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
268 );
269
270 assert_eq!(signed, "host;x-amz-content-sha256;x-amz-date");
271 assert!(canonical.starts_with("GET\n/test.txt\n\n"));
272 assert!(canonical.contains("host:examplebucket.s3.amazonaws.com\n"));
273 }
274
275 #[test]
276 fn produces_the_documented_authorization_header() {
277 let headers = vec![
280 ("host".to_string(), "examplebucket.s3.amazonaws.com".to_string()),
281 ("range".to_string(), "bytes=0-9".to_string()),
282 (
283 "x-amz-content-sha256".to_string(),
284 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_string(),
285 ),
286 ("x-amz-date".to_string(), "20130524T000000Z".to_string()),
287 ];
288
289 let header = authorization(
290 &example(),
291 "GET",
292 "/test.txt",
293 "",
294 &headers,
295 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
296 );
297
298 assert!(header.contains("Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request"));
299 assert!(header.contains("SignedHeaders=host;range;x-amz-content-sha256;x-amz-date"));
300 assert!(
301 header.ends_with(
302 "Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41"
303 ),
304 "{header}"
305 );
306 }
307
308 #[test]
309 fn encodes_keys_without_touching_the_separators() {
310 assert_eq!(encode_path("/photos/my holiday.jpg"), "/photos/my%20holiday.jpg");
311 assert_eq!(encode_path("/a/b/c"), "/a/b/c");
312 assert_eq!(encode_segment("a+b=c"), "a%2Bb%3Dc");
313 }
314
315 #[test]
316 fn header_order_does_not_change_the_signature() {
317 let ordered = vec![
318 ("host".to_string(), "example.com".to_string()),
319 ("x-amz-date".to_string(), "20130524T000000Z".to_string()),
320 ];
321 let reversed: Vec<_> = ordered.iter().cloned().rev().collect();
322
323 assert_eq!(
324 authorization(&example(), "GET", "/x", "", &ordered, UNSIGNED_PAYLOAD),
325 authorization(&example(), "GET", "/x", "", &reversed, UNSIGNED_PAYLOAD)
326 );
327 }
328}