mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-15 17:43:13 +00:00
refactor(sse): sink managed-SSE attribution into the shared encryption-keys module (#6017)
* refactor(sse): sink managed-SSE attribution into the shared encryption-keys module Moves the managed-SSE classifier — stored_managed_encryption_key, contains_managed_encryption_metadata, normalize_managed_metadata — and the SSEType enum from rustfs/src/storage/sse.rs into crates/utils/src/http/object_encryption_keys.rs, the module that already owns every constant they read. This is PR-B0 of rustfs/backlog#1643: crates/scanner must never depend on the rustfs binary crate, so encryption attribution has to live in a shared lower layer before the scanner can report per-scheme coverage without growing a second classifier. SSEType moves wholesale (option a): its only impl is the dependency-free audit_label(), so the enum relocates verbatim (audit_label becomes pub) and rustfs::storage::sse re-exports it, keeping every existing path compiling. The one piece that cannot move verbatim is normalize_managed_metadata's KMS-context branch, which needs base64 and serde_json — dependencies rustfs-utils does not have and does not gain here. The shared normalizer instead takes an injected Option<fn(&str) -> Option<String>> context recoder; sse.rs passes recode_minio_kms_context, the old inline chain verbatim including the silent skip on decode failure. stored_managed_encryption_key passes no recoder because the context mapping only ever inserts the context key, which the key-id lookup never reads, so its output is identical. Every metadata lookup stays a case-sensitive exact match (lowercase x-amz-* stored forms, TitleCase MinIO-internal names) per the backlog#1775 trap; new shared-module tests pin that, and a source-scan test in sse.rs asserts the classifier has exactly one definition so a second copy cannot silently return. * fix(utils): satisfy encryption key test clippy --------- Co-authored-by: cxymds <cxymds@gmail.com>
This commit is contained in:
@@ -25,7 +25,8 @@
|
||||
// The lowercase stored forms, matching exactly what encryption_material_to_metadata
|
||||
// persists. The read-path SSE-C check is case-sensitive, so restoring under any
|
||||
// other casing would classify the replica as managed-SSE and reject SSE-C GETs.
|
||||
use super::headers::{SSEC_ALGORITHM_HEADER, SSEC_KEY_HEADER, SSEC_KEY_MD5_HEADER};
|
||||
use super::headers::{AMZ_ENCRYPTION_KMS, SSEC_ALGORITHM_HEADER, SSEC_KEY_HEADER, SSEC_KEY_MD5_HEADER};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub const INTERNAL_ENCRYPTION_KEY_ID_HEADER: &str = "x-rustfs-encryption-key-id";
|
||||
pub const INTERNAL_ENCRYPTION_KEY_HEADER: &str = "x-rustfs-encryption-key";
|
||||
@@ -165,6 +166,143 @@ pub fn is_replication_stripped_encryption_key(key: &str) -> bool {
|
||||
|| super::starts_with_ignore_ascii_case(key, RUSTFS_INTERNAL_ENCRYPTION_PREFIX)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Managed-SSE attribution (shared classifier)
|
||||
// ============================================================================
|
||||
//
|
||||
// Single source of truth for classifying stored managed-SSE (SSE-S3 / SSE-KMS)
|
||||
// object metadata. These live here — rather than in the `rustfs` binary
|
||||
// crate's SSE module — so lower-layer consumers such as the scanner can
|
||||
// attribute encrypted objects without growing a second copy of the
|
||||
// normalization/classification logic (backlog#1643 PR-B0). The binary crate
|
||||
// re-exports them from `rustfs::storage::sse`, and a source-scan test there
|
||||
// pins that no second definition reappears.
|
||||
//
|
||||
// Every metadata lookup below is a case-SENSITIVE exact match on the stored
|
||||
// `HashMap<String, String>` keys, mirroring the SSE read path. Do not
|
||||
// "harmonize" these with the lowercase-normalizing helpers in
|
||||
// `header_compat.rs`: the lowercase `x-amz-*` stored forms and the TitleCase
|
||||
// MinIO-internal names are load-bearing exactly as written.
|
||||
|
||||
/// Type of encryption used
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SSEType {
|
||||
/// SSE-S3 (AES256)
|
||||
SseS3,
|
||||
/// SSE-KMS (aws:kms)
|
||||
SseKms,
|
||||
/// SSE-C (customer-provided key)
|
||||
SseC,
|
||||
}
|
||||
|
||||
impl SSEType {
|
||||
/// Stable scheme name for audit consumers.
|
||||
pub fn audit_label(self) -> &'static str {
|
||||
match self {
|
||||
SSEType::SseS3 => "SSE-S3",
|
||||
SSEType::SseKms => "SSE-KMS",
|
||||
SSEType::SseC => "SSE-C",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recodes a stored MinIO KMS context value — base64-wrapped JSON under
|
||||
/// [`MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER`] — into the plain-JSON form
|
||||
/// RustFS stores under [`INTERNAL_ENCRYPTION_CONTEXT_HEADER`].
|
||||
///
|
||||
/// Injected by callers because this crate deliberately carries no JSON codec.
|
||||
/// Returning `None` skips the context mapping, matching the historical
|
||||
/// silent-skip on a value that fails to decode.
|
||||
pub type KmsContextRecoder = fn(&str) -> Option<String>;
|
||||
|
||||
/// True when the stored metadata carries a managed-SSE (SSE-S3 / SSE-KMS)
|
||||
/// encryption envelope, under either the RustFS-branded or the MinIO-branded
|
||||
/// internal keys.
|
||||
pub fn contains_managed_encryption_metadata(metadata: &HashMap<String, String>) -> bool {
|
||||
metadata.contains_key(INTERNAL_ENCRYPTION_KEY_HEADER)
|
||||
|| metadata.contains_key(MINIO_INTERNAL_ENCRYPTION_S3_SEALED_KEY_HEADER)
|
||||
|| metadata.contains_key(MINIO_INTERNAL_ENCRYPTION_KMS_SEALED_KEY_HEADER)
|
||||
|| metadata.contains_key(MINIO_INTERNAL_ENCRYPTION_KMS_DATA_KEY_HEADER)
|
||||
|| metadata.contains_key(MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER)
|
||||
}
|
||||
|
||||
/// Maps the MinIO-branded internal SSE keys onto the RustFS-branded stored
|
||||
/// keys (the dual internal metadata keys invariant). RustFS-branded keys
|
||||
/// already present always win; every source lookup is a case-sensitive exact
|
||||
/// match on the specific TitleCase MinIO names.
|
||||
pub fn normalize_managed_metadata(
|
||||
metadata: &HashMap<String, String>,
|
||||
recode_kms_context: Option<KmsContextRecoder>,
|
||||
) -> HashMap<String, String> {
|
||||
let mut normalized = metadata.clone();
|
||||
|
||||
if !normalized.contains_key(INTERNAL_ENCRYPTION_KEY_HEADER)
|
||||
&& let Some(value) = metadata
|
||||
.get(MINIO_INTERNAL_ENCRYPTION_KMS_DATA_KEY_HEADER)
|
||||
.or_else(|| metadata.get(MINIO_INTERNAL_ENCRYPTION_KMS_SEALED_KEY_HEADER))
|
||||
.or_else(|| metadata.get(MINIO_INTERNAL_ENCRYPTION_S3_SEALED_KEY_HEADER))
|
||||
.or_else(|| metadata.get(MINIO_INTERNAL_ENCRYPTION_SSEC_SEALED_KEY_HEADER))
|
||||
{
|
||||
normalized.insert(INTERNAL_ENCRYPTION_KEY_HEADER.to_string(), value.clone());
|
||||
}
|
||||
|
||||
if !normalized.contains_key(INTERNAL_ENCRYPTION_IV_HEADER)
|
||||
&& let Some(value) = metadata.get(MINIO_INTERNAL_ENCRYPTION_IV_HEADER)
|
||||
{
|
||||
normalized.insert(INTERNAL_ENCRYPTION_IV_HEADER.to_string(), value.clone());
|
||||
}
|
||||
|
||||
if !normalized.contains_key(INTERNAL_ENCRYPTION_ALGORITHM_HEADER)
|
||||
&& let Some(value) = metadata.get(MINIO_INTERNAL_ENCRYPTION_ALGORITHM_HEADER)
|
||||
{
|
||||
normalized.insert(INTERNAL_ENCRYPTION_ALGORITHM_HEADER.to_string(), value.clone());
|
||||
}
|
||||
|
||||
if !normalized.contains_key(INTERNAL_ENCRYPTION_KEY_ID_HEADER)
|
||||
&& let Some(value) = metadata.get(MINIO_INTERNAL_ENCRYPTION_KMS_KEY_ID_HEADER)
|
||||
{
|
||||
normalized.insert(INTERNAL_ENCRYPTION_KEY_ID_HEADER.to_string(), value.clone());
|
||||
}
|
||||
|
||||
if !normalized.contains_key(INTERNAL_ENCRYPTION_CONTEXT_HEADER)
|
||||
&& let Some(value) = metadata.get(MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER)
|
||||
&& let Some(recode) = recode_kms_context
|
||||
&& let Some(encoded) = recode(value)
|
||||
{
|
||||
normalized.insert(INTERNAL_ENCRYPTION_CONTEXT_HEADER.to_string(), encoded);
|
||||
}
|
||||
|
||||
normalized
|
||||
}
|
||||
|
||||
/// Resolve the scheme and KMS key a stored managed-SSE object was wrapped with.
|
||||
///
|
||||
/// Mirrors the lookup `apply_managed_decryption_material` performs, so both agree on
|
||||
/// which key a read is authorized against.
|
||||
///
|
||||
/// No [`KmsContextRecoder`] is taken: the context mapping only ever inserts
|
||||
/// [`INTERNAL_ENCRYPTION_CONTEXT_HEADER`], which this lookup never reads, so
|
||||
/// the result is identical with or without it.
|
||||
pub fn stored_managed_encryption_key(metadata: &HashMap<String, String>) -> Option<(SSEType, String)> {
|
||||
if !contains_managed_encryption_metadata(metadata) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Case-sensitive: the SSE writer stores the scheme under the lowercase
|
||||
// `x-amz-server-side-encryption` key; other casings are not stored forms.
|
||||
let sse_type = match metadata.get("x-amz-server-side-encryption")?.as_str() {
|
||||
AMZ_ENCRYPTION_KMS => SSEType::SseKms,
|
||||
_ => SSEType::SseS3,
|
||||
};
|
||||
let key_id = normalize_managed_metadata(metadata, None)
|
||||
.get(INTERNAL_ENCRYPTION_KEY_ID_HEADER)
|
||||
.or_else(|| metadata.get("x-amz-server-side-encryption-aws-kms-key-id"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "default".to_string());
|
||||
|
||||
Some((sse_type, key_id))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -273,6 +411,134 @@ mod tests {
|
||||
assert!(!is_replication_stripped_encryption_key("content-type"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_envelope_predicate_matches_both_key_families() {
|
||||
assert!(!contains_managed_encryption_metadata(&HashMap::new()));
|
||||
|
||||
for key in [
|
||||
INTERNAL_ENCRYPTION_KEY_HEADER,
|
||||
MINIO_INTERNAL_ENCRYPTION_S3_SEALED_KEY_HEADER,
|
||||
MINIO_INTERNAL_ENCRYPTION_KMS_SEALED_KEY_HEADER,
|
||||
MINIO_INTERNAL_ENCRYPTION_KMS_DATA_KEY_HEADER,
|
||||
MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER,
|
||||
] {
|
||||
let single = HashMap::from([(key.to_string(), "value".to_string())]);
|
||||
assert!(contains_managed_encryption_metadata(&single), "{key} must classify as managed SSE");
|
||||
}
|
||||
|
||||
// SSE-C material alone is not a managed envelope.
|
||||
let ssec_only = HashMap::from([(SSEC_ALGORITHM_HEADER.to_string(), "AES256".to_string())]);
|
||||
assert!(!contains_managed_encryption_metadata(&ssec_only));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_maps_minio_keys_onto_missing_rustfs_keys_only() {
|
||||
let metadata = HashMap::from([
|
||||
(MINIO_INTERNAL_ENCRYPTION_KMS_DATA_KEY_HEADER.to_string(), "minio-dek".to_string()),
|
||||
(MINIO_INTERNAL_ENCRYPTION_IV_HEADER.to_string(), "minio-iv".to_string()),
|
||||
(MINIO_INTERNAL_ENCRYPTION_ALGORITHM_HEADER.to_string(), "DAREv2-HMAC-SHA256".to_string()),
|
||||
(MINIO_INTERNAL_ENCRYPTION_KMS_KEY_ID_HEADER.to_string(), "minio-key".to_string()),
|
||||
]);
|
||||
|
||||
let normalized = normalize_managed_metadata(&metadata, None);
|
||||
assert_eq!(normalized.get(INTERNAL_ENCRYPTION_KEY_HEADER).map(String::as_str), Some("minio-dek"));
|
||||
assert_eq!(normalized.get(INTERNAL_ENCRYPTION_IV_HEADER).map(String::as_str), Some("minio-iv"));
|
||||
assert_eq!(
|
||||
normalized.get(INTERNAL_ENCRYPTION_ALGORITHM_HEADER).map(String::as_str),
|
||||
Some("DAREv2-HMAC-SHA256")
|
||||
);
|
||||
assert_eq!(normalized.get(INTERNAL_ENCRYPTION_KEY_ID_HEADER).map(String::as_str), Some("minio-key"));
|
||||
|
||||
// Existing RustFS-branded keys always win over the MinIO twins.
|
||||
let mut both = metadata;
|
||||
both.insert(INTERNAL_ENCRYPTION_KEY_ID_HEADER.to_string(), "rustfs-key".to_string());
|
||||
assert_eq!(
|
||||
normalize_managed_metadata(&both, None)
|
||||
.get(INTERNAL_ENCRYPTION_KEY_ID_HEADER)
|
||||
.map(String::as_str),
|
||||
Some("rustfs-key")
|
||||
);
|
||||
|
||||
// The mapping is a case-sensitive exact match on the TitleCase MinIO
|
||||
// names; a lowercased twin must not normalize.
|
||||
let lowercased = HashMap::from([(MINIO_INTERNAL_ENCRYPTION_KMS_KEY_ID_HEADER.to_lowercase(), "minio-key".to_string())]);
|
||||
assert!(!normalize_managed_metadata(&lowercased, None).contains_key(INTERNAL_ENCRYPTION_KEY_ID_HEADER));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_recodes_kms_context_only_through_the_injected_codec() {
|
||||
let metadata = HashMap::from([(MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER.to_string(), "encoded-context".to_string())]);
|
||||
|
||||
// Without a codec the context stays unnormalized.
|
||||
assert!(!normalize_managed_metadata(&metadata, None).contains_key(INTERNAL_ENCRYPTION_CONTEXT_HEADER));
|
||||
|
||||
// A codec that fails to decode also leaves it unnormalized.
|
||||
fn reject(_value: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
assert!(!normalize_managed_metadata(&metadata, Some(reject)).contains_key(INTERNAL_ENCRYPTION_CONTEXT_HEADER));
|
||||
|
||||
fn recode(value: &str) -> Option<String> {
|
||||
Some(format!("recoded:{value}"))
|
||||
}
|
||||
assert_eq!(
|
||||
normalize_managed_metadata(&metadata, Some(recode))
|
||||
.get(INTERNAL_ENCRYPTION_CONTEXT_HEADER)
|
||||
.map(String::as_str),
|
||||
Some("recoded:encoded-context")
|
||||
);
|
||||
|
||||
// A stored RustFS context wins without invoking the codec.
|
||||
let mut both = metadata;
|
||||
both.insert(INTERNAL_ENCRYPTION_CONTEXT_HEADER.to_string(), "stored-context".to_string());
|
||||
assert_eq!(
|
||||
normalize_managed_metadata(&both, Some(recode))
|
||||
.get(INTERNAL_ENCRYPTION_CONTEXT_HEADER)
|
||||
.map(String::as_str),
|
||||
Some("stored-context")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stored_managed_encryption_key_attributes_scheme_and_key() {
|
||||
// Plaintext metadata carries no managed envelope.
|
||||
assert!(stored_managed_encryption_key(&HashMap::new()).is_none());
|
||||
|
||||
// A managed envelope without the stored SSE marker cannot be attributed.
|
||||
let envelope_only = HashMap::from([(INTERNAL_ENCRYPTION_KEY_HEADER.to_string(), "dek".to_string())]);
|
||||
assert!(stored_managed_encryption_key(&envelope_only).is_none());
|
||||
|
||||
// The stored SSE marker is the lowercase form; a TitleCase key is not
|
||||
// a stored form and must not be recognized.
|
||||
let mut titlecase = envelope_only.clone();
|
||||
titlecase.insert("X-Amz-Server-Side-Encryption".to_string(), "aws:kms".to_string());
|
||||
assert!(stored_managed_encryption_key(&titlecase).is_none());
|
||||
|
||||
let mut sse_s3 = envelope_only.clone();
|
||||
sse_s3.insert("x-amz-server-side-encryption".to_string(), "AES256".to_string());
|
||||
assert_eq!(stored_managed_encryption_key(&sse_s3), Some((SSEType::SseS3, "default".to_string())));
|
||||
|
||||
let mut sse_kms = envelope_only;
|
||||
sse_kms.insert("x-amz-server-side-encryption".to_string(), "aws:kms".to_string());
|
||||
assert_eq!(stored_managed_encryption_key(&sse_kms), Some((SSEType::SseKms, "default".to_string())));
|
||||
|
||||
// Key-id precedence: RustFS stored key id, then the MinIO twin, then
|
||||
// the lowercase amz key id, then "default".
|
||||
sse_kms.insert("x-amz-server-side-encryption-aws-kms-key-id".to_string(), "amz-key".to_string());
|
||||
assert_eq!(stored_managed_encryption_key(&sse_kms), Some((SSEType::SseKms, "amz-key".to_string())));
|
||||
sse_kms.insert(MINIO_INTERNAL_ENCRYPTION_KMS_KEY_ID_HEADER.to_string(), "minio-key".to_string());
|
||||
assert_eq!(stored_managed_encryption_key(&sse_kms), Some((SSEType::SseKms, "minio-key".to_string())));
|
||||
sse_kms.insert(INTERNAL_ENCRYPTION_KEY_ID_HEADER.to_string(), "rustfs-key".to_string());
|
||||
assert_eq!(stored_managed_encryption_key(&sse_kms), Some((SSEType::SseKms, "rustfs-key".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sse_type_audit_labels_are_stable() {
|
||||
assert_eq!(SSEType::SseS3.audit_label(), "SSE-S3");
|
||||
assert_eq!(SSEType::SseKms.audit_label(), "SSE-KMS");
|
||||
assert_eq!(SSEType::SseC.audit_label(), "SSE-C");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_prefixes_cover_every_transport_value_key() {
|
||||
// Every transport key that carries material must match a redaction
|
||||
|
||||
+57
-100
@@ -119,8 +119,14 @@ use rustfs_utils::http::object_encryption_keys::{
|
||||
MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER, MINIO_INTERNAL_ENCRYPTION_KMS_DATA_KEY_HEADER,
|
||||
MINIO_INTERNAL_ENCRYPTION_KMS_KEY_ID_HEADER, MINIO_INTERNAL_ENCRYPTION_KMS_SEALED_KEY_HEADER,
|
||||
MINIO_INTERNAL_ENCRYPTION_MULTIPART_HEADER, MINIO_INTERNAL_ENCRYPTION_S3_SEALED_KEY_HEADER,
|
||||
MINIO_INTERNAL_ENCRYPTION_SSEC_SEALED_KEY_HEADER, SSEC_ORIGINAL_SIZE_HEADER,
|
||||
MINIO_INTERNAL_ENCRYPTION_SSEC_SEALED_KEY_HEADER, SSEC_ORIGINAL_SIZE_HEADER, normalize_managed_metadata,
|
||||
stored_managed_encryption_key,
|
||||
};
|
||||
// The managed-SSE classifier lives in the shared encryption-keys module so the
|
||||
// scanner can reuse it (backlog#1643 PR-B0); these re-exports keep the
|
||||
// historical `crate::storage::sse` paths compiling.
|
||||
pub use rustfs_utils::http::object_encryption_keys::SSEType;
|
||||
pub(crate) use rustfs_utils::http::object_encryption_keys::contains_managed_encryption_metadata;
|
||||
#[cfg(feature = "rio-v2")]
|
||||
const MINIO_INTERNAL_ENCRYPTION_SEAL_ALGORITHM: &str = "DAREv2-HMAC-SHA256";
|
||||
#[cfg(feature = "rio-v2")]
|
||||
@@ -783,28 +789,6 @@ pub struct DecryptionMaterial {
|
||||
pub key_kind: EncryptionKeyKind,
|
||||
}
|
||||
|
||||
/// Type of encryption used
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SSEType {
|
||||
/// SSE-S3 (AES256)
|
||||
SseS3,
|
||||
/// SSE-KMS (aws:kms)
|
||||
SseKms,
|
||||
/// SSE-C (customer-provided key)
|
||||
SseC,
|
||||
}
|
||||
|
||||
impl SSEType {
|
||||
/// Stable scheme name for audit consumers.
|
||||
fn audit_label(self) -> &'static str {
|
||||
match self {
|
||||
SSEType::SseS3 => "SSE-S3",
|
||||
SSEType::SseKms => "SSE-KMS",
|
||||
SSEType::SseC => "SSE-C",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EncryptionKeyKind {
|
||||
Direct,
|
||||
@@ -1064,28 +1048,6 @@ pub async fn authorize_sse_kms_object_read(
|
||||
result
|
||||
}
|
||||
|
||||
/// Resolve the scheme and KMS key a stored managed-SSE object was wrapped with.
|
||||
///
|
||||
/// Mirrors the lookup `apply_managed_decryption_material` performs, so both agree on
|
||||
/// which key a read is authorized against.
|
||||
fn stored_managed_encryption_key(metadata: &HashMap<String, String>) -> Option<(SSEType, String)> {
|
||||
if !contains_managed_encryption_metadata(metadata) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let sse_type = match metadata.get("x-amz-server-side-encryption")?.as_str() {
|
||||
ServerSideEncryption::AWS_KMS => SSEType::SseKms,
|
||||
_ => SSEType::SseS3,
|
||||
};
|
||||
let key_id = normalize_managed_metadata(metadata)
|
||||
.get(INTERNAL_ENCRYPTION_KEY_ID_HEADER)
|
||||
.or_else(|| metadata.get("x-amz-server-side-encryption-aws-kms-key-id"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "default".to_string());
|
||||
|
||||
Some((sse_type, key_id))
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Data-plane KMS audit attachment (SSE-S3 / SSE-KMS)
|
||||
// ============================================================================
|
||||
@@ -1339,7 +1301,9 @@ fn envelope_master_key_version(envelope_bytes: &[u8]) -> Option<u32> {
|
||||
/// Master-key version of the envelope stored on an object, for the audit
|
||||
/// summary of a read against that object.
|
||||
fn stored_envelope_master_key_version(metadata: &HashMap<String, String>) -> Option<u32> {
|
||||
let encoded = normalize_managed_metadata(metadata);
|
||||
// No context recoder: the recode only ever inserts the context key, which
|
||||
// this lookup never reads, so the normalized result is identical without it.
|
||||
let encoded = normalize_managed_metadata(metadata, None);
|
||||
let encoded = encoded.get(INTERNAL_ENCRYPTION_KEY_HEADER)?;
|
||||
let envelope = BASE64_STANDARD.decode(encoded).ok()?;
|
||||
envelope_master_key_version(&envelope)
|
||||
@@ -2487,7 +2451,7 @@ async fn apply_managed_decryption_material_inner(
|
||||
|
||||
// Safe: presence is guaranteed by the contains_key check above.
|
||||
let server_side_encryption = metadata.get("x-amz-server-side-encryption").cloned().unwrap_or_default();
|
||||
let normalized_metadata = normalize_managed_metadata(metadata);
|
||||
let normalized_metadata = normalize_managed_metadata(metadata, Some(recode_minio_kms_context));
|
||||
|
||||
let encryption_type = match server_side_encryption.as_str() {
|
||||
ServerSideEncryption::AES256 => SSEType::SseS3,
|
||||
@@ -3229,14 +3193,6 @@ pub fn mark_encrypted_multipart_metadata(metadata: &mut HashMap<String, String>)
|
||||
metadata.insert(MINIO_INTERNAL_ENCRYPTION_MULTIPART_HEADER.to_string(), String::new());
|
||||
}
|
||||
|
||||
pub(crate) fn contains_managed_encryption_metadata(metadata: &HashMap<String, String>) -> bool {
|
||||
metadata.contains_key(INTERNAL_ENCRYPTION_KEY_HEADER)
|
||||
|| metadata.contains_key(MINIO_INTERNAL_ENCRYPTION_S3_SEALED_KEY_HEADER)
|
||||
|| metadata.contains_key(MINIO_INTERNAL_ENCRYPTION_KMS_SEALED_KEY_HEADER)
|
||||
|| metadata.contains_key(MINIO_INTERNAL_ENCRYPTION_KMS_DATA_KEY_HEADER)
|
||||
|| metadata.contains_key(MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER)
|
||||
}
|
||||
|
||||
#[cfg(feature = "rio-v2")]
|
||||
fn is_legacy_rustfs_managed_metadata(metadata: &HashMap<String, String>) -> bool {
|
||||
metadata.contains_key(INTERNAL_ENCRYPTION_KEY_HEADER)
|
||||
@@ -3282,47 +3238,16 @@ fn parse_minio_managed_sealed_key(
|
||||
Ok(Some(ManagedSealedKey { iv, sealed_key }))
|
||||
}
|
||||
|
||||
fn normalize_managed_metadata(metadata: &HashMap<String, String>) -> HashMap<String, String> {
|
||||
let mut normalized = metadata.clone();
|
||||
|
||||
if !normalized.contains_key(INTERNAL_ENCRYPTION_KEY_HEADER)
|
||||
&& let Some(value) = metadata
|
||||
.get(MINIO_INTERNAL_ENCRYPTION_KMS_DATA_KEY_HEADER)
|
||||
.or_else(|| metadata.get(MINIO_INTERNAL_ENCRYPTION_KMS_SEALED_KEY_HEADER))
|
||||
.or_else(|| metadata.get(MINIO_INTERNAL_ENCRYPTION_S3_SEALED_KEY_HEADER))
|
||||
.or_else(|| metadata.get(MINIO_INTERNAL_ENCRYPTION_SSEC_SEALED_KEY_HEADER))
|
||||
{
|
||||
normalized.insert(INTERNAL_ENCRYPTION_KEY_HEADER.to_string(), value.clone());
|
||||
}
|
||||
|
||||
if !normalized.contains_key(INTERNAL_ENCRYPTION_IV_HEADER)
|
||||
&& let Some(value) = metadata.get(MINIO_INTERNAL_ENCRYPTION_IV_HEADER)
|
||||
{
|
||||
normalized.insert(INTERNAL_ENCRYPTION_IV_HEADER.to_string(), value.clone());
|
||||
}
|
||||
|
||||
if !normalized.contains_key(INTERNAL_ENCRYPTION_ALGORITHM_HEADER)
|
||||
&& let Some(value) = metadata.get(MINIO_INTERNAL_ENCRYPTION_ALGORITHM_HEADER)
|
||||
{
|
||||
normalized.insert(INTERNAL_ENCRYPTION_ALGORITHM_HEADER.to_string(), value.clone());
|
||||
}
|
||||
|
||||
if !normalized.contains_key(INTERNAL_ENCRYPTION_KEY_ID_HEADER)
|
||||
&& let Some(value) = metadata.get(MINIO_INTERNAL_ENCRYPTION_KMS_KEY_ID_HEADER)
|
||||
{
|
||||
normalized.insert(INTERNAL_ENCRYPTION_KEY_ID_HEADER.to_string(), value.clone());
|
||||
}
|
||||
|
||||
if !normalized.contains_key(INTERNAL_ENCRYPTION_CONTEXT_HEADER)
|
||||
&& let Some(value) = metadata.get(MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER)
|
||||
&& let Ok(decoded) = BASE64_STANDARD.decode(value)
|
||||
&& let Ok(context) = serde_json::from_slice::<HashMap<String, String>>(&decoded)
|
||||
&& let Ok(encoded) = serde_json::to_string(&context)
|
||||
{
|
||||
normalized.insert(INTERNAL_ENCRYPTION_CONTEXT_HEADER.to_string(), encoded);
|
||||
}
|
||||
|
||||
normalized
|
||||
/// Recodes a stored MinIO KMS context value (base64-wrapped JSON) into the
|
||||
/// plain-JSON form RustFS stores under [`INTERNAL_ENCRYPTION_CONTEXT_HEADER`].
|
||||
///
|
||||
/// Injected into the shared [`normalize_managed_metadata`] because the shared
|
||||
/// crate carries no JSON codec; any decode failure returns `None`, which skips
|
||||
/// the context mapping exactly like the historical inline `if let Ok` chain.
|
||||
fn recode_minio_kms_context(value: &str) -> Option<String> {
|
||||
let decoded = BASE64_STANDARD.decode(value).ok()?;
|
||||
let context = serde_json::from_slice::<HashMap<String, String>>(&decoded).ok()?;
|
||||
serde_json::to_string(&context).ok()
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -3473,9 +3398,9 @@ mod tests {
|
||||
encryption_material_to_metadata, extract_server_side_encryption_from_headers, extract_ssec_params_from_headers,
|
||||
extract_ssekms_context_from_headers, generate_ssec_nonce, is_managed_sse, kms_operation_error,
|
||||
map_get_object_reader_error, mark_encrypted_multipart_metadata, md5_base64, normalize_managed_metadata,
|
||||
reset_sse_dek_provider, resolve_effective_kms_key_id, sse_decryption, sse_encryption, sse_prepare_encryption,
|
||||
strip_managed_encryption_metadata, validate_sse_headers_for_read, validate_sse_headers_for_write, validate_ssec_for_read,
|
||||
validate_ssec_params, verify_ssec_key_match,
|
||||
recode_minio_kms_context, reset_sse_dek_provider, resolve_effective_kms_key_id, sse_decryption, sse_encryption,
|
||||
sse_prepare_encryption, strip_managed_encryption_metadata, validate_sse_headers_for_read, validate_sse_headers_for_write,
|
||||
validate_ssec_for_read, validate_ssec_params, verify_ssec_key_match,
|
||||
};
|
||||
#[cfg(feature = "rio-v2")]
|
||||
use super::{
|
||||
@@ -3484,6 +3409,38 @@ mod tests {
|
||||
};
|
||||
use rustfs_utils::http::headers::SSEC_ALGORITHM_HEADER;
|
||||
|
||||
/// backlog#1643 PR-B0 acceptance guard: the managed-SSE classifier must
|
||||
/// have exactly one definition — in the shared encryption-keys module —
|
||||
/// so the scanner and the S3 layer can never disagree on attribution.
|
||||
/// This module may only re-export or call it.
|
||||
#[test]
|
||||
fn managed_sse_classifier_has_exactly_one_definition() {
|
||||
let classifier_fns = [
|
||||
"contains_managed_encryption_metadata",
|
||||
"normalize_managed_metadata",
|
||||
"stored_managed_encryption_key",
|
||||
];
|
||||
|
||||
let sse_src = include_str!("sse.rs");
|
||||
let shared_src =
|
||||
std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/../crates/utils/src/http/object_encryption_keys.rs"))
|
||||
.expect("shared encryption-keys module should be readable");
|
||||
|
||||
for name in classifier_fns {
|
||||
// Built at runtime so this test's own source cannot satisfy the scan.
|
||||
let definition = format!("fn {name}(");
|
||||
assert!(
|
||||
!sse_src.contains(&definition),
|
||||
"{name} must not be redefined in storage/sse.rs; call the shared rustfs_utils::http::object_encryption_keys implementation instead"
|
||||
);
|
||||
assert_eq!(
|
||||
shared_src.matches(&definition).count(),
|
||||
1,
|
||||
"{name} must be defined exactly once, in the shared encryption-keys module"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssec_read_headers_are_sensitive() {
|
||||
let headers = super::build_ssec_read_headers(
|
||||
@@ -4869,7 +4826,7 @@ mod tests {
|
||||
(MINIO_INTERNAL_ENCRYPTION_KMS_KEY_ID_HEADER.to_string(), "default".to_string()),
|
||||
]);
|
||||
|
||||
let normalized = normalize_managed_metadata(&metadata);
|
||||
let normalized = normalize_managed_metadata(&metadata, Some(recode_minio_kms_context));
|
||||
|
||||
assert_eq!(
|
||||
normalized.get(INTERNAL_ENCRYPTION_KEY_HEADER),
|
||||
|
||||
Reference in New Issue
Block a user