mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-29 08:27:06 +00:00
test(e2e): require exact SSE-C errors (#6546)
This commit is contained in:
@@ -24,6 +24,7 @@
|
||||
|
||||
use crate::common::{RustFSTestEnvironment, awscurl_get, awscurl_post, init_logging as common_init_logging, local_http_client};
|
||||
use aws_sdk_s3::Client;
|
||||
use aws_sdk_s3::error::{ProvideErrorMetadata, SdkError};
|
||||
use aws_sdk_s3::primitives::ByteStream;
|
||||
use aws_sdk_s3::types::ServerSideEncryption;
|
||||
use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
|
||||
@@ -50,6 +51,9 @@ pub const VAULT_TOKEN: &str = "dev-root-token";
|
||||
pub const VAULT_TRANSIT_PATH: &str = "transit";
|
||||
pub const VAULT_KEY_NAME: &str = "rustfs-master-key";
|
||||
pub const ENV_TEST_VAULT_BIN: &str = "RUSTFS_TEST_VAULT_BIN";
|
||||
pub const SSE_C_KEY_MISMATCH_MESSAGE: &str =
|
||||
"The provided encryption parameters did not match the ones used originally to encrypt the object.";
|
||||
pub const SSE_C_MISSING_PARAMETERS_MESSAGE: &str = "The object was stored using a form of Server Side Encryption. The correct parameters must be provided to retrieve the object.";
|
||||
|
||||
/// Initialize tracing for KMS tests with KMS-specific log levels
|
||||
pub fn init_logging() {
|
||||
@@ -63,6 +67,24 @@ pub fn sse_customer_key_md5_base64(key: &str) -> String {
|
||||
BASE64.encode(hasher.finalize())
|
||||
}
|
||||
|
||||
pub fn assert_s3_error<T, E>(result: Result<T, SdkError<E>>, status: u16, code: &str, message: &str, context: &str)
|
||||
where
|
||||
T: std::fmt::Debug,
|
||||
E: ProvideErrorMetadata + std::fmt::Debug,
|
||||
{
|
||||
let error = result.expect_err(context);
|
||||
assert_eq!(
|
||||
error.raw_response().map(|response| response.status().as_u16()),
|
||||
Some(status),
|
||||
"{context}: unexpected HTTP status: {error:?}"
|
||||
);
|
||||
let service_error = error
|
||||
.as_service_error()
|
||||
.expect("request failure should retain an S3 service error");
|
||||
assert_eq!(service_error.code(), Some(code), "{context}: unexpected error code: {error:?}");
|
||||
assert_eq!(service_error.message(), Some(message), "{context}: unexpected error message: {error:?}");
|
||||
}
|
||||
|
||||
pub async fn kms_admin_request(
|
||||
base_url: &str,
|
||||
method: http::Method,
|
||||
@@ -559,7 +581,13 @@ pub async fn test_error_scenarios(s3_client: &Client, bucket: &str) -> Result<()
|
||||
.send()
|
||||
.await;
|
||||
|
||||
assert!(wrong_key_result.is_err(), "Download with wrong SSE-C key should fail");
|
||||
assert_s3_error(
|
||||
wrong_key_result,
|
||||
400,
|
||||
"InvalidRequest",
|
||||
SSE_C_KEY_MISMATCH_MESSAGE,
|
||||
"download with a wrong SSE-C key must be rejected",
|
||||
);
|
||||
info!("✅ Correctly rejected download with wrong SSE-C key");
|
||||
|
||||
info!("Error scenario tests completed successfully");
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
//! complex workflows.
|
||||
|
||||
use super::common::{
|
||||
EncryptionType, LocalKMSTestEnvironment, MultipartTestConfig, create_sse_c_config, sse_customer_key_md5_base64,
|
||||
test_all_multipart_encryption_types, test_kms_key_management, test_multipart_upload_with_config, test_sse_c_encryption,
|
||||
test_sse_kms_encryption, test_sse_s3_encryption,
|
||||
EncryptionType, LocalKMSTestEnvironment, MultipartTestConfig, SSE_C_KEY_MISMATCH_MESSAGE, assert_s3_error,
|
||||
create_sse_c_config, sse_customer_key_md5_base64, test_all_multipart_encryption_types, test_kms_key_management,
|
||||
test_multipart_upload_with_config, test_sse_c_encryption, test_sse_kms_encryption, test_sse_s3_encryption,
|
||||
};
|
||||
use crate::common::{TEST_BUCKET, init_logging};
|
||||
use tracing::info;
|
||||
@@ -191,7 +191,13 @@ async fn test_comprehensive_key_isolation() -> Result<(), Box<dyn std::error::Er
|
||||
.send()
|
||||
.await;
|
||||
|
||||
assert!(wrong_read_result.is_err(), "The encrypted file should not be readable with the wrong key");
|
||||
assert_s3_error(
|
||||
wrong_read_result,
|
||||
400,
|
||||
"InvalidRequest",
|
||||
SSE_C_KEY_MISMATCH_MESSAGE,
|
||||
"multipart SSE-C object GET with a wrong key must be rejected",
|
||||
);
|
||||
info!("✅ Confirm that key isolation is working correctly");
|
||||
|
||||
kms_env.base_env.delete_test_bucket(TEST_BUCKET).await?;
|
||||
|
||||
@@ -21,21 +21,14 @@
|
||||
//! - Concurrent encryption operations
|
||||
//! - Security validation tests
|
||||
|
||||
use super::common::{LocalKMSTestEnvironment, sse_customer_key_md5_base64};
|
||||
use super::common::{LocalKMSTestEnvironment, SSE_C_KEY_MISMATCH_MESSAGE, assert_s3_error, sse_customer_key_md5_base64};
|
||||
use crate::common::{TEST_BUCKET, init_logging};
|
||||
use aws_sdk_s3::types::ServerSideEncryption;
|
||||
use base64::Engine;
|
||||
use md5::{Digest as Md5Digest, Md5};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Semaphore;
|
||||
use tracing::{info, warn};
|
||||
|
||||
fn md5_hex(input: impl AsRef<[u8]>) -> String {
|
||||
let mut hasher = Md5::new();
|
||||
hasher.update(input.as_ref());
|
||||
hex::encode(hasher.finalize())
|
||||
}
|
||||
|
||||
/// Test encryption of zero-byte files (empty files)
|
||||
#[tokio::test]
|
||||
async fn test_kms_zero_byte_file_encryption() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
@@ -295,7 +288,7 @@ async fn test_kms_invalid_key_scenarios() -> Result<(), Box<dyn std::error::Erro
|
||||
info!("🔍 Testing invalid SSE-C key length");
|
||||
let invalid_short_key = "short"; // Too short
|
||||
let invalid_key_b64 = base64::engine::general_purpose::STANDARD.encode(invalid_short_key);
|
||||
let invalid_key_md5 = md5_hex(invalid_short_key);
|
||||
let invalid_key_md5 = sse_customer_key_md5_base64(invalid_short_key);
|
||||
|
||||
let invalid_key_result = s3_client
|
||||
.put_object()
|
||||
@@ -308,14 +301,32 @@ async fn test_kms_invalid_key_scenarios() -> Result<(), Box<dyn std::error::Erro
|
||||
.send()
|
||||
.await;
|
||||
|
||||
assert!(invalid_key_result.is_err(), "Should reject invalid key length");
|
||||
assert_s3_error(
|
||||
invalid_key_result,
|
||||
400,
|
||||
"InvalidRequest",
|
||||
"SSE-C key must be 32 bytes (256 bits), got 5 bytes.",
|
||||
"invalid SSE-C key length must be rejected",
|
||||
);
|
||||
assert_s3_error(
|
||||
s3_client
|
||||
.get_object()
|
||||
.bucket(TEST_BUCKET)
|
||||
.key("test-invalid-key-length")
|
||||
.send()
|
||||
.await,
|
||||
404,
|
||||
"NoSuchKey",
|
||||
"The specified key does not exist.",
|
||||
"rejected invalid-key PUT must not create an object",
|
||||
);
|
||||
info!("✅ Correctly rejected invalid key length");
|
||||
|
||||
// Test 2: Mismatched MD5 for SSE-C
|
||||
info!("🔍 Testing mismatched MD5 for SSE-C key");
|
||||
let valid_key = "01234567890123456789012345678901";
|
||||
let valid_key_b64 = base64::engine::general_purpose::STANDARD.encode(valid_key);
|
||||
let wrong_md5 = "wrongmd5hash12345678901234567890"; // Wrong MD5
|
||||
let wrong_md5 = sse_customer_key_md5_base64("98765432109876543210987654321098");
|
||||
|
||||
let wrong_md5_result = s3_client
|
||||
.put_object()
|
||||
@@ -324,11 +335,24 @@ async fn test_kms_invalid_key_scenarios() -> Result<(), Box<dyn std::error::Erro
|
||||
.body(aws_sdk_s3::primitives::ByteStream::from(test_data.to_vec()))
|
||||
.sse_customer_algorithm("AES256")
|
||||
.sse_customer_key(&valid_key_b64)
|
||||
.sse_customer_key_md5(wrong_md5)
|
||||
.sse_customer_key_md5(&wrong_md5)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
assert!(wrong_md5_result.is_err(), "Should reject mismatched MD5");
|
||||
assert_s3_error(
|
||||
wrong_md5_result,
|
||||
400,
|
||||
"InvalidRequest",
|
||||
"The calculated MD5 hash of the key did not match the hash that was provided.",
|
||||
"mismatched SSE-C key MD5 must be rejected",
|
||||
);
|
||||
assert_s3_error(
|
||||
s3_client.get_object().bucket(TEST_BUCKET).key("test-wrong-md5").send().await,
|
||||
404,
|
||||
"NoSuchKey",
|
||||
"The specified key does not exist.",
|
||||
"rejected mismatched-MD5 PUT must not create an object",
|
||||
);
|
||||
info!("✅ Correctly rejected mismatched MD5");
|
||||
|
||||
// Test 3: Try to access SSE-C object without providing key
|
||||
@@ -355,7 +379,28 @@ async fn test_kms_invalid_key_scenarios() -> Result<(), Box<dyn std::error::Erro
|
||||
.send()
|
||||
.await;
|
||||
|
||||
assert!(no_key_result.is_err(), "Should require SSE-C key for access");
|
||||
assert_s3_error(
|
||||
no_key_result,
|
||||
400,
|
||||
"InvalidRequest",
|
||||
"The object was stored using a form of Server Side Encryption. The correct parameters must be provided to retrieve the object.",
|
||||
"SSE-C object GET without a customer key must be rejected",
|
||||
);
|
||||
|
||||
let recovered = s3_client
|
||||
.get_object()
|
||||
.bucket(TEST_BUCKET)
|
||||
.key("test-sse-c-no-key-access")
|
||||
.sse_customer_algorithm("AES256")
|
||||
.sse_customer_key(&valid_key_b64)
|
||||
.sse_customer_key_md5(&valid_key_md5)
|
||||
.send()
|
||||
.await?
|
||||
.body
|
||||
.collect()
|
||||
.await?
|
||||
.into_bytes();
|
||||
assert_eq!(recovered.as_ref(), test_data, "failed GET must not corrupt the SSE-C object");
|
||||
info!("✅ Correctly required SSE-C key for access");
|
||||
|
||||
kms_env.base_env.delete_test_bucket(TEST_BUCKET).await?;
|
||||
@@ -563,7 +608,13 @@ async fn test_kms_key_validation_security() -> Result<(), Box<dyn std::error::Er
|
||||
.send()
|
||||
.await;
|
||||
|
||||
assert!(wrong_key_result.is_err(), "Should not be able to decrypt with wrong key");
|
||||
assert_s3_error(
|
||||
wrong_key_result,
|
||||
400,
|
||||
"InvalidRequest",
|
||||
SSE_C_KEY_MISMATCH_MESSAGE,
|
||||
"SSE-C object GET with the wrong customer key must be rejected",
|
||||
);
|
||||
info!("✅ Key isolation verified - wrong key cannot decrypt data");
|
||||
|
||||
kms_env.base_env.delete_test_bucket(TEST_BUCKET).await?;
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
//! - Complete encryption/decryption lifecycle
|
||||
|
||||
use super::common::{
|
||||
LocalKMSTestEnvironment, get_kms_status, sse_customer_key_md5_base64, test_kms_key_management, test_sse_c_encryption,
|
||||
LocalKMSTestEnvironment, SSE_C_KEY_MISMATCH_MESSAGE, assert_s3_error, get_kms_status, sse_customer_key_md5_base64,
|
||||
test_kms_key_management, test_sse_c_encryption,
|
||||
};
|
||||
use crate::common::{TEST_BUCKET, init_logging};
|
||||
use tracing::{error, info};
|
||||
@@ -196,7 +197,13 @@ async fn test_local_kms_key_isolation() {
|
||||
.send()
|
||||
.await;
|
||||
|
||||
assert!(wrong_key_result.is_err(), "Should not be able to decrypt object1 with key2");
|
||||
assert_s3_error(
|
||||
wrong_key_result,
|
||||
400,
|
||||
"InvalidRequest",
|
||||
SSE_C_KEY_MISMATCH_MESSAGE,
|
||||
"local SSE-C object GET with a wrong key must be rejected",
|
||||
);
|
||||
|
||||
kms_env
|
||||
.base_env
|
||||
|
||||
@@ -22,9 +22,9 @@ use crate::common::{TEST_BUCKET, init_logging};
|
||||
use tracing::{error, info};
|
||||
|
||||
use super::common::{
|
||||
VAULT_KEY_NAME, VaultTestEnvironment, get_kms_status, sse_customer_key_md5_base64, start_kms,
|
||||
test_all_multipart_encryption_types, test_error_scenarios, test_kms_key_management, test_sse_c_encryption,
|
||||
test_sse_kms_encryption, test_sse_s3_encryption,
|
||||
SSE_C_KEY_MISMATCH_MESSAGE, VAULT_KEY_NAME, VaultTestEnvironment, assert_s3_error, get_kms_status,
|
||||
sse_customer_key_md5_base64, start_kms, test_all_multipart_encryption_types, test_error_scenarios, test_kms_key_management,
|
||||
test_sse_c_encryption, test_sse_kms_encryption, test_sse_s3_encryption,
|
||||
};
|
||||
|
||||
/// Helper that brings up Vault, configures RustFS, and starts the KMS service.
|
||||
@@ -182,7 +182,13 @@ async fn test_vault_kms_key_isolation() -> Result<(), Box<dyn std::error::Error
|
||||
.sse_customer_key_md5(&key2_md5)
|
||||
.send()
|
||||
.await;
|
||||
assert!(wrong_key.is_err(), "Object1 should not decrypt with key2");
|
||||
assert_s3_error(
|
||||
wrong_key,
|
||||
400,
|
||||
"InvalidRequest",
|
||||
SSE_C_KEY_MISMATCH_MESSAGE,
|
||||
"Vault-backed SSE-C object GET with a wrong key must be rejected",
|
||||
);
|
||||
|
||||
context
|
||||
.base_env()
|
||||
|
||||
@@ -20,7 +20,10 @@ use crate::fake_s3_target::{
|
||||
FAKE_ACCESS_KEY, FAKE_SECRET_KEY, FakeS3Target, FaultAction as FakeTargetFault, Operation as FakeTargetOperation,
|
||||
RequestRecord,
|
||||
};
|
||||
use crate::kms::common::{create_key_with_specific_id, sse_customer_key_md5_base64};
|
||||
use crate::kms::common::{
|
||||
SSE_C_KEY_MISMATCH_MESSAGE, SSE_C_MISSING_PARAMETERS_MESSAGE, assert_s3_error, create_key_with_specific_id,
|
||||
sse_customer_key_md5_base64,
|
||||
};
|
||||
use crate::storage_api::replication_extension::BucketTargetSys;
|
||||
use aws_sdk_s3::config::{Credentials, Region};
|
||||
use aws_sdk_s3::error::ProvideErrorMetadata;
|
||||
@@ -4375,7 +4378,13 @@ async fn test_bucket_replication_sse_c_contract() -> TestResult {
|
||||
// Without the customer key the replica must not be readable — the direct
|
||||
// detection point for a silent-plaintext replica (backlog#1291).
|
||||
let plain_read = target_client.get_object().bucket(&target_bucket).key(key).send().await;
|
||||
assert!(plain_read.is_err(), "SSE-C replica must not be readable without the customer key");
|
||||
assert_s3_error(
|
||||
plain_read,
|
||||
400,
|
||||
"InvalidRequest",
|
||||
SSE_C_MISSING_PARAMETERS_MESSAGE,
|
||||
"SSE-C replica must not be readable without the customer key",
|
||||
);
|
||||
|
||||
// A wrong customer key must fail too.
|
||||
let wrong_key = BASE64_STANDARD.encode("99999999999999999999999999999999");
|
||||
@@ -4389,7 +4398,13 @@ async fn test_bucket_replication_sse_c_contract() -> TestResult {
|
||||
.sse_customer_key_md5(&wrong_key_md5)
|
||||
.send()
|
||||
.await;
|
||||
assert!(wrong_read.is_err(), "SSE-C replica must reject a wrong customer key");
|
||||
assert_s3_error(
|
||||
wrong_read,
|
||||
400,
|
||||
"InvalidRequest",
|
||||
SSE_C_KEY_MISMATCH_MESSAGE,
|
||||
"SSE-C replica must reject a wrong customer key",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -4486,9 +4501,12 @@ async fn test_bucket_replication_sse_c_multipart_passthrough() -> TestResult {
|
||||
assert_eq!(replica.body.collect().await?.into_bytes().as_ref(), payload.as_slice());
|
||||
|
||||
let plain_read = target_client.get_object().bucket(&target_bucket).key(key).send().await;
|
||||
assert!(
|
||||
plain_read.is_err(),
|
||||
"SSE-C multipart replica must not be readable without the customer key"
|
||||
assert_s3_error(
|
||||
plain_read,
|
||||
400,
|
||||
"InvalidRequest",
|
||||
SSE_C_MISSING_PARAMETERS_MESSAGE,
|
||||
"SSE-C multipart replica must not be readable without the customer key",
|
||||
);
|
||||
|
||||
// Stability across scanner cycles: convergence must hold for passthrough.
|
||||
@@ -4892,15 +4910,12 @@ async fn test_bucket_replication_sse_c_existing_object_resync() -> TestResult {
|
||||
assert_eq!(replica.body.collect().await?.into_bytes().as_ref(), body.as_slice());
|
||||
|
||||
// No plaintext leak: the replica stays unreadable without the key.
|
||||
assert!(
|
||||
target_client
|
||||
.get_object()
|
||||
.bucket(target_bucket)
|
||||
.key(key)
|
||||
.send()
|
||||
.await
|
||||
.is_err(),
|
||||
"SSE-C replica must not be readable without the customer key"
|
||||
assert_s3_error(
|
||||
target_client.get_object().bucket(target_bucket).key(key).send().await,
|
||||
400,
|
||||
"InvalidRequest",
|
||||
SSE_C_MISSING_PARAMETERS_MESSAGE,
|
||||
"SSE-C resynced replica must not be readable without the customer key",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user