use crate::{
aws_math::get_sha256,
s3::{self, S3DateTime, S3HeadersBuilder, ValidS3Url},
};
use hmac::{Hmac, Mac};
use sha2::Sha256;
use time::Date;
fn aws_iot_sign(url: &ValidS3Url) -> String {
let dt = Date::from_calendar_date(2021, time::Month::August, 11)
.unwrap()
.with_hms(0, 15, 58)
.unwrap()
.assume_utc();
let headers: Vec<(&'static str, String)> = vec![
("Host", "iam.amazonaws.com".to_owned()),
(
"Content-Type",
"application/x-www-form-urlencoded; charset=utf-8".to_owned(),
),
("X-Amz-Date", "20210811T001558Z".to_owned()),
];
let options = S3HeadersBuilder::new(url)
.set_access_key("AKIAIOSFODNN7EXAMPLE")
.set_secret_key("wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY")
.set_region("us-east-1")
.set_service("iam")
.set_datetime(S3DateTime::UnixTimestamp(dt.unix_timestamp()))
.set_method("GET")
.set_headers(&headers)
.set_payload_hash("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
s3::get_authorization_header(options)
}
fn extract_signature(auth_header: &str) -> &str {
auth_header.rsplit("Signature=").next().unwrap()
}
#[test]
fn get_headers_uses_consistent_datetime_issue_2() {
let url = ValidS3Url::parse("https://examplebucket.s3.amazonaws.com/test.txt").unwrap();
let options = S3HeadersBuilder::new(&url)
.set_access_key("AKIAIOSFODNN7EXAMPLE")
.set_secret_key("wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY")
.set_region("us-east-1")
.set_service("s3")
.set_datetime(S3DateTime::Now)
.set_method("GET")
.set_payload_hash(crate::s3::EMPTY_PAYLOAD_SHA);
let headers = s3::get_headers(options);
let amz_date = headers
.iter()
.find(|(k, _)| *k == "x-amz-date")
.map(|(_, v)| v.as_str())
.expect("x-amz-date header must be present");
let auth = headers
.iter()
.find(|(k, _)| *k == "Authorization")
.map(|(_, v)| v.as_str())
.expect("Authorization header must be present");
let scope_date = auth
.split('/')
.nth(1)
.expect("credential scope must contain a date segment");
assert_eq!(
&amz_date[..8],
scope_date,
"x-amz-date date ({}) must match credential scope date ({}) — issue #2",
&amz_date[..8],
scope_date,
);
}
#[test]
fn resolved_datetime_returns_held_value() {
let dt = time::OffsetDateTime::from_unix_timestamp(1_234_567_890).unwrap();
assert_eq!(S3DateTime::Resolved(dt).get_offset_datetime(), dt);
}
#[test]
fn aws_iot_kat_iam_listusers_nominal() {
let url = ValidS3Url::parse("https://iam.amazonaws.com/?Action=ListUsers&Version=2010-05-08")
.unwrap();
assert_eq!(
extract_signature(&aws_iot_sign(&url)),
"20fdb62349e7104f9ce4184a444fedfbd19e40a5e31d57d433689c5a5138fa99"
);
}
#[test]
#[ignore = "diverges: the IoT SDK double-encodes '=' in query values; our crate follows the SigV4 spec (single-encode to %3D). Investigate if needed."]
fn aws_iot_kat_query_value_has_equals() {
let url = ValidS3Url::parse("https://iam.amazonaws.com/?quantum==value").unwrap();
assert_eq!(
extract_signature(&aws_iot_sign(&url)),
"2e005dbe8d1223309467fc3f3b14310110bd45358a4f598e9f5e32723036461d"
);
}
#[test]
fn aws_iot_kat_query_no_param_value() {
let url = ValidS3Url::parse("https://iam.amazonaws.com/?param=¶m2=").unwrap();
assert_eq!(
extract_signature(&aws_iot_sign(&url)),
"9eed8862e36ac9861f0ea0be863ef6d825de854c8eb9da072637dcc64e5ef919"
);
}
#[test]
fn test_get_object() {
let url = ValidS3Url::parse("https://jsonlog.s3.amazonaws.com/test.json").unwrap();
let options = S3HeadersBuilder::new(&url)
.set_access_key("some_access_key")
.set_secret_key("some_secret_key")
.set_region("some_place")
.set_datetime(S3DateTime::UnixTimestamp(0))
.set_method("GET")
.set_service("s3");
let result = s3::get_headers(options);
assert_eq!(result, vec![
("Host", "jsonlog.s3.amazonaws.com".to_owned()),
("x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_owned()),
("x-amz-date", "19700101T000000Z".to_owned()),
(
"Authorization",
"AWS4-HMAC-SHA256 Credential=some_access_key/19700101/some_place/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=521595a9eeee7092d3b2cc49d4db7cb828a5db5c7ad5136c149db0b0e7277f83".to_owned()
)
])
}
#[test]
fn test_put_object() {
let url = ValidS3Url::parse("https://examplebucket.s3.amazonaws.com/test$file.text").unwrap();
let headers = &[("x-amz-storage-class", "REDUCED_REDUNDANCY".to_owned())];
let content = b"".as_slice();
let sha = get_sha256(content);
let options = S3HeadersBuilder::new(&url)
.set_access_key("some_access_key")
.set_secret_key("some_secret_key")
.set_region("some_place")
.set_datetime(S3DateTime::UnixTimestamp(1369324800)) .set_headers(headers)
.set_method("PUT")
.set_service("s3")
.set_payload_hash(&sha);
let result = s3::get_headers(options);
assert_eq!(result, vec![
("x-amz-storage-class", "REDUCED_REDUNDANCY".to_owned()),
("Host", "examplebucket.s3.amazonaws.com".to_owned()),
("x-amz-content-sha256", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855".to_owned()),
("x-amz-date", "20130523T160000Z".to_owned()),
(
"Authorization",
"AWS4-HMAC-SHA256 Credential=some_access_key/20130523/some_place/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-storage-class,Signature=7e2911c8225f7591609bcbdc2faf8c443a898d8c83fc35b6a23f0b0e8084da60".to_owned()
)
])
}
fn fixture_datetime() -> time::OffsetDateTime {
Date::from_calendar_date(2013, 5.try_into().unwrap(), 24)
.unwrap()
.with_hms(0, 0, 0)
.unwrap()
.assume_utc()
}
const FIXTURE_CANONICAL_QUERY: &str = "X-Amz-Algorithm=AWS4-HMAC-SHA256\
&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request\
&X-Amz-Date=20130524T000000Z\
&X-Amz-Expires=86400\
&X-Amz-SignedHeaders=host";
#[test]
fn test_presign_get_signature_matches_kat_primitives() {
let datetime = fixture_datetime();
let expected_canonical_request = format!(
"GET\n/test.txt\n{}\nhost:examplebucket.s3.amazonaws.com\n\nhost\nUNSIGNED-PAYLOAD",
FIXTURE_CANONICAL_QUERY
);
let string_to_sign = crate::aws_format::string_to_sign(
&datetime,
"us-east-1",
"s3",
&expected_canonical_request,
);
let signing_key = crate::aws_math::get_signature_key(
&datetime,
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"us-east-1",
"s3",
);
let mut hmac = Hmac::<Sha256>::new_from_slice(&signing_key).unwrap();
hmac.update(string_to_sign.as_bytes());
let expected_signature = hex::encode(hmac.finalize().into_bytes());
let url = ValidS3Url::parse("https://examplebucket.s3.amazonaws.com/test.txt").unwrap();
let presigned = s3::presign_get(
&url,
"AKIAIOSFODNN7EXAMPLE",
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"us-east-1",
"s3",
S3DateTime::UnixTimestamp(datetime.unix_timestamp()),
86400,
);
assert_eq!(
tail_signature(&presigned),
expected_signature,
"presigned url: {presigned}"
);
}
#[test]
fn test_presign_get_url_shape() {
let datetime = fixture_datetime();
let url = ValidS3Url::parse("https://examplebucket.s3.amazonaws.com/test.txt").unwrap();
let presigned = s3::presign_get(
&url,
"AKIAIOSFODNN7EXAMPLE",
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"us-east-1",
"s3",
S3DateTime::UnixTimestamp(datetime.unix_timestamp()),
86400,
);
assert!(
presigned.starts_with("https://examplebucket.s3.amazonaws.com/test.txt?"),
"presigned url: {presigned}"
);
assert!(presigned.contains("X-Amz-Algorithm=AWS4-HMAC-SHA256"));
assert!(presigned.contains("X-Amz-Date=20130524T000000Z"));
assert!(presigned.contains("X-Amz-Expires=86400"));
assert!(presigned.contains("X-Amz-SignedHeaders=host"));
let sig = tail_signature(&presigned);
assert_eq!(sig.len(), 64);
assert!(sig.bytes().all(|b| b.is_ascii_hexdigit()));
}
#[test]
fn test_presign_get_is_deterministic() {
let datetime = fixture_datetime();
let url = ValidS3Url::parse("https://examplebucket.s3.amazonaws.com/test.txt").unwrap();
let a = s3::presign_get(
&url,
"AKIAIOSFODNN7EXAMPLE",
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"us-east-1",
"s3",
S3DateTime::UnixTimestamp(datetime.unix_timestamp()),
3600,
);
let b = s3::presign_get(
&url,
"AKIAIOSFODNN7EXAMPLE",
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"us-east-1",
"s3",
S3DateTime::UnixTimestamp(datetime.unix_timestamp()),
3600,
);
assert_eq!(a, b);
}
#[test]
fn test_presign_get_expiry_changes_signature() {
let datetime = fixture_datetime();
let url = ValidS3Url::parse("https://examplebucket.s3.amazonaws.com/test.txt").unwrap();
let ts = S3DateTime::UnixTimestamp(datetime.unix_timestamp());
let short = s3::presign_get(
&url,
"AKIAIOSFODNN7EXAMPLE",
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"us-east-1",
"s3",
ts,
3600,
);
let long = s3::presign_get(
&url,
"AKIAIOSFODNN7EXAMPLE",
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"us-east-1",
"s3",
ts,
7200,
);
assert_ne!(tail_signature(&short), tail_signature(&long));
}
fn tail_signature(presigned_url: &str) -> String {
let marker = "&X-Amz-Signature=";
let idx = presigned_url
.find(marker)
.expect("presigned url must carry X-Amz-Signature");
presigned_url[idx + marker.len()..].to_owned()
}
#[test]
fn test_presign_get_with_key_matches_presign_get() {
let datetime = fixture_datetime();
let url = ValidS3Url::parse("https://examplebucket.s3.amazonaws.com/test.txt").unwrap();
let ts = S3DateTime::UnixTimestamp(datetime.unix_timestamp());
let direct = s3::presign_get(
&url,
"AKIAIOSFODNN7EXAMPLE",
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"us-east-1",
"s3",
ts,
3600,
);
let signing_key = crate::aws_math::get_signature_key(
&datetime,
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"us-east-1",
"s3",
);
let with_key = s3::presign_get_with_key(
&url,
"AKIAIOSFODNN7EXAMPLE",
"us-east-1",
"s3",
datetime,
3600,
&signing_key,
);
assert_eq!(direct, with_key);
assert_eq!(tail_signature(&direct), tail_signature(&with_key));
}