use std::collections::BTreeMap;
use std::ops::Deref;
use chrono::DateTime;
use chrono::Utc;
use hmac::Mac;
use reqwest::Request;
use sha2::Digest;
use sha2::Sha256;
type Hmac = hmac::Hmac<Sha256>;
pub trait SignatureProvider {
fn algorithm(&self) -> &str;
fn secret_key_prefix(&self) -> &str;
fn request_type(&self) -> &str;
fn region(&self) -> &str;
fn service(&self) -> &str;
fn date_header_name(&self) -> &str;
fn content_hash_header_name(&self) -> &str;
fn access_key_id(&self) -> &str;
fn secret_access_key(&self) -> &str;
}
pub struct RequestSigner<P>(P);
impl<P> RequestSigner<P>
where
P: SignatureProvider,
{
pub fn new(provider: P) -> Self {
Self(provider)
}
pub fn sign(&self, date: DateTime<Utc>, request: &Request) -> Option<String> {
let scope = format!(
"{date}/{region}/{service}/{request_type}",
date = date.format("%Y%m%d"),
region = self.0.region(),
service = self.0.service(),
request_type = self.0.request_type()
);
let canonical_request = self.create_canonical_request(request);
let string_to_sign = self.create_string_to_sign(date, &scope, &canonical_request);
let signing_key = self.derive_signing_key(date)?;
let mut hmac = Hmac::new_from_slice(&signing_key).ok()?;
hmac.update(string_to_sign.as_bytes());
let signature = hex::encode(hmac.finalize().into_bytes());
Some(format!(
"{algorithm} \
Credential={access_key_id}/{scope},SignedHeaders=host;{content_hash_header_name};\
{date_header_name},Signature={signature}",
algorithm = self.0.algorithm(),
access_key_id = self.0.access_key_id(),
content_hash_header_name = self.0.content_hash_header_name(),
date_header_name = self.0.date_header_name(),
))
}
fn create_canonical_request(&self, request: &Request) -> String {
let url = request.url();
let mut parameters: BTreeMap<_, Vec<_>> = BTreeMap::new();
for (k, v) in url.query_pairs() {
parameters
.entry(urlencoding::encode(&k).into_owned())
.or_default()
.push(urlencoding::encode(&v).into_owned());
}
let mut query_string = String::new();
for (key, values) in parameters {
for value in values {
if !query_string.is_empty() {
query_string.push('&');
}
query_string.push_str(&key);
query_string.push('=');
query_string.push_str(&value);
}
}
let date = request
.headers()
.get(self.0.date_header_name())
.expect("request missing date header");
let content_hash = request
.headers()
.get(self.0.content_hash_header_name())
.expect("request missing content hash header");
format!(
"\
{method}
{path}
{query_string}
host:{domain}
{content_hash_header}:{content_hash}
{date_header}:{date}
host;{content_hash_header};{date_header}
{content_hash}",
method = request.method(),
path = url.path(),
domain = request.url().domain().expect("should have domain").trim(),
date_header = self.0.date_header_name(),
date = date.to_str().expect("date should be a string").trim(),
content_hash_header = self.0.content_hash_header_name(),
content_hash = content_hash
.to_str()
.expect("content hash should be a string")
.trim()
)
}
fn create_string_to_sign(
&self,
date: DateTime<Utc>,
scope: &str,
canonical_request: &str,
) -> String {
let mut hash = Sha256::new();
hash.update(canonical_request);
let hash = hash.finalize();
format!(
"{algorithm}\n{date}\n{scope}\n{hash}",
algorithm = self.0.algorithm(),
date = date.format("%Y%m%dT%H%M%SZ"),
hash = hex::encode(hash)
)
}
fn derive_signing_key<'a>(
&self,
date: DateTime<Utc>,
) -> Option<impl Deref<Target = [u8]> + use<'a, P>> {
let mut hmac = Hmac::new_from_slice(
format!(
"{prefix}{secret_access_key}",
prefix = self.0.secret_key_prefix(),
secret_access_key = self.0.secret_access_key()
)
.as_bytes(),
)
.ok()?;
hmac.update(format!("{date}", date = date.format("%Y%m%d")).as_bytes());
let date_key = hmac.finalize().into_bytes();
let mut hmac = Hmac::new_from_slice(&date_key).ok()?;
hmac.update(self.0.region().as_bytes());
let date_region_key = hmac.finalize().into_bytes();
let mut hmac = Hmac::new_from_slice(&date_region_key).ok()?;
hmac.update(self.0.service().as_bytes());
let date_region_service_key = hmac.finalize().into_bytes();
let mut hmac = Hmac::new_from_slice(&date_region_service_key).ok()?;
hmac.update(self.0.request_type().as_bytes());
Some(hmac.finalize().into_bytes())
}
}