#![cfg_attr(
not(test),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::todo,
clippy::unimplemented,
clippy::indexing_slicing,
clippy::string_slice,
clippy::arithmetic_side_effects,
)
)]
use std::fmt::Write as _;
use hmac::{Hmac, Mac};
use sha2::{Digest as _, Sha256};
type HmacSha256 = Hmac<Sha256>;
pub const ALGORITHM: &str = "AWS4-HMAC-SHA256";
pub const AWS4_REQUEST: &str = "aws4_request";
pub const EMPTY_PAYLOAD_SHA256: &str =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
pub type Header = (String, String);
#[must_use]
pub fn sha256_hex(data: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
hex::encode(hasher.finalize())
}
#[must_use]
pub fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
HmacSha256::new_from_slice(key).map_or([0u8; 32], |mut mac| {
mac.update(data);
mac.finalize().into_bytes().into()
})
}
#[must_use]
pub fn uri_encode(s: &str, encode_slash: bool) -> String {
let mut out = String::with_capacity(s.len());
for &b in s.as_bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
out.push(b as char);
}
b'/' if !encode_slash => out.push('/'),
_ => {
let _ = write!(out, "%{b:02X}");
}
}
}
out
}
#[must_use]
pub fn signed_headers_list(headers: &[Header]) -> String {
headers
.iter()
.map(|(name, _)| name.as_str())
.collect::<Vec<_>>()
.join(";")
}
#[must_use]
pub fn canonical_query(params: &[(&str, &str)]) -> String {
let mut encoded: Vec<(String, String)> = params
.iter()
.map(|(k, v)| (uri_encode(k, true), uri_encode(v, true)))
.collect();
encoded.sort();
encoded
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join("&")
}
#[must_use]
pub fn canonical_request(
method: &str,
canonical_uri: &str,
canonical_query: &str,
headers: &[Header],
payload_hash: &str,
) -> String {
let canonical_headers: String = headers
.iter()
.fold(String::new(), |mut acc, (name, value)| {
let _ = writeln!(acc, "{name}:{}", collapse_whitespace(value));
acc
});
let signed = signed_headers_list(headers);
format!(
"{method}\n{canonical_uri}\n{canonical_query}\n{canonical_headers}\n{signed}\n{payload_hash}"
)
}
fn collapse_whitespace(value: &str) -> String {
value.split_whitespace().collect::<Vec<_>>().join(" ")
}
#[must_use]
pub fn credential_scope(date: &str, region: &str, service: &str) -> String {
format!("{date}/{region}/{service}/{AWS4_REQUEST}")
}
#[must_use]
pub fn string_to_sign(amz_date: &str, scope: &str, canonical_request_hash: &str) -> String {
format!("{ALGORITHM}\n{amz_date}\n{scope}\n{canonical_request_hash}")
}
#[must_use]
pub fn signing_key(secret: &str, date: &str, region: &str, service: &str) -> [u8; 32] {
let k_date = hmac_sha256(format!("AWS4{secret}").as_bytes(), date.as_bytes());
let k_region = hmac_sha256(&k_date, region.as_bytes());
let k_service = hmac_sha256(&k_region, service.as_bytes());
hmac_sha256(&k_service, AWS4_REQUEST.as_bytes())
}
#[must_use]
pub fn signature(
secret: &str,
date: &str,
region: &str,
service: &str,
amz_date: &str,
scope: &str,
canonical_req: &str,
) -> String {
let key = signing_key(secret, date, region, service);
let sts = string_to_sign(amz_date, scope, &sha256_hex(canonical_req.as_bytes()));
hex::encode(hmac_sha256(&key, sts.as_bytes()))
}
#[must_use]
pub fn authorization_header(
access_key_id: &str,
scope: &str,
headers: &[Header],
signature: &str,
) -> String {
format!(
"{ALGORITHM} Credential={access_key_id}/{scope}, SignedHeaders={}, Signature={signature}",
signed_headers_list(headers),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_payload_hash_is_the_sha256_of_the_empty_string() {
assert_eq!(sha256_hex(b""), EMPTY_PAYLOAD_SHA256);
}
#[test]
fn uri_encode_follows_the_aws_unreserved_set() {
assert_eq!(uri_encode("abcXYZ019-._~", true), "abcXYZ019-._~");
assert_eq!(uri_encode("a/b", false), "a/b");
assert_eq!(uri_encode("a/b", true), "a%2Fb");
assert_eq!(uri_encode("a b+c", true), "a%20b%2Bc");
assert_eq!(uri_encode("ΓΌ", true), "%C3%BC");
}
#[test]
fn canonical_query_sorts_and_encodes() {
assert_eq!(
canonical_query(&[("prefix", "a/b"), ("list-type", "2")]),
"list-type=2&prefix=a%2Fb"
);
assert_eq!(canonical_query(&[]), "");
}
#[test]
fn header_values_are_trimmed_and_internally_collapsed() {
let headers = vec![("x-test".to_owned(), " a b\tc ".to_owned())];
assert!(
canonical_request("GET", "/", "", &headers, "H").contains("x-test:a b c\n"),
"SigV4 collapses sequential whitespace in a header value"
);
}
#[test]
fn canonical_request_has_the_documented_shape() {
let headers = vec![
("host".to_owned(), "example.com".to_owned()),
("x-amz-date".to_owned(), " 20260902T000000Z ".to_owned()),
];
assert_eq!(
canonical_request("PUT", "/bucket/key", "", &headers, "PAYLOAD"),
"PUT\n/bucket/key\n\nhost:example.com\nx-amz-date:20260902T000000Z\n\n\
host;x-amz-date\nPAYLOAD"
);
}
#[test]
fn matches_the_published_aws_test_vector() {
let headers = vec![
("host".to_owned(), "example.amazonaws.com".to_owned()),
("x-amz-date".to_owned(), "20150830T123600Z".to_owned()),
];
let canonical = canonical_request("GET", "/", "", &headers, EMPTY_PAYLOAD_SHA256);
assert_eq!(
canonical,
"GET\n/\n\nhost:example.amazonaws.com\nx-amz-date:20150830T123600Z\n\n\
host;x-amz-date\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
let scope = credential_scope("20150830", "us-east-1", "service");
assert_eq!(scope, "20150830/us-east-1/service/aws4_request");
let sig = signature(
"wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
"20150830",
"us-east-1",
"service",
"20150830T123600Z",
&scope,
&canonical,
);
assert_eq!(
sig,
"5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31"
);
assert_eq!(
authorization_header("AKIDEXAMPLE", &scope, &headers, &sig),
"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, \
SignedHeaders=host;x-amz-date, \
Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31"
);
}
#[test]
fn signing_key_is_deterministic_and_scope_separated() {
let a = signing_key("secret", "20260902", "us-east-1", "s3");
let b = signing_key("secret", "20260902", "us-east-1", "s3");
let c = signing_key("secret", "20260902", "eu-west-1", "s3");
assert_eq!(a, b);
assert_ne!(a, c);
}
}