mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-29 08:27:06 +00:00
perf(sse): drop the second KMS decrypt from encrypted GET responses (#6597)
perf(sse): classify GET response headers without a second KMS unwrap An SSE-KMS GET performed two backend Decrypt calls per request: the object layer's encryption resolver unwraps the envelope to build the decrypted stream, and the S3 layer then called sse_decryption again purely to derive response headers, discarding the returned key bytes. Replace the S3-layer call with classify_sse_read_response, which reproduces that call's behavior from stored metadata alone: SSE-C validation errors and precedence, per-key kms:Decrypt authorization ahead of every other failure mode, and the request's KMS audit summary fields. The success outcome stays honest because a failed unwrap aborts the read in the object layer before response classification is reached. Tests cover header parity against the unwrap-based path, audit-tag parity for allowed and denied principals, SSE-C validation parity, and prove classification needs no DEK provider at all.
This commit is contained in:
@@ -96,10 +96,10 @@ use super::storage_api::object_usecase::set_disk::{
|
|||||||
get_lock_acquire_timeout, get_object_disk_read_timeout, is_valid_storage_class,
|
get_lock_acquire_timeout, get_object_disk_read_timeout, is_valid_storage_class,
|
||||||
};
|
};
|
||||||
use super::storage_api::object_usecase::sse::{
|
use super::storage_api::object_usecase::sse::{
|
||||||
DecryptionRequest, EncryptionRequest, SSEType, SseKmsPrincipal, apply_bucket_default_lock_retention,
|
DecryptionRequest, EncryptionRequest, SseKmsPrincipal, apply_bucket_default_lock_retention, authorize_sse_kms_object_read,
|
||||||
authorize_sse_kms_object_read, bucket_default_write_sse, build_ssec_read_headers, encryption_material_to_metadata,
|
bucket_default_write_sse, build_ssec_read_headers, classify_sse_read_response, encryption_material_to_metadata,
|
||||||
extract_server_side_encryption_from_headers, extract_ssec_params_from_headers, extract_ssekms_context_from_headers,
|
extract_server_side_encryption_from_headers, extract_ssec_params_from_headers, extract_ssekms_context_from_headers,
|
||||||
get_buffer_size_opt_in, load_bucket_object_lock_config_state, map_get_object_reader_error, sse_decryption, sse_encryption,
|
get_buffer_size_opt_in, load_bucket_object_lock_config_state, map_get_object_reader_error, sse_encryption,
|
||||||
validate_bucket_object_lock_enabled_state,
|
validate_bucket_object_lock_enabled_state,
|
||||||
};
|
};
|
||||||
use super::storage_api::object_usecase::storage_class as storageclass;
|
use super::storage_api::object_usecase::storage_class as storageclass;
|
||||||
@@ -5325,21 +5325,19 @@ impl DefaultObjectUsecase {
|
|||||||
encryption_applied,
|
encryption_applied,
|
||||||
final_stream,
|
final_stream,
|
||||||
buffered_body,
|
buffered_body,
|
||||||
) = match sse_decryption(decryption_request).await? {
|
) = match classify_sse_read_response(decryption_request).await? {
|
||||||
Some(material) => {
|
// The stream is already decrypted by the object layer's encryption
|
||||||
let server_side_encryption = Some(material.server_side_encryption.clone());
|
// resolver; only the response headers, authorization and audit
|
||||||
let sse_customer_algorithm = matches!(material.sse_type, SSEType::SseC).then_some(material.algorithm.clone());
|
// summary are derived here, without a second KMS unwrap.
|
||||||
let sse_customer_key_md5 = material.customer_key_md5.clone();
|
Some(headers) => (
|
||||||
(
|
Some(headers.server_side_encryption),
|
||||||
server_side_encryption,
|
headers.sse_customer_algorithm,
|
||||||
sse_customer_algorithm,
|
headers.sse_customer_key_md5,
|
||||||
sse_customer_key_md5,
|
headers.ssekms_key_id,
|
||||||
material.kms_key_id,
|
true,
|
||||||
true,
|
wrap_reader(stream),
|
||||||
wrap_reader(stream),
|
None,
|
||||||
None,
|
),
|
||||||
)
|
|
||||||
}
|
|
||||||
None => (None, None, None, None, false, wrap_reader(stream), buffered_body),
|
None => (None, None, None, None, false, wrap_reader(stream), buffered_body),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1045,12 +1045,12 @@ pub(crate) mod request_context {
|
|||||||
pub(crate) mod sse {
|
pub(crate) mod sse {
|
||||||
pub(crate) use crate::storage::storage_api::sse_consumer::{
|
pub(crate) use crate::storage::storage_api::sse_consumer::{
|
||||||
DecryptionRequest, EncryptionRequest, PrepareEncryptionRequest, SseKmsPrincipal, apply_bucket_default_lock_retention,
|
DecryptionRequest, EncryptionRequest, PrepareEncryptionRequest, SseKmsPrincipal, apply_bucket_default_lock_retention,
|
||||||
authorize_sse_kms_object_read, extract_server_side_encryption_from_headers, get_buffer_size_opt_in,
|
authorize_sse_kms_object_read, classify_sse_read_response, extract_server_side_encryption_from_headers,
|
||||||
load_bucket_object_lock_config_state, sse_decryption, sse_encryption, sse_prepare_encryption,
|
get_buffer_size_opt_in, load_bucket_object_lock_config_state, sse_decryption, sse_encryption, sse_prepare_encryption,
|
||||||
validate_bucket_object_lock_enabled_state,
|
validate_bucket_object_lock_enabled_state,
|
||||||
};
|
};
|
||||||
pub(crate) use crate::storage::storage_api::sse_consumer::{
|
pub(crate) use crate::storage::storage_api::sse_consumer::{
|
||||||
EncryptionKeyKind, SSEType, bucket_default_write_sse, build_ssec_read_headers, encryption_material_to_metadata,
|
EncryptionKeyKind, bucket_default_write_sse, build_ssec_read_headers, encryption_material_to_metadata,
|
||||||
extract_ssec_params_from_headers, extract_ssekms_context_from_headers, map_get_object_reader_error,
|
extract_ssec_params_from_headers, extract_ssekms_context_from_headers, map_get_object_reader_error,
|
||||||
mark_encrypted_multipart_metadata,
|
mark_encrypted_multipart_metadata,
|
||||||
};
|
};
|
||||||
|
|||||||
+322
-6
@@ -792,6 +792,7 @@ pub struct DecryptionMaterial {
|
|||||||
pub sse_type: SSEType,
|
pub sse_type: SSEType,
|
||||||
pub server_side_encryption: ServerSideEncryption,
|
pub server_side_encryption: ServerSideEncryption,
|
||||||
pub kms_key_id: Option<SSEKMSKeyId>,
|
pub kms_key_id: Option<SSEKMSKeyId>,
|
||||||
|
#[allow(unused)]
|
||||||
pub algorithm: SSECustomerAlgorithm,
|
pub algorithm: SSECustomerAlgorithm,
|
||||||
pub customer_key_md5: Option<SSECustomerKeyMD5>, // if use SSE-C, check key md5
|
pub customer_key_md5: Option<SSECustomerKeyMD5>, // if use SSE-C, check key md5
|
||||||
|
|
||||||
@@ -2108,6 +2109,129 @@ pub async fn sse_decryption(request: DecryptionRequest<'_>) -> Result<Option<Dec
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Response headers derived for a decrypted read without unwrapping the data key.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SseReadResponseHeaders {
|
||||||
|
pub server_side_encryption: ServerSideEncryption,
|
||||||
|
pub sse_customer_algorithm: Option<SSECustomerAlgorithm>,
|
||||||
|
pub sse_customer_key_md5: Option<SSECustomerKeyMD5>,
|
||||||
|
pub ssekms_key_id: Option<SSEKMSKeyId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-side response classification for an object whose payload the object
|
||||||
|
/// layer's encryption resolver already decrypted.
|
||||||
|
///
|
||||||
|
/// [`sse_decryption`] both unwraps the data key and derives the response
|
||||||
|
/// headers. The GET path only needs the latter — its stream comes out of the
|
||||||
|
/// object layer decrypted (see [`SseObjectEncryptionResolver`]) — so calling
|
||||||
|
/// [`sse_decryption`] there performed a second KMS `Decrypt` per request whose
|
||||||
|
/// key bytes were discarded. This function reproduces that call's header,
|
||||||
|
/// validation, authorization and audit behavior from stored metadata alone.
|
||||||
|
///
|
||||||
|
/// Contract, mirrored from [`sse_decryption`] item by item:
|
||||||
|
/// - SSE-C: missing request key/MD5 and stored/request MD5 mismatch fail with
|
||||||
|
/// the same errors and precedence, and the provided key is fully validated.
|
||||||
|
/// - Managed SSE: per-key `kms:Decrypt` authorization runs ahead of every other
|
||||||
|
/// failure mode, and the request's KMS audit summary records the same
|
||||||
|
/// scheme/key/version/outcome fields the unwrap-based path recorded. The
|
||||||
|
/// success outcome is honest because a failed unwrap aborts the read in the
|
||||||
|
/// object layer before response classification is ever reached.
|
||||||
|
/// - Objects without SSE metadata — and managed metadata whose scheme cannot
|
||||||
|
/// be established in this build — classify as `None`, exactly where
|
||||||
|
/// [`sse_decryption`] returned `None`.
|
||||||
|
pub async fn classify_sse_read_response(request: DecryptionRequest<'_>) -> Result<Option<SseReadResponseHeaders>, ApiError> {
|
||||||
|
if request
|
||||||
|
.metadata
|
||||||
|
.contains_key("x-amz-server-side-encryption-customer-algorithm")
|
||||||
|
{
|
||||||
|
let (key, key_md5) = match (request.sse_customer_key, request.sse_customer_key_md5) {
|
||||||
|
(Some(k), Some(md5)) => (k, md5),
|
||||||
|
_ => {
|
||||||
|
return Err(ssec_invalid_request(
|
||||||
|
"The object was stored using a form of Server Side Encryption. \
|
||||||
|
The correct parameters must be provided to retrieve the object.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let stored_md5 = request.metadata.get("x-amz-server-side-encryption-customer-key-md5");
|
||||||
|
verify_ssec_key_match(key_md5, stored_md5)?;
|
||||||
|
|
||||||
|
let algorithm = request
|
||||||
|
.metadata
|
||||||
|
.get("x-amz-server-side-encryption-customer-algorithm")
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or("AES256");
|
||||||
|
validate_ssec_params(SsecParams {
|
||||||
|
algorithm: algorithm.to_string(),
|
||||||
|
key: key.to_string(),
|
||||||
|
key_md5: key_md5.to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
return Ok(Some(SseReadResponseHeaders {
|
||||||
|
server_side_encryption: ServerSideEncryption::from_static(ServerSideEncryption::AES256),
|
||||||
|
sse_customer_algorithm: Some(SSECustomerAlgorithm::from(algorithm)),
|
||||||
|
sse_customer_key_md5: Some(key_md5.clone()),
|
||||||
|
ssekms_key_id: None,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !contains_managed_encryption_metadata(request.metadata) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sse_type = match request.metadata.get("x-amz-server-side-encryption").map(String::as_str) {
|
||||||
|
Some(ServerSideEncryption::AWS_KMS) => SSEType::SseKms,
|
||||||
|
Some(_) => SSEType::SseS3,
|
||||||
|
#[cfg(feature = "rio-v2")]
|
||||||
|
None => match infer_minio_managed_sse_type(request.metadata) {
|
||||||
|
Some(sse_type) => sse_type,
|
||||||
|
None => return Ok(None),
|
||||||
|
},
|
||||||
|
#[cfg(not(feature = "rio-v2"))]
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Same key-id resolution chain as the unwrap path, so authorization and the
|
||||||
|
// response header name the same key.
|
||||||
|
let normalized_metadata = normalize_managed_metadata(request.metadata, Some(recode_minio_kms_context));
|
||||||
|
let kms_key_id = normalized_metadata
|
||||||
|
.get(INTERNAL_ENCRYPTION_KEY_ID_HEADER)
|
||||||
|
.or_else(|| request.metadata.get("x-amz-server-side-encryption-aws-kms-key-id"))
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| "default".to_string());
|
||||||
|
|
||||||
|
// Ahead of every other failure mode, so a denied caller learns nothing
|
||||||
|
// about the key beyond "not yours".
|
||||||
|
if let Err(error) = authorize_sse_kms_key(request.principal, sse_type, KmsAction::DecryptAction, &kms_key_id).await {
|
||||||
|
if let Some((sse_type, key_id)) = stored_managed_encryption_key(request.metadata) {
|
||||||
|
record_managed_kms_outcome(
|
||||||
|
request.principal,
|
||||||
|
sse_type,
|
||||||
|
Some(&key_id),
|
||||||
|
|| stored_envelope_master_key_version(request.metadata),
|
||||||
|
Err(&error),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
record_managed_kms_outcome(
|
||||||
|
request.principal,
|
||||||
|
sse_type,
|
||||||
|
Some(&kms_key_id),
|
||||||
|
|| stored_envelope_master_key_version(request.metadata),
|
||||||
|
Ok(()),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(Some(SseReadResponseHeaders {
|
||||||
|
server_side_encryption: ServerSideEncryption::from(managed_sse_public_header(sse_type).to_string()),
|
||||||
|
sse_customer_algorithm: None,
|
||||||
|
sse_customer_key_md5: None,
|
||||||
|
ssekms_key_id: Some(SSEKMSKeyId::from(kms_key_id)),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Internal Implementation - SSE-C
|
// Internal Implementation - SSE-C
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -3663,12 +3787,12 @@ mod tests {
|
|||||||
ObjectEncryptionResolver, PrepareEncryptionRequest, ReadEncryptionMode, ReadEncryptionRequest, SSEC_ORIGINAL_SIZE_HEADER,
|
ObjectEncryptionResolver, PrepareEncryptionRequest, ReadEncryptionMode, ReadEncryptionRequest, SSEC_ORIGINAL_SIZE_HEADER,
|
||||||
SSEType, SseDekProvider, SseKmsPrincipal, SseObjectEncryptionResolver, SsecParams, StorageError, TestSseDekProvider,
|
SSEType, SseDekProvider, SseKmsPrincipal, SseObjectEncryptionResolver, SsecParams, StorageError, TestSseDekProvider,
|
||||||
apply_managed_decryption_material, apply_managed_encryption_material, authorize_sse_kms_object_read,
|
apply_managed_decryption_material, apply_managed_encryption_material, authorize_sse_kms_object_read,
|
||||||
encryption_material_to_metadata, extract_server_side_encryption_from_headers, extract_ssec_params_from_headers,
|
classify_sse_read_response, encryption_material_to_metadata, extract_server_side_encryption_from_headers,
|
||||||
extract_ssekms_context_from_headers, generate_ssec_nonce, is_managed_sse, kms_operation_error,
|
extract_ssec_params_from_headers, extract_ssekms_context_from_headers, generate_ssec_nonce, is_managed_sse,
|
||||||
map_get_object_reader_error, mark_encrypted_multipart_metadata, md5_base64, normalize_managed_metadata,
|
kms_operation_error, map_get_object_reader_error, mark_encrypted_multipart_metadata, md5_base64,
|
||||||
recode_minio_kms_context, reset_sse_dek_provider, resolve_effective_kms_key_id, sse_decryption, sse_encryption,
|
normalize_managed_metadata, recode_minio_kms_context, reset_sse_dek_provider, resolve_effective_kms_key_id,
|
||||||
sse_prepare_encryption, strip_managed_encryption_metadata, validate_sse_headers_for_read, validate_sse_headers_for_write,
|
sse_decryption, sse_encryption, sse_prepare_encryption, strip_managed_encryption_metadata, validate_sse_headers_for_read,
|
||||||
validate_ssec_for_read, validate_ssec_params, verify_ssec_key_match,
|
validate_sse_headers_for_write, validate_ssec_for_read, validate_ssec_params, verify_ssec_key_match,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "rio-v2")]
|
#[cfg(feature = "rio-v2")]
|
||||||
use super::{
|
use super::{
|
||||||
@@ -6711,6 +6835,198 @@ mod tests {
|
|||||||
assert_eq!(audit_tag(&tags, "kmsErrorClass").as_deref(), Some("access_denied"));
|
assert_eq!(audit_tag(&tags, "kmsErrorClass").as_deref(), Some("access_denied"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========================================================================
|
||||||
|
// Read-side response classification (single-decrypt GET path)
|
||||||
|
// ========================================================================
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn classification_reproduces_managed_read_headers_and_audit_without_a_kms_unwrap() {
|
||||||
|
use rustfs_kms::types::{CreateKeyRequest, KeyUsage};
|
||||||
|
let _guard = lock_sse_test_state().await;
|
||||||
|
|
||||||
|
reset_sse_dek_provider();
|
||||||
|
let manager = configure_test_global_local_kms().await;
|
||||||
|
manager
|
||||||
|
.get_encryption_service()
|
||||||
|
.await
|
||||||
|
.expect("encryption service should exist")
|
||||||
|
.create_key(CreateKeyRequest {
|
||||||
|
key_name: Some("classify-key".to_string()),
|
||||||
|
key_usage: KeyUsage::EncryptDecrypt,
|
||||||
|
description: None,
|
||||||
|
policy: None,
|
||||||
|
tags: HashMap::new(),
|
||||||
|
origin: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("kms test key should be created");
|
||||||
|
let provider = KmsSseDekProvider::new_with_service_manager(manager.clone())
|
||||||
|
.await
|
||||||
|
.expect("kms provider should initialize from the configured test manager");
|
||||||
|
super::set_sse_dek_provider_for_test(Arc::new(provider));
|
||||||
|
|
||||||
|
let material = sse_encryption(EncryptionRequest {
|
||||||
|
bucket: "finance",
|
||||||
|
key: "ledger.csv",
|
||||||
|
server_side_encryption: Some(ServerSideEncryption::from_static(ServerSideEncryption::AWS_KMS)),
|
||||||
|
ssekms_key_id: Some("classify-key".to_string()),
|
||||||
|
ssekms_context: None,
|
||||||
|
sse_customer_algorithm: None,
|
||||||
|
sse_customer_key: None,
|
||||||
|
sse_customer_key_md5: None,
|
||||||
|
content_size: 128,
|
||||||
|
principal: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("sse-kms encryption should succeed")
|
||||||
|
.expect("managed sse-kms material");
|
||||||
|
let metadata = encryption_material_to_metadata(&material).expect("kms metadata should serialize");
|
||||||
|
|
||||||
|
// Header parity against the unwrap-based path, on the same metadata.
|
||||||
|
let unwrapped = sse_decryption(DecryptionRequest {
|
||||||
|
bucket: "finance",
|
||||||
|
key: "ledger.csv",
|
||||||
|
metadata: &metadata,
|
||||||
|
sse_customer_key: None,
|
||||||
|
sse_customer_key_md5: None,
|
||||||
|
principal: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("sse-kms decryption should succeed")
|
||||||
|
.expect("managed sse-kms material");
|
||||||
|
|
||||||
|
// Classification must not need a data-key unwrap at all: drop the
|
||||||
|
// provider before classifying, so any KMS round trip would fail loudly.
|
||||||
|
reset_sse_dek_provider();
|
||||||
|
|
||||||
|
let (read_principal, read_audit) = audited_principal(false, true);
|
||||||
|
let headers = classify_sse_read_response(DecryptionRequest {
|
||||||
|
bucket: "finance",
|
||||||
|
key: "ledger.csv",
|
||||||
|
metadata: &metadata,
|
||||||
|
sse_customer_key: None,
|
||||||
|
sse_customer_key_md5: None,
|
||||||
|
principal: Some(&read_principal),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("classification should succeed without a dek provider")
|
||||||
|
.expect("managed metadata must classify as encrypted");
|
||||||
|
|
||||||
|
assert_eq!(headers.server_side_encryption, unwrapped.server_side_encryption);
|
||||||
|
assert_eq!(headers.ssekms_key_id, unwrapped.kms_key_id);
|
||||||
|
assert_eq!(headers.sse_customer_algorithm, None);
|
||||||
|
assert_eq!(headers.sse_customer_key_md5, None);
|
||||||
|
|
||||||
|
// Audit parity: the same summary fields the unwrap-based read recorded.
|
||||||
|
let read_tags = read_audit.audit_tags();
|
||||||
|
assert_eq!(audit_tag(&read_tags, "sseType").as_deref(), Some("SSE-KMS"));
|
||||||
|
assert_eq!(audit_tag(&read_tags, "kmsKeyId").as_deref(), Some("classify-key"));
|
||||||
|
assert_eq!(audit_tag(&read_tags, "kmsOutcome").as_deref(), Some("success"));
|
||||||
|
assert_eq!(audit_tag(&read_tags, "kmsErrorClass"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn classification_denies_an_unauthorized_principal_with_the_same_audit_summary() {
|
||||||
|
let (principal, audit) = audited_principal(true, false);
|
||||||
|
|
||||||
|
let error = classify_sse_read_response(DecryptionRequest {
|
||||||
|
bucket: "finance",
|
||||||
|
key: "ledger.csv",
|
||||||
|
metadata: &sse_kms_object_metadata(),
|
||||||
|
sse_customer_key: None,
|
||||||
|
sse_customer_key_md5: None,
|
||||||
|
principal: Some(&principal),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect_err("an unauthorized principal must not classify against the key");
|
||||||
|
assert_eq!(error.code, S3ErrorCode::AccessDenied);
|
||||||
|
|
||||||
|
let tags = audit.audit_tags();
|
||||||
|
assert_eq!(audit_tag(&tags, "sseType").as_deref(), Some("SSE-KMS"));
|
||||||
|
assert_eq!(audit_tag(&tags, "kmsKeyId").as_deref(), Some("finance-key"));
|
||||||
|
assert_eq!(audit_tag(&tags, "kmsOutcome").as_deref(), Some("failure"));
|
||||||
|
assert_eq!(audit_tag(&tags, "kmsErrorClass").as_deref(), Some("access_denied"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn classification_matches_ssec_validation_and_headers() {
|
||||||
|
let key = [0x42u8; 32];
|
||||||
|
let key_b64 = BASE64_STANDARD.encode(key);
|
||||||
|
let key_md5 = md5_base64(key);
|
||||||
|
let metadata = HashMap::from([
|
||||||
|
("x-amz-server-side-encryption-customer-algorithm".to_string(), "AES256".to_string()),
|
||||||
|
("x-amz-server-side-encryption-customer-key-md5".to_string(), key_md5.clone()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let headers = classify_sse_read_response(DecryptionRequest {
|
||||||
|
bucket: "finance",
|
||||||
|
key: "ledger.csv",
|
||||||
|
metadata: &metadata,
|
||||||
|
sse_customer_key: Some(&SSECustomerKey::from(key_b64.clone())),
|
||||||
|
sse_customer_key_md5: Some(&SSECustomerKeyMD5::from(key_md5.clone())),
|
||||||
|
principal: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("a matching customer key should classify")
|
||||||
|
.expect("SSE-C metadata must classify as encrypted");
|
||||||
|
assert_eq!(headers.server_side_encryption.as_str(), ServerSideEncryption::AES256);
|
||||||
|
assert_eq!(headers.sse_customer_algorithm.as_deref(), Some("AES256"));
|
||||||
|
assert_eq!(headers.sse_customer_key_md5.as_deref(), Some(key_md5.as_str()));
|
||||||
|
assert_eq!(headers.ssekms_key_id, None);
|
||||||
|
|
||||||
|
// Same error and precedence as `sse_decryption` when the key is absent…
|
||||||
|
let missing = classify_sse_read_response(DecryptionRequest {
|
||||||
|
bucket: "finance",
|
||||||
|
key: "ledger.csv",
|
||||||
|
metadata: &metadata,
|
||||||
|
sse_customer_key: None,
|
||||||
|
sse_customer_key_md5: None,
|
||||||
|
principal: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect_err("an SSE-C object must not classify without the customer key");
|
||||||
|
assert_eq!(missing.code, S3ErrorCode::InvalidRequest);
|
||||||
|
|
||||||
|
// …and when the provided key does not match the stored digest.
|
||||||
|
let other_key = [0x43u8; 32];
|
||||||
|
let mismatch = classify_sse_read_response(DecryptionRequest {
|
||||||
|
bucket: "finance",
|
||||||
|
key: "ledger.csv",
|
||||||
|
metadata: &metadata,
|
||||||
|
sse_customer_key: Some(&SSECustomerKey::from(BASE64_STANDARD.encode(other_key))),
|
||||||
|
sse_customer_key_md5: Some(&SSECustomerKeyMD5::from(md5_base64(other_key))),
|
||||||
|
principal: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect_err("a mismatched customer key must not classify");
|
||||||
|
let parity = sse_decryption(DecryptionRequest {
|
||||||
|
bucket: "finance",
|
||||||
|
key: "ledger.csv",
|
||||||
|
metadata: &metadata,
|
||||||
|
sse_customer_key: Some(&SSECustomerKey::from(BASE64_STANDARD.encode(other_key))),
|
||||||
|
sse_customer_key_md5: Some(&SSECustomerKeyMD5::from(md5_base64(other_key))),
|
||||||
|
principal: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect_err("the unwrap-based path rejects the same mismatch");
|
||||||
|
assert_eq!(mismatch.code, parity.code);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn classification_reports_plaintext_objects_as_unencrypted() {
|
||||||
|
let result = classify_sse_read_response(DecryptionRequest {
|
||||||
|
bucket: "finance",
|
||||||
|
key: "ledger.csv",
|
||||||
|
metadata: &HashMap::new(),
|
||||||
|
sse_customer_key: None,
|
||||||
|
sse_customer_key_md5: None,
|
||||||
|
principal: None,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("plaintext metadata should classify cleanly");
|
||||||
|
assert!(result.is_none(), "objects without SSE metadata carry no SSE response headers");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn kms_backend_failures_keep_the_kms_audit_error_class() {
|
fn kms_backend_failures_keep_the_kms_audit_error_class() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -110,8 +110,9 @@ pub(crate) use super::ecfs_extend::{
|
|||||||
};
|
};
|
||||||
pub(crate) use super::sse::{
|
pub(crate) use super::sse::{
|
||||||
DecryptionRequest, EncryptionRequest, PrepareEncryptionRequest, SseKmsPrincipal, authorize_sse_kms_object_read,
|
DecryptionRequest, EncryptionRequest, PrepareEncryptionRequest, SseKmsPrincipal, authorize_sse_kms_object_read,
|
||||||
extract_server_side_encryption_from_headers, sse_decryption, sse_encryption, sse_prepare_encryption,
|
classify_sse_read_response, extract_server_side_encryption_from_headers, sse_decryption, sse_encryption,
|
||||||
strip_managed_encryption_metadata, validate_sse_headers_for_read, validate_sse_headers_for_write, validate_ssec_for_read,
|
sse_prepare_encryption, strip_managed_encryption_metadata, validate_sse_headers_for_read, validate_sse_headers_for_write,
|
||||||
|
validate_ssec_for_read,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) mod access_consumer {
|
pub(crate) mod access_consumer {
|
||||||
@@ -355,14 +356,14 @@ pub(crate) mod s3_api_consumer {
|
|||||||
|
|
||||||
pub(crate) mod sse_consumer {
|
pub(crate) mod sse_consumer {
|
||||||
pub(crate) use super::super::sse::{
|
pub(crate) use super::super::sse::{
|
||||||
EncryptionKeyKind, SSEType, bucket_default_write_sse, build_ssec_read_headers, encryption_material_to_metadata,
|
EncryptionKeyKind, bucket_default_write_sse, build_ssec_read_headers, encryption_material_to_metadata,
|
||||||
extract_ssec_params_from_headers, extract_ssekms_context_from_headers, log_sse_kms_key_policy_mode,
|
extract_ssec_params_from_headers, extract_ssekms_context_from_headers, log_sse_kms_key_policy_mode,
|
||||||
map_get_object_reader_error, mark_encrypted_multipart_metadata,
|
map_get_object_reader_error, mark_encrypted_multipart_metadata,
|
||||||
};
|
};
|
||||||
pub(crate) use super::{
|
pub(crate) use super::{
|
||||||
DecryptionRequest, EncryptionRequest, PrepareEncryptionRequest, SseKmsPrincipal, apply_bucket_default_lock_retention,
|
DecryptionRequest, EncryptionRequest, PrepareEncryptionRequest, SseKmsPrincipal, apply_bucket_default_lock_retention,
|
||||||
authorize_sse_kms_object_read, extract_server_side_encryption_from_headers, get_buffer_size_opt_in,
|
authorize_sse_kms_object_read, classify_sse_read_response, extract_server_side_encryption_from_headers,
|
||||||
load_bucket_object_lock_config_state, sse_decryption, sse_encryption, sse_prepare_encryption,
|
get_buffer_size_opt_in, load_bucket_object_lock_config_state, sse_decryption, sse_encryption, sse_prepare_encryption,
|
||||||
validate_bucket_object_lock_enabled_state,
|
validate_bucket_object_lock_enabled_state,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user