mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-10 07:06:53 +00:00
fix(ecstore): support MinIO DARE fixture compatibility (#3590)
This commit is contained in:
@@ -2887,7 +2887,9 @@ mod tests {
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn expiry_enqueue_reports_missed_without_worker_channel() {
|
||||
let before = global_metrics().report().await.lifecycle_expiry;
|
||||
let state = ExpiryState::new();
|
||||
let mut state = state.write().await;
|
||||
let object = ObjectInfo {
|
||||
@@ -2904,12 +2906,9 @@ mod tests {
|
||||
|
||||
assert!(!queued);
|
||||
assert_eq!(state.stats.missed_tasks(), 1);
|
||||
let expiry = global_metrics().report().await.lifecycle_expiry;
|
||||
assert_eq!(expiry.current_queue_capacity, 0);
|
||||
assert_eq!(expiry.current_queued, 0);
|
||||
assert_eq!(expiry.current_active, 0);
|
||||
assert_eq!(expiry.current_workers, 0);
|
||||
assert_eq!(expiry.queue_missed, 1);
|
||||
let after = global_metrics().report().await.lifecycle_expiry;
|
||||
assert!(after.queue_missed >= before.queue_missed.saturating_add(1));
|
||||
assert!(after.scanner_missed >= before.scanner_missed.saturating_add(1));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -154,6 +154,13 @@ pub fn get_global_endpoints_opt() -> Option<EndpointServerPools> {
|
||||
GLOBAL_Endpoints.get().cloned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub async fn reset_local_disk_test_state() {
|
||||
GLOBAL_LOCAL_DISK_MAP.write().await.clear();
|
||||
GLOBAL_LOCAL_DISK_ID_MAP.write().await.clear();
|
||||
GLOBAL_LOCAL_DISK_SET_DRIVES.write().await.clear();
|
||||
}
|
||||
|
||||
pub async fn is_first_cluster_node_local() -> bool {
|
||||
get_global_endpoints().first_local()
|
||||
}
|
||||
|
||||
@@ -1082,7 +1082,7 @@ mod tests {
|
||||
use crate::disk::endpoint::Endpoint;
|
||||
use crate::disk::local::LocalDisk;
|
||||
use crate::endpoints::{Endpoints, PoolEndpoints};
|
||||
use crate::global::{GLOBAL_LOCAL_DISK_ID_MAP, GLOBAL_LOCAL_DISK_MAP, GLOBAL_LOCAL_DISK_SET_DRIVES};
|
||||
use crate::global::reset_local_disk_test_state;
|
||||
use crate::store::init_local_disks;
|
||||
use rustfs_filemeta::FileInfo;
|
||||
use serial_test::serial;
|
||||
@@ -1110,12 +1110,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
async fn reset_local_disk_globals() {
|
||||
GLOBAL_LOCAL_DISK_MAP.write().await.clear();
|
||||
GLOBAL_LOCAL_DISK_ID_MAP.write().await.clear();
|
||||
GLOBAL_LOCAL_DISK_SET_DRIVES.write().await.clear();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestPeerS3Client {
|
||||
pools: Option<Vec<usize>>,
|
||||
@@ -1292,7 +1286,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn local_get_bucket_info_survives_prior_walk_timeout() {
|
||||
reset_local_disk_globals().await;
|
||||
reset_local_disk_test_state().await;
|
||||
|
||||
let temp_dir = TempDir::new().expect("create temp dir for local peer listing regression");
|
||||
let disks = init_test_local_disks(&temp_dir, 1, "local-get-bucket-info-survives-prior-walk-timeout").await;
|
||||
@@ -1337,13 +1331,13 @@ mod tests {
|
||||
})
|
||||
.await;
|
||||
|
||||
reset_local_disk_globals().await;
|
||||
reset_local_disk_test_state().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn local_get_bucket_info_requires_local_write_quorum() {
|
||||
reset_local_disk_globals().await;
|
||||
reset_local_disk_test_state().await;
|
||||
|
||||
let temp_dir = TempDir::new().expect("create temp dir for partial bucket regression");
|
||||
let disks = init_test_local_disks(&temp_dir, 2, "local-get-bucket-info-requires-local-write-quorum").await;
|
||||
@@ -1360,13 +1354,13 @@ mod tests {
|
||||
|
||||
assert_eq!(err, Error::ErasureWriteQuorum);
|
||||
|
||||
reset_local_disk_globals().await;
|
||||
reset_local_disk_test_state().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn local_peer_filters_disks_by_pool() {
|
||||
reset_local_disk_globals().await;
|
||||
reset_local_disk_test_state().await;
|
||||
|
||||
let temp_dir = TempDir::new().expect("create temp dir for pool filtered local peer regression");
|
||||
let disks = init_test_local_disks_for_pools(&temp_dir, &[(0, 2), (1, 2)], "local-peer-filters-disks-by-pool").await;
|
||||
@@ -1399,13 +1393,13 @@ mod tests {
|
||||
.expect("pool 1 local listing should succeed against its own disks");
|
||||
assert!(pool1_buckets.is_empty());
|
||||
|
||||
reset_local_disk_globals().await;
|
||||
reset_local_disk_test_state().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn heal_bucket_local_recreates_missing_bucket_volumes() {
|
||||
reset_local_disk_globals().await;
|
||||
reset_local_disk_test_state().await;
|
||||
|
||||
let temp_dir = TempDir::new().expect("create temp dir for bucket heal regression");
|
||||
let disks = init_test_local_disks(&temp_dir, 2, "heal-bucket-local-recreates-missing-bucket-volumes").await;
|
||||
@@ -1434,7 +1428,7 @@ mod tests {
|
||||
disk.stat_volume(bucket).await.expect("bucket should exist after heal");
|
||||
}
|
||||
|
||||
reset_local_disk_globals().await;
|
||||
reset_local_disk_test_state().await;
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -841,17 +841,11 @@ impl ServerPoolsAvailableSpace {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::endpoints::{Endpoints, PoolEndpoints};
|
||||
use crate::global::{GLOBAL_LOCAL_DISK_ID_MAP, GLOBAL_LOCAL_DISK_MAP, GLOBAL_LOCAL_DISK_SET_DRIVES};
|
||||
use crate::global::{GLOBAL_LOCAL_DISK_ID_MAP, reset_local_disk_test_state};
|
||||
use crate::store_init::{connect_load_init_formats, init_disks};
|
||||
use serial_test::serial;
|
||||
use tempfile::TempDir;
|
||||
|
||||
async fn reset_local_disk_globals() {
|
||||
GLOBAL_LOCAL_DISK_MAP.write().await.clear();
|
||||
GLOBAL_LOCAL_DISK_ID_MAP.write().await.clear();
|
||||
GLOBAL_LOCAL_DISK_SET_DRIVES.write().await.clear();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_get_disk_infos() {
|
||||
let disks = vec![None, None]; // Empty disks for testing
|
||||
@@ -906,7 +900,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_find_local_disk_by_ref_backfills_uuid_map() {
|
||||
reset_local_disk_globals().await;
|
||||
reset_local_disk_test_state().await;
|
||||
|
||||
let temp_dir = TempDir::new().expect("create temp dir for local disk ref test");
|
||||
let disk_paths = (0..4)
|
||||
@@ -967,7 +961,7 @@ mod tests {
|
||||
Some(first_disk.endpoint().to_string())
|
||||
);
|
||||
|
||||
reset_local_disk_globals().await;
|
||||
reset_local_disk_test_state().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use super::*;
|
||||
#[cfg(feature = "rio-v2")]
|
||||
use aes_gcm::aead::Payload;
|
||||
use aes_gcm::{
|
||||
Aes256Gcm, Key, Nonce,
|
||||
aead::{Aead, KeyInit},
|
||||
};
|
||||
use base64::{Engine, engine::general_purpose::STANDARD as BASE64_STANDARD};
|
||||
#[cfg(feature = "rio-v2")]
|
||||
use chacha20poly1305::ChaCha20Poly1305;
|
||||
#[cfg(feature = "rio-v2")]
|
||||
use hmac::{Hmac, Mac};
|
||||
use md5::{Digest, Md5};
|
||||
use rustfs_kms::{service_manager::get_global_encryption_service, types::ObjectEncryptionContext};
|
||||
@@ -53,6 +57,8 @@ const DARE_VERSION_20: u8 = 0x20;
|
||||
#[cfg(feature = "rio-v2")]
|
||||
const DARE_CIPHER_AES_256_GCM: u8 = 0x00;
|
||||
#[cfg(feature = "rio-v2")]
|
||||
const DARE_CIPHER_CHACHA20_POLY1305: u8 = 0x01;
|
||||
#[cfg(feature = "rio-v2")]
|
||||
const DARE_HEADER_SIZE: usize = 16;
|
||||
#[cfg(feature = "rio-v2")]
|
||||
const DARE_TAG_SIZE: usize = 16;
|
||||
@@ -1025,6 +1031,35 @@ fn metadata_get<'a>(metadata: &'a HashMap<String, String>, key: &str) -> Option<
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "rio-v2")]
|
||||
fn is_supported_sealed_object_key_cipher(cipher: u8) -> bool {
|
||||
matches!(cipher, DARE_CIPHER_AES_256_GCM | DARE_CIPHER_CHACHA20_POLY1305)
|
||||
}
|
||||
|
||||
#[cfg(feature = "rio-v2")]
|
||||
fn decrypt_sealed_object_key_payload(sealing_key: [u8; 32], header: &[u8], sealed_key: &[u8]) -> Result<Vec<u8>> {
|
||||
let nonce = &header[4..16];
|
||||
let ciphertext = &sealed_key[DARE_HEADER_SIZE..];
|
||||
let aad = &header[..4];
|
||||
match header[1] {
|
||||
DARE_CIPHER_AES_256_GCM => {
|
||||
let cipher = Aes256Gcm::new_from_slice(&sealing_key)
|
||||
.map_err(|err| Error::other(format!("invalid AES-GCM sealing key: {err}")))?;
|
||||
let nonce = Nonce::try_from(nonce).map_err(|_| Error::other("invalid sealed object-key package nonce"))?;
|
||||
cipher.decrypt(&nonce, Payload { msg: ciphertext, aad })
|
||||
}
|
||||
DARE_CIPHER_CHACHA20_POLY1305 => {
|
||||
let cipher = ChaCha20Poly1305::new_from_slice(&sealing_key)
|
||||
.map_err(|err| Error::other(format!("invalid ChaCha20-Poly1305 sealing key: {err}")))?;
|
||||
let nonce =
|
||||
chacha20poly1305::Nonce::try_from(nonce).map_err(|_| Error::other("invalid sealed object-key package nonce"))?;
|
||||
cipher.decrypt(&nonce, Payload { msg: ciphertext, aad })
|
||||
}
|
||||
_ => return Err(Error::other("unsupported sealed object-key DARE header")),
|
||||
}
|
||||
.map_err(|err| Error::other(format!("failed to unseal object key: {err}")))
|
||||
}
|
||||
|
||||
async fn resolve_encryption_material(oi: &ObjectInfo, headers: &HeaderMap<HeaderValue>) -> Result<EncryptionMaterial> {
|
||||
if metadata_get(&oi.user_defined, SSEC_ALGORITHM_HEADER).is_some() {
|
||||
return resolve_ssec_material(oi, headers);
|
||||
@@ -1154,7 +1189,7 @@ fn try_unseal_minio_object_key(
|
||||
return Ok(None);
|
||||
};
|
||||
let header = &sealed_key[..DARE_HEADER_SIZE];
|
||||
if header[0] != DARE_VERSION_20 || header[1] != DARE_CIPHER_AES_256_GCM {
|
||||
if header[0] != DARE_VERSION_20 || !is_supported_sealed_object_key_cipher(header[1]) {
|
||||
return Err(Error::other("unsupported sealed object-key DARE header"));
|
||||
}
|
||||
if u16::from_le_bytes([header[2], header[3]]) != 31 || header[4] & 0x80 == 0 {
|
||||
@@ -1162,17 +1197,7 @@ fn try_unseal_minio_object_key(
|
||||
}
|
||||
|
||||
let sealing_key = derive_sealing_key(external_key, iv, managed_sse_domain(metadata), bucket, object);
|
||||
let cipher = Aes256Gcm::new_from_slice(&sealing_key).map_err(|err| Error::other(format!("invalid sealing key: {err}")))?;
|
||||
let nonce = Nonce::try_from(&header[4..16]).map_err(|_| Error::other("invalid sealed object-key package nonce"))?;
|
||||
let plaintext = cipher
|
||||
.decrypt(
|
||||
&nonce,
|
||||
aes_gcm::aead::Payload {
|
||||
msg: &sealed_key[DARE_HEADER_SIZE..],
|
||||
aad: &header[..4],
|
||||
},
|
||||
)
|
||||
.map_err(|err| Error::other(format!("failed to unseal object key: {err}")))?;
|
||||
let plaintext = decrypt_sealed_object_key_payload(sealing_key, header, &sealed_key)?;
|
||||
let object_key: [u8; 32] = plaintext
|
||||
.as_slice()
|
||||
.try_into()
|
||||
@@ -1888,34 +1913,70 @@ mod tests {
|
||||
object: &str,
|
||||
data_key: [u8; 32],
|
||||
object_key: [u8; 32],
|
||||
) -> ([u8; 32], Vec<u8>) {
|
||||
seal_managed_s3_object_key_for_test_with_cipher(bucket, object, data_key, object_key, DARE_CIPHER_AES_256_GCM)
|
||||
}
|
||||
|
||||
#[cfg(feature = "rio-v2")]
|
||||
fn seal_managed_s3_object_key_for_test_with_cipher(
|
||||
bucket: &str,
|
||||
object: &str,
|
||||
data_key: [u8; 32],
|
||||
object_key: [u8; 32],
|
||||
cipher_id: u8,
|
||||
) -> ([u8; 32], Vec<u8>) {
|
||||
let iv = [0x24u8; SEALED_KEY_IV_SIZE];
|
||||
let sealing_key = derive_sealing_key(data_key, iv, "SSE-S3", bucket, object);
|
||||
let cipher = Aes256Gcm::new_from_slice(&sealing_key).expect("valid sealing key");
|
||||
|
||||
let mut header = [0u8; DARE_HEADER_SIZE];
|
||||
header[0] = DARE_VERSION_20;
|
||||
header[1] = DARE_CIPHER_AES_256_GCM;
|
||||
header[1] = cipher_id;
|
||||
header[2..4].copy_from_slice(&31u16.to_le_bytes());
|
||||
header[4] = 0x80;
|
||||
header[5..16].copy_from_slice(&[0x46u8; 11]);
|
||||
|
||||
let nonce = Nonce::try_from(&header[4..16]).expect("valid nonce");
|
||||
let ciphertext = match cipher_id {
|
||||
DARE_CIPHER_AES_256_GCM => {
|
||||
let cipher = Aes256Gcm::new_from_slice(&sealing_key).expect("valid sealing key");
|
||||
let nonce = Nonce::try_from(&header[4..16]).expect("valid nonce");
|
||||
cipher
|
||||
.encrypt(
|
||||
&nonce,
|
||||
Payload {
|
||||
msg: &object_key,
|
||||
aad: &header[..4],
|
||||
},
|
||||
)
|
||||
.expect("seal managed object key")
|
||||
}
|
||||
DARE_CIPHER_CHACHA20_POLY1305 => {
|
||||
let cipher = ChaCha20Poly1305::new_from_slice(&sealing_key).expect("valid sealing key");
|
||||
let nonce = chacha20poly1305::Nonce::try_from(&header[4..16]).expect("valid nonce");
|
||||
cipher
|
||||
.encrypt(
|
||||
&nonce,
|
||||
Payload {
|
||||
msg: &object_key,
|
||||
aad: &header[..4],
|
||||
},
|
||||
)
|
||||
.expect("seal managed object key")
|
||||
}
|
||||
_ => panic!("unsupported test cipher"),
|
||||
};
|
||||
let mut sealed = header.to_vec();
|
||||
sealed.extend_from_slice(
|
||||
&cipher
|
||||
.encrypt(
|
||||
&nonce,
|
||||
aes_gcm::aead::Payload {
|
||||
msg: &object_key,
|
||||
aad: &header[..4],
|
||||
},
|
||||
)
|
||||
.expect("seal managed object key"),
|
||||
);
|
||||
sealed.extend_from_slice(&ciphertext);
|
||||
(iv, sealed)
|
||||
}
|
||||
|
||||
#[cfg(feature = "rio-v2")]
|
||||
#[test]
|
||||
fn test_supported_sealed_object_key_cipher_accepts_current_minio_fixture_value() {
|
||||
assert!(is_supported_sealed_object_key_cipher(DARE_CIPHER_AES_256_GCM));
|
||||
assert!(is_supported_sealed_object_key_cipher(DARE_CIPHER_CHACHA20_POLY1305));
|
||||
assert!(!is_supported_sealed_object_key_cipher(0x02));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_managed_material_accepts_case_insensitive_metadata_keys() {
|
||||
async_with_vars([("__RUSTFS_SSE_SIMPLE_CMK", Some(BASE64_STANDARD.encode([0u8; 32])))], async {
|
||||
@@ -1937,6 +1998,47 @@ mod tests {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[cfg(feature = "rio-v2")]
|
||||
#[tokio::test]
|
||||
async fn resolve_managed_material_accepts_chacha20_poly1305_header_variant() {
|
||||
async_with_vars([("__RUSTFS_SSE_SIMPLE_CMK", Some(BASE64_STANDARD.encode([0u8; 32])))], async {
|
||||
let data_key = [0x24; 32];
|
||||
let object_key = [0x33; 32];
|
||||
let (iv, sealed_key) = seal_managed_s3_object_key_for_test_with_cipher(
|
||||
"bucket",
|
||||
"object",
|
||||
data_key,
|
||||
object_key,
|
||||
DARE_CIPHER_CHACHA20_POLY1305,
|
||||
);
|
||||
|
||||
let encrypted_dek = encrypt_managed_dek_for_test(data_key, [0u8; 32]);
|
||||
let metadata = HashMap::from([
|
||||
(
|
||||
MINIO_INTERNAL_ENCRYPTION_S3_SEALED_KEY_HEADER.to_string(),
|
||||
BASE64_STANDARD.encode(sealed_key),
|
||||
),
|
||||
(MINIO_INTERNAL_ENCRYPTION_IV_HEADER.to_string(), BASE64_STANDARD.encode(iv)),
|
||||
(
|
||||
MINIO_INTERNAL_ENCRYPTION_ALGORITHM_HEADER.to_string(),
|
||||
MINIO_INTERNAL_ENCRYPTION_SEAL_ALGORITHM.to_string(),
|
||||
),
|
||||
(
|
||||
MINIO_INTERNAL_ENCRYPTION_KMS_DATA_KEY_HEADER.to_string(),
|
||||
BASE64_STANDARD.encode(encrypted_dek.as_bytes()),
|
||||
),
|
||||
(MINIO_INTERNAL_ENCRYPTION_KMS_KEY_ID_HEADER.to_string(), "default".to_string()),
|
||||
]);
|
||||
|
||||
let material = resolve_managed_material("bucket", "object", &metadata)
|
||||
.await
|
||||
.expect("managed material should accept current MinIO header variant");
|
||||
assert_eq!(material.key_kind, EncryptionKeyKind::Object);
|
||||
assert_eq!(material.key_bytes, object_key);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_encryption_material_accepts_case_insensitive_metadata_keys() {
|
||||
async_with_vars([("__RUSTFS_SSE_SIMPLE_CMK", Some(BASE64_STANDARD.encode([0u8; 32])))], async {
|
||||
|
||||
Reference in New Issue
Block a user