1use chrono::{DateTime, Utc};
56use hmac::{Hmac, Mac};
57use sha2::{Digest, Sha256};
58use std::{collections::BTreeMap, str};
59use url::Url;
60use urlencoding::encode as url_encode;
61
62type HeadersMap = BTreeMap<String, String>;
63
64type HmacSha256 = Hmac<Sha256>;
65
66const LONG_DATETIME_FMT: &str = "%Y%m%dT%H%M%SZ";
67const SHORT_DATE_FMT: &str = "%Y%m%d";
68
69#[macro_use]
70extern crate error_chain;
71
72#[allow(unexpected_cfgs)]
73mod errors {
74 error_chain! {}
75}
76
77pub use errors::*;
78
79fn canonical_query_string(uri: &Url) -> String {
84 let mut qs = BTreeMap::new();
85 uri.query_pairs().for_each(|(k, v)| {
86 qs.insert(
87 url_encode(k.as_ref()).to_string(),
88 url_encode(&v).to_string(),
89 );
90 });
91 let kv: Vec<String> = qs.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
92 kv.join("&")
93}
94
95fn canonical_header_string(headers: &HeadersMap) -> String {
98 let key_values = headers
99 .iter()
100 .filter_map(|(key, value)| {
101 let k = key.as_str().to_lowercase();
102 if k.starts_with("x-mhl-") || k == "host" {
103 Some(k + ":" + value.as_str().trim())
104 } else {
105 None
106 }
107 })
108 .collect::<Vec<String>>();
109 key_values.join("\n")
110}
111
112fn signed_header_string(headers: &HeadersMap) -> String {
115 let keys = headers
116 .keys()
117 .filter_map(|key| {
118 let k = key.as_str().to_lowercase();
119 if k.starts_with("x-mhl-") || k == "host" {
120 Some(k)
121 } else {
122 None
123 }
124 })
125 .collect::<Vec<String>>();
126 keys.join(";")
127}
128
129fn canonical_request(
132 method: &str,
133 url: &Url,
134 headers: &HeadersMap,
135 payload_sha256: &str,
136) -> String {
137 format!(
138 "{method}\n{uri}\n{query_string}\n{headers}\n\n{signed}\n{sha256}",
139 method = method,
140 uri = url.path().to_ascii_lowercase(),
141 query_string = canonical_query_string(url),
142 headers = canonical_header_string(headers),
143 signed = signed_header_string(headers),
144 sha256 = payload_sha256
145 )
146}
147
148fn scope_string(date_time: &DateTime<Utc>, region: &str, service: &str) -> String {
151 format!(
152 "{date}/{region}/{service}",
153 date = date_time.format(SHORT_DATE_FMT),
154 region = region,
155 service = service
156 )
157}
158
159fn string_to_sign(
163 date_time: &DateTime<Utc>,
164 region: &str,
165 canonical_req: &str,
166 service: &str,
167) -> String {
168 let mut hasher = Sha256::default();
171 hasher.update(canonical_req.as_bytes());
172 let string_to = format!(
173 "MHL4-HMAC-SHA256\n{timestamp}\n{scope}\n{hash}",
174 timestamp = date_time.format(LONG_DATETIME_FMT),
175 scope = scope_string(date_time, region, service),
176 hash = hex::encode(hasher.finalize().as_slice())
177 );
178 string_to
179}
180
181fn signing_key(
185 date_time: &DateTime<Utc>,
186 secret_key: &str,
187 region: &str,
188 service: &str,
189) -> Result<Vec<u8>> {
190 let secret = format!("MHL{}", secret_key);
191 let mut date_hmac =
192 HmacSha256::new_from_slice(secret.as_bytes()).chain_err(|| "error hashing secret")?;
193 date_hmac.update(date_time.format(SHORT_DATE_FMT).to_string().as_bytes());
194 let mut region_hmac = HmacSha256::new_from_slice(&date_hmac.finalize().into_bytes())
195 .chain_err(|| "error hashing date")?;
196 region_hmac.update(region.to_string().as_bytes());
197 let mut service_hmac = HmacSha256::new_from_slice(®ion_hmac.finalize().into_bytes())
198 .chain_err(|| "error hashing region")?;
199 service_hmac.update(service.as_bytes());
200 let mut signing_hmac = HmacSha256::new_from_slice(&service_hmac.finalize().into_bytes())
201 .chain_err(|| "error hashing service")?;
202 signing_hmac.update(b"mhl_request");
203 Ok(signing_hmac.finalize().into_bytes().to_vec())
204}
205
206fn authorization_header(
209 access_key: &str,
210 date_time: &DateTime<Utc>,
211 region: &str,
212 signed_headers: &str,
213 signature: &str,
214 service: &str,
215) -> String {
216 format!(
217 "MHL-HMAC-SHA256 Credential={access_key}/{scope},\
218 SignedHeaders={signed_headers},Signature={signature}",
219 access_key = access_key,
220 scope = scope_string(date_time, region, service),
221 signed_headers = signed_headers,
222 signature = signature
223 )
224}
225
226#[allow(clippy::too_many_arguments)]
227pub fn verification(
229 method: &str,
230 payload_hash: &str,
231 url_string: &str,
232 headers: &HeadersMap,
233 date_time: &DateTime<Utc>,
234 secret: &str,
235 region: &str,
236 service: &str,
237) -> Result<String> {
238 let url = Url::parse(url_string).chain_err(|| "error parsing url")?;
239 let canonical = canonical_request(&method.to_uppercase(), &url, headers, payload_hash);
240
241 let string_to_sign = string_to_sign(date_time, region, &canonical, service);
242 let signing_key = signing_key(date_time, secret, region, service)?;
243 let mut hmac =
244 Hmac::<Sha256>::new_from_slice(&signing_key).chain_err(|| "error hashing signing key")?;
245 hmac.update(string_to_sign.as_bytes());
246 Ok(hex::encode(hmac.finalize().into_bytes()))
247}
248pub struct Signature {
251 pub auth_header: String,
252 pub date_time: String,
253}
254
255#[allow(clippy::too_many_arguments)]
256pub fn signature(
258 url: &url::Url,
259 method: &str,
260 access: &str,
261 secret: &str,
262 region: &str,
263 service: &str,
264 machineid: &str,
265 hostname: &str,
266 payload_hash: &str,
267 nonce: &str,
268) -> Result<Signature> {
269 const LONG_DATE_TIME: &str = "%Y%m%dT%H%M%SZ";
270 let host_port = url
271 .host()
272 .chain_err(|| "Error parsing host from url")?
273 .to_string()
274 + &if let Some(port) = url.port() {
275 format!(":{}", port)
276 } else {
277 "".to_string()
278 };
279 let uri = url.as_str().trim_end_matches('/');
280 let mut headers = HeadersMap::new();
281
282 headers.insert("host".to_string(), host_port);
283
284 headers.insert("x-mhl-content-sha256".to_string(), payload_hash.to_string());
285 let date_time = Utc::now();
286 let date_time_string = date_time.format(LONG_DATE_TIME).to_string();
287 headers.insert("x-mhl-hostname".to_string(), hostname.to_string());
288 headers.insert("x-mhl-date".to_string(), date_time_string.clone());
289 headers.insert("x-mhl-nonce".to_string(), nonce.to_string());
290 headers.insert("x-mhl-mid".to_string(), machineid.to_string());
291
292 let signature = verification(
293 method,
294 payload_hash,
295 uri,
296 &headers,
297 &date_time,
298 secret,
299 region,
300 service,
301 )?;
302 let auth = authorization_header(
303 access,
304 &date_time,
305 region,
306 &signed_header_string(&headers),
307 &signature,
308 service,
309 );
310 Ok(Signature {
311 auth_header: auth,
312 date_time: date_time_string,
313 })
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use chrono::{NaiveDateTime, TimeZone, Utc};
320
321 #[test]
322 fn test_signature() -> Result<()> {
323 let method = "GET";
324 let payload_hash = "UNSIGNED-PAYLOAD";
325 let region = "global";
326 let service = "test";
327 let url = url::Url::parse("https://ubifaq.com/endpoint").unwrap();
328 let secret = "ivegotthesecret";
329 let nonce = "1234567";
330 let machineid = "mid";
331 let hostname = "hostname";
332
333 let sig = signature(
334 &url,
335 method,
336 "ivegotthekey",
337 secret,
338 region,
339 service,
340 machineid,
341 hostname,
342 payload_hash,
343 nonce,
344 )
345 .unwrap();
346
347 let fixdate =
348 NaiveDateTime::parse_from_str(sig.date_time.as_str(), "%Y%m%dT%H%M%SZ").unwrap();
349 let parsedate = DateTime::<Utc>::from_naive_utc_and_offset(fixdate, Utc);
350
351 println!("parsed {}", parsedate);
352 println!("sig date {}", &sig.date_time);
353
354 let mut headers = HeadersMap::new();
355 headers.insert("host".to_string(), "ubifaq.com".to_string());
356 headers.insert("x-mhl-content-sha256".to_string(), payload_hash.to_string());
357 headers.insert("x-mhl-date".to_string(), sig.date_time);
358 headers.insert("x-mhl-nonce".to_string(), nonce.to_string());
359 headers.insert("x-mhl-mid".to_string(), machineid.to_string());
360 headers.insert("x-mhl-hostname".to_string(), hostname.to_string());
361
362 let verification = verification(
363 method,
364 payload_hash,
365 url.as_str(),
366 &headers,
367 &parsedate,
368 secret,
369 region,
370 service,
371 )?;
372
373 println!("auth header{}", sig.auth_header);
374 println!("verified sig{}", verification.as_str());
375
376 assert!(sig.auth_header.to_string().contains(verification.as_str()));
377
378 Ok(())
379 }
380 #[test]
381 fn test_verification() -> Result<()> {
382 const EXPECTED_SIGNATURE: &str =
383 "ef3abe3ccf173e7e6374faf6ae74fba2149e29343bc635e976c4e9a47d534c92";
384 let url = "https://mehal.tech";
385 let method = "GET";
386 let payload_hash = "UNSIGNED-PAYLOAD";
387 let date_time = Utc.with_ymd_and_hms(2022, 2, 2, 0, 0, 0).unwrap();
388 let secret = "ivegotthesecret";
389 let region = "global";
390 let service = "brog";
391 let mut headers = HeadersMap::new();
392 headers.insert("host".to_string(), "aws.com".to_string());
393 headers.insert("x-mhl-content-sha256".to_string(), payload_hash.to_string());
394
395 let signature = verification(
396 method,
397 payload_hash,
398 url,
399 &headers,
400 &date_time,
401 secret,
402 region,
403 service,
404 )?;
405 assert_eq!(EXPECTED_SIGNATURE, signature);
406 Ok(())
407 }
408}