#![allow(clippy::wildcard_imports)]
use sha2::{Digest, Sha256};
#[must_use]
pub fn hmac_sha256(key: &[u8], message: &[u8]) -> [u8; 32] {
fn pad(key: &[u8], byte: u8) -> [u8; 64] {
let mut block = [0u8; 64];
if key.len() > 64 {
let digest = Sha256::digest(key);
block[..32].copy_from_slice(&digest);
} else {
block[..key.len()].copy_from_slice(key);
}
for b in &mut block {
*b ^= byte;
}
block
}
let mut inner = Sha256::new();
inner.update(pad(key, 0x36));
inner.update(message);
let inner_digest: [u8; 32] = inner.finalize().into();
let mut outer = Sha256::new();
outer.update(pad(key, 0x5c));
outer.update(inner_digest);
outer.finalize().into()
}
fn to_hex(bytes: &[u8]) -> String {
use std::fmt::Write;
let mut out = String::with_capacity(bytes.len() * 2);
for b in bytes {
let _ = write!(out, "{b:02x}");
}
out
}
#[must_use]
pub fn sha256_hex(data: &[u8]) -> String {
to_hex(&Sha256::digest(data))
}
fn uri_encode(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for byte in value.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(byte as char);
}
_ => {
use std::fmt::Write;
let _ = write!(out, "%{byte:02X}");
}
}
}
out
}
pub struct SignedUrl {
pub host: String,
pub path: String,
pub query: Vec<(String, String)>,
}
pub struct SigV4Credentials<'a> {
pub access_key: &'a str,
pub secret_key: &'a str,
pub region: &'a str,
pub service: &'a str,
}
#[must_use]
pub fn authorization_header(
method: &str,
url: &SignedUrl,
creds: &SigV4Credentials<'_>,
amz_date: &str,
content_type: Option<&str>,
payload_hash: &str,
) -> String {
let mut header_lines: Vec<(String, String)> = Vec::new();
if let Some(ct) = content_type {
header_lines.push(("content-type".into(), ct.trim().to_string()));
}
header_lines.push(("host".into(), url.host.clone()));
header_lines.push(("x-amz-date".into(), amz_date.to_string()));
header_lines.sort();
let canonical_headers = header_lines.iter().fold(String::new(), |mut acc, (n, v)| {
use std::fmt::Write;
let _ = write!(acc, "{n}:{v}");
acc.push('\n');
acc
});
let signed_headers = header_lines
.iter()
.map(|(n, _)| n.as_str())
.collect::<Vec<_>>()
.join(";");
let mut query: Vec<(String, String)> = url
.query
.iter()
.map(|(n, v)| (uri_encode(n), uri_encode(v)))
.collect();
query.sort();
let canonical_query = query
.iter()
.map(|(n, v)| format!("{n}={v}"))
.collect::<Vec<_>>()
.join("&");
let canonical_uri = url
.path
.split('/')
.map(uri_encode)
.collect::<Vec<_>>()
.join("/");
let canonical_request = format!(
"{method}\n{canonical_uri}\n{canonical_query}\n{canonical_headers}\n{signed_headers}\n{payload_hash}"
);
let credential_scope = format!(
"{}/{}/{}/aws4_request",
&amz_date[..8],
creds.region,
creds.service
);
let string_to_sign = format!(
"AWS4-HMAC-SHA256\n{amz_date}\n{credential_scope}\n{}",
sha256_hex(canonical_request.as_bytes())
);
let k_date = hmac_sha256(
format!("AWS4{}", creds.secret_key).as_bytes(),
&amz_date.as_bytes()[..8],
);
let k_region = hmac_sha256(&k_date, creds.region.as_bytes());
let k_service = hmac_sha256(&k_region, creds.service.as_bytes());
let k_signing = hmac_sha256(&k_service, b"aws4_request");
let signature = hmac_sha256(&k_signing, string_to_sign.as_bytes());
let signature_hex = to_hex(&signature);
format!(
"AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}",
creds.access_key, credential_scope, signed_headers, signature_hex
)
}
pub fn amz_date_now() -> (String, String) {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let days_since_epoch = secs / 86_400;
let secs_today = secs % 86_400;
let z = i64::try_from(days_since_epoch).unwrap_or(0) + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u64;
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let year = if m <= 2 { y + 1 } else { y };
let stamp = format!(
"{year:04}{m:02}{d:02}T{hour:02}{minute:02}{second:02}Z",
hour = secs_today / 3600,
minute = (secs_today % 3600) / 60,
second = secs_today % 60
);
(stamp.clone(), stamp[..8].to_string())
}
pub const EMPTY_PAYLOAD_SHA256: &str =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hmac_matches_rfc4231_vector() {
let key = [0x0b; 20];
let mac = hmac_sha256(&key, b"Hi There");
let hex = to_hex(&mac);
assert_eq!(
hex,
"b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7"
);
}
#[test]
fn hmac_long_key_is_hashed_first() {
let key = [0xaa; 131];
let mac = hmac_sha256(
&key,
b"Test Using Larger Than Block-Size Key - Hash Key First",
);
assert_eq!(
to_hex(&mac),
"60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54"
);
}
#[test]
fn empty_payload_hash_is_the_standard_constant() {
assert_eq!(sha256_hex(b""), EMPTY_PAYLOAD_SHA256);
}
#[test]
fn authorization_header_shape() {
let url = SignedUrl {
host: "polly.us-east-1.amazonaws.com".into(),
path: "/v1/synthesis".into(),
query: Vec::new(),
};
let creds = SigV4Credentials {
access_key: "AKIDEXAMPLE",
secret_key: "secret",
region: "us-east-1",
service: "polly",
};
let payload_hash = sha256_hex(b"{}");
let auth = authorization_header(
"POST",
&url,
&creds,
"20260926T120000Z",
Some("application/json"),
&payload_hash,
);
assert!(
auth.starts_with(
"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20260926/us-east-1/polly/aws4_request, "
),
"{auth}"
);
assert!(
auth.contains("SignedHeaders=content-type;host;x-amz-date"),
"{auth}"
);
assert!(auth.contains("Signature="), "{auth}");
let again = authorization_header(
"POST",
&url,
&creds,
"20260926T120000Z",
Some("application/json"),
&sha256_hex(b"{}"),
);
assert_eq!(auth, again);
}
#[test]
fn query_pairs_are_encoded_and_sorted() {
let url = SignedUrl {
host: "h".into(),
path: "/".into(),
query: vec![("b".into(), "x y".into()), ("a".into(), "ü".into())],
};
let creds = SigV4Credentials {
access_key: "k",
secret_key: "s",
region: "r",
service: "svc",
};
let auth = authorization_header(
"GET",
&url,
&creds,
"20260926T120000Z",
None,
EMPTY_PAYLOAD_SHA256,
);
assert!(auth.contains("Signature="));
assert_eq!(uri_encode("x y"), "x%20y");
assert_eq!(uri_encode("ü"), "%C3%BC");
assert_eq!(uri_encode("a-b~._z"), "a-b~._z");
}
}