mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
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:
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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 || {
|
||||
|
||||
Reference in New Issue
Block a user