fix(sse): separate SSE-S3 and KMS key providers

This commit is contained in:
唐小鸭
2026-07-22 15:19:24 +08:00
parent a044d11443
commit 5ea9a1fd8f
15 changed files with 895 additions and 246 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ moka = { workspace = true, features = ["future"] }
# Additional dependencies
md5 = { workspace = true }
arc-swap = { workspace = true }
rustfs-utils = { workspace = true }
rustfs-utils = { workspace = true, features = ["http"] }
rustfs-security-governance = { workspace = true }
# HTTP client for Vault
+35
View File
@@ -44,6 +44,23 @@ pub struct DataKeyEnvelope {
pub created_at: Zoned,
}
/// Return whether bytes contain a complete RustFS KMS data-key envelope.
pub fn is_data_key_envelope(ciphertext: &[u8]) -> bool {
const MAX_ENVELOPE_SIZE: usize = 64 * 1024;
if ciphertext.is_empty() || ciphertext.len() > MAX_ENVELOPE_SIZE {
return false;
}
serde_json::from_slice::<DataKeyEnvelope>(ciphertext).is_ok_and(|envelope| {
!envelope.key_id.trim().is_empty()
&& !envelope.master_key_id.trim().is_empty()
&& envelope.key_spec == "AES_256"
&& !envelope.encrypted_key.is_empty()
&& (envelope.nonce.is_empty() || envelope.nonce.len() == 12)
})
}
/// Trait for encrypting and decrypting data encryption keys (DEK)
///
/// This trait abstracts the encryption operations used to protect
@@ -293,6 +310,24 @@ mod tests {
assert_eq!(deserialized.key_id, envelope.key_id);
assert_eq!(deserialized.master_key_id, envelope.master_key_id);
assert_eq!(deserialized.encrypted_key, envelope.encrypted_key);
assert!(is_data_key_envelope(&serialized));
assert!(!is_data_key_envelope(b"not-a-kms-envelope"));
let mut boundary_envelope = serialized;
boundary_envelope.resize(64 * 1024, b' ');
assert!(is_data_key_envelope(&boundary_envelope));
boundary_envelope.push(b' ');
assert!(!is_data_key_envelope(&boundary_envelope));
let mut invalid = serde_json::to_value(&envelope).expect("Envelope should convert to JSON");
invalid["key_spec"] = serde_json::Value::String("AES_128".to_string());
assert!(!is_data_key_envelope(
&serde_json::to_vec(&invalid).expect("Invalid envelope should serialize")
));
invalid["key_spec"] = serde_json::Value::String("AES_256".to_string());
invalid["unknown"] = serde_json::Value::Bool(true);
assert!(is_data_key_envelope(
&serde_json::to_vec(&invalid).expect("Envelope with unknown field should serialize")
));
}
#[tokio::test]
+1 -1
View File
@@ -17,4 +17,4 @@
pub mod ciphers;
pub mod dek;
pub use dek::{AesDekCrypto, DataKeyEnvelope, DekCrypto, generate_key_material};
pub use dek::{AesDekCrypto, DataKeyEnvelope, DekCrypto, generate_key_material, is_data_key_envelope};
+5
View File
@@ -70,6 +70,7 @@ mod cache;
pub mod config;
mod encryption;
mod error;
mod managed_context;
pub mod manager;
pub mod service;
pub mod service_manager;
@@ -83,7 +84,11 @@ pub use api_types::{
TagKeyRequest, TagKeyResponse, UntagKeyRequest, UntagKeyResponse, UpdateKeyDescriptionRequest, UpdateKeyDescriptionResponse,
};
pub use config::*;
pub use encryption::is_data_key_envelope;
pub use error::{KmsError, Result};
pub use managed_context::{
MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER, RUSTFS_ENCRYPTION_CONTEXT_HEADER, decode_managed_kms_context,
};
pub use manager::KmsManager;
pub use service::{DataKey, ObjectEncryptionService};
pub use service_manager::{
+110
View File
@@ -0,0 +1,110 @@
// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::{KmsError, Result};
use base64::{Engine, engine::general_purpose::STANDARD as BASE64_STANDARD};
use rustfs_utils::http::get_consistent_metadata_value;
use std::collections::HashMap;
pub const RUSTFS_ENCRYPTION_CONTEXT_HEADER: &str = "x-rustfs-encryption-context";
pub const MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-Context";
pub fn decode_managed_kms_context(metadata: &HashMap<String, String>) -> Result<Option<HashMap<String, String>>> {
let minio_context = consistent_value(metadata, MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER)?
.map(|context| {
let decoded = BASE64_STANDARD
.decode(context)
.map_err(|err| KmsError::serialization_error(format!("Failed to decode MinIO KMS context: {err}")))?;
serde_json::from_slice(&decoded)
.map_err(|err| KmsError::serialization_error(format!("Failed to parse MinIO KMS context: {err}")))
})
.transpose()?;
let rustfs_context = consistent_value(metadata, RUSTFS_ENCRYPTION_CONTEXT_HEADER)?
.map(|context| {
serde_json::from_str(context)
.map_err(|err| KmsError::serialization_error(format!("Failed to parse RustFS KMS context: {err}")))
})
.transpose()?;
match (minio_context, rustfs_context) {
(Some(minio), Some(rustfs)) if minio != rustfs => {
Err(KmsError::context_mismatch("Conflicting RustFS and MinIO KMS contexts"))
}
(Some(context), _) | (_, Some(context)) => Ok(Some(context)),
(None, None) => Ok(None),
}
}
fn consistent_value<'a>(metadata: &'a HashMap<String, String>, name: &str) -> Result<Option<&'a str>> {
get_consistent_metadata_value(metadata, name)
.map_err(|_| KmsError::validation_error(format!("Conflicting managed encryption metadata for {name}")))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decode_context_accepts_compatible_headers_and_rejects_conflicts() {
let expected = HashMap::from([("tenant".to_string(), "alpha".to_string())]);
let metadata = HashMap::from([
(
RUSTFS_ENCRYPTION_CONTEXT_HEADER.to_string(),
serde_json::to_string(&expected).expect("RustFS KMS context should serialize"),
),
(
MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER.to_string(),
BASE64_STANDARD.encode(serde_json::to_vec(&expected).expect("MinIO KMS context should serialize")),
),
]);
assert_eq!(
decode_managed_kms_context(&metadata).expect("matching KMS contexts should parse"),
Some(expected.clone())
);
assert_eq!(
decode_managed_kms_context(&HashMap::from([(
RUSTFS_ENCRYPTION_CONTEXT_HEADER.to_string(),
serde_json::to_string(&expected).expect("legacy RustFS KMS context should serialize"),
)]))
.expect("legacy RustFS KMS context should parse"),
Some(expected)
);
let conflicting = HashMap::from([
(
RUSTFS_ENCRYPTION_CONTEXT_HEADER.to_string(),
serde_json::to_string(&HashMap::from([("tenant", "alpha")])).expect("RustFS KMS context should serialize"),
),
(
MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER.to_string(),
BASE64_STANDARD.encode(
serde_json::to_vec(&HashMap::from([("tenant", "beta")])).expect("MinIO KMS context should serialize"),
),
),
]);
assert!(decode_managed_kms_context(&conflicting).is_err());
assert!(
decode_managed_kms_context(&HashMap::from([(
MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER.to_string(),
"not-base64".to_string(),
)]))
.is_err()
);
assert!(
decode_managed_kms_context(&HashMap::from([(RUSTFS_ENCRYPTION_CONTEXT_HEADER.to_string(), "not-json".to_string(),)]))
.is_err()
);
}
}