fix(s3): align encrypted checksums and multipart completion (#7025)

* fix(s3): align encrypted checksum handling

* test(s3): align multipart SSE-C completion

* fix(ecstore): scope startup helper to tests
This commit is contained in:
Zhengchao An
2026-09-02 01:35:49 +08:00
committed by GitHub
parent 7dcfdb3320
commit 40a2470feb
11 changed files with 271 additions and 16 deletions
+5 -2
View File
@@ -101,7 +101,7 @@ use rustfs_utils::CompressionAlgorithm;
#[cfg(test)]
use rustfs_utils::http::insert_header;
use rustfs_utils::http::{
SUFFIX_MAX_TOTAL_OBJECT_SIZE, SUFFIX_REPLICATION_PRESERVE_CIPHERTEXT, SUFFIX_REPLICATION_STATUS,
SUFFIX_MAX_TOTAL_OBJECT_SIZE, SUFFIX_PLAINTEXT_CHECKSUM, SUFFIX_REPLICATION_PRESERVE_CIPHERTEXT, SUFFIX_REPLICATION_STATUS,
SUFFIX_REPLICATION_TIMESTAMP, SUFFIX_SOURCE_REPLICATION_REQUEST, contains_key_str, get_consistent_str, get_header,
get_source_scheme,
headers::{AMZ_DECODED_CONTENT_LENGTH, AMZ_OBJECT_TAGGING, AMZ_STORAGE_CLASS},
@@ -653,7 +653,7 @@ impl DefaultMultipartUsecase {
content_size: 0,
principal: None,
}
.validate_multipart_ssec(&multipart_info.user_defined)?;
.validate_complete_multipart_ssec(&multipart_info.user_defined)?;
}
let cache_adapter = self.object_data_cache();
let _ = invalidate_object_data_cache_before_mutation(&cache_adapter, &bucket, &key).await;
@@ -1023,6 +1023,9 @@ impl DefaultMultipartUsecase {
..Default::default()
});
}
if effective_sse.is_some() && opts.want_checksum.is_some() {
insert_str(&mut opts.user_defined, SUFFIX_PLAINTEXT_CHECKSUM, "true".to_string());
}
let MultipartUploadResult {
upload_id,
+5
View File
@@ -575,6 +575,7 @@ impl DefaultObjectUsecase {
}
strip_managed_encryption_metadata(&mut user_defined);
remove_str(&mut user_defined, SUFFIX_PLAINTEXT_CHECKSUM);
let destination_storage_class = storage_class
.as_ref()
@@ -665,6 +666,7 @@ impl DefaultObjectUsecase {
// none is requested, carry the source object's stored checksum over unchanged — the copy
// does not alter the plaintext, so re-hashing would be wasted work and would flatten a
// multipart composite value.
let destination_has_checksum = requested_checksum_type.is_some() || src_checksum.is_some();
match requested_checksum_type {
Some(checksum_type) => {
reader.add_calculated_checksum(checksum_type).map_err(ApiError::from)?;
@@ -696,6 +698,9 @@ impl DefaultObjectUsecase {
write_plan = write_plan.with_encryption(material.write_encryption(None));
user_defined.extend(encryption_material_to_metadata(&material)?);
if destination_has_checksum {
insert_str(&mut user_defined, SUFFIX_PLAINTEXT_CHECKSUM, "true".to_string());
}
}
reader = write_plan.apply(reader, actual_size).map_err(ApiError::from)?;
+3 -3
View File
@@ -145,9 +145,9 @@ use rustfs_utils::http::headers::{SSEC_ALGORITHM_HEADER, SSEC_KEY_HEADER, SSEC_K
use rustfs_utils::http::insert_header;
use rustfs_utils::http::{
AMZ_BUCKET_REPLICATION_STATUS, AMZ_CHECKSUM_MODE, AMZ_CHECKSUM_TYPE, AMZ_WEBSITE_REDIRECT_LOCATION, CONTENT_TYPE,
SUFFIX_ACTUAL_SIZE, SUFFIX_COMPRESSION, SUFFIX_COMPRESSION_SIZE, SUFFIX_REPLICA_STATUS, SUFFIX_REPLICA_TIMESTAMP,
SUFFIX_REPLICATION_STATUS, SUFFIX_REPLICATION_TIMESTAMP, SUFFIX_RESTORE_OPERATION_ID, SUFFIX_SOURCE_REPLICATION_CHECK,
SUFFIX_SOURCE_REPLICATION_REQUEST, get_header,
SUFFIX_ACTUAL_SIZE, SUFFIX_COMPRESSION, SUFFIX_COMPRESSION_SIZE, SUFFIX_PLAINTEXT_CHECKSUM, SUFFIX_REPLICA_STATUS,
SUFFIX_REPLICA_TIMESTAMP, SUFFIX_REPLICATION_STATUS, SUFFIX_REPLICATION_TIMESTAMP, SUFFIX_RESTORE_OPERATION_ID,
SUFFIX_SOURCE_REPLICATION_CHECK, SUFFIX_SOURCE_REPLICATION_REQUEST, get_header,
headers::{
AMZ_CONTENT_SHA256, AMZ_DECODED_CONTENT_LENGTH, AMZ_MINIO_SNOWBALL_IGNORE_DIRS, AMZ_MINIO_SNOWBALL_IGNORE_ERRORS,
AMZ_MINIO_SNOWBALL_PREFIX, AMZ_OBJECT_LOCK_LEGAL_HOLD, AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, AMZ_OBJECT_LOCK_MODE,
+4
View File
@@ -1447,6 +1447,10 @@ impl DefaultObjectUsecase {
let encryption_metadata = encryption_material_to_metadata(&material)?;
metadata.extend(encryption_metadata.clone());
opts.user_defined.extend(encryption_metadata);
if opts.want_checksum.is_some() {
insert_str(&mut metadata, SUFFIX_PLAINTEXT_CHECKSUM, "true".to_string());
insert_str(&mut opts.user_defined, SUFFIX_PLAINTEXT_CHECKSUM, "true".to_string());
}
}
reader = write_plan.apply(reader, actual_size).map_err(ApiError::from)?;
+77
View File
@@ -460,6 +460,28 @@ pub struct EncryptionRequest<'a> {
}
impl EncryptionRequest<'_> {
pub fn validate_complete_multipart_ssec(&self, user_defined: &HashMap<String, String>) -> Result<(), ApiError> {
let request_uses_ssec =
self.sse_customer_algorithm.is_some() || self.sse_customer_key.is_some() || self.sse_customer_key_md5.is_some();
if !request_uses_ssec {
let stored_algorithm = user_defined.get("x-amz-server-side-encryption-customer-algorithm");
let stored_key_md5 = user_defined.get("x-amz-server-side-encryption-customer-key-md5");
return match (stored_algorithm, stored_key_md5) {
(None, None) => Ok(()),
(Some(algorithm), Some(key_md5))
if algorithm == DEFAULT_SSE_ALGORITHM
&& BASE64_STANDARD
.decode_to_vec(key_md5)
.is_ok_and(|decoded| decoded.len() == 16) =>
{
Ok(())
}
_ => Err(ssec_invalid_request("The multipart upload contains invalid SSE-C metadata.")),
};
}
self.validate_multipart_ssec(user_defined)
}
pub fn validate_multipart_ssec(&self, user_defined: &HashMap<String, String>) -> Result<(), ApiError> {
let stored_algorithm = user_defined.get("x-amz-server-side-encryption-customer-algorithm");
let stored_key_md5 = user_defined.get("x-amz-server-side-encryption-customer-key-md5");
@@ -6374,6 +6396,61 @@ mod tests {
);
}
#[test]
fn test_validate_complete_multipart_ssec_allows_omitted_parameters() {
let no_ssec = EncryptionRequest {
sse_customer_algorithm: None,
sse_customer_key: None,
sse_customer_key_md5: None,
..multipart_ssec_request(42)
};
assert!(no_ssec.validate_complete_multipart_ssec(&multipart_ssec_metadata(42)).is_ok());
assert!(no_ssec.validate_complete_multipart_ssec(&HashMap::new()).is_ok());
for invalid_metadata in [
HashMap::from([("x-amz-server-side-encryption-customer-algorithm".to_string(), "AES256".to_string())]),
HashMap::from([
("x-amz-server-side-encryption-customer-algorithm".to_string(), "AES128".to_string()),
("x-amz-server-side-encryption-customer-key-md5".to_string(), md5_base64([42u8; 32])),
]),
HashMap::from([
("x-amz-server-side-encryption-customer-algorithm".to_string(), "AES256".to_string()),
("x-amz-server-side-encryption-customer-key-md5".to_string(), "invalid".to_string()),
]),
] {
assert_eq!(
no_ssec
.validate_complete_multipart_ssec(&invalid_metadata)
.expect_err("corrupt SSE-C session metadata must fail closed")
.code,
S3ErrorCode::InvalidRequest
);
}
}
#[test]
fn test_validate_complete_multipart_ssec_still_validates_present_parameters() {
let partial = EncryptionRequest {
sse_customer_key: None,
..multipart_ssec_request(42)
};
assert_eq!(
partial
.validate_complete_multipart_ssec(&multipart_ssec_metadata(42))
.expect_err("partial SSE-C parameters must fail")
.code,
S3ErrorCode::InvalidRequest
);
assert_eq!(
multipart_ssec_request(43)
.validate_complete_multipart_ssec(&multipart_ssec_metadata(42))
.expect_err("wrong SSE-C parameters must fail")
.code,
S3ErrorCode::InvalidRequest
);
}
#[test]
fn test_validate_multipart_ssec_rejects_wrong_key_without_leaking_it() {
let request = multipart_ssec_request(43);