fix(storage): harden local SSE-S3 fallback (#3564)

* fix: harden local SSE-S3 fallback

* test: update managed SSE-S3 assumptions

* chore: keep issue plan local only

* test(ci): seed local SSE key for s3-tests
This commit is contained in:
houseme
2026-06-18 15:29:52 +08:00
committed by GitHub
parent c76e4dc6bc
commit fc93a27974
4 changed files with 386 additions and 245 deletions
+3 -3
View File
@@ -2220,11 +2220,11 @@ mod tests {
}
#[tokio::test]
async fn test_get_object_reader_uses_local_managed_fallback_without_env() {
async fn test_get_object_reader_uses_local_managed_fallback_with_explicit_sse_s3_key() {
async_with_vars(
[
("__RUSTFS_SSE_SIMPLE_CMK", None::<String>),
("RUSTFS_SSE_S3_MASTER_KEY", None::<String>),
("RUSTFS_SSE_S3_MASTER_KEY", Some(BASE64_STANDARD.encode([0u8; 32]))),
],
async {
let plaintext = b"managed-local-fallback".to_vec();
@@ -2288,7 +2288,7 @@ mod tests {
&HeaderMap::new(),
)
.await
.expect("managed encrypted reads should fall back to the local SSE-S3 key");
.expect("managed encrypted reads should use the configured local SSE-S3 key");
let mut actual = Vec::new();
reader.read_to_end(&mut actual).await.expect("read managed plaintext");
+171 -160
View File
@@ -1342,6 +1342,7 @@ mod tests {
AMZ_OBJECT_LOCK_LEGAL_HOLD_LOWER, AMZ_OBJECT_LOCK_MODE_LOWER, AMZ_OBJECT_LOCK_RETAIN_UNTIL_DATE_LOWER,
};
use std::{collections::HashMap, io::Cursor};
use temp_env::async_with_vars;
use tokio::io::AsyncReadExt;
fn build_request<T>(input: T, method: Method) -> S3Request<T> {
@@ -1457,172 +1458,182 @@ mod tests {
#[tokio::test]
async fn managed_multipart_roundtrip_preserves_session_nonce_between_parts() {
let prepare_request = PrepareEncryptionRequest {
bucket: "bucket",
key: "object",
server_side_encryption: Some(ServerSideEncryption::from_static(ServerSideEncryption::AES256)),
ssekms_key_id: None,
ssekms_context: None,
sse_customer_algorithm: None,
sse_customer_key: None,
sse_customer_key_md5: None,
};
let session_material = sse_prepare_encryption(prepare_request)
.await
.expect("prepare multipart encryption")
.expect("managed multipart session material");
let mut session_metadata = encryption_material_to_metadata(&session_material);
mark_encrypted_multipart_metadata(&mut session_metadata);
let local_sse_master_key = base64_simd::STANDARD.encode_to_string([0x24u8; 32]);
async_with_vars(
[
("__RUSTFS_SSE_SIMPLE_CMK", None::<String>),
("RUSTFS_SSE_S3_MASTER_KEY", Some(local_sse_master_key)),
],
async {
let prepare_request = PrepareEncryptionRequest {
bucket: "bucket",
key: "object",
server_side_encryption: Some(ServerSideEncryption::from_static(ServerSideEncryption::AES256)),
ssekms_key_id: None,
ssekms_context: None,
sse_customer_algorithm: None,
sse_customer_key: None,
sse_customer_key_md5: None,
};
let session_material = sse_prepare_encryption(prepare_request)
.await
.expect("prepare multipart encryption")
.expect("managed multipart session material");
let mut session_metadata = encryption_material_to_metadata(&session_material);
mark_encrypted_multipart_metadata(&mut session_metadata);
let part_one_plaintext = vec![0x31; rustfs_rio::DEFAULT_ENCRYPTION_BLOCK_SIZE + 23];
let part_two_plaintext = vec![0x32; rustfs_rio::DEFAULT_ENCRYPTION_BLOCK_SIZE * 2 + 7];
let part_one_plaintext = vec![0x31; rustfs_rio::DEFAULT_ENCRYPTION_BLOCK_SIZE + 23];
let part_two_plaintext = vec![0x32; rustfs_rio::DEFAULT_ENCRYPTION_BLOCK_SIZE * 2 + 7];
let part_one_material = sse_decryption(DecryptionRequest {
bucket: "bucket",
key: "object",
metadata: &session_metadata,
sse_customer_key: None,
sse_customer_key_md5: None,
})
.await
.expect("decrypt session one")
.expect("part one material");
let mut encrypted_one = Vec::new();
#[cfg(feature = "rio-v2")]
let mut part_one_reader = match part_one_material.key_kind {
crate::storage::sse::EncryptionKeyKind::Object => EncryptReader::new_multipart_with_object_key(
Cursor::new(part_one_plaintext.clone()),
part_one_material.key_bytes,
1,
),
crate::storage::sse::EncryptionKeyKind::Direct => EncryptReader::new_multipart(
Cursor::new(part_one_plaintext.clone()),
part_one_material.key_bytes,
part_one_material.base_nonce,
1,
),
};
#[cfg(not(feature = "rio-v2"))]
let mut part_one_reader = EncryptReader::new_multipart(
Cursor::new(part_one_plaintext.clone()),
part_one_material.key_bytes,
part_one_material.base_nonce,
1,
);
part_one_reader
.read_to_end(&mut encrypted_one)
.await
.expect("read encrypted part one");
let part_one_material = sse_decryption(DecryptionRequest {
bucket: "bucket",
key: "object",
metadata: &session_metadata,
sse_customer_key: None,
sse_customer_key_md5: None,
})
.await
.expect("decrypt session one")
.expect("part one material");
let mut encrypted_one = Vec::new();
#[cfg(feature = "rio-v2")]
let mut part_one_reader = match part_one_material.key_kind {
crate::storage::sse::EncryptionKeyKind::Object => EncryptReader::new_multipart_with_object_key(
Cursor::new(part_one_plaintext.clone()),
part_one_material.key_bytes,
1,
),
crate::storage::sse::EncryptionKeyKind::Direct => EncryptReader::new_multipart(
Cursor::new(part_one_plaintext.clone()),
part_one_material.key_bytes,
part_one_material.base_nonce,
1,
),
};
#[cfg(not(feature = "rio-v2"))]
let mut part_one_reader = EncryptReader::new_multipart(
Cursor::new(part_one_plaintext.clone()),
part_one_material.key_bytes,
part_one_material.base_nonce,
1,
);
part_one_reader
.read_to_end(&mut encrypted_one)
.await
.expect("read encrypted part one");
let part_two_material = sse_decryption(DecryptionRequest {
bucket: "bucket",
key: "object",
metadata: &session_metadata,
sse_customer_key: None,
sse_customer_key_md5: None,
})
.await
.expect("decrypt session two")
.expect("part two material");
let mut encrypted_two = Vec::new();
#[cfg(feature = "rio-v2")]
let mut part_two_reader = match part_two_material.key_kind {
crate::storage::sse::EncryptionKeyKind::Object => EncryptReader::new_multipart_with_object_key(
Cursor::new(part_two_plaintext.clone()),
part_two_material.key_bytes,
2,
),
crate::storage::sse::EncryptionKeyKind::Direct => EncryptReader::new_multipart(
Cursor::new(part_two_plaintext.clone()),
part_two_material.key_bytes,
part_two_material.base_nonce,
2,
),
};
#[cfg(not(feature = "rio-v2"))]
let mut part_two_reader = EncryptReader::new_multipart(
Cursor::new(part_two_plaintext.clone()),
part_two_material.key_bytes,
part_two_material.base_nonce,
2,
);
part_two_reader
.read_to_end(&mut encrypted_two)
.await
.expect("read encrypted part two");
let part_two_material = sse_decryption(DecryptionRequest {
bucket: "bucket",
key: "object",
metadata: &session_metadata,
sse_customer_key: None,
sse_customer_key_md5: None,
})
.await
.expect("decrypt session two")
.expect("part two material");
let mut encrypted_two = Vec::new();
#[cfg(feature = "rio-v2")]
let mut part_two_reader = match part_two_material.key_kind {
crate::storage::sse::EncryptionKeyKind::Object => EncryptReader::new_multipart_with_object_key(
Cursor::new(part_two_plaintext.clone()),
part_two_material.key_bytes,
2,
),
crate::storage::sse::EncryptionKeyKind::Direct => EncryptReader::new_multipart(
Cursor::new(part_two_plaintext.clone()),
part_two_material.key_bytes,
part_two_material.base_nonce,
2,
),
};
#[cfg(not(feature = "rio-v2"))]
let mut part_two_reader = EncryptReader::new_multipart(
Cursor::new(part_two_plaintext.clone()),
part_two_material.key_bytes,
part_two_material.base_nonce,
2,
);
part_two_reader
.read_to_end(&mut encrypted_two)
.await
.expect("read encrypted part two");
if session_material.key_kind == crate::storage::sse::EncryptionKeyKind::Object {
assert!(session_metadata.contains_key("X-Minio-Internal-Encrypted-Multipart"));
assert!(session_metadata.contains_key("X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key"));
} else {
assert!(session_metadata.contains_key("x-rustfs-encryption-iv"));
}
if session_material.key_kind == crate::storage::sse::EncryptionKeyKind::Object {
assert!(session_metadata.contains_key("X-Minio-Internal-Encrypted-Multipart"));
assert!(session_metadata.contains_key("X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key"));
} else {
assert!(session_metadata.contains_key("x-rustfs-encryption-iv"));
}
let parts = vec![
ObjectPartInfo {
number: 1,
size: encrypted_one.len(),
actual_size: part_one_plaintext.len() as i64,
..Default::default()
let parts = vec![
ObjectPartInfo {
number: 1,
size: encrypted_one.len(),
actual_size: part_one_plaintext.len() as i64,
..Default::default()
},
ObjectPartInfo {
number: 2,
size: encrypted_two.len(),
actual_size: part_two_plaintext.len() as i64,
..Default::default()
},
];
let mut encrypted_stream = Vec::with_capacity(encrypted_one.len() + encrypted_two.len());
encrypted_stream.extend_from_slice(&encrypted_one);
encrypted_stream.extend_from_slice(&encrypted_two);
let decryption_material = sse_decryption(DecryptionRequest {
bucket: "bucket",
key: "object",
metadata: &session_metadata,
sse_customer_key: None,
sse_customer_key_md5: None,
})
.await
.expect("decrypt multipart")
.expect("managed decryption material");
let plaintext_size = multipart_plaintext_size(&parts, -1);
#[cfg(feature = "rio-v2")]
let decrypted_stream = match decryption_material.key_kind {
crate::storage::sse::EncryptionKeyKind::Object => boxed_reader(DecryptReader::new_multipart_with_object_key(
wrap_reader(Cursor::new(encrypted_stream)),
decryption_material.key_bytes,
multipart_part_numbers(&parts),
)),
crate::storage::sse::EncryptionKeyKind::Direct => boxed_reader(DecryptReader::new_multipart(
wrap_reader(Cursor::new(encrypted_stream)),
decryption_material.key_bytes,
decryption_material.base_nonce,
multipart_part_numbers(&parts),
)),
};
#[cfg(not(feature = "rio-v2"))]
let decrypted_stream = boxed_reader(DecryptReader::new_multipart(
wrap_reader(Cursor::new(encrypted_stream)),
decryption_material.key_bytes,
decryption_material.base_nonce,
multipart_part_numbers(&parts),
));
let mut decrypted_reader = HardLimitReader::new(decrypted_stream, plaintext_size);
let mut decrypted = Vec::new();
decrypted_reader
.read_to_end(&mut decrypted)
.await
.expect("read decrypted multipart data");
let mut expected = part_one_plaintext;
expected.extend_from_slice(&part_two_plaintext);
assert_eq!(plaintext_size, expected.len() as i64);
assert_eq!(decrypted, expected);
},
ObjectPartInfo {
number: 2,
size: encrypted_two.len(),
actual_size: part_two_plaintext.len() as i64,
..Default::default()
},
];
let mut encrypted_stream = Vec::with_capacity(encrypted_one.len() + encrypted_two.len());
encrypted_stream.extend_from_slice(&encrypted_one);
encrypted_stream.extend_from_slice(&encrypted_two);
let decryption_material = sse_decryption(DecryptionRequest {
bucket: "bucket",
key: "object",
metadata: &session_metadata,
sse_customer_key: None,
sse_customer_key_md5: None,
})
.await
.expect("decrypt multipart")
.expect("managed decryption material");
let plaintext_size = multipart_plaintext_size(&parts, -1);
#[cfg(feature = "rio-v2")]
let decrypted_stream = match decryption_material.key_kind {
crate::storage::sse::EncryptionKeyKind::Object => boxed_reader(DecryptReader::new_multipart_with_object_key(
wrap_reader(Cursor::new(encrypted_stream)),
decryption_material.key_bytes,
multipart_part_numbers(&parts),
)),
crate::storage::sse::EncryptionKeyKind::Direct => boxed_reader(DecryptReader::new_multipart(
wrap_reader(Cursor::new(encrypted_stream)),
decryption_material.key_bytes,
decryption_material.base_nonce,
multipart_part_numbers(&parts),
)),
};
#[cfg(not(feature = "rio-v2"))]
let decrypted_stream = boxed_reader(DecryptReader::new_multipart(
wrap_reader(Cursor::new(encrypted_stream)),
decryption_material.key_bytes,
decryption_material.base_nonce,
multipart_part_numbers(&parts),
));
let mut decrypted_reader = HardLimitReader::new(decrypted_stream, plaintext_size);
let mut decrypted = Vec::new();
decrypted_reader
.read_to_end(&mut decrypted)
.await
.expect("read decrypted multipart data");
let mut expected = part_one_plaintext;
expected.extend_from_slice(&part_two_plaintext);
assert_eq!(plaintext_size, expected.len() as i64);
assert_eq!(decrypted, expected);
)
.await;
}
#[tokio::test]
+204 -82
View File
@@ -1876,30 +1876,27 @@ impl TestSseDekProvider {
}
/// Create a local SSE DEK provider for SSE-S3 when KMS is not configured.
/// Uses RUSTFS_SSE_S3_MASTER_KEY (base64 32-byte) if set; otherwise a built-in default.
/// Allows PUT/GET to work without KMS (backward compatible).
pub fn new_for_local_sse() -> Self {
let master_key = match get_env_opt_str("RUSTFS_SSE_S3_MASTER_KEY") {
Some(v) if !v.trim().is_empty() => match BASE64_STANDARD.decode(v.trim()) {
Ok(decoded) if decoded.len() == 32 => {
let mut arr = [0u8; 32];
arr.copy_from_slice(&decoded[..32]);
tracing::info!("Using RUSTFS_SSE_S3_MASTER_KEY for SSE-S3 (KMS not configured)");
arr
}
_ => {
tracing::warn!("RUSTFS_SSE_S3_MASTER_KEY invalid (expected base64 32 bytes); using default for SSE-S3");
[0u8; 32]
}
},
_ => {
tracing::debug!(
"KMS not configured; using built-in default key for SSE-S3 (set RUSTFS_SSE_S3_MASTER_KEY for production)"
);
[0u8; 32]
}
/// Requires RUSTFS_SSE_S3_MASTER_KEY to be a valid base64-encoded 32-byte key.
pub fn new_for_local_sse() -> Result<Self, ApiError> {
let Some(raw_value) = get_env_opt_str("RUSTFS_SSE_S3_MASTER_KEY").filter(|value| !value.trim().is_empty()) else {
return Err(ApiError::from(StorageError::other(
"SSE-S3 requires RUSTFS_SSE_S3_MASTER_KEY to be set to a base64-encoded 32-byte key when KMS is not configured",
)));
};
Self { master_key }
let decoded = BASE64_STANDARD.decode(raw_value.trim()).map_err(|err| {
ApiError::from(StorageError::other(format!(
"RUSTFS_SSE_S3_MASTER_KEY must be valid base64 for SSE-S3 when KMS is not configured: {err}"
)))
})?;
let master_key: [u8; 32] = decoded.try_into().map_err(|_| {
ApiError::from(StorageError::other(
"RUSTFS_SSE_S3_MASTER_KEY must decode to exactly 32 bytes for SSE-S3 when KMS is not configured",
))
})?;
tracing::info!("Using RUSTFS_SSE_S3_MASTER_KEY for SSE-S3 (KMS not configured)");
Ok(Self { master_key })
}
// Simple encryption of DEK
@@ -1908,7 +1905,9 @@ impl TestSseDekProvider {
let key = Key::<Aes256Gcm>::from(cmk_value);
let cipher = Aes256Gcm::new(&key);
let nonce = Nonce::from([0u8; 12]);
let mut nonce_bytes = [0u8; 12];
rand::rng().fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from(nonce_bytes);
let ciphertext = cipher
.encrypt(&nonce, dek.as_slice())
.map_err(|_| ApiError::from(StorageError::other("Failed to encrypt DEK")))?;
@@ -2038,7 +2037,7 @@ pub async fn get_sse_dek_provider() -> Result<Arc<dyn SseDekProvider>, ApiError>
Arc::new(TestSseDekProvider::new())
} else {
debug!("Using local SSE-S3 provider (KMS not configured)");
Arc::new(TestSseDekProvider::new_for_local_sse())
Arc::new(TestSseDekProvider::new_for_local_sse()?)
};
let mut slot = GLOBAL_SSE_DEK_PROVIDER
@@ -2334,6 +2333,7 @@ mod tests {
use http::HeaderValue;
use rustfs_rio::{DecryptReader, EncryptReader};
use std::sync::OnceLock;
use temp_env::async_with_vars;
use tokio::sync::Mutex;
static SSE_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
@@ -2342,6 +2342,10 @@ mod tests {
SSE_TEST_LOCK.get_or_init(|| Mutex::new(())).lock().await
}
fn local_sse_master_key_b64() -> String {
BASE64_STANDARD.encode([0x24u8; 32])
}
#[test]
fn test_extract_ssec_params_from_headers() {
let mut headers = http::HeaderMap::new();
@@ -2961,26 +2965,41 @@ mod tests {
#[tokio::test]
async fn test_sse_encryption_omits_kms_header_for_sse_s3_objects() {
let request = EncryptionRequest {
bucket: "test-bucket",
key: "test-key",
server_side_encryption: Some(ServerSideEncryption::from_static(ServerSideEncryption::AES256)),
ssekms_key_id: None,
ssekms_context: None,
sse_customer_algorithm: None,
sse_customer_key: None,
sse_customer_key_md5: None,
content_size: 1024,
};
let _guard = lock_sse_test_state().await;
reset_sse_dek_provider();
let local_sse_master_key = local_sse_master_key_b64();
let material = sse_encryption(request).await.expect("sse-s3 encryption should succeed");
let material = material.expect("managed sse-s3 encryption should return material");
let metadata = encryption_material_to_metadata(&material);
async_with_vars(
[
("__RUSTFS_SSE_SIMPLE_CMK", None::<&str>),
("RUSTFS_SSE_S3_MASTER_KEY", Some(local_sse_master_key.as_str())),
],
async {
let request = EncryptionRequest {
bucket: "test-bucket",
key: "test-key",
server_side_encryption: Some(ServerSideEncryption::from_static(ServerSideEncryption::AES256)),
ssekms_key_id: None,
ssekms_context: None,
sse_customer_algorithm: None,
sse_customer_key: None,
sse_customer_key_md5: None,
content_size: 1024,
};
assert_eq!(material.kms_key_id, None);
assert_eq!(metadata.get("x-amz-server-side-encryption").map(String::as_str), Some("AES256"));
assert!(!metadata.contains_key("x-amz-server-side-encryption-aws-kms-key-id"));
assert_eq!(metadata.get(INTERNAL_ENCRYPTION_KEY_ID_HEADER).map(String::as_str), Some("default"));
let material = sse_encryption(request).await.expect("sse-s3 encryption should succeed");
let material = material.expect("managed sse-s3 encryption should return material");
let metadata = encryption_material_to_metadata(&material);
assert_eq!(material.kms_key_id, None);
assert_eq!(metadata.get("x-amz-server-side-encryption").map(String::as_str), Some("AES256"));
assert!(!metadata.contains_key("x-amz-server-side-encryption-aws-kms-key-id"));
assert_eq!(metadata.get(INTERNAL_ENCRYPTION_KEY_ID_HEADER).map(String::as_str), Some("default"));
},
)
.await;
reset_sse_dek_provider();
}
#[test]
@@ -3037,51 +3056,63 @@ mod tests {
if let Some(manager) = rustfs_kms::get_global_kms_service_manager() {
let _ = manager.stop().await;
}
let local_sse_master_key = local_sse_master_key_b64();
let request = EncryptionRequest {
bucket: "bucket",
key: "object",
server_side_encryption: Some(ServerSideEncryption::from_static(ServerSideEncryption::AES256)),
ssekms_key_id: None,
ssekms_context: None,
sse_customer_algorithm: None,
sse_customer_key: None,
sse_customer_key_md5: None,
content_size: 4096,
};
async_with_vars(
[
("__RUSTFS_SSE_SIMPLE_CMK", None::<&str>),
("RUSTFS_SSE_S3_MASTER_KEY", Some(local_sse_master_key.as_str())),
],
async {
let request = EncryptionRequest {
bucket: "bucket",
key: "object",
server_side_encryption: Some(ServerSideEncryption::from_static(ServerSideEncryption::AES256)),
ssekms_key_id: None,
ssekms_context: None,
sse_customer_algorithm: None,
sse_customer_key: None,
sse_customer_key_md5: None,
content_size: 4096,
};
let material = sse_encryption(request)
.await
.expect("managed sse encryption")
.expect("managed sse material");
assert_eq!(material.key_kind, EncryptionKeyKind::Object);
let material = sse_encryption(request)
.await
.expect("managed sse encryption")
.expect("managed sse material");
assert_eq!(material.key_kind, EncryptionKeyKind::Object);
let metadata = encryption_material_to_metadata(&material);
assert!(!metadata.contains_key(INTERNAL_ENCRYPTION_KEY_HEADER));
assert!(!metadata.contains_key(INTERNAL_ENCRYPTION_IV_HEADER));
let metadata = encryption_material_to_metadata(&material);
assert!(!metadata.contains_key(INTERNAL_ENCRYPTION_KEY_HEADER));
assert!(!metadata.contains_key(INTERNAL_ENCRYPTION_IV_HEADER));
let sealing_iv = metadata
.get(MINIO_INTERNAL_ENCRYPTION_IV_HEADER)
.expect("minio sealing iv should be stored");
let sealed_key = metadata
.get(MINIO_INTERNAL_ENCRYPTION_S3_SEALED_KEY_HEADER)
.expect("minio sealed key should be stored");
assert_eq!(BASE64_STANDARD.decode(sealing_iv).expect("decode iv").len(), SEALED_KEY_IV_SIZE);
assert_eq!(BASE64_STANDARD.decode(sealed_key).expect("decode sealed key").len(), SEALED_KEY_SIZE);
let sealing_iv = metadata
.get(MINIO_INTERNAL_ENCRYPTION_IV_HEADER)
.expect("minio sealing iv should be stored");
let sealed_key = metadata
.get(MINIO_INTERNAL_ENCRYPTION_S3_SEALED_KEY_HEADER)
.expect("minio sealed key should be stored");
assert_eq!(BASE64_STANDARD.decode(sealing_iv).expect("decode iv").len(), SEALED_KEY_IV_SIZE);
assert_eq!(BASE64_STANDARD.decode(sealed_key).expect("decode sealed key").len(), SEALED_KEY_SIZE);
let decrypted = sse_decryption(DecryptionRequest {
bucket: "bucket",
key: "object",
metadata: &metadata,
sse_customer_key: None,
sse_customer_key_md5: None,
})
.await
.expect("managed sse decryption")
.expect("managed decryption material");
let decrypted = sse_decryption(DecryptionRequest {
bucket: "bucket",
key: "object",
metadata: &metadata,
sse_customer_key: None,
sse_customer_key_md5: None,
})
.await
.expect("managed sse decryption")
.expect("managed decryption material");
assert_eq!(decrypted.key_kind, EncryptionKeyKind::Object);
assert_eq!(decrypted.key_bytes, material.key_bytes);
assert_eq!(decrypted.key_kind, EncryptionKeyKind::Object);
assert_eq!(decrypted.key_bytes, material.key_bytes);
},
)
.await;
reset_sse_dek_provider();
}
#[cfg(feature = "rio-v2")]
@@ -3381,6 +3412,97 @@ mod tests {
println!("✅ Different nonces produce different ciphertext - test passed!");
}
#[test]
fn test_encrypt_dek_uses_random_nonce_prefixes() {
let dek = [0x11u8; 32];
let cmk = [0x22u8; 32];
let encrypted_a = TestSseDekProvider::encrypt_dek(dek, cmk).expect("first DEK wrap should succeed");
let encrypted_b = TestSseDekProvider::encrypt_dek(dek, cmk).expect("second DEK wrap should succeed");
let nonce_a = encrypted_a.split(':').next().expect("wrapped DEK should contain nonce");
let nonce_b = encrypted_b.split(':').next().expect("wrapped DEK should contain nonce");
assert_ne!(nonce_a, nonce_b, "each DEK wrap should use a distinct nonce prefix");
}
#[test]
fn test_decrypt_dek_accepts_legacy_zero_nonce_payload() {
let dek = [0x33u8; 32];
let cmk = [0x44u8; 32];
let cipher = Aes256Gcm::new(&Key::<Aes256Gcm>::from(cmk));
let legacy_nonce = Nonce::from([0u8; 12]);
let ciphertext = cipher
.encrypt(&legacy_nonce, dek.as_slice())
.expect("legacy wrap should succeed");
let legacy_payload = format!("{}:{}", BASE64_STANDARD.encode(legacy_nonce), BASE64_STANDARD.encode(ciphertext));
let decrypted = TestSseDekProvider::decrypt_dek(&legacy_payload, cmk).expect("legacy payload should remain decryptable");
assert_eq!(decrypted, dek);
}
#[tokio::test]
async fn test_sse_encryption_fails_closed_without_local_sse_master_key() {
let _guard = lock_sse_test_state().await;
reset_sse_dek_provider();
async_with_vars(
[
("__RUSTFS_SSE_SIMPLE_CMK", None::<&str>),
("RUSTFS_SSE_S3_MASTER_KEY", None::<&str>),
],
async {
let err = sse_encryption(EncryptionRequest {
bucket: "test-bucket",
key: "test-key",
server_side_encryption: Some(ServerSideEncryption::from_static(ServerSideEncryption::AES256)),
ssekms_key_id: None,
ssekms_context: None,
sse_customer_algorithm: None,
sse_customer_key: None,
sse_customer_key_md5: None,
content_size: 1024,
})
.await
.expect_err("SSE-S3 should fail closed without a configured local master key");
assert!(err.message.contains("RUSTFS_SSE_S3_MASTER_KEY"));
},
)
.await;
reset_sse_dek_provider();
}
#[tokio::test]
async fn test_sse_encryption_fails_closed_with_invalid_local_sse_master_key() {
let _guard = lock_sse_test_state().await;
reset_sse_dek_provider();
async_with_vars(
[
("__RUSTFS_SSE_SIMPLE_CMK", None::<&str>),
("RUSTFS_SSE_S3_MASTER_KEY", Some("not-base64")),
],
async {
let err = sse_encryption(EncryptionRequest {
bucket: "test-bucket",
key: "test-key",
server_side_encryption: Some(ServerSideEncryption::from_static(ServerSideEncryption::AES256)),
ssekms_key_id: None,
ssekms_context: None,
sse_customer_algorithm: None,
sse_customer_key: None,
sse_customer_key_md5: None,
content_size: 1024,
})
.await
.expect_err("SSE-S3 should fail closed with an invalid local master key");
assert!(err.message.contains("valid base64"));
},
)
.await;
reset_sse_dek_provider();
}
#[tokio::test]
async fn test_simple_sse_dek_provider_decrypt_with_encrypted_dek() {
use std::io::Cursor;
+8
View File
@@ -196,6 +196,7 @@ S3TESTS_CONF="${S3TESTS_CONF:-s3tests.conf}"
DEPLOY_MODE="${DEPLOY_MODE:-build}"
RUSTFS_BINARY="${RUSTFS_BINARY:-}"
NO_CACHE="${NO_CACHE:-false}"
S3TESTS_LOCAL_SSE_MASTER_KEY_DEFAULT="MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY="
# Additional directories (SCRIPT_DIR and PROJECT_ROOT defined earlier)
ARTIFACTS_DIR="${PROJECT_ROOT}/artifacts/s3tests-${TEST_MODE}"
@@ -210,6 +211,11 @@ fi
DATA_DIR="${DATA_BASE}/test-data/${CONTAINER_NAME}"
RUSTFS_PID=""
if [ "${DEPLOY_MODE}" != "existing" ] && [ -z "${RUSTFS_SSE_S3_MASTER_KEY:-}" ]; then
export RUSTFS_SSE_S3_MASTER_KEY="${S3TESTS_LOCAL_SSE_MASTER_KEY_DEFAULT}"
log_info "Using deterministic local SSE-S3 master key for the s3-tests harness"
fi
show_usage() {
cat << EOF
Usage: $0 [OPTIONS]
@@ -233,6 +239,7 @@ Environment Variables:
S3_SECRET_KEY - Main user secret key (default: rustfsadmin)
S3_ALT_ACCESS_KEY - Alt user access key (default: rustfsalt)
S3_ALT_SECRET_KEY - Alt user secret key (default: rustfsalt)
RUSTFS_SSE_S3_MASTER_KEY - Optional base64 32-byte key for local managed SSE fallback
MAXFAIL - Stop after N failures (default: 1)
XDIST - Enable parallel execution with N workers (default: 0)
MARKEXPR - pytest marker expression (default: no marker filtering)
@@ -449,6 +456,7 @@ elif [ "${DEPLOY_MODE}" = "docker" ]; then
-e RUSTFS_ADDRESS=0.0.0.0:9000 \
-e RUSTFS_ACCESS_KEY="${S3_ACCESS_KEY}" \
-e RUSTFS_SECRET_KEY="${S3_SECRET_KEY}" \
-e RUSTFS_SSE_S3_MASTER_KEY="${RUSTFS_SSE_S3_MASTER_KEY}" \
-e RUSTFS_VOLUMES="/data/rustfs0 /data/rustfs1 /data/rustfs2 /data/rustfs3" \
-v "/tmp/${CONTAINER_NAME}:/data" \
rustfs-ci || {