mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-07 12:35:54 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user