use chrono::{DateTime, Utc};
use hmac::{Hmac, Mac, digest::FixedOutput};
use sha2::{Digest, Sha256};
mod hmac_msg;
type HmacSha256 = Hmac<Sha256>;
const UTC_ISO_FORMAT: &'static str = "%Y%m%dT%H%M%SZ";
const DATE_FORMAT: &'static str = "%Y%m%d";
pub struct SignatureRequest<'a> {
headers: Vec<(String, &'a str)>,
http_verb: &'a str,
raw_uri: &'a str,
access_key_id: &'a str,
secret_access_key: &'a str,
time: DateTime<Utc>,
region: &'a str,
service: &'a str,
body: &'a [u8],
}
fn format_headers<'a, T>(headers: &'a T) -> Vec<(String, &'a str)>
where
&'a T: IntoIterator<Item = (&'a &'a str, &'a &'a str)>,
{
let mut h: Vec<_> = headers
.into_iter()
.map(|(k, v)| (k.to_lowercase(), v.trim()))
.collect();
h.sort_by(|(k1, _), (k2, _)| k1.cmp(k2));
h
}
fn compose_headers(sorted_headers: &Vec<(String, &str)>) -> String {
let cap: usize = sorted_headers.iter().fold(0, |acc, (k, v)| {
acc + k.len() + 1 + v.len() + 1
});
let ch = sorted_headers
.iter()
.fold(String::with_capacity(cap), |acc, (k, v)| {
acc + k + ":" + v.trim() + "\n"
});
ch
}
fn compose_request(
raw_uri: &str,
http_verb: &str,
sorted_headers: &[(String, &str)],
signed_headers: &[&str],
payload: &[u8],
) -> String {
let uri = url::Url::parse(raw_uri).unwrap();
let mut creq = String::with_capacity(256);
creq.push_str(http_verb);
creq.push_str("\n");
creq.push_str(uri.path());
creq.push_str("\n");
let mut query: Vec<_> = uri.query_pairs().collect();
if query.len() > 0 {
query.sort_by(|(k1, _), (k2, _)| k1.cmp(k2));
let pairs: Vec<_> = query
.iter()
.map(|(k, v)| {
format!(
"{}={}",
percent_encoding::percent_encode(
k.as_bytes(),
percent_encoding::NON_ALPHANUMERIC
),
percent_encoding::percent_encode(
v.as_bytes(),
percent_encoding::NON_ALPHANUMERIC
)
)
})
.collect();
creq.push_str(&pairs.join("&"));
}
creq.push_str("\n");
if sorted_headers.len() > 0 {
sorted_headers.iter().for_each(|(k, v)| {
creq.push_str(k);
creq.push_str(":");
creq.push_str(v);
creq.push_str("\n");
});
}
creq.push_str("\n");
creq.push_str(&signed_headers.join(";"));
creq.push_str("\n");
creq.push_str(sha256_hex_from_ref(payload).as_str());
creq
}
impl<'a> SignatureRequest<'a> {
pub fn new<T>(
headers: &'a T,
http_verb: &'a str,
raw_uri: &'a str,
access_key_id: &'a str,
secret_access_key: &'a str,
time: DateTime<Utc>,
region: &'a str,
service: &'a str,
body: &'a [u8],
) -> Self
where
&'a T: IntoIterator<Item = (&'a &'a str, &'a &'a str)>,
{
let headers = format_headers(headers);
SignatureRequest {
headers: headers,
http_verb,
raw_uri,
access_key_id,
secret_access_key,
time,
region,
service,
body,
}
}
}
impl<'a> SignatureRequest<'a> {
pub fn canonical_headers(&'a self) -> String {
compose_headers(&self.headers)
}
pub fn canonical_request(&self) -> String {
let signed_headers: Vec<_> = self.headers.iter().map(|(k, _)| k.as_str()).collect();
let creq = compose_request(
self.raw_uri,
self.http_verb,
&self.headers,
signed_headers.as_slice(),
self.body,
);
println!("Creq:\n");
println!("{creq}END");
println!("\n\n");
creq
}
pub fn string_to_sign(&self) -> String {
let creq_hash = sha256_hex_from_ref(self.canonical_request());
let mut sts = String::with_capacity(256);
sts.push_str("AWS4-HMAC-SHA256\n");
sts.push_str(self.time.format(UTC_ISO_FORMAT).to_string().as_str());
sts.push_str("\n");
sts.push_str(self.time.format(DATE_FORMAT).to_string().as_str());
sts.push('/');
sts.push_str(self.region);
sts.push('/');
sts.push_str(self.service);
sts.push('/');
sts.push_str("aws4_request");
sts.push_str("\n");
sts.push_str(&creq_hash);
println!("STS:");
println!("{sts}END");
println!("\n\n");
sts
}
pub fn sign(&self) -> String {
let sk = generate_signing_key(self.secret_access_key, self.time, self.region, self.service);
sign_string(sk, self.string_to_sign().as_bytes())
}
pub fn authorization_header_with_signature(&self, signature: &str) -> String {
let mut hd = String::with_capacity(256);
hd.extend(["AWS4-HMAC-SHA256 ", "Credential=", self.access_key_id]);
hd.extend(["/", self.time.format(DATE_FORMAT).to_string().as_str()]);
hd.extend(["/", self.region]);
hd.extend(["/", self.service]);
hd.extend(["/", "aws4_request", ",", "SignedHeaders="]);
let sh = &self
.headers
.iter()
.map(|(k, _)| k.as_str())
.collect::<Vec<_>>()
.join(";");
hd.extend([sh, ",", "Signature=", signature]);
hd
}
pub fn signed_authorization_header(&self) -> String {
let sig = self.sign();
self.authorization_header_with_signature(&sig)
}
}
pub fn sign_string(with_key: impl AsRef<[u8]>, str_slice: &[u8]) -> String {
let mut hasher = HmacSha256::new_from_slice(with_key.as_ref()).unwrap();
hasher.update(str_slice);
let data = hasher.finalize_fixed();
hex::encode(data)
}
pub fn generate_signing_key<'a>(
secret_access_key: &str,
time: DateTime<Utc>,
region: &str,
service: &str,
) -> impl AsRef<[u8]> {
let secret = format!("AWS4{}", secret_access_key);
let t = time.format(DATE_FORMAT).to_string();
let mut chain = hmac_msg::HmacMsg::new(vec![secret.as_str(), t.as_str()]);
chain
.segment(region)
.segment(service)
.segment("aws4_request")
.finalize_fixed()
}
pub(crate) fn sha256_hex_from_ref(bytes: impl AsRef<[u8]>) -> String {
let mut hasher = Sha256::new();
hasher.update(bytes);
hex::encode(hasher.finalize_fixed())
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use chrono::{DateTime, TimeZone, Utc};
use crate::{
SignatureRequest, UTC_ISO_FORMAT, compose_headers, compose_request, format_headers,
generate_signing_key, sha256_hex_from_ref, sign_string,
};
#[test]
fn test_compose_request() {
let raw_uri = "https://my-s3-compatible.cloud.host.com/v1/path/to/bucket?version=201511&tag=My Custom Tag";
let http_verb = "GET";
let mut m = HashMap::new();
m.insert("Cba", "lkalkshdlh ");
m.insert("AbC", " lkalkshdlh ");
m.insert("JkC", " lkalkshdlh ");
m.insert("LoOhkjhaC", "lkalkshdlh");
let sorted_headers = format_headers(&m);
let signed_headers = vec!["host"];
let res = compose_request(
raw_uri,
http_verb,
&sorted_headers,
signed_headers.as_slice(),
b"",
);
assert_eq!(
"GET\n/v1/path/to/bucket\ntag=My%20Custom%20Tag&version=201511\nabc:lkalkshdlh\ncba:lkalkshdlh\njkc:lkalkshdlh\nloohkjhac:lkalkshdlh\n\nhost\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
res
);
}
#[test]
fn test_compose_headers() {
let mut m = HashMap::new();
m.insert("Cba", "lkalkshdlh ");
m.insert("AbC", " lkalkshdlh ");
m.insert("JkC", " lkalkshdlh ");
m.insert("LoOhkjhaC", "lkalkshdlh");
let m = format_headers(&m);
let res = compose_headers(&m);
assert_eq!(
"abc:lkalkshdlh\ncba:lkalkshdlh\njkc:lkalkshdlh\nloohkjhac:lkalkshdlh\n",
res
);
}
#[test]
fn test_signature_req_from_aws_docu() {
let access_key = "AKIAIOSFODNN7EXAMPLE";
let secret = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
let time: DateTime<Utc> = Utc.with_ymd_and_hms(2013, 5, 24, 00, 00, 0).unwrap();
let formatted = format!("{}", time.format(UTC_ISO_FORMAT));
let content_hash = sha256_hex_from_ref([]);
let mut m = HashMap::new();
m.insert("Host", "examplebucket.s3.amazonaws.com");
m.insert("X-Amz-Date", &formatted);
m.insert("X-Amz-Content-Sha256", &content_hash);
m.insert("Range", "bytes=0-9");
let empty_body = vec![];
let url = "https://examplebucket.s3.amazonaws.com/test.txt";
let method = "GET";
let region = "us-east-1";
let service = "s3";
let req = SignatureRequest::new(
&m,
method,
url,
access_key,
secret,
time,
region,
service,
&empty_body,
);
let signature = req.sign();
assert_eq!(
"f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41",
signature
);
let auth_header = req.authorization_header_with_signature(&signature);
assert_eq!(
"AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=host;range;x-amz-content-sha256;x-amz-date,Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41",
auth_header
);
}
#[test]
fn test_signature_calculation() {
let secret = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
let creq = r#"AWS4-HMAC-SHA256
20150830T123600Z
20150830/us-east-1/iam/aws4_request
f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59"#;
let time: DateTime<Utc> = Utc.with_ymd_and_hms(2015, 8, 30, 12, 36, 0).unwrap();
let formatted = format!("{}", time.format(UTC_ISO_FORMAT));
assert_eq!("20150830T123600Z", formatted);
let derived_key = generate_signing_key(secret, time, "us-east-1", "iam");
let signature = sign_string(derived_key, creq.as_bytes());
let expected = "5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7";
assert_eq!(expected, &signature);
}
#[test]
fn sign_payload_empty_string() {
let expected = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
let actual = sha256_hex_from_ref([]);
assert_eq!(expected, actual);
}
}