use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use base16ct::lower::encode_string as hex_encode_crate;
use hmac::{Hmac, Mac};
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
use super::http::url_encode;
use super::types::{S3Error, S3Request};
#[derive(Debug, Clone)]
pub struct AuthResult {
pub access_key: String,
pub region: String,
pub service: String,
pub date: String,
pub is_valid: bool,
}
pub fn verify_signature(
request: &S3Request,
access_key: &str,
secret_key: &str,
) -> Result<AuthResult, S3Error> {
let auth_header = request
.header("authorization")
.ok_or(S3Error::AccessDenied)?;
let parsed = parse_authorization_header(auth_header)?;
if parsed.access_key != access_key {
return Err(S3Error::InvalidAccessKeyId);
}
let x_amz_date = request
.header("x-amz-date")
.ok_or(S3Error::InvalidArgument("Missing x-amz-date header".into()))?;
let content_sha256 = request
.header("x-amz-content-sha256")
.map(|s| s.as_str())
.unwrap_or("UNSIGNED-PAYLOAD");
let canonical_request = build_canonical_request(
request.method.as_str(),
&request_path(request),
&request.query,
&request.headers,
&parsed.signed_headers,
content_sha256,
);
let string_to_sign =
build_string_to_sign(x_amz_date, &parsed.credential_scope, &canonical_request);
let expected_signature = calculate_signature(
secret_key,
&parsed.date,
&parsed.region,
&parsed.service,
&string_to_sign,
);
let is_valid = constant_time_compare(&expected_signature, &parsed.signature);
Ok(AuthResult {
access_key: parsed.access_key,
region: parsed.region,
service: parsed.service,
date: parsed.date,
is_valid,
})
}
fn request_path(request: &S3Request) -> String {
let mut path = String::from("/");
if let Some(ref bucket) = request.bucket {
path.push_str(bucket);
if let Some(ref key) = request.key {
path.push('/');
path.push_str(key);
}
}
path
}
struct ParsedAuthHeader {
access_key: String,
credential_scope: String,
date: String,
region: String,
service: String,
signed_headers: Vec<String>,
signature: String,
}
fn parse_authorization_header(header: &str) -> Result<ParsedAuthHeader, S3Error> {
let header = header
.strip_prefix("AWS4-HMAC-SHA256 ")
.ok_or(S3Error::SignatureDoesNotMatch)?;
let mut credential = None;
let mut signed_headers = None;
let mut signature = None;
for part in header.split(", ") {
if let Some(val) = part.strip_prefix("Credential=") {
credential = Some(val.to_string());
} else if let Some(val) = part.strip_prefix("SignedHeaders=") {
signed_headers = Some(val.to_string());
} else if let Some(val) = part.strip_prefix("Signature=") {
signature = Some(val.to_string());
}
}
let credential = credential.ok_or(S3Error::SignatureDoesNotMatch)?;
let signed_headers_str = signed_headers.ok_or(S3Error::SignatureDoesNotMatch)?;
let signature = signature.ok_or(S3Error::SignatureDoesNotMatch)?;
let cred_parts: Vec<&str> = credential.split('/').collect();
if cred_parts.len() != 5 {
return Err(S3Error::SignatureDoesNotMatch);
}
let access_key = cred_parts[0].to_string();
let date = cred_parts[1].to_string();
let region = cred_parts[2].to_string();
let service = cred_parts[3].to_string();
let credential_scope = cred_parts[1..].join("/");
let signed_headers: Vec<String> = signed_headers_str
.split(';')
.map(|s| s.to_string())
.collect();
Ok(ParsedAuthHeader {
access_key,
credential_scope,
date,
region,
service,
signed_headers,
signature,
})
}
fn build_canonical_request(
method: &str,
path: &str,
query: &BTreeMap<String, String>,
headers: &BTreeMap<String, String>,
signed_headers: &[String],
payload_hash: &str,
) -> String {
let mut result = String::new();
result.push_str(method);
result.push('\n');
result.push_str(&canonical_uri(path));
result.push('\n');
result.push_str(&canonical_query_string(query));
result.push('\n');
result.push_str(&canonical_headers(headers, signed_headers));
result.push('\n');
result.push_str(&signed_headers.join(";"));
result.push('\n');
result.push_str(payload_hash);
result
}
fn canonical_uri(path: &str) -> String {
let segments: Vec<&str> = path.split('/').collect();
let encoded: Vec<String> = segments.iter().map(|s| uri_encode(s, false)).collect();
encoded.join("/")
}
fn canonical_query_string(query: &BTreeMap<String, String>) -> String {
if query.is_empty() {
return String::new();
}
let mut pairs: Vec<String> = query
.iter()
.map(|(k, v)| alloc::format!("{}={}", uri_encode(k, true), uri_encode(v, true)))
.collect();
pairs.sort();
pairs.join("&")
}
fn canonical_headers(headers: &BTreeMap<String, String>, signed_headers: &[String]) -> String {
let mut result = String::new();
for header_name in signed_headers {
let lower_name = header_name.to_lowercase();
let value = headers
.iter()
.find(|(k, _)| k.to_lowercase() == lower_name)
.map(|(_, v)| v.clone())
.unwrap_or_default();
let trimmed = value.split_whitespace().collect::<Vec<_>>().join(" ");
result.push_str(&lower_name);
result.push(':');
result.push_str(&trimmed);
result.push('\n');
}
result
}
fn uri_encode(s: &str, encode_slash: bool) -> String {
let mut result = String::with_capacity(s.len() * 3);
for c in s.chars() {
match c {
'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' => {
result.push(c);
}
'/' if !encode_slash => {
result.push(c);
}
_ => {
for byte in c.to_string().as_bytes() {
result.push_str(&alloc::format!("%{:02X}", byte));
}
}
}
}
result
}
fn build_string_to_sign(
date_time: &str,
credential_scope: &str,
canonical_request: &str,
) -> String {
let mut result = String::new();
result.push_str("AWS4-HMAC-SHA256\n");
result.push_str(date_time);
result.push('\n');
result.push_str(credential_scope);
result.push('\n');
let hash = sha256_hex(canonical_request.as_bytes());
result.push_str(&hash);
result
}
fn calculate_signature(
secret_key: &str,
date: &str,
region: &str,
service: &str,
string_to_sign: &str,
) -> String {
let k_secret = alloc::format!("AWS4{}", secret_key);
let k_date = hmac_sha256(k_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());
let k_signing = hmac_sha256(&k_service, b"aws4_request");
let signature = hmac_sha256(&k_signing, string_to_sign.as_bytes());
hex_encode(&signature)
}
type HmacSha256 = Hmac<Sha256>;
fn sha256_hex(data: &[u8]) -> String {
hex_encode(&sha256(data))
}
fn sha256(data: &[u8]) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(data);
let result = hasher.finalize();
let mut output = [0u8; 32];
output.copy_from_slice(&result);
output
}
fn hmac_sha256(key: &[u8], data: &[u8]) -> [u8; 32] {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC can take key of any size");
mac.update(data);
let result = mac.finalize();
let mut output = [0u8; 32];
output.copy_from_slice(&result.into_bytes());
output
}
fn hex_encode(bytes: &[u8]) -> String {
hex_encode_crate(bytes)
}
fn constant_time_compare(a: &str, b: &str) -> bool {
if a.len() != b.len() {
return false;
}
a.as_bytes().ct_eq(b.as_bytes()).into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sha256() {
let hash = sha256(b"");
let expected = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
assert_eq!(hex_encode(&hash), expected);
let hash = sha256(b"abc");
let expected = "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";
assert_eq!(hex_encode(&hash), expected);
}
#[test]
fn test_hmac_sha256() {
let key = b"key";
let data = b"The quick brown fox jumps over the lazy dog";
let hmac = hmac_sha256(key, data);
let expected = "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8";
assert_eq!(hex_encode(&hmac), expected);
}
#[test]
fn test_hex_encode() {
assert_eq!(hex_encode(&[0x12, 0x34, 0xab, 0xcd]), "1234abcd");
assert_eq!(hex_encode(&[0x00, 0xff]), "00ff");
}
#[test]
fn test_constant_time_compare() {
assert!(constant_time_compare("abc", "abc"));
assert!(!constant_time_compare("abc", "abd"));
assert!(!constant_time_compare("abc", "ab"));
}
#[test]
fn test_uri_encode() {
assert_eq!(uri_encode("hello world", true), "hello%20world");
assert_eq!(uri_encode("a/b/c", false), "a/b/c");
assert_eq!(uri_encode("a/b/c", true), "a%2Fb%2Fc");
}
#[test]
fn test_canonical_query_string() {
let mut query = BTreeMap::new();
query.insert("b".into(), "2".into());
query.insert("a".into(), "1".into());
let result = canonical_query_string(&query);
assert_eq!(result, "a=1&b=2");
}
#[test]
fn test_parse_authorization_header() {
let header = "AWS4-HMAC-SHA256 Credential=AKID/20240101/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=abc123";
let parsed = parse_authorization_header(header).unwrap();
assert_eq!(parsed.access_key, "AKID");
assert_eq!(parsed.date, "20240101");
assert_eq!(parsed.region, "us-east-1");
assert_eq!(parsed.service, "s3");
assert_eq!(parsed.signature, "abc123");
assert_eq!(parsed.signed_headers, vec!["host", "x-amz-date"]);
}
#[test]
fn test_canonical_headers() {
let mut headers = BTreeMap::new();
headers.insert("Host".into(), "example.com".into());
headers.insert("X-Amz-Date".into(), "20240101T000000Z".into());
let signed = vec!["host".into(), "x-amz-date".into()];
let result = canonical_headers(&headers, &signed);
assert_eq!(result, "host:example.com\nx-amz-date:20240101T000000Z\n");
}
#[test]
fn test_calculate_signature() {
let signature = calculate_signature(
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"20240101",
"us-east-1",
"s3",
"test-string-to-sign",
);
assert_eq!(signature.len(), 64);
assert!(signature.chars().all(|c| c.is_ascii_hexdigit()));
}
}