use crate::auth::SecretKey;
use crate::http::OrderedHeaders;
use crate::http::OrderedQs;
use crate::utils::crypto::hmac_sha1;
use std::ops::Not;
use hyper::Method;
fn base64(data: impl AsRef<[u8]>) -> String {
base64_simd::STANDARD.encode_to_string(data)
}
pub fn calculate_signature(secret_key: &SecretKey, string_to_sign: &str) -> String {
base64(hmac_sha1(secret_key.expose(), string_to_sign))
}
const INCLUDED_QUERY: &[&str] = &[
"accelerate",
"acl",
"analytics",
"cors",
"defaultObjectAcl",
"delete",
"inventory",
"lifecycle",
"location",
"logging",
"metrics",
"notification",
"object-lock",
"partNumber",
"policy",
"replication",
"requestPayment",
"response-cache-control",
"response-content-disposition",
"response-content-encoding",
"response-content-language",
"response-content-type",
"response-expires",
"restore",
"select",
"select-type",
"storageClass",
"tagging",
"torrent",
"uploadId",
"uploads",
"versionId",
"versioning",
"versions",
"website",
];
#[derive(Debug, Clone, Copy)]
pub enum Mode {
HeaderAuth,
PresignedUrl,
}
pub fn create_string_to_sign(
mode: Mode,
method: &Method,
uri_path: &str,
qs: Option<&OrderedQs>,
headers: &OrderedHeaders<'_>,
virtual_host_bucket: Option<&str>,
) -> String {
let mut ans = String::with_capacity(256);
{
ans.push_str(method.as_str());
ans.push('\n');
}
{
if let Some(v) = headers.get_unique("content-md5") {
ans.push_str(v);
}
ans.push('\n');
}
{
if let Some(v) = headers.get_unique("content-type") {
ans.push_str(v);
}
ans.push('\n');
}
match mode {
Mode::HeaderAuth => {
let mut date = headers.get_unique("date").unwrap_or_default();
if headers.get_unique("x-amz-date").is_some() {
date = "";
}
ans.push_str(date);
ans.push('\n');
}
Mode::PresignedUrl => {
let expires = qs.and_then(|qs| qs.get_unique("Expires")).unwrap_or_default();
ans.push_str(expires);
ans.push('\n');
}
}
{
let mut last = "";
for &(name, _) in headers.as_ref() {
if name.starts_with("x-amz-").not() {
continue;
}
if name == last {
continue;
}
last = name;
ans.push_str(name);
ans.push(':');
let mut iter = headers.get_all(name);
let first = iter.next().unwrap();
ans.push_str(first.trim());
for value in iter {
ans.push(',');
ans.push_str(value.trim());
}
ans.push('\n');
}
}
{
if let Some(bucket) = virtual_host_bucket {
ans.push('/');
ans.push_str(bucket);
}
ans.push_str(uri_path);
if let Some(qs) = qs {
let mut is_first = true;
for q in INCLUDED_QUERY {
if let Some(v) = qs.get_unique(q) {
if is_first {
ans.push('?');
is_first = false;
} else {
ans.push('&');
}
ans.push_str(q);
if v.is_empty().not() {
ans.push('=');
ans.push_str(v);
}
}
}
}
}
ans
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sorted() {
for w in INCLUDED_QUERY.windows(2) {
assert!(w[0] < w[1], "{w:?}");
}
}
#[allow(clippy::too_many_lines)]
#[test]
fn examples() {
let access_key = "AKIAIOSFODNN7EXAMPLE";
let secret_key = SecretKey::from("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY");
{
let method = &Method::GET;
let uri_path = "/photos/puppy.jpg";
let headers = OrderedHeaders::from_slice_unchecked(&[("date", "Tue, 27 Mar 2007 19:36:42 +0000")]);
let qs = None;
let vh_bucket = Some("awsexamplebucket1");
let string_to_sign = create_string_to_sign(Mode::HeaderAuth, method, uri_path, qs, &headers, vh_bucket);
let signature = calculate_signature(&secret_key, &string_to_sign);
assert_eq!(
string_to_sign,
concat!(
"GET\n",
"\n",
"\n",
"Tue, 27 Mar 2007 19:36:42 +0000\n",
"/awsexamplebucket1/photos/puppy.jpg",
)
);
assert_eq!(signature, "qgk2+6Sv9/oM7G3qLEjTH1a1l1g=");
}
{
let method = &Method::PUT;
let uri_path = "/photos/puppy.jpg";
let headers = OrderedHeaders::from_slice_unchecked(&[
("content-type", "image/jpeg"),
("date", "Tue, 27 Mar 2007 21:15:45 +0000"),
]);
let qs = None;
let vh_bucket = Some("awsexamplebucket1");
let string_to_sign = create_string_to_sign(Mode::HeaderAuth, method, uri_path, qs, &headers, vh_bucket);
let signature = calculate_signature(&secret_key, &string_to_sign);
assert_eq!(
string_to_sign,
concat!(
"PUT\n",
"\n",
"image/jpeg\n",
"Tue, 27 Mar 2007 21:15:45 +0000\n",
"/awsexamplebucket1/photos/puppy.jpg",
)
);
assert_eq!(signature, "iqRzw+ileNPu1fhspnRs8nOjjIA=");
}
{
let method = &Method::GET;
let uri_path = "/";
let headers = OrderedHeaders::from_slice_unchecked(&[("date", "Tue, 27 Mar 2007 19:42:41 +0000")]);
let qs = None;
let vh_bucket = Some("awsexamplebucket1");
let string_to_sign = create_string_to_sign(Mode::HeaderAuth, method, uri_path, qs, &headers, vh_bucket);
let signature = calculate_signature(&secret_key, &string_to_sign);
assert_eq!(
string_to_sign,
concat!(
"GET\n",
"\n",
"\n",
"Tue, 27 Mar 2007 19:42:41 +0000\n", "/awsexamplebucket1/",
)
);
assert_eq!(signature, "m0WP8eCtspQl5Ahe6L1SozdX9YA=");
}
{
let method = &Method::GET;
let uri_path = "/";
let qs = OrderedQs::from_vec_unchecked(vec![("acl".into(), String::new())]);
let headers = OrderedHeaders::from_slice_unchecked(&[("date", "Tue, 27 Mar 2007 19:44:46 +0000")]);
let vh_bucket = Some("awsexamplebucket1");
let string_to_sign = create_string_to_sign(Mode::HeaderAuth, method, uri_path, Some(&qs), &headers, vh_bucket);
let signature = calculate_signature(&secret_key, &string_to_sign);
assert_eq!(
string_to_sign,
concat!(
"GET\n",
"\n",
"\n",
"Tue, 27 Mar 2007 19:44:46 +0000\n", "/awsexamplebucket1/?acl",
)
);
assert_eq!(signature, "82ZHiFIjc+WbcwFKGUVEQspPn+0=");
}
{
let method = &Method::DELETE;
let uri_path = "/awsexamplebucket1/photos/puppy.jpg";
let headers = OrderedHeaders::from_slice_unchecked(&[
("date", "Tue, 27 Mar 2007 21:20:27 +0000"),
("x-amz-date", "Tue, 27 Mar 2007 21:20:26 +0000"),
]);
let qs = None;
let vh_bucket = None;
let string_to_sign = create_string_to_sign(Mode::HeaderAuth, method, uri_path, qs, &headers, vh_bucket);
let signature = calculate_signature(&secret_key, &string_to_sign);
assert_eq!(
string_to_sign,
concat!(
"DELETE\n",
"\n",
"\n",
"\n",
"x-amz-date:Tue, 27 Mar 2007 21:20:26 +0000\n",
"/awsexamplebucket1/photos/puppy.jpg",
)
);
assert_eq!(signature, "Ri1hpB1zpS9pGqR7y8kuNFCl4sE=");
}
{
let method = &Method::PUT;
let uri_path = "/db-backup.dat.gz";
let headers = OrderedHeaders::from_slice_unchecked(&[
("date", "Tue, 27 Mar 2007 21:06:08 +0000"),
("x-amz-acl", "public-read"),
("content-type", "application/x-download"),
("content-md5", "4gJE4saaMU4BqNR0kLY+lw=="),
("x-amz-meta-reviewedby", "joe@example.com"),
("x-amz-meta-reviewedby", "jane@example.com"),
("x-amz-meta-filechecksum", "0x02661779"),
("x-amz-meta-checksumalgorithm", "crc32"),
("content-disposition", "attachment; filename=database.dat"),
("content-encoding", "gzip"),
("content-length", "5913339"),
]);
let qs = None;
let vh_bucket = Some("static.example.com");
let string_to_sign = create_string_to_sign(Mode::HeaderAuth, method, uri_path, qs, &headers, vh_bucket);
let signature = calculate_signature(&secret_key, &string_to_sign);
assert_eq!(
string_to_sign,
concat!(
"PUT\n",
"4gJE4saaMU4BqNR0kLY+lw==\n",
"application/x-download\n",
"Tue, 27 Mar 2007 21:06:08 +0000\n",
"x-amz-acl:public-read\n",
"x-amz-meta-checksumalgorithm:crc32\n",
"x-amz-meta-filechecksum:0x02661779\n",
"x-amz-meta-reviewedby:joe@example.com,jane@example.com\n",
"/static.example.com/db-backup.dat.gz",
)
);
assert_eq!(signature, "jtBQa0Aq+DkULFI8qrpwIjGEx0E=");
}
{
let method = &Method::GET;
let uri_path = "/";
let headers = OrderedHeaders::from_slice_unchecked(&[("date", "Wed, 28 Mar 2007 01:29:59 +0000")]);
let qs = None;
let vh_bucket = None;
let string_to_sign = create_string_to_sign(Mode::HeaderAuth, method, uri_path, qs, &headers, vh_bucket);
let signature = calculate_signature(&secret_key, &string_to_sign);
assert_eq!(
string_to_sign,
concat!(
"GET\n",
"\n",
"\n",
"Wed, 28 Mar 2007 01:29:59 +0000\n", "/",
)
);
assert_eq!(signature, "qGdzdERIC03wnaRNKh6OqZehG9s=");
}
{
let method = &Method::GET;
let uri_path = "/dictionary/fran%C3%A7ais/pr%c3%a9f%c3%a8re";
let headers = OrderedHeaders::from_slice_unchecked(&[("date", "Wed, 28 Mar 2007 01:49:49 +0000")]);
let qs = None;
let vh_bucket = None;
let string_to_sign = create_string_to_sign(Mode::HeaderAuth, method, uri_path, qs, &headers, vh_bucket);
let signature = calculate_signature(&secret_key, &string_to_sign);
assert_eq!(
string_to_sign,
concat!(
"GET\n",
"\n",
"\n",
"Wed, 28 Mar 2007 01:49:49 +0000\n",
"/dictionary/fran%C3%A7ais/pr%c3%a9f%c3%a8re",
)
);
assert_eq!(signature, "DNEZGsoieTZ92F3bUfSPQcbGmlM=");
}
{
let method = &Method::GET;
let uri_path = "/photos/puppy.jpg";
let headers = OrderedHeaders::default();
let qs = OrderedQs::parse(concat!(
"AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE",
"&Signature=1No4mq5ETf02z8aet9voy6gui6E%3D",
"&Expires=1175139620",
))
.unwrap();
let vh_bucket = Some("awsexamplebucket1");
let presigned_url = super::super::PresignedUrlV2::parse(&qs).unwrap();
assert_eq!(presigned_url.access_key, access_key);
let string_to_sign = create_string_to_sign(Mode::PresignedUrl, method, uri_path, Some(&qs), &headers, vh_bucket);
let signature = calculate_signature(&secret_key, &string_to_sign);
assert_eq!(
string_to_sign,
concat!(
"GET\n",
"\n",
"\n",
"1175139620\n", "/awsexamplebucket1/photos/puppy.jpg",
)
);
assert_eq!(signature, presigned_url.signature);
}
}
#[test]
fn regression_sig_v2_x_amz_date() {
let secret_key = SecretKey::from("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY");
let method = &Method::GET;
let uri_path = "/mybucket/myobject";
let headers = OrderedHeaders::from_slice_unchecked(&[
("date", "Thu, 14 Mar 2024 12:00:00 +0000"),
("x-amz-date", "Thu, 14 Mar 2024 12:00:00 +0000"),
]);
let qs = None;
let vh_bucket = None;
let string_to_sign = create_string_to_sign(Mode::HeaderAuth, method, uri_path, qs, &headers, vh_bucket);
assert_eq!(
string_to_sign,
concat!(
"GET\n",
"\n",
"\n",
"\n", "x-amz-date:Thu, 14 Mar 2024 12:00:00 +0000\n",
"/mybucket/myobject",
)
);
let sig = calculate_signature(&secret_key, &string_to_sign);
assert!(!sig.is_empty());
}
#[test]
fn regression_sig_v2_virtual_hosted_bucket() {
let secret_key = SecretKey::from("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY");
let method = &Method::GET;
let uri_path = "/myobject";
let headers = OrderedHeaders::from_slice_unchecked(&[("date", "Thu, 14 Mar 2024 12:00:00 +0000")]);
let qs = None;
let vh_bucket = Some("mybucket");
let string_to_sign = create_string_to_sign(Mode::HeaderAuth, method, uri_path, qs, &headers, vh_bucket);
assert_eq!(
string_to_sign,
concat!("GET\n", "\n", "\n", "Thu, 14 Mar 2024 12:00:00 +0000\n", "/mybucket/myobject",)
);
let sig = calculate_signature(&secret_key, &string_to_sign);
assert!(!sig.is_empty());
}
#[test]
fn regression_sig_v2_presigned_expires() {
let secret_key = SecretKey::from("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY");
let method = &Method::GET;
let uri_path = "/myobject";
let headers = OrderedHeaders::from_slice_unchecked(&[("date", "Thu, 14 Mar 2024 12:00:00 +0000")]);
let qs = OrderedQs::from_vec_unchecked(vec![("Expires".into(), "1710417600".into())]);
let vh_bucket = Some("mybucket");
let string_to_sign = create_string_to_sign(Mode::PresignedUrl, method, uri_path, Some(&qs), &headers, vh_bucket);
assert_eq!(
string_to_sign,
concat!(
"GET\n",
"\n",
"\n",
"1710417600\n", "/mybucket/myobject",
)
);
let sig = calculate_signature(&secret_key, &string_to_sign);
assert!(!sig.is_empty());
}
}