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:
唐小鸭
2026-09-16 12:05:04 +08:00
committed by GitHub
parent a7341abdbd
commit 95c06966cc
+86 -1
View File
@@ -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);
}
}