use crate::auth::S3Auth;
use crate::auth::SecretKey;
use crate::config::{S3Config, S3ConfigProvider};
use crate::error::*;
use crate::http;
use crate::http::{AwsChunkedStream, Body, Multipart, MultipartLimits};
use crate::http::{OrderedHeaders, OrderedQs};
use crate::post_policy::PostPolicy;
use crate::protocol::TrailingHeaders;
use crate::sig_v2;
use crate::sig_v2::{AuthorizationV2, PostSignatureV2, PresignedUrlV2};
use crate::sig_v4;
use crate::sig_v4::AmzContentSha256;
use crate::sig_v4::AmzDate;
use crate::sig_v4::UploadStream;
use crate::sig_v4::{AuthorizationV4, CredentialV4, PostSignatureV4, PresignedUrlV4};
use crate::stream::ByteStream as _;
use crate::utils::crypto::hex_sha256;
use crate::utils::is_base64_encoded;
use std::mem;
use std::ops::Not;
use std::sync::Arc;
use hyper::Method;
use hyper::Uri;
use mime::Mime;
use subtle::ConstantTimeEq;
use tracing::debug;
const MAX_STS_BODY_SIZE: usize = 8192;
fn extract_amz_content_sha256<'a>(hs: &'_ OrderedHeaders<'a>) -> S3Result<Option<AmzContentSha256<'a>>> {
let Some(val) = hs.get_unique(crate::header::X_AMZ_CONTENT_SHA256) else { return Ok(None) };
match AmzContentSha256::parse(val) {
Ok(x) => Ok(Some(x)),
Err(e) => {
Err(s3_error!(e, SignatureDoesNotMatch, "invalid header: x-amz-content-sha256"))
}
}
}
fn extract_authorization_v4<'a>(hs: &'_ OrderedHeaders<'a>) -> S3Result<Option<AuthorizationV4<'a>>> {
let Some(val) = hs.get_unique(crate::header::AUTHORIZATION) else { return Ok(None) };
match AuthorizationV4::parse(val) {
Ok(x) => Ok(Some(x)),
Err(e) => Err(invalid_request!(e, "invalid header: authorization")),
}
}
fn extract_amz_date(hs: &'_ OrderedHeaders<'_>) -> S3Result<Option<AmzDate>> {
let Some(val) = hs.get_unique(crate::header::X_AMZ_DATE) else { return Ok(None) };
match AmzDate::parse(val) {
Ok(x) => Ok(Some(x)),
Err(e) => Err(invalid_request!(e, "invalid header: x-amz-date")),
}
}
pub struct SignatureContext<'a> {
pub auth: Option<&'a dyn S3Auth>,
pub config: &'a Arc<dyn S3ConfigProvider>,
pub req_version: ::http::Version,
pub req_method: &'a Method,
pub req_uri: &'a Uri,
pub req_body: &'a mut Body,
pub qs: Option<&'a OrderedQs>,
pub hs: OrderedHeaders<'a>,
pub decoded_uri_path: &'a str,
pub raw_uri_path: &'a str,
pub vh_bucket: Option<&'a str>,
pub content_length: Option<u64>,
pub mime: Option<Mime>,
pub decoded_content_length: Option<usize>,
pub transformed_body: Option<Body>,
pub multipart: Option<Multipart>,
pub trailing_headers: Option<TrailingHeaders>,
}
#[derive(Debug)]
pub struct CredentialsExt {
pub access_key: String,
pub secret_key: SecretKey,
pub region: Option<String>,
pub service: Option<String>,
}
fn require_auth(auth: Option<&dyn S3Auth>) -> S3Result<&dyn S3Auth> {
auth.ok_or_else(|| s3_error!(NotImplemented, "This service has no authentication provider"))
}
fn has_unencoded_reserved_path_char(path: &str) -> bool {
path.bytes().any(|b| {
!matches!(
b,
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'/' | b'%'
)
})
}
struct SignatureVerificationContext<'a> {
expected_signature: &'a str,
raw_uri_path: &'a str,
secret_key: &'a SecretKey,
amz_date: &'a AmzDate,
region: &'a str,
service: &'a str,
}
fn sig_v4_signatures_match(actual_signature: &str, expected_signature: &str) -> bool {
actual_signature.as_bytes().ct_eq(expected_signature.as_bytes()).into()
}
fn validate_sig_v4_clock_skew(amz_date: &AmzDate, now: time::OffsetDateTime, config: &S3Config) -> S3Result<()> {
let request_time = amz_date.to_time().ok_or_else(|| invalid_request!("invalid amz date"))?;
let duration = now - request_time;
let max_skew_time = time::Duration::seconds(i64::from(config.presigned_url_max_skew_time_secs));
if duration.abs() > max_skew_time {
return Err(s3_error!(RequestTimeTooSkewed, "request time is too far from server time"));
}
Ok(())
}
impl SignatureVerificationContext<'_> {
fn verify_with_raw_path_fallback(
&self,
canonical_request: &str,
raw_canonical_request: impl FnOnce() -> String,
) -> S3Result<String> {
let string_to_sign = sig_v4::create_string_to_sign(canonical_request, self.amz_date, self.region, self.service);
let signature = sig_v4::calculate_signature(&string_to_sign, self.secret_key, self.amz_date, self.region, self.service);
if sig_v4_signatures_match(&signature, self.expected_signature) {
return Ok(signature);
}
if !has_unencoded_reserved_path_char(self.raw_uri_path) {
debug!(?signature, expected=?self.expected_signature, "signature mismatch");
return Err(s3_error!(SignatureDoesNotMatch));
}
let canonical_request = raw_canonical_request();
let string_to_sign = sig_v4::create_string_to_sign(&canonical_request, self.amz_date, self.region, self.service);
let raw_signature =
sig_v4::calculate_signature(&string_to_sign, self.secret_key, self.amz_date, self.region, self.service);
if !sig_v4_signatures_match(&raw_signature, self.expected_signature) {
debug!(?signature, ?raw_signature, expected=?self.expected_signature, "signature mismatch");
return Err(s3_error!(SignatureDoesNotMatch));
}
Ok(raw_signature)
}
}
impl SignatureContext<'_> {
pub async fn check(&mut self) -> S3Result<Option<CredentialsExt>> {
if self.req_method == Method::POST
&& let Some(ref mime) = self.mime
&& mime.type_() == mime::MULTIPART
&& mime.subtype() == mime::FORM_DATA
{
return self.check_post_signature().await;
}
if let Some(result) = self.v2_check().await {
debug!("checked signature v2");
return Ok(Some(result?));
}
if let Some(result) = self.v4_check().await {
debug!("checked signature v4");
return Ok(Some(result?));
}
Ok(None)
}
#[tracing::instrument(skip(self))]
async fn check_post_signature(&mut self) -> S3Result<Option<CredentialsExt>> {
let multipart = {
let Some(mime) = self.mime.as_ref() else {
return Err(invalid_request!("internal error: mime was unexpectedly None"));
};
let boundary = mime
.get_param(mime::BOUNDARY)
.ok_or_else(|| invalid_request!("missing boundary"))?;
let body = mem::take(self.req_body);
let config = self.config.snapshot();
let limits = MultipartLimits {
max_field_size: config.form_max_field_size,
max_fields_size: config.form_max_fields_size,
max_parts: config.form_max_parts,
};
http::transform_multipart(body, boundary.as_str().as_bytes(), limits)
.await
.map_err(|e| s3_error!(e, MalformedPOSTRequest))?
};
debug!(?multipart);
if multipart.find_field_value("x-amz-signature").is_some() {
debug!("checking post signature v4");
return Ok(Some(self.v4_check_post_signature(multipart).await?));
}
if multipart.find_field_value("signature").is_some() {
debug!("checking post signature v2");
return Ok(Some(self.v2_check_post_signature(multipart).await?));
}
self.multipart = Some(multipart);
Ok(None)
}
#[tracing::instrument(skip(self))]
pub async fn v4_check(&mut self) -> Option<S3Result<CredentialsExt>> {
if let Some(qs) = self.qs
&& qs.has("X-Amz-Signature")
{
debug!("checking presigned url");
return Some(self.v4_check_presigned_url().await);
}
if self.hs.get_unique(crate::header::AUTHORIZATION).is_some() {
debug!("checking header auth");
return Some(self.v4_check_header_auth().await);
}
None
}
pub async fn v4_check_post_signature(&mut self, multipart: Multipart) -> S3Result<CredentialsExt> {
let auth = require_auth(self.auth)?;
let info = PostSignatureV4::extract(&multipart).ok_or_else(|| invalid_request!("missing required multipart fields"))?;
if is_base64_encoded(info.policy.as_bytes()).not() {
return Err(invalid_request!("invalid field: policy"));
}
if info.x_amz_algorithm != "AWS4-HMAC-SHA256" {
return Err(s3_error!(
NotImplemented,
"x-amz-algorithm other than AWS4-HMAC-SHA256 is not implemented"
));
}
let credential =
CredentialV4::parse(info.x_amz_credential).map_err(|_| invalid_request!("invalid field: x-amz-credential"))?;
let amz_date = AmzDate::parse(info.x_amz_date).map_err(|_| invalid_request!("invalid field: x-amz-date"))?;
{
let policy = PostPolicy::from_base64(info.policy).map_err(|e| s3_error!(e, InvalidPolicyDocument))?;
let policy_date = policy.eq_condition_value("x-amz-date");
if policy_date != Some(info.x_amz_date) {
return Err(s3_error!(InvalidPolicyDocument, "x-amz-date does not match policy"));
}
let policy_credential = policy.eq_condition_value("x-amz-credential");
if policy_credential != Some(info.x_amz_credential) {
return Err(s3_error!(InvalidPolicyDocument, "x-amz-credential does not match policy"));
}
let policy_algo = policy.eq_condition_value("x-amz-algorithm");
if policy_algo != Some(info.x_amz_algorithm) {
return Err(s3_error!(InvalidPolicyDocument, "x-amz-algorithm does not match policy"));
}
}
if credential.date != amz_date.fmt_date().as_str() {
return Err(s3_error!(SignatureDoesNotMatch, "credential scope date does not match x-amz-date"));
}
validate_sig_v4_clock_skew(&amz_date, time::OffsetDateTime::now_utc(), &self.config.snapshot())?;
let access_key = credential.access_key_id.to_owned();
let secret_key = auth.get_secret_key(&access_key).await?;
let region = credential.aws_region;
let service = credential.aws_service;
if !matches!(service, "s3" | "sts") {
return Err(s3_error!(
NotImplemented,
"unknown service '{}' in credential scope; expected 's3' or 'sts'",
service,
));
}
let string_to_sign = info.policy;
let signature = sig_v4::calculate_signature(string_to_sign, &secret_key, &amz_date, region, service);
let expected_signature = info.x_amz_signature;
if !sig_v4_signatures_match(&signature, expected_signature) {
debug!(?signature, expected=?expected_signature, "signature mismatch");
return Err(s3_error!(SignatureDoesNotMatch));
}
let region = region.to_owned();
let service = service.to_owned();
self.multipart = Some(multipart);
Ok(CredentialsExt {
access_key,
secret_key,
region: Some(region),
service: Some(service),
})
}
pub async fn v4_check_presigned_url(&mut self) -> S3Result<CredentialsExt> {
let qs = self.qs.unwrap();
let presigned_url = PresignedUrlV4::parse(qs).map_err(|err| invalid_request!(err, "missing presigned url v4 fields"))?;
if presigned_url.algorithm != "AWS4-HMAC-SHA256" {
return Err(s3_error!(
NotImplemented,
"X-Amz-Algorithm other than AWS4-HMAC-SHA256 is not implemented"
));
}
if presigned_url.credential.date != presigned_url.amz_date.fmt_date().as_str() {
return Err(s3_error!(SignatureDoesNotMatch, "credential scope date does not match x-amz-date"));
}
let amz_content_sha256 = extract_amz_content_sha256(&self.hs)?;
if amz_content_sha256.is_some_and(|v| v.is_streaming()) {
return Err(s3_error!(NotImplemented, "streaming payload for presigned URLs is not implemented"));
}
{
let now = time::OffsetDateTime::now_utc();
let date = presigned_url
.amz_date
.to_time()
.ok_or_else(|| invalid_request!("invalid amz date"))?;
let duration = now - date;
let config = self.config.snapshot();
let max_skew_time = time::Duration::seconds(i64::from(config.presigned_url_max_skew_time_secs));
if duration.is_negative() && duration.abs() > max_skew_time {
return Err(s3_error!(RequestTimeTooSkewed, "request date is later than server time too much"));
}
if duration > presigned_url.expires {
return Err(s3_error!(AccessDenied, "Request has expired"));
}
}
let auth = require_auth(self.auth)?;
let access_key = presigned_url.credential.access_key_id;
let secret_key = auth.get_secret_key(access_key).await?;
let region = presigned_url.credential.aws_region;
let service = presigned_url.credential.aws_service;
if !matches!(service, "s3" | "sts") {
return Err(s3_error!(
NotImplemented,
"unknown service '{}' in credential scope; expected 's3' or 'sts'",
service,
));
}
let expected_signature = presigned_url.signature;
let headers = self.hs.find_multiple_with_on_missing(&presigned_url.signed_headers, |name| {
if name == "host"
&& matches!(self.req_version, ::http::Version::HTTP_2 | ::http::Version::HTTP_3)
&& let Some(authority) = self.req_uri.authority()
{
return Some(authority.as_str());
}
None
});
let method = &self.req_method;
let amz_date = &presigned_url.amz_date;
let verifier = SignatureVerificationContext {
expected_signature,
raw_uri_path: self.raw_uri_path,
secret_key: &secret_key,
amz_date,
region,
service,
};
let canonical_request = sig_v4::create_presigned_canonical_request(method, self.decoded_uri_path, qs.as_ref(), &headers);
verifier.verify_with_raw_path_fallback(&canonical_request, || {
sig_v4::create_presigned_canonical_request_with_raw_uri_path(method, self.raw_uri_path, qs.as_ref(), &headers)
})?;
if let Some(AmzContentSha256::SingleChunk(expected_checksum)) = amz_content_sha256 {
let length = if let Some(content_length) = self.content_length {
usize::try_from(content_length).map_err(|_| invalid_request!("content-length exceeds platform limits"))?
} else {
self.req_body
.remaining_length()
.exact()
.ok_or_else(|| s3_error!(MissingContentLength, "missing header: content-length"))?
};
let body = mem::take(self.req_body);
let stream = UploadStream::new(body, length, expected_checksum)
.map_err(|_| invalid_request!("invalid header: x-amz-content-sha256"))?;
*self.req_body = Body::from(stream.into_byte_stream());
}
Ok(CredentialsExt {
access_key: access_key.into(),
secret_key,
region: Some(region.into()),
service: Some(service.into()),
})
}
#[tracing::instrument(skip(self))]
#[allow(clippy::too_many_lines)]
pub async fn v4_check_header_auth(&mut self) -> S3Result<CredentialsExt> {
let authorization: AuthorizationV4<'_> = {
let mut a = extract_authorization_v4(&self.hs)?.unwrap();
a.signed_headers.sort_unstable();
a
};
let region = authorization.credential.aws_region;
let service = authorization.credential.aws_service;
if !matches!(service, "s3" | "sts") {
return Err(s3_error!(
NotImplemented,
"unknown service '{}' in credential scope; expected 's3' or 'sts'",
service,
));
}
let auth = require_auth(self.auth)?;
let amz_date = extract_amz_date(&self.hs)?.ok_or_else(|| invalid_request!("missing header: x-amz-date"))?;
if authorization.credential.date != amz_date.fmt_date().as_str() {
return Err(s3_error!(SignatureDoesNotMatch, "credential scope date does not match x-amz-date"));
}
validate_sig_v4_clock_skew(&amz_date, time::OffsetDateTime::now_utc(), &self.config.snapshot())?;
let amz_content_sha256 = extract_amz_content_sha256(&self.hs)?;
if service == "s3" && amz_content_sha256.is_none() {
return Err(invalid_request!("missing header: x-amz-content-sha256"));
}
let access_key = authorization.credential.access_key_id;
let secret_key = auth.get_secret_key(access_key).await?;
let is_stream = amz_content_sha256.is_some_and(|v| v.is_streaming());
let expected_signature = authorization.signature;
let method = &self.req_method;
let query_strings: &[(String, String)] = self.qs.as_ref().map_or(&[], AsRef::as_ref);
let headers = self.hs.find_multiple_with_on_missing(&authorization.signed_headers, |name| {
if name == "host"
&& self.req_version == ::http::Version::HTTP_2
&& let Some(authority) = self.req_uri.authority()
{
return Some(authority.as_str());
}
None
});
let sts_payload_hash;
let payload = match amz_content_sha256 {
Some(AmzContentSha256::StreamingAws4HmacSha256Payload) => sig_v4::Payload::MultipleChunks,
Some(AmzContentSha256::StreamingAws4HmacSha256PayloadTrailer) => sig_v4::Payload::MultipleChunksWithTrailer,
Some(AmzContentSha256::UnsignedPayload) => sig_v4::Payload::Unsigned,
Some(AmzContentSha256::StreamingUnsignedPayloadTrailer) => sig_v4::Payload::UnsignedMultipleChunksWithTrailer,
Some(AmzContentSha256::SingleChunk(payload_checksum)) => sig_v4::Payload::SingleChunk(payload_checksum),
Some(
AmzContentSha256::StreamingAws4EcdsaP256Sha256Payload
| AmzContentSha256::StreamingAws4EcdsaP256Sha256PayloadTrailer,
) => {
return Err(s3_error!(NotImplemented, "AWS4-ECDSA-P256-SHA256 signing method is not implemented yet"));
}
None => {
if service == "sts" {
let body_bytes = self
.req_body
.store_all_limited(MAX_STS_BODY_SIZE)
.await
.map_err(|e| invalid_request!("failed to read STS request body: {}", e))?;
sts_payload_hash = hex_sha256(&body_bytes, str::to_owned);
sig_v4::Payload::SingleChunk(&sts_payload_hash)
} else {
return Err(invalid_request!("missing header: x-amz-content-sha256"));
}
}
};
let verifier = SignatureVerificationContext {
expected_signature,
raw_uri_path: self.raw_uri_path,
secret_key: &secret_key,
amz_date: &amz_date,
region,
service,
};
let canonical_request = sig_v4::create_canonical_request(method, self.decoded_uri_path, query_strings, &headers, payload);
let signature = verifier.verify_with_raw_path_fallback(&canonical_request, || {
sig_v4::create_canonical_request_with_raw_uri_path(method, self.raw_uri_path, query_strings, &headers, payload)
})?;
if is_stream {
let has_trailer = amz_content_sha256.is_some_and(|v| v.has_trailer());
if has_trailer && self.hs.get_unique("x-amz-trailer").is_none() {
return Err(invalid_request!("missing header: x-amz-trailer"));
}
let decoded_content_length = self
.decoded_content_length
.ok_or_else(|| s3_error!(MissingContentLength, "missing header: x-amz-decoded-content-length"))?;
let unsigned = matches!(amz_content_sha256, Some(AmzContentSha256::StreamingUnsignedPayloadTrailer));
let stream = AwsChunkedStream::new(
mem::take(self.req_body),
signature.into(),
amz_date,
region.into(),
service.into(),
secret_key.clone(),
decoded_content_length,
unsigned,
);
debug!(len=?stream.exact_remaining_length(), "aws-chunked");
let trailers = stream.trailing_headers_handle();
self.transformed_body = Some(Body::from(stream.into_byte_stream()));
self.trailing_headers = Some(trailers);
} else if let Some(AmzContentSha256::SingleChunk(expected_checksum)) = amz_content_sha256 {
let length = if let Some(content_length) = self.content_length {
usize::try_from(content_length).map_err(|_| invalid_request!("content-length exceeds platform limits"))?
} else {
self.req_body
.remaining_length()
.exact()
.ok_or_else(|| s3_error!(MissingContentLength, "missing header: content-length"))?
};
let body = mem::take(self.req_body);
let stream = UploadStream::new(body, length, expected_checksum)
.map_err(|_| invalid_request!("invalid header: x-amz-content-sha256"))?;
*self.req_body = Body::from(stream.into_byte_stream());
} else if matches!(amz_content_sha256, Some(AmzContentSha256::UnsignedPayload)) {
if self.content_length.is_none() && self.req_body.remaining_length().exact().is_none() {
return Err(s3_error!(MissingContentLength, "missing header: content-length"));
}
}
Ok(CredentialsExt {
access_key: access_key.into(),
secret_key,
region: Some(region.into()),
service: Some(service.into()),
})
}
#[tracing::instrument(skip(self))]
pub async fn v2_check(&mut self) -> Option<S3Result<CredentialsExt>> {
if let Some(qs) = self.qs
&& qs.has("Signature")
{
debug!("checking presigned url");
return Some(self.v2_check_presigned_url().await);
}
if let Some(auth) = self.hs.get_unique(crate::header::AUTHORIZATION)
&& let Ok(auth) = AuthorizationV2::parse(auth)
{
debug!("checking header auth");
return Some(self.v2_check_header_auth(auth).await);
}
None
}
pub async fn v2_check_header_auth(&mut self, auth_v2: AuthorizationV2<'_>) -> S3Result<CredentialsExt> {
let method = &self.req_method;
let date = self.hs.get_unique("date").or_else(|| self.hs.get_unique("x-amz-date"));
if date.is_none() {
return Err(invalid_request!("missing date"));
}
let auth = require_auth(self.auth)?;
let access_key = auth_v2.access_key;
let secret_key = auth.get_secret_key(access_key).await?;
let string_to_sign = sig_v2::create_string_to_sign(
sig_v2::Mode::HeaderAuth,
method,
self.req_uri.path(),
self.qs,
&self.hs,
self.vh_bucket,
);
let signature = sig_v2::calculate_signature(&secret_key, &string_to_sign);
debug!(?string_to_sign, "sig_v2 header_auth");
let expected_signature = auth_v2.signature;
if signature != expected_signature {
debug!(?signature, expected=?expected_signature, "signature mismatch");
return Err(s3_error!(SignatureDoesNotMatch));
}
Ok(CredentialsExt {
access_key: access_key.into(),
secret_key,
region: None,
service: Some("s3".into()),
})
}
pub async fn v2_check_post_signature(&mut self, multipart: Multipart) -> S3Result<CredentialsExt> {
let auth = require_auth(self.auth)?;
let info = PostSignatureV2::extract(&multipart).ok_or_else(|| invalid_request!("missing required multipart fields"))?;
if is_base64_encoded(info.policy.as_bytes()).not() {
return Err(invalid_request!("invalid field: policy"));
}
let access_key = info.access_key_id.to_owned();
let secret_key = auth.get_secret_key(&access_key).await?;
let string_to_sign = info.policy;
let signature = sig_v2::calculate_signature(&secret_key, string_to_sign);
let expected_signature = info.signature;
if signature != expected_signature {
debug!(?signature, expected=?expected_signature, "signature mismatch");
return Err(s3_error!(SignatureDoesNotMatch));
}
self.multipart = Some(multipart);
Ok(CredentialsExt {
access_key,
secret_key,
region: None,
service: Some("s3".into()),
})
}
pub async fn v2_check_presigned_url(&mut self) -> S3Result<CredentialsExt> {
let qs = self.qs.unwrap(); let presigned_url = PresignedUrlV2::parse(qs).map_err(|err| invalid_request!(err, "missing presigned url v2 fields"))?;
if time::OffsetDateTime::now_utc() > presigned_url.expires_time {
return Err(s3_error!(AccessDenied, "Request has expired"));
}
let auth = require_auth(self.auth)?;
let access_key = presigned_url.access_key;
let secret_key = auth.get_secret_key(access_key).await?;
let string_to_sign = sig_v2::create_string_to_sign(
sig_v2::Mode::PresignedUrl,
self.req_method,
self.req_uri.path(),
self.qs,
&self.hs,
self.vh_bucket,
);
let signature = sig_v2::calculate_signature(&secret_key, &string_to_sign);
let expected_signature = presigned_url.signature;
if signature != expected_signature {
debug!(?signature, expected=?expected_signature, "signature mismatch");
return Err(s3_error!(SignatureDoesNotMatch));
}
Ok(CredentialsExt {
access_key: access_key.into(),
secret_key,
region: None,
service: Some("s3".into()),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fmt_current_amz_date(dt: time::OffsetDateTime) -> String {
format!(
"{:04}{:02}{:02}T{:02}{:02}{:02}Z",
dt.year(),
u8::from(dt.month()),
dt.day(),
dt.hour(),
dt.minute(),
dt.second()
)
}
#[test]
fn test_extract_amz_content_sha256_missing() {
let headers =
OrderedHeaders::from_slice_unchecked(&[("host", "example.s3.amazonaws.com"), ("x-amz-date", "20130524T000000Z")]);
let result = extract_amz_content_sha256(&headers).unwrap();
assert!(result.is_none());
}
#[test]
fn test_extract_amz_content_sha256_present() {
let headers = OrderedHeaders::from_slice_unchecked(&[
("host", "example.s3.amazonaws.com"),
("x-amz-content-sha256", "UNSIGNED-PAYLOAD"),
("x-amz-date", "20130524T000000Z"),
]);
let result = extract_amz_content_sha256(&headers).unwrap();
assert!(result.is_some());
assert!(matches!(result.unwrap(), AmzContentSha256::UnsignedPayload));
}
#[test]
fn test_extract_amz_content_sha256_invalid() {
let headers = OrderedHeaders::from_slice_unchecked(&[
("host", "example.s3.amazonaws.com"),
("x-amz-content-sha256", "INVALID-VALUE"),
("x-amz-date", "20130524T000000Z"),
]);
let result = extract_amz_content_sha256(&headers);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.message().unwrap().contains("x-amz-content-sha256"));
}
#[test]
fn raw_path_fallback_rejects_missing_or_mismatched_signatures() {
let secret_key: SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY".into();
let amz_date = AmzDate::parse("20130524T000000Z").unwrap();
let method = Method::GET;
let headers = OrderedHeaders::from_slice_unchecked(&[
("host", "s3.amazonaws.com"),
("x-amz-content-sha256", "UNSIGNED-PAYLOAD"),
("x-amz-date", "20130524T000000Z"),
]);
let canonical_request = sig_v4::create_canonical_request(
&method,
"/test-bucket/path",
&[] as &[(&str, &str)],
&headers,
sig_v4::Payload::Unsigned,
);
let verifier = SignatureVerificationContext {
expected_signature: "0000000000000000000000000000000000000000000000000000000000000000",
raw_uri_path: "/test-bucket/path",
secret_key: &secret_key,
amz_date: &amz_date,
region: "us-east-1",
service: "s3",
};
let err = verifier
.verify_with_raw_path_fallback(&canonical_request, || panic!("raw fallback should not be attempted"))
.expect_err("signature mismatch without raw reserved characters should be rejected");
assert_eq!(err.code(), &S3ErrorCode::SignatureDoesNotMatch);
let canonical_request = sig_v4::create_canonical_request(
&method,
"/test-bucket/path=",
&[] as &[(&str, &str)],
&headers,
sig_v4::Payload::Unsigned,
);
let verifier = SignatureVerificationContext {
expected_signature: "0000000000000000000000000000000000000000000000000000000000000000",
raw_uri_path: "/test-bucket/path=",
secret_key: &secret_key,
amz_date: &amz_date,
region: "us-east-1",
service: "s3",
};
let err = verifier
.verify_with_raw_path_fallback(&canonical_request, || {
sig_v4::create_canonical_request_with_raw_uri_path(
&method,
"/test-bucket/path=",
&[] as &[(&str, &str)],
&headers,
sig_v4::Payload::Unsigned,
)
})
.expect_err("raw fallback signature mismatch should be rejected");
assert_eq!(err.code(), &S3ErrorCode::SignatureDoesNotMatch);
}
#[tokio::test]
async fn post_signature_allows_anonymous() {
use crate::config::{S3ConfigProvider, StaticConfigProvider};
use std::sync::Arc;
let boundary = "boundary123";
let body = format!(
"\r\n--{boundary}\r\n\
Content-Disposition: form-data; name=\"key\"; filename=\"key\"\r\n\r\n\
foo.txt\r\n\
--{boundary}\r\n\
Content-Disposition: form-data; name=\"file\"; filename=\"file.txt\"\r\n\
Content-Type: text/plain\r\n\r\n\
file content\r\n\
--{boundary}--\r\n"
);
let mut body = Body::from(body);
let mime: Mime = format!("multipart/form-data; boundary={boundary}").parse().unwrap();
let config: Arc<dyn S3ConfigProvider> = Arc::new(StaticConfigProvider::default());
let method = Method::POST;
let uri = Uri::from_static("http://localhost/test-bucket");
let mut cx = SignatureContext {
auth: None,
config: &config,
req_version: ::http::Version::HTTP_11,
req_method: &method,
req_uri: &uri,
req_body: &mut body,
qs: None,
hs: OrderedHeaders::from_slice_unchecked(&[]),
decoded_uri_path: "/test-bucket",
raw_uri_path: "/test-bucket",
vh_bucket: None,
content_length: None,
mime: Some(mime),
decoded_content_length: None,
transformed_body: None,
multipart: None,
trailing_headers: None,
};
let credentials = cx.check().await.unwrap();
assert!(credentials.is_none(), "anonymous POST should not require credentials");
let multipart = cx.multipart.expect("multipart should be stored");
assert_eq!(multipart.find_field_value("key"), Some("foo.txt"));
assert_eq!(multipart.file.name, "file.txt");
}
#[tokio::test]
async fn test_sts_body_hash_computation() {
use crate::utils::crypto::hex_sha256;
let body_content = b"Action=AssumeRole&RoleArn=arn:aws:iam::123456789012:role/test-role&RoleSessionName=test-session";
let hash = hex_sha256(body_content, str::to_owned);
assert_eq!(hash.len(), 64);
assert!(hash.chars().all(|c| c.is_ascii_hexdigit()));
let hash2 = hex_sha256(body_content, str::to_owned);
assert_eq!(hash, hash2);
}
#[tokio::test]
async fn test_sts_body_size_limit_enforced() {
use bytes::Bytes;
let large_body = vec![b'x'; MAX_STS_BODY_SIZE + 1];
let mut body = Body::from(Bytes::from(large_body));
let result = body.store_all_limited(MAX_STS_BODY_SIZE).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_sts_body_within_limit() {
use bytes::Bytes;
let small_body = b"Action=AssumeRole&RoleArn=test&RoleSessionName=session";
let mut body = Body::from(Bytes::from(&small_body[..]));
let result = body.store_all_limited(MAX_STS_BODY_SIZE).await;
assert!(result.is_ok());
let bytes = result.unwrap();
assert_eq!(&bytes[..], &small_body[..]);
}
#[test]
fn test_sts_max_body_size_constant() {
assert_eq!(MAX_STS_BODY_SIZE, 8192);
}
#[tokio::test]
async fn v4_presigned_url_rejects_unknown_service() {
use crate::S3ErrorCode;
use crate::auth::SecretKey;
use crate::config::{S3ConfigProvider, StaticConfigProvider};
use std::sync::Arc;
let qs = OrderedQs::parse(concat!(
"X-Amz-Algorithm=AWS4-HMAC-SHA256",
"&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fcustom-svc%2Faws4_request",
"&X-Amz-Date=20130524T000000Z",
"&X-Amz-Expires=999999999",
"&X-Amz-SignedHeaders=host",
"&X-Amz-Signature=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
))
.unwrap();
let access_key = "AKIAIOSFODNN7EXAMPLE";
let secret_key: SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY".into();
let auth = crate::auth::SimpleAuth::from_single(access_key, secret_key);
let config: Arc<dyn S3ConfigProvider> = Arc::new(StaticConfigProvider::default());
let method = Method::GET;
let uri = Uri::from_static("https://s3.amazonaws.com/test.txt");
let mut body = Body::empty();
let mut cx = SignatureContext {
auth: Some(&auth),
config: &config,
req_version: ::http::Version::HTTP_11,
req_method: &method,
req_uri: &uri,
req_body: &mut body,
qs: Some(&qs),
hs: OrderedHeaders::from_slice_unchecked(&[]),
decoded_uri_path: "/test.txt",
raw_uri_path: "/test.txt",
vh_bucket: None,
content_length: None,
mime: None,
decoded_content_length: None,
transformed_body: None,
multipart: None,
trailing_headers: None,
};
let err = cx
.v4_check_presigned_url()
.await
.expect_err("unknown service must be rejected");
assert_eq!(err.code(), &S3ErrorCode::NotImplemented);
}
#[tokio::test]
async fn v4_presigned_url_accepts_standard_and_raw_uri_path_signatures() {
use crate::auth::SecretKey;
use crate::auth::SimpleAuth;
use crate::config::{S3ConfigProvider, StaticConfigProvider};
use std::sync::Arc;
let access_key = "AKIAIOSFODNN7EXAMPLE";
let secret_key: SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY".into();
let auth = SimpleAuth::from_single(access_key, secret_key.clone());
let config: Arc<dyn S3ConfigProvider> = Arc::new(StaticConfigProvider::default());
let method = Method::GET;
let uri = Uri::from_static("https://s3.amazonaws.com/test-bucket/path/sitemap.xmlage=");
let decoded_uri_path = "/test-bucket/path/sitemap.xmlage=";
let raw_uri_path = "/test-bucket/path/sitemap.xmlage=";
let amz_date = AmzDate::parse("20130524T000000Z").unwrap();
let headers_for_signing = OrderedHeaders::from_slice_unchecked(&[("host", "s3.amazonaws.com")]);
let query_strings_for_signing = &[
("X-Amz-Algorithm", "AWS4-HMAC-SHA256"),
("X-Amz-Credential", "AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request"),
("X-Amz-Date", "20130524T000000Z"),
("X-Amz-Expires", "999999999"),
("X-Amz-SignedHeaders", "host"),
];
let canonical_requests = [
sig_v4::create_presigned_canonical_request(
&method,
decoded_uri_path,
query_strings_for_signing,
&headers_for_signing,
),
sig_v4::create_presigned_canonical_request_with_raw_uri_path(
&method,
raw_uri_path,
query_strings_for_signing,
&headers_for_signing,
),
];
assert_ne!(canonical_requests[0], canonical_requests[1]);
for canonical_request in canonical_requests {
let string_to_sign = sig_v4::create_string_to_sign(&canonical_request, &amz_date, "us-east-1", "s3");
let signature = sig_v4::calculate_signature(&string_to_sign, &secret_key, &amz_date, "us-east-1", "s3");
let qs = OrderedQs::parse(&format!(
"{}&X-Amz-Signature={signature}",
concat!(
"X-Amz-Algorithm=AWS4-HMAC-SHA256",
"&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request",
"&X-Amz-Date=20130524T000000Z",
"&X-Amz-Expires=999999999",
"&X-Amz-SignedHeaders=host"
)
))
.unwrap();
let headers = OrderedHeaders::from_slice_unchecked(&[("host", "s3.amazonaws.com")]);
let mut body = Body::empty();
let mut cx = SignatureContext {
auth: Some(&auth),
config: &config,
req_version: ::http::Version::HTTP_11,
req_method: &method,
req_uri: &uri,
req_body: &mut body,
qs: Some(&qs),
hs: headers,
decoded_uri_path,
raw_uri_path,
vh_bucket: None,
content_length: None,
mime: None,
decoded_content_length: None,
transformed_body: None,
multipart: None,
trailing_headers: None,
};
let cred = cx
.v4_check_presigned_url()
.await
.expect("valid presigned URL with a raw '=' URI path should succeed");
assert_eq!(cred.access_key, access_key);
}
}
#[tokio::test]
async fn v4_presigned_url_uses_http2_authority_for_signed_host() {
use crate::auth::SecretKey;
use crate::auth::SimpleAuth;
use crate::config::{S3ConfigProvider, StaticConfigProvider};
use std::sync::Arc;
let access_key = "AKIAIOSFODNN7EXAMPLE";
let secret_key: SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY".into();
let auth = SimpleAuth::from_single(access_key, secret_key.clone());
let config: Arc<dyn S3ConfigProvider> = Arc::new(StaticConfigProvider::default());
let method = Method::GET;
let uri = Uri::from_static("https://s3.amazonaws.com/test-bucket/path/sitemap.xmlage=");
let decoded_uri_path = "/test-bucket/path/sitemap.xmlage=";
let raw_uri_path = "/test-bucket/path/sitemap.xmlage=";
let amz_date = AmzDate::parse("20130524T000000Z").unwrap();
let headers_for_signing = OrderedHeaders::from_slice_unchecked(&[("host", "s3.amazonaws.com")]);
let query_strings_for_signing = &[
("X-Amz-Algorithm", "AWS4-HMAC-SHA256"),
("X-Amz-Credential", "AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request"),
("X-Amz-Date", "20130524T000000Z"),
("X-Amz-Expires", "999999999"),
("X-Amz-SignedHeaders", "host"),
];
let canonical_request = sig_v4::create_presigned_canonical_request(
&method,
decoded_uri_path,
query_strings_for_signing,
&headers_for_signing,
);
let string_to_sign = sig_v4::create_string_to_sign(&canonical_request, &amz_date, "us-east-1", "s3");
let signature = sig_v4::calculate_signature(&string_to_sign, &secret_key, &amz_date, "us-east-1", "s3");
let qs = OrderedQs::parse(&format!(
"{}&X-Amz-Signature={signature}",
concat!(
"X-Amz-Algorithm=AWS4-HMAC-SHA256",
"&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request",
"&X-Amz-Date=20130524T000000Z",
"&X-Amz-Expires=999999999",
"&X-Amz-SignedHeaders=host"
)
))
.unwrap();
let mut body = Body::empty();
let mut cx = SignatureContext {
auth: Some(&auth),
config: &config,
req_version: ::http::Version::HTTP_2,
req_method: &method,
req_uri: &uri,
req_body: &mut body,
qs: Some(&qs),
hs: OrderedHeaders::from_slice_unchecked(&[]),
decoded_uri_path,
raw_uri_path,
vh_bucket: None,
content_length: None,
mime: None,
decoded_content_length: None,
transformed_body: None,
multipart: None,
trailing_headers: None,
};
let cred = cx
.v4_check_presigned_url()
.await
.expect("HTTP/2 authority should be used for a signed host header");
assert_eq!(cred.access_key, access_key);
}
#[tokio::test]
async fn v4_presigned_url_put_with_valid_content_sha256() {
use crate::auth::SecretKey;
use crate::auth::SimpleAuth;
use crate::config::{S3Config, S3ConfigProvider, StaticConfigProvider};
use bytes::Bytes;
use std::sync::Arc;
let access_key = "AKIAIOSFODNN7EXAMPLE";
let secret_key: SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY".into();
let auth = SimpleAuth::from_single(access_key, secret_key.clone());
let s3_config = S3Config {
presigned_url_max_skew_time_secs: u32::MAX,
..Default::default()
};
let config: Arc<dyn S3ConfigProvider> = Arc::new(StaticConfigProvider::new(Arc::new(s3_config)));
let body_data = b"hello world";
let content_sha256 = hex_sha256(body_data, str::to_owned);
let method = Method::PUT;
let uri = Uri::from_static("https://s3.amazonaws.com/test-bucket/test-key");
let amz_date = AmzDate::parse("20130524T000000Z").unwrap();
let headers_for_signing = OrderedHeaders::from_slice_unchecked(&[("host", "s3.amazonaws.com")]);
let query_strings_for_signing = &[
("X-Amz-Algorithm", "AWS4-HMAC-SHA256"),
("X-Amz-Credential", "AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request"),
("X-Amz-Date", "20130524T000000Z"),
("X-Amz-Expires", "999999999"),
("X-Amz-SignedHeaders", "host"),
];
let canonical_request = sig_v4::create_presigned_canonical_request(
&method,
"/test-bucket/test-key",
query_strings_for_signing,
&headers_for_signing,
);
let string_to_sign = sig_v4::create_string_to_sign(&canonical_request, &amz_date, "us-east-1", "s3");
let signature = sig_v4::calculate_signature(&string_to_sign, &secret_key, &amz_date, "us-east-1", "s3");
let qs = OrderedQs::parse(&format!(
"X-Amz-Algorithm=AWS4-HMAC-SHA256&\
X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&\
X-Amz-Date=20130524T000000Z&\
X-Amz-Expires=999999999&\
X-Amz-SignedHeaders=host&\
X-Amz-Signature={signature}"
))
.unwrap();
let headers = OrderedHeaders::from_slice_unchecked(&[
("host", "s3.amazonaws.com"),
("x-amz-content-sha256", content_sha256.as_str()),
]);
let mut body = Body::from(Bytes::from_static(body_data));
let mut cx = SignatureContext {
auth: Some(&auth),
config: &config,
req_version: ::http::Version::HTTP_11,
req_method: &method,
req_uri: &uri,
req_body: &mut body,
qs: Some(&qs),
hs: headers,
decoded_uri_path: "/test-bucket/test-key",
raw_uri_path: "/test-bucket/test-key",
vh_bucket: None,
content_length: Some(body_data.len() as u64),
mime: None,
decoded_content_length: None,
transformed_body: None,
multipart: None,
trailing_headers: None,
};
let cred = cx
.v4_check_presigned_url()
.await
.expect("PUT presigned URL with valid content-sha256 should succeed");
assert_eq!(cred.access_key, access_key);
let stored = cx
.req_body
.store_all_limited(100)
.await
.expect("body should be readable through UploadStream");
assert_eq!(stored, &body_data[..]);
}
#[tokio::test]
async fn v4_presigned_url_put_rejects_streaming_content_sha256() {
use crate::auth::SecretKey;
use crate::auth::SimpleAuth;
use crate::config::{S3Config, S3ConfigProvider, StaticConfigProvider};
use bytes::Bytes;
use std::sync::Arc;
let access_key = "AKIAIOSFODNN7EXAMPLE";
let secret_key: SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY".into();
let auth = SimpleAuth::from_single(access_key, secret_key.clone());
let s3_config = S3Config {
presigned_url_max_skew_time_secs: u32::MAX,
..Default::default()
};
let config: Arc<dyn S3ConfigProvider> = Arc::new(StaticConfigProvider::new(Arc::new(s3_config)));
let body_data = b"hello";
let method = Method::PUT;
let uri = Uri::from_static("https://s3.amazonaws.com/test-bucket/test-key");
let amz_date = AmzDate::parse("20130524T000000Z").unwrap();
let headers_for_signing = OrderedHeaders::from_slice_unchecked(&[("host", "s3.amazonaws.com")]);
let query_strings_for_signing = &[
("X-Amz-Algorithm", "AWS4-HMAC-SHA256"),
("X-Amz-Credential", "AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request"),
("X-Amz-Date", "20130524T000000Z"),
("X-Amz-Expires", "999999999"),
("X-Amz-SignedHeaders", "host"),
];
let canonical_request = sig_v4::create_presigned_canonical_request(
&method,
"/test-bucket/test-key",
query_strings_for_signing,
&headers_for_signing,
);
let string_to_sign = sig_v4::create_string_to_sign(&canonical_request, &amz_date, "us-east-1", "s3");
let signature = sig_v4::calculate_signature(&string_to_sign, &secret_key, &amz_date, "us-east-1", "s3");
let qs = OrderedQs::parse(&format!(
"X-Amz-Algorithm=AWS4-HMAC-SHA256&\
X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&\
X-Amz-Date=20130524T000000Z&\
X-Amz-Expires=999999999&\
X-Amz-SignedHeaders=host&\
X-Amz-Signature={signature}"
))
.unwrap();
let headers = OrderedHeaders::from_slice_unchecked(&[
("host", "s3.amazonaws.com"),
("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"),
]);
let mut body = Body::from(Bytes::from_static(body_data));
let mut cx = SignatureContext {
auth: Some(&auth),
config: &config,
req_version: ::http::Version::HTTP_11,
req_method: &method,
req_uri: &uri,
req_body: &mut body,
qs: Some(&qs),
hs: headers,
decoded_uri_path: "/test-bucket/test-key",
raw_uri_path: "/test-bucket/test-key",
vh_bucket: None,
content_length: Some(body_data.len() as u64),
mime: None,
decoded_content_length: None,
transformed_body: None,
multipart: None,
trailing_headers: None,
};
let err = cx
.v4_check_presigned_url()
.await
.expect_err("streaming content-sha256 should be rejected");
assert_eq!(err.code(), &S3ErrorCode::NotImplemented);
}
#[tokio::test]
async fn v4_header_auth_accepts_standard_and_raw_uri_path_signatures() {
use crate::auth::SecretKey;
use crate::auth::SimpleAuth;
use crate::config::{S3Config, S3ConfigProvider, StaticConfigProvider};
use std::sync::Arc;
let access_key = "AKIAIOSFODNN7EXAMPLE";
let secret_key: SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY".into();
let auth = SimpleAuth::from_single(access_key, secret_key.clone());
let s3_config = S3Config {
presigned_url_max_skew_time_secs: u32::MAX,
..Default::default()
};
let config: Arc<dyn S3ConfigProvider> = Arc::new(StaticConfigProvider::new(Arc::new(s3_config)));
let method = Method::GET;
let uri = Uri::from_static("https://s3.amazonaws.com/test-bucket/path/sitemap.xmlage=");
let decoded_uri_path = "/test-bucket/path/sitemap.xmlage=";
let raw_uri_path = "/test-bucket/path/sitemap.xmlage=";
let amz_date = AmzDate::parse("20130524T000000Z").unwrap();
let headers_for_signing = OrderedHeaders::from_slice_unchecked(&[
("host", "s3.amazonaws.com"),
("x-amz-content-sha256", "UNSIGNED-PAYLOAD"),
("x-amz-date", "20130524T000000Z"),
]);
let canonical_requests = [
sig_v4::create_canonical_request(
&method,
decoded_uri_path,
&[] as &[(&str, &str)],
&headers_for_signing,
sig_v4::Payload::Unsigned,
),
sig_v4::create_canonical_request_with_raw_uri_path(
&method,
raw_uri_path,
&[] as &[(&str, &str)],
&headers_for_signing,
sig_v4::Payload::Unsigned,
),
];
for canonical_request in canonical_requests {
let string_to_sign = sig_v4::create_string_to_sign(&canonical_request, &amz_date, "us-east-1", "s3");
let signature = sig_v4::calculate_signature(&string_to_sign, &secret_key, &amz_date, "us-east-1", "s3");
let authorization = format!(
"AWS4-HMAC-SHA256 Credential={access_key}/20130524/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature={signature}"
);
let headers = OrderedHeaders::from_slice_unchecked(&[
("authorization", authorization.as_str()),
("host", "s3.amazonaws.com"),
("x-amz-content-sha256", "UNSIGNED-PAYLOAD"),
("x-amz-date", "20130524T000000Z"),
]);
let mut body = Body::empty();
let mut cx = SignatureContext {
auth: Some(&auth),
config: &config,
req_version: ::http::Version::HTTP_11,
req_method: &method,
req_uri: &uri,
req_body: &mut body,
qs: None,
hs: headers,
decoded_uri_path,
raw_uri_path,
vh_bucket: None,
content_length: Some(0),
mime: None,
decoded_content_length: None,
transformed_body: None,
multipart: None,
trailing_headers: None,
};
let cred = cx
.v4_check_header_auth()
.await
.expect("valid SigV4 auth with a raw '=' URI path should succeed");
assert_eq!(cred.access_key, access_key);
}
}
#[tokio::test]
async fn v4_header_auth_uses_http2_authority_for_signed_host() {
use crate::auth::SecretKey;
use crate::auth::SimpleAuth;
use crate::config::{S3Config, S3ConfigProvider, StaticConfigProvider};
use std::sync::Arc;
let access_key = "AKIAIOSFODNN7EXAMPLE";
let secret_key: SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY".into();
let auth = SimpleAuth::from_single(access_key, secret_key.clone());
let s3_config = S3Config {
presigned_url_max_skew_time_secs: u32::MAX,
..Default::default()
};
let config: Arc<dyn S3ConfigProvider> = Arc::new(StaticConfigProvider::new(Arc::new(s3_config)));
let method = Method::GET;
let uri = Uri::from_static("https://s3.amazonaws.com/test-bucket/path/sitemap.xmlage=");
let decoded_uri_path = "/test-bucket/path/sitemap.xmlage=";
let raw_uri_path = "/test-bucket/path/sitemap.xmlage=";
let amz_date = AmzDate::parse("20130524T000000Z").unwrap();
let headers_for_signing = OrderedHeaders::from_slice_unchecked(&[
("host", "s3.amazonaws.com"),
("x-amz-content-sha256", "UNSIGNED-PAYLOAD"),
("x-amz-date", "20130524T000000Z"),
]);
let canonical_request = sig_v4::create_canonical_request(
&method,
decoded_uri_path,
&[] as &[(&str, &str)],
&headers_for_signing,
sig_v4::Payload::Unsigned,
);
let string_to_sign = sig_v4::create_string_to_sign(&canonical_request, &amz_date, "us-east-1", "s3");
let signature = sig_v4::calculate_signature(&string_to_sign, &secret_key, &amz_date, "us-east-1", "s3");
let authorization = format!(
"AWS4-HMAC-SHA256 Credential={access_key}/20130524/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature={signature}"
);
let headers = OrderedHeaders::from_slice_unchecked(&[
("authorization", authorization.as_str()),
("x-amz-content-sha256", "UNSIGNED-PAYLOAD"),
("x-amz-date", "20130524T000000Z"),
]);
let mut body = Body::empty();
let mut cx = SignatureContext {
auth: Some(&auth),
config: &config,
req_version: ::http::Version::HTTP_2,
req_method: &method,
req_uri: &uri,
req_body: &mut body,
qs: None,
hs: headers,
decoded_uri_path,
raw_uri_path,
vh_bucket: None,
content_length: Some(0),
mime: None,
decoded_content_length: None,
transformed_body: None,
multipart: None,
trailing_headers: None,
};
let cred = cx
.v4_check_header_auth()
.await
.expect("HTTP/2 authority should be used for a signed host header");
assert_eq!(cred.access_key, access_key);
}
#[tokio::test]
async fn v4_header_auth_raw_uri_path_signature_seeds_streaming_body() {
use crate::auth::SecretKey;
use crate::auth::SimpleAuth;
use crate::config::{S3Config, S3ConfigProvider, StaticConfigProvider};
use bytes::Bytes;
use std::sync::Arc;
let access_key = "AKIAIOSFODNN7EXAMPLE";
let secret_key: SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY".into();
let auth = SimpleAuth::from_single(access_key, secret_key.clone());
let s3_config = S3Config {
presigned_url_max_skew_time_secs: u32::MAX,
..Default::default()
};
let config: Arc<dyn S3ConfigProvider> = Arc::new(StaticConfigProvider::new(Arc::new(s3_config)));
let method = Method::PUT;
let uri = Uri::from_static("https://s3.amazonaws.com/test-bucket/path/sitemap.xmlage=");
let decoded_uri_path = "/test-bucket/path/sitemap.xmlage=";
let raw_uri_path = "/test-bucket/path/sitemap.xmlage=";
let amz_date = AmzDate::parse("20130524T000000Z").unwrap();
let chunk_data = Bytes::from_static(b"hello");
let decoded_content_length = chunk_data.len();
let headers_for_signing = OrderedHeaders::from_slice_unchecked(&[
("host", "s3.amazonaws.com"),
("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"),
("x-amz-date", "20130524T000000Z"),
("x-amz-decoded-content-length", "5"),
]);
let standard_canonical_request = sig_v4::create_canonical_request(
&method,
decoded_uri_path,
&[] as &[(&str, &str)],
&headers_for_signing,
sig_v4::Payload::MultipleChunks,
);
let raw_canonical_request = sig_v4::create_canonical_request_with_raw_uri_path(
&method,
raw_uri_path,
&[] as &[(&str, &str)],
&headers_for_signing,
sig_v4::Payload::MultipleChunks,
);
assert_ne!(standard_canonical_request, raw_canonical_request);
let seed_string_to_sign = sig_v4::create_string_to_sign(&raw_canonical_request, &amz_date, "us-east-1", "s3");
let seed_signature = sig_v4::calculate_signature(&seed_string_to_sign, &secret_key, &amz_date, "us-east-1", "s3");
let chunk_string_to_sign =
sig_v4::create_chunk_string_to_sign(&amz_date, "us-east-1", "s3", &seed_signature, std::slice::from_ref(&chunk_data));
let chunk_signature = sig_v4::calculate_signature(&chunk_string_to_sign, &secret_key, &amz_date, "us-east-1", "s3");
let final_string_to_sign = sig_v4::create_chunk_string_to_sign(&amz_date, "us-east-1", "s3", &chunk_signature, &[]);
let final_signature = sig_v4::calculate_signature(&final_string_to_sign, &secret_key, &amz_date, "us-east-1", "s3");
let mut streaming_body = Vec::new();
streaming_body.extend_from_slice(format!("{:x};chunk-signature={chunk_signature}\r\n", chunk_data.len()).as_bytes());
streaming_body.extend_from_slice(&chunk_data);
streaming_body.extend_from_slice(b"\r\n");
streaming_body.extend_from_slice(format!("0;chunk-signature={final_signature}\r\n\r\n").as_bytes());
let content_length = u64::try_from(streaming_body.len()).unwrap();
let authorization = format!(
"AWS4-HMAC-SHA256 Credential={access_key}/20130524/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length, Signature={seed_signature}"
);
let headers = OrderedHeaders::from_slice_unchecked(&[
("authorization", authorization.as_str()),
("host", "s3.amazonaws.com"),
("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"),
("x-amz-date", "20130524T000000Z"),
("x-amz-decoded-content-length", "5"),
]);
let mut body = Body::from(Bytes::from(streaming_body));
let mut cx = SignatureContext {
auth: Some(&auth),
config: &config,
req_version: ::http::Version::HTTP_11,
req_method: &method,
req_uri: &uri,
req_body: &mut body,
qs: None,
hs: headers,
decoded_uri_path,
raw_uri_path,
vh_bucket: None,
content_length: Some(content_length),
mime: None,
decoded_content_length: Some(decoded_content_length),
transformed_body: None,
multipart: None,
trailing_headers: None,
};
let cred = cx
.v4_check_header_auth()
.await
.expect("valid streaming SigV4 auth with a raw '=' URI path should succeed");
assert_eq!(cred.access_key, access_key);
let mut transformed_body = cx.transformed_body.take().expect("streaming body should be transformed");
let decoded_body = transformed_body
.store_all_limited(decoded_content_length)
.await
.expect("raw-path seed signature should validate aws-chunked body");
assert_eq!(decoded_body, chunk_data);
}
#[tokio::test]
async fn v2_header_auth_returns_no_region() {
use crate::auth::SecretKey;
use crate::config::{S3ConfigProvider, StaticConfigProvider};
use std::sync::Arc;
let access_key = "AKIAIOSFODNN7EXAMPLE";
let secret_key: SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY".into();
let auth = crate::auth::SimpleAuth::from_single(access_key, secret_key.clone());
let config: Arc<dyn S3ConfigProvider> = Arc::new(StaticConfigProvider::default());
let date = "Fri, 24 Jan 2030 12:00:00 +0000";
let hs = OrderedHeaders::from_slice_unchecked(&[("date", date), ("host", "s3.amazonaws.com")]);
let method = Method::GET;
let uri = Uri::from_static("https://s3.amazonaws.com/test-bucket/test-key");
let mut body = Body::empty();
let string_to_sign = crate::sig_v2::create_string_to_sign(
crate::sig_v2::Mode::HeaderAuth,
&method,
"/test-bucket/test-key",
None,
&hs,
None,
);
let signature = crate::sig_v2::calculate_signature(&secret_key, &string_to_sign);
let auth_v2 = AuthorizationV2 {
access_key,
signature: &signature,
};
let mut cx = SignatureContext {
auth: Some(&auth),
config: &config,
req_version: ::http::Version::HTTP_11,
req_method: &method,
req_uri: &uri,
req_body: &mut body,
qs: None,
hs,
decoded_uri_path: "/test-bucket/test-key",
raw_uri_path: "/test-bucket/test-key",
vh_bucket: None,
content_length: None,
mime: None,
decoded_content_length: None,
transformed_body: None,
multipart: None,
trailing_headers: None,
};
let cred = cx
.v2_check_header_auth(auth_v2)
.await
.expect("valid SigV2 auth should succeed");
assert_eq!(cred.region, None, "SigV2 carries no region");
assert_eq!(cred.service.as_deref(), Some("s3"), "SigV2 service is always 's3'");
}
#[test]
fn sig_v4_signatures_match_reports_match_and_mismatch() {
assert!(sig_v4_signatures_match("abcd", "abcd"));
assert!(!sig_v4_signatures_match("abcd", "abce"));
assert!(!sig_v4_signatures_match("abcd", "abc"));
}
#[tokio::test]
async fn v4_header_auth_rejects_stale_request_time() {
use crate::S3ErrorCode;
use crate::auth::SecretKey;
use crate::auth::SimpleAuth;
use crate::config::{S3ConfigProvider, StaticConfigProvider};
use std::sync::Arc;
let access_key = "AKIAIOSFODNN7EXAMPLE";
let secret_key: SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY".into();
let auth = SimpleAuth::from_single(access_key, secret_key.clone());
let config: Arc<dyn S3ConfigProvider> = Arc::new(StaticConfigProvider::default());
let skew = time::Duration::seconds(i64::from(config.snapshot().presigned_url_max_skew_time_secs));
let request_time = time::OffsetDateTime::now_utc() - skew - time::Duration::minutes(1);
let amz_date_str = fmt_current_amz_date(request_time);
let amz_date = AmzDate::parse(&amz_date_str).unwrap();
let method = Method::GET;
let uri = Uri::from_static("https://s3.amazonaws.com/test.txt");
let headers_for_signing = OrderedHeaders::from_slice_unchecked(&[
("host", "s3.amazonaws.com"),
("x-amz-content-sha256", "UNSIGNED-PAYLOAD"),
("x-amz-date", amz_date_str.as_str()),
]);
let canonical_request = sig_v4::create_canonical_request(
&method,
"/test.txt",
&[] as &[(&str, &str)],
&headers_for_signing,
sig_v4::Payload::Unsigned,
);
let string_to_sign = sig_v4::create_string_to_sign(&canonical_request, &amz_date, "us-east-1", "s3");
let signature = sig_v4::calculate_signature(&string_to_sign, &secret_key, &amz_date, "us-east-1", "s3");
let authorization = format!(
"AWS4-HMAC-SHA256 Credential={access_key}/{}/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature={signature}",
amz_date.fmt_date()
);
let headers = OrderedHeaders::from_slice_unchecked(&[
("authorization", authorization.as_str()),
("host", "s3.amazonaws.com"),
("x-amz-content-sha256", "UNSIGNED-PAYLOAD"),
("x-amz-date", amz_date_str.as_str()),
]);
let mut body = Body::empty();
let mut cx = SignatureContext {
auth: Some(&auth),
config: &config,
req_version: ::http::Version::HTTP_11,
req_method: &method,
req_uri: &uri,
req_body: &mut body,
qs: None,
hs: headers,
decoded_uri_path: "/test.txt",
raw_uri_path: "/test.txt",
vh_bucket: None,
content_length: Some(0),
mime: None,
decoded_content_length: None,
transformed_body: None,
multipart: None,
trailing_headers: None,
};
let err = cx
.v4_check_header_auth()
.await
.expect_err("stale signed header request should be rejected");
assert_eq!(err.code(), &S3ErrorCode::RequestTimeTooSkewed);
}
#[tokio::test]
async fn v4_post_signature_rejects_stale_request_time() {
use crate::S3ErrorCode;
use crate::auth::SecretKey;
use crate::auth::SimpleAuth;
use crate::config::{S3ConfigProvider, StaticConfigProvider};
use std::sync::Arc;
let access_key = "AKIAIOSFODNN7EXAMPLE";
let secret_key: SecretKey = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY".into();
let auth = SimpleAuth::from_single(access_key, secret_key.clone());
let config: Arc<dyn S3ConfigProvider> = Arc::new(StaticConfigProvider::default());
let skew = time::Duration::seconds(i64::from(config.snapshot().presigned_url_max_skew_time_secs));
let request_time = time::OffsetDateTime::now_utc() - skew - time::Duration::minutes(1);
let amz_date_str = fmt_current_amz_date(request_time);
let amz_date = AmzDate::parse(&amz_date_str).unwrap();
let policy_json = format!(
r#"{{"expiration":"2099-01-01T00:00:00Z","conditions":[{{"x-amz-date":"{amz_date}"}},{{"x-amz-credential":"{access_key}/{date}/us-east-1/s3/aws4_request"}},{{"x-amz-algorithm":"AWS4-HMAC-SHA256"}}]}}"#,
amz_date = amz_date_str,
access_key = access_key,
date = amz_date.fmt_date(),
);
let policy_b64 = base64_simd::STANDARD.encode_to_string(&policy_json);
let signature = sig_v4::calculate_signature(&policy_b64, &secret_key, &amz_date, "us-east-1", "s3");
let boundary = "boundary123";
let body = format!(
concat!(
"\r\n--{boundary}\r\n",
"Content-Disposition: form-data; name=\"x-amz-signature\"\r\n\r\n",
"{signature}\r\n",
"--{boundary}\r\n",
"Content-Disposition: form-data; name=\"policy\"\r\n\r\n",
"{policy_b64}\r\n",
"--{boundary}\r\n",
"Content-Disposition: form-data; name=\"x-amz-algorithm\"\r\n\r\n",
"AWS4-HMAC-SHA256\r\n",
"--{boundary}\r\n",
"Content-Disposition: form-data; name=\"x-amz-credential\"\r\n\r\n",
"{access_key}/{date}/us-east-1/s3/aws4_request\r\n",
"--{boundary}\r\n",
"Content-Disposition: form-data; name=\"x-amz-date\"\r\n\r\n",
"{amz_date}\r\n",
"--{boundary}\r\n",
"Content-Disposition: form-data; name=\"file\"; filename=\"a.txt\"\r\n",
"Content-Type: text/plain\r\n\r\n",
"hello\r\n",
"--{boundary}--\r\n"
),
access_key = access_key,
amz_date = amz_date_str,
boundary = boundary,
date = amz_date.fmt_date(),
policy_b64 = policy_b64,
signature = signature,
);
let mime: Mime = format!("multipart/form-data; boundary={boundary}").parse().unwrap();
let method = Method::POST;
let uri = Uri::from_static("http://localhost/test-bucket");
let mut body = Body::from(body);
let mut cx = SignatureContext {
auth: Some(&auth),
config: &config,
req_version: ::http::Version::HTTP_11,
req_method: &method,
req_uri: &uri,
req_body: &mut body,
qs: None,
hs: OrderedHeaders::from_slice_unchecked(&[]),
decoded_uri_path: "/test-bucket",
raw_uri_path: "/test-bucket",
vh_bucket: None,
content_length: None,
mime: Some(mime),
decoded_content_length: None,
transformed_body: None,
multipart: None,
trailing_headers: None,
};
let err = cx
.check_post_signature()
.await
.expect_err("stale signed POST policy should be rejected");
assert_eq!(err.code(), &S3ErrorCode::RequestTimeTooSkewed);
}
}