mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-19 01:54:50 +08:00
perf(rustfs): skip redundant content-sha256 pass on signed PUTs (#7928)
A SigV4 header-signed or presigned PUT whose x-amz-content-sha256 header carries a real digest is already wrapped in a hash-checking UploadStream by the s3s auth layer, so the storage-side HashReader was hashing the same payload a second time. Hand the digest to the storage layer only when it arrives in a presigned query string alone, the one case s3s does not read.
This commit is contained in:
@@ -1043,9 +1043,25 @@ pub fn parse_copy_source_range(range_str: &str) -> S3Result<HTTPRangeSpec> {
|
||||
Err(s3_error!(InvalidArgument, "Invalid range format"))
|
||||
}
|
||||
}
|
||||
/// Returns the `x-amz-content-sha256` digest the storage layer still has to
|
||||
/// verify itself, or `None` when the payload hash has already been enforced.
|
||||
///
|
||||
/// The s3s auth layer wraps a single-chunk body in a hash-checking
|
||||
/// `UploadStream` whenever the `x-amz-content-sha256` *header* carries a real
|
||||
/// digest, for both header-signed and presigned SigV4 requests, and rejects a
|
||||
/// malformed header outright. Every request that reaches a handler with that
|
||||
/// header has therefore already had its body verified (or is about to fail in
|
||||
/// the body stream with the same `BadDigest` mapping), so hashing it again in
|
||||
/// `HashReader` would cost a second full SHA-256 pass per PUT for no extra
|
||||
/// protection. Streaming payloads are verified chunk by chunk by s3s and never
|
||||
/// produced a digest here.
|
||||
///
|
||||
/// s3s reads the header only. The one case it does not cover is a presigned
|
||||
/// request whose digest arrives in the query string alone, which is why that
|
||||
/// path still hands the digest to the storage layer.
|
||||
pub(crate) fn get_content_sha256_with_query(headers: &HeaderMap<HeaderValue>, query: Option<&str>) -> Option<String> {
|
||||
match get_request_auth_type_with_query(headers, query) {
|
||||
AuthType::Presigned | AuthType::Signed => {
|
||||
AuthType::Presigned if !headers.contains_key(AMZ_CONTENT_SHA256) => {
|
||||
if skip_content_sha256_cksum_with_query(headers, query) {
|
||||
None
|
||||
} else {
|
||||
@@ -2747,4 +2763,73 @@ mod tests {
|
||||
"disguised key must be namespaced back under x-amz-meta-: {metadata:?}"
|
||||
);
|
||||
}
|
||||
|
||||
const PAYLOAD_SHA256: &str = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
|
||||
const PRESIGNED_QUERY: &str = "X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ak%2F20260916%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=deadbeef";
|
||||
|
||||
fn signed_v4_headers(content_sha256: &'static str) -> HeaderMap<HeaderValue> {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"authorization",
|
||||
HeaderValue::from_static(
|
||||
"AWS4-HMAC-SHA256 Credential=ak/20260916/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=deadbeef",
|
||||
),
|
||||
);
|
||||
headers.insert("x-amz-content-sha256", HeaderValue::from_static(content_sha256));
|
||||
headers
|
||||
}
|
||||
|
||||
/// The s3s auth layer already wraps a header-signed single-chunk body in a
|
||||
/// hash-checking stream, so the storage layer must not schedule a second
|
||||
/// SHA-256 pass over the same payload.
|
||||
#[test]
|
||||
fn signed_single_chunk_put_leaves_payload_sha256_to_the_auth_layer() {
|
||||
let headers = signed_v4_headers(PAYLOAD_SHA256);
|
||||
assert_eq!(super::get_content_sha256_with_query(&headers, None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn streaming_signed_put_never_hands_a_digest_to_the_storage_layer() {
|
||||
for value in [
|
||||
"STREAMING-AWS4-HMAC-SHA256-PAYLOAD",
|
||||
"STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER",
|
||||
"STREAMING-UNSIGNED-PAYLOAD-TRAILER",
|
||||
"UNSIGNED-PAYLOAD",
|
||||
] {
|
||||
let headers = signed_v4_headers(value);
|
||||
assert_eq!(super::get_content_sha256_with_query(&headers, None), None, "{value}");
|
||||
}
|
||||
}
|
||||
|
||||
/// s3s verifies the header digest of a presigned request too, so a header
|
||||
/// digest is left to the auth layer exactly like the header-signed case.
|
||||
#[test]
|
||||
fn presigned_request_with_header_digest_leaves_payload_sha256_to_the_auth_layer() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-amz-content-sha256", HeaderValue::from_static(PAYLOAD_SHA256));
|
||||
assert_eq!(super::get_content_sha256_with_query(&headers, Some(PRESIGNED_QUERY)), None);
|
||||
}
|
||||
|
||||
/// s3s only reads the header, so a digest that arrives in the query string
|
||||
/// alone is the one case the storage layer still verifies itself.
|
||||
#[test]
|
||||
fn presigned_request_with_query_only_digest_is_still_verified_by_the_storage_layer() {
|
||||
let headers = HeaderMap::new();
|
||||
|
||||
let query = format!("{PRESIGNED_QUERY}&x-amz-content-sha256={PAYLOAD_SHA256}");
|
||||
assert_eq!(
|
||||
super::get_content_sha256_with_query(&headers, Some(&query)),
|
||||
Some(PAYLOAD_SHA256.to_string())
|
||||
);
|
||||
|
||||
let query = format!("{PRESIGNED_QUERY}&x-amz-content-sha256=UNSIGNED-PAYLOAD");
|
||||
assert_eq!(super::get_content_sha256_with_query(&headers, Some(&query)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anonymous_request_hands_no_digest_to_the_storage_layer() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("x-amz-content-sha256", HeaderValue::from_static(PAYLOAD_SHA256));
|
||||
assert_eq!(super::get_content_sha256_with_query(&headers, None), None);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user