use chrono::{DateTime, Utc};
use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;
use url::Url;
use urlencoding::encode as url_encode;
type HeadersMap = BTreeMap<String, String>;
type HmacSha256 = Hmac<Sha256>;
const LONG_DATETIME_FMT: &str = "%Y%m%dT%H%M%SZ";
const SHORT_DATE_FMT: &str = "%Y%m%d";
#[macro_use]
extern crate error_chain;
mod errors {
error_chain! {}
}
pub use errors::*;
fn canonical_query_string(uri: &Url) -> String {
let mut qs = BTreeMap::new();
uri.query_pairs().for_each(|(k, v)| {
qs.insert(
url_encode(k.as_ref()).to_string(),
url_encode(&v).to_string(),
);
});
let kv: Vec<String> = qs.iter().map(|(k, v)| format!("{}={}", k, v)).collect();
kv.join("&")
}
fn canonical_header_string(headers: &HeadersMap) -> String {
let key_values = headers
.iter()
.filter_map(|(key, value)| {
let k = key.as_str().to_lowercase();
if k.starts_with("x-mhl-") || k == "host" {
Some(k + ":" + value.as_str().trim())
} else {
None
}
})
.collect::<Vec<String>>();
key_values.join("\n")
}
fn signed_header_string(headers: &HeadersMap) -> String {
let keys = headers
.keys()
.filter_map(|key| {
let k = key.as_str().to_lowercase();
if k.starts_with("x-mhl-") || k == "host" {
Some(k)
} else {
None
}
})
.collect::<Vec<String>>();
keys.join(";")
}
fn canonical_request(
method: &str,
url: &Url,
headers: &HeadersMap,
payload_sha256: &str,
) -> String {
format!(
"{method}\n{uri}\n{query_string}\n{headers}\n\n{signed}\n{sha256}",
method = method,
uri = url.path().to_ascii_lowercase(),
query_string = canonical_query_string(url),
headers = canonical_header_string(headers),
signed = signed_header_string(headers),
sha256 = payload_sha256
)
}
fn scope_string(date_time: &DateTime<Utc>, region: &str) -> String {
format!(
"{date}/{region}/brog/gitops_request",
date = date_time.format(SHORT_DATE_FMT),
region = region
)
}
fn string_to_sign(date_time: &DateTime<Utc>, region: &str, canonical_req: &str) -> String {
let mut hasher = Sha256::default();
hasher.update(canonical_req.as_bytes());
let string_to = format!(
"MHL4-HMAC-SHA256\n{timestamp}\n{scope}\n{hash}",
timestamp = date_time.format(LONG_DATETIME_FMT),
scope = scope_string(date_time, region),
hash = hex::encode(hasher.finalize().as_slice())
);
string_to
}
fn signing_key(
date_time: &DateTime<Utc>,
secret_key: &str,
region: &str,
service: &str,
) -> Result<Vec<u8>> {
let secret = format!("MHL{}", secret_key);
let mut date_hmac =
HmacSha256::new_from_slice(secret.as_bytes()).chain_err(|| "error hashing secret")?;
date_hmac.update(date_time.format(SHORT_DATE_FMT).to_string().as_bytes());
let mut region_hmac = HmacSha256::new_from_slice(&date_hmac.finalize().into_bytes())
.chain_err(|| "error hashing date")?;
region_hmac.update(region.to_string().as_bytes());
let mut service_hmac = HmacSha256::new_from_slice(®ion_hmac.finalize().into_bytes())
.chain_err(|| "error hashing region")?;
service_hmac.update(service.as_bytes());
let mut signing_hmac = HmacSha256::new_from_slice(&service_hmac.finalize().into_bytes())
.chain_err(|| "error hashing service")?;
signing_hmac.update(b"mhl_request");
Ok(signing_hmac.finalize().into_bytes().to_vec())
}
fn authorization_header(
access_key: &str,
date_time: &DateTime<Utc>,
region: &str,
signed_headers: &str,
signature: &str,
) -> String {
format!(
"AWS4-HMAC-SHA256 Credential={access_key}/{scope},\
SignedHeaders={signed_headers},Signature={signature}",
access_key = access_key,
scope = scope_string(date_time, region),
signed_headers = signed_headers,
signature = signature
)
}
#[allow(clippy::too_many_arguments)]
pub fn create_sign(
method: &str,
payload_hash: &str,
url_string: &str,
headers: &HeadersMap,
date_time: &DateTime<Utc>,
secret: &str,
region: &str,
service: &str,
) -> Result<String> {
let url = Url::parse(url_string).chain_err(|| "error parsing url")?;
let canonical = canonical_request(&method.to_uppercase(), &url, headers, payload_hash);
let string_to_sign = string_to_sign(date_time, region, &canonical);
let signing_key = signing_key(date_time, secret, region, service)?;
let mut hmac =
Hmac::<Sha256>::new_from_slice(&signing_key).chain_err(|| "error hashing signing key")?;
hmac.update(string_to_sign.as_bytes());
Ok(hex::encode(hmac.finalize().into_bytes()))
}
pub struct Signature {
pub auth_header: String,
pub date_time: String,
}
#[allow(clippy::too_many_arguments)]
pub fn signature(
url: &url::Url,
method: &str,
access: &str,
secret: &str,
region: &str,
service: &str,
machineid: &str,
payload_hash: &str,
) -> Result<Signature> {
const LONG_DATE_TIME: &str = "%Y%m%dT%H%M%SZ";
let host_port = url
.host()
.chain_err(|| "Error parsing host from url")?
.to_string()
+ &if let Some(port) = url.port() {
format!(":{}", port)
} else {
"".to_string()
};
let uri = url.as_str().trim_end_matches('/');
let mut headers = HeadersMap::new();
headers.insert("host".to_string(), host_port);
headers.insert("x-mhl-content-sha256".to_string(), payload_hash.to_string());
let date_time = Utc::now();
let date_time_string = date_time.format(LONG_DATE_TIME).to_string();
headers.insert("x-mhl-date".to_string(), date_time_string.clone());
headers.insert("x-mhl-mid".to_string(), machineid.to_string());
let signature = create_sign(
method,
payload_hash,
uri,
&headers,
&date_time,
secret,
region,
service,
)?;
let auth = authorization_header(
access,
&date_time,
region,
&signed_header_string(&headers),
&signature,
);
Ok(Signature {
auth_header: auth,
date_time: date_time_string,
})
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{TimeZone, Utc};
#[test]
fn test_signature() -> Result<()> {
const EXPECTED_SIGNATURE: &str =
"c0cc110abe9ace556024b35da8abd9aa1d00dea5a468df7f438e7cf0d22b2f93";
let url = "https://mehal.tech";
let method = "GET";
let payload_hash = "UNSIGNED-PAYLOAD";
let date_time = Utc.with_ymd_and_hms(2022, 2, 2, 0, 0, 0).unwrap();
let secret = "ivegotthesecret";
let region = "global";
let service = "brog";
let mut headers = HeadersMap::new();
headers.insert("host".to_string(), "aws.com".to_string());
headers.insert("x-mhl-content-sha256".to_string(), payload_hash.to_string());
let signature = create_sign(
method,
payload_hash,
url,
&headers,
&date_time,
secret,
region,
service,
)?;
assert_eq!(EXPECTED_SIGNATURE, signature);
Ok(())
}
}