1use chrono::{DateTime, Utc};
2use http::header::HeaderMap;
3use url::Url;
4use std::collections::HashMap;
5use sha256::{digest};
6
7const SHORT_DATE: &str = "%Y%m%d";
8const LONG_DATETIME: &str = "%Y%m%dT%H%M%SZ";
9
10#[derive(Debug)]
11pub struct AwsSign<'a, T: 'a>
12where
13 &'a T: std::iter::IntoIterator<Item = (&'a String, &'a String)>,
14{
15 method: &'a str,
16 url: Url,
17 datetime: &'a DateTime<Utc>,
18 region: &'a str,
19 access_key: &'a str,
20 secret_key: &'a str,
21 headers: T,
22
23 service: &'a str,
52
53 body: &'a [u8],
55}
56
57impl<'a> AwsSign<'a, HashMap<String, String>> {
58 pub fn new<B: AsRef<[u8]> + ?Sized>(
59 method: &'a str,
60 url: &'a str,
61 datetime: &'a DateTime<Utc>,
62 headers: &'a HeaderMap,
63 region: &'a str,
64 access_key: &'a str,
65 secret_key: &'a str,
66 service: &'a str,
67 body: &'a B,
68 ) -> Self {
69 let url: Url = url.parse().unwrap();
70 let headers: HashMap<String, String> = headers
71 .iter()
72 .filter_map(|(key, value)| {
73 if let Ok(value_inner) = value.to_str() {
74 Some((key.as_str().to_owned(), value_inner.to_owned()))
75 } else {
76 None
77 }
78 })
79 .collect();
80 Self {
81 method,
82 url,
83 datetime,
84 region,
85 access_key,
86 secret_key,
87 headers,
88 service,
89 body: body.as_ref(),
90 }
91 }
92}
93
94impl<'a, T> AwsSign<'a, T>
95where
96 &'a T: std::iter::IntoIterator<Item = (&'a String, &'a String)>,
97{
98 pub fn canonical_header_string(&'a self) -> String {
101 let mut keyvalues = self
102 .headers
103 .into_iter()
104 .map(|(key, value)| key.to_lowercase() + ":" + value.trim())
105 .collect::<Vec<String>>();
106 keyvalues.sort();
107 keyvalues.join("\n")
108 }
109
110 pub fn signed_header_string(&'a self) -> String {
111 let mut keys = self
112 .headers
113 .into_iter()
114 .map(|(key, _)| key.to_lowercase())
115 .collect::<Vec<String>>();
116 keys.sort();
117 keys.join(";")
118 }
119
120 pub fn canonical_request(&'a self) -> String {
121 let url: &str = self.url.path().into();
122
123 format!(
124 "{method}\n{uri}\n{query_string}\n{headers}\n\n{signed}\n{sha256}",
125 method = self.method,
126 uri = url,
127 query_string = canonical_query_string(&self.url),
128 headers = self.canonical_header_string(),
129 signed = self.signed_header_string(),
130 sha256 = digest(self.body),
131 )
132 }
133 pub fn sign(&'a self) -> String {
134 let canonical = self.canonical_request();
135 let string_to_sign = string_to_sign(self.datetime, self.region, &canonical, self.service);
136 let signing_key = signing_key(self.datetime, self.secret_key, self.region, self.service);
137 let key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, &signing_key.unwrap());
138 let tag = ring::hmac::sign(&key, string_to_sign.as_bytes());
139 let signature = hex::encode(tag.as_ref());
140 let signed_headers = self.signed_header_string();
141
142 format!(
143 "AWS4-HMAC-SHA256 Credential={access_key}/{scope},\
144 SignedHeaders={signed_headers},Signature={signature}",
145 access_key = self.access_key,
146 scope = scope_string(self.datetime, self.region, self.service),
147 signed_headers = signed_headers,
148 signature = signature
149 )
150 }
151}
152
153pub fn uri_encode(string: &str, encode_slash: bool) -> String {
154 let mut result = String::with_capacity(string.len() * 2);
155 for c in string.chars() {
156 match c {
157 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '~' | '.' => result.push(c),
158 '/' if encode_slash => result.push_str("%2F"),
159 '/' if !encode_slash => result.push('/'),
160 _ => {
161 result.push('%');
162 result.push_str(
163 &format!("{}", c)
164 .bytes()
165 .map(|b| format!("{:02X}", b))
166 .collect::<String>(),
167 );
168 }
169 }
170 }
171 result
172}
173
174pub fn canonical_query_string(uri: &Url) -> String {
175 let mut keyvalues = uri
176 .query_pairs()
177 .map(|(key, value)| uri_encode(&key, true) + "=" + &uri_encode(&value, true))
178 .collect::<Vec<String>>();
179 keyvalues.sort();
180 keyvalues.join("&")
181}
182
183pub fn scope_string(datetime: &DateTime<Utc>, region: &str, service: &str) -> String {
184 format!(
185 "{date}/{region}/{service}/aws4_request",
186 date = datetime.format(SHORT_DATE),
187 region = region,
188 service = service
189 )
190}
191
192pub fn string_to_sign(datetime: &DateTime<Utc>, region: &str, canonical_req: &str, service: &str) -> String {
193 let hash = ring::digest::digest(&ring::digest::SHA256, canonical_req.as_bytes());
194 format!(
195 "AWS4-HMAC-SHA256\n{timestamp}\n{scope}\n{hash}",
196 timestamp = datetime.format(LONG_DATETIME),
197 scope = scope_string(datetime, region, service),
198 hash = hex::encode(hash.as_ref())
199 )
200}
201
202pub fn signing_key(
203 datetime: &DateTime<Utc>,
204 secret_key: &str,
205 region: &str,
206 service: &str,
207) -> Result<Vec<u8>, String> {
208 let secret = String::from("AWS4") + secret_key;
209
210 let date_key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, secret.as_bytes());
211 let date_tag = ring::hmac::sign(
212 &date_key,
213 datetime.format(SHORT_DATE).to_string().as_bytes(),
214 );
215
216 let region_key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, date_tag.as_ref());
217 let region_tag = ring::hmac::sign(®ion_key, region.to_string().as_bytes());
218
219 let service_key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, region_tag.as_ref());
220 let service_tag = ring::hmac::sign(&service_key, service.as_bytes());
221
222 let signing_key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, service_tag.as_ref());
223 let signing_tag = ring::hmac::sign(&signing_key, b"aws4_request");
224 Ok(signing_tag.as_ref().to_vec())
225}
226
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn sample_canonical_request() {
234 let datetime = chrono::Utc::now();
235 let url: &str = "https://hi.s3.us-east-1.amazonaws.com/Prod/graphql";
236 let map: HeaderMap = HeaderMap::new();
237 let aws_sign = AwsSign::new(
238 "GET",
239 url,
240 &datetime,
241 &map,
242 "us-east-1",
243 "a",
244 "b",
245 "s3",
246 ""
247 );
248 let s = aws_sign.canonical_request();
249 assert_eq!(s, "GET\n/Prod/graphql\n\n\n\n\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
250 }
251
252 #[test]
253 fn sample_canonical_request_using_u8_body() {
254 let datetime = chrono::Utc::now();
255 let url: &str = "https://hi.s3.us-east-1.amazonaws.com/Prod/graphql";
256 let map: HeaderMap = HeaderMap::new();
257 let aws_sign = AwsSign::new(
258 "GET",
259 url,
260 &datetime,
261 &map,
262 "us-east-1",
263 "a",
264 "b",
265 "s3",
266 "".as_bytes(),
267 );
268 let s = aws_sign.canonical_request();
269 assert_eq!(s, "GET\n/Prod/graphql\n\n\n\n\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
270 }
271
272 #[test]
273 fn sample_canonical_request_using_vec_body() {
274 let datetime = chrono::Utc::now();
275 let url: &str = "https://hi.s3.us-east-1.amazonaws.com/Prod/graphql";
276 let map: HeaderMap = HeaderMap::new();
277 let body = Vec::new();
278 let aws_sign = AwsSign::new(
279 "GET",
280 url,
281 &datetime,
282 &map,
283 "us-east-1",
284 "a",
285 "b",
286 "s3",
287 &body,
288 );
289 let s = aws_sign.canonical_request();
290 assert_eq!(s, "GET\n/Prod/graphql\n\n\n\n\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
291 }
292}