mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-16 09:58:21 +00:00
d2b1003612
* perf(get): share inline shards and lock clients Co-Authored-By: heihutu <heihutu@gmail.com> * perf(ecstore): converge PUT encoding on contiguous blocks Co-Authored-By: heihutu <heihutu@gmail.com> * perf(get): cache codec streaming gate config Co-Authored-By: heihutu <heihutu@gmail.com> * fix(sse): redact projected customer headers Co-Authored-By: heihutu <heihutu@gmail.com> * perf(ecstore): collapse GET metadata snapshots Co-Authored-By: heihutu <heihutu@gmail.com> * perf(ecstore): reuse decode stripe scratch Co-Authored-By: heihutu <heihutu@gmail.com> * refactor(ecstore): trim decode scratch adapters Co-Authored-By: heihutu <heihutu@gmail.com> * test(ecstore): adapt transition checks to metadata snapshots Co-Authored-By: heihutu <heihutu@gmail.com> * perf(get): release metadata snapshots at ownership boundary Co-Authored-By: heihutu <heihutu@gmail.com> * refactor(ecstore): close cumulative fast-path findings Co-Authored-By: heihutu <heihutu@gmail.com> * fix(storage): preserve lock and header invariants Co-Authored-By: heihutu <heihutu@gmail.com> * test(ecstore): adapt cumulative paths after rebase Co-Authored-By: heihutu <heihutu@gmail.com> * fix(rio-v2): adapt generated metadata fixture Co-Authored-By: heihutu <heihutu@gmail.com> --------- Co-authored-by: heihutu <heihutu@gmail.com>
294 lines
16 KiB
Rust
294 lines
16 KiB
Rust
// 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.
|
|
|
|
//! Canonical metadata keys persisted for encrypted objects and the replication
|
|
//! transport mapping that carries SSE-C material between sites.
|
|
//!
|
|
//! The stored-key constants are the single source of truth shared by the SSE
|
|
//! writer (`rustfs::storage::sse`), the replication boundary (`rustfs_ecstore`),
|
|
//! and log redaction (`rustfs_filemeta`). Keys listed in
|
|
//! [`SSEC_REPLICATION_TRANSPORT_HEADERS`] are renamed onto the wire for SSE-C
|
|
//! ciphertext passthrough; every other encryption key must be stripped from
|
|
//! outbound replication metadata via [`is_replication_stripped_encryption_key`].
|
|
|
|
// 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};
|
|
|
|
pub const INTERNAL_ENCRYPTION_KEY_ID_HEADER: &str = "x-rustfs-encryption-key-id";
|
|
pub const INTERNAL_ENCRYPTION_KEY_HEADER: &str = "x-rustfs-encryption-key";
|
|
pub const INTERNAL_ENCRYPTION_IV_HEADER: &str = "x-rustfs-encryption-iv";
|
|
/// Carries the AEAD algorithm the object was sealed with.
|
|
///
|
|
/// The S3 `x-amz-server-side-encryption` header records the *SSE mode*
|
|
/// (`AES256` / `aws:kms`), not the cipher, so it cannot round-trip
|
|
/// `ChaCha20Poly1305`. Without this header a ChaCha-sealed object comes back
|
|
/// from the projection claiming `aws:kms` and is then opened with the wrong
|
|
/// cipher.
|
|
pub const INTERNAL_ENCRYPTION_ALGORITHM_HEADER: &str = "x-rustfs-encryption-algorithm";
|
|
pub const INTERNAL_ENCRYPTION_ORIGINAL_SIZE_HEADER: &str = "x-rustfs-encryption-original-size";
|
|
pub const INTERNAL_ENCRYPTION_CONTEXT_HEADER: &str = "x-rustfs-encryption-context";
|
|
pub const INTERNAL_ENCRYPTION_TAG_HEADER: &str = "x-rustfs-encryption-tag";
|
|
pub const SSEC_ORIGINAL_SIZE_HEADER: &str = "x-amz-server-side-encryption-customer-original-size";
|
|
pub const MINIO_INTERNAL_ENCRYPTION_MULTIPART_HEADER: &str = "X-Minio-Internal-Encrypted-Multipart";
|
|
pub const MINIO_INTERNAL_ENCRYPTION_IV_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-Iv";
|
|
pub const MINIO_INTERNAL_ENCRYPTION_ALGORITHM_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-Seal-Algorithm";
|
|
pub const MINIO_INTERNAL_ENCRYPTION_SSEC_SEALED_KEY_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-Sealed-Key";
|
|
pub const MINIO_INTERNAL_ENCRYPTION_S3_SEALED_KEY_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-S3-Sealed-Key";
|
|
pub const MINIO_INTERNAL_ENCRYPTION_KMS_SEALED_KEY_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-Kms-Sealed-Key";
|
|
pub const MINIO_INTERNAL_ENCRYPTION_KMS_KEY_ID_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Key-Id";
|
|
pub const MINIO_INTERNAL_ENCRYPTION_KMS_DATA_KEY_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-S3-Kms-Sealed-Key";
|
|
pub const MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER: &str = "X-Minio-Internal-Server-Side-Encryption-Context";
|
|
|
|
/// Reserved RustFS-branded twin of the MinIO-internal SSE key family.
|
|
///
|
|
/// No RustFS writer emits these keys today — the SSE writer persists the
|
|
/// MinIO-branded `X-Minio-Internal-Server-Side-Encryption-*` keys verbatim for
|
|
/// interoperability — but redaction (`rustfs_filemeta`) and replication
|
|
/// stripping treat the family as sensitive so that a future or third-party
|
|
/// writer cannot leak sealed material through the reserved names.
|
|
pub const RUSTFS_INTERNAL_ENCRYPTION_PREFIX: &str = "x-rustfs-internal-server-side-encryption-";
|
|
|
|
pub const REPLICATION_SSEC_ALGORITHM_HEADER: &str = "X-Rustfs-Replication-Ssec-Algorithm";
|
|
pub const REPLICATION_SSEC_KEY_MD5_HEADER: &str = "X-Rustfs-Replication-Ssec-Key-Md5";
|
|
pub const REPLICATION_SSEC_ORIGINAL_SIZE_HEADER: &str = "X-Rustfs-Replication-Ssec-Original-Size";
|
|
pub const REPLICATION_ENCRYPTION_IV_HEADER: &str = "X-Rustfs-Replication-Encryption-Iv";
|
|
pub const REPLICATION_SSE_IV_HEADER: &str = "X-Rustfs-Replication-Server-Side-Encryption-Iv";
|
|
pub const REPLICATION_SSE_SEAL_ALGORITHM_HEADER: &str = "X-Rustfs-Replication-Server-Side-Encryption-Seal-Algorithm";
|
|
pub const REPLICATION_SSE_SEALED_KEY_HEADER: &str = "X-Rustfs-Replication-Server-Side-Encryption-Sealed-Key";
|
|
pub const REPLICATION_ENCRYPTED_MULTIPART_HEADER: &str = "X-Rustfs-Replication-Encrypted-Multipart";
|
|
|
|
/// Stored SSE-C metadata keys and the wire names they replicate under.
|
|
///
|
|
/// Source keys must match what `encryption_material_to_metadata` persists; the
|
|
/// reconciliation test in `rustfs::storage::sse` pins that correspondence.
|
|
pub const SSEC_REPLICATION_TRANSPORT_HEADERS: &[(&str, &str)] = &[
|
|
(SSEC_ALGORITHM_HEADER, REPLICATION_SSEC_ALGORITHM_HEADER),
|
|
(SSEC_KEY_MD5_HEADER, REPLICATION_SSEC_KEY_MD5_HEADER),
|
|
(SSEC_ORIGINAL_SIZE_HEADER, REPLICATION_SSEC_ORIGINAL_SIZE_HEADER),
|
|
(INTERNAL_ENCRYPTION_IV_HEADER, REPLICATION_ENCRYPTION_IV_HEADER),
|
|
(MINIO_INTERNAL_ENCRYPTION_IV_HEADER, REPLICATION_SSE_IV_HEADER),
|
|
(MINIO_INTERNAL_ENCRYPTION_ALGORITHM_HEADER, REPLICATION_SSE_SEAL_ALGORITHM_HEADER),
|
|
(MINIO_INTERNAL_ENCRYPTION_SSEC_SEALED_KEY_HEADER, REPLICATION_SSE_SEALED_KEY_HEADER),
|
|
(MINIO_INTERNAL_ENCRYPTION_MULTIPART_HEADER, REPLICATION_ENCRYPTED_MULTIPART_HEADER),
|
|
];
|
|
|
|
/// Retains only the SSE-C headers consumed by object readers and marks their
|
|
/// values sensitive so instrumented storage calls cannot expose key material.
|
|
pub fn project_ssec_transport_headers(headers: &http::HeaderMap) -> http::HeaderMap {
|
|
let mut projected = http::HeaderMap::new();
|
|
for name in [SSEC_ALGORITHM_HEADER, SSEC_KEY_HEADER, SSEC_KEY_MD5_HEADER] {
|
|
if let Some(value) = headers.get(name) {
|
|
let mut value = value.clone();
|
|
value.set_sensitive(true);
|
|
projected.insert(name, value);
|
|
}
|
|
}
|
|
projected
|
|
}
|
|
|
|
/// Prefixes of replication SSE transport keys whose values carry encryption
|
|
/// material and must never reach logs. Consumed by `rustfs_filemeta` redaction.
|
|
pub const REPLICATION_SSE_TRANSPORT_PREFIXES: &[&str] = &[
|
|
"x-rustfs-replication-server-side-encryption-",
|
|
"x-rustfs-replication-encryption-",
|
|
"x-rustfs-replication-ssec-",
|
|
];
|
|
|
|
/// Returns true when the request carries any SSE-C replication transport
|
|
/// header — the signal that an authorized replication PUT is a ciphertext
|
|
/// passthrough and the receiver must not re-encrypt or compress the body.
|
|
pub fn has_ssec_transport_headers(headers: &http::HeaderMap) -> bool {
|
|
headers.keys().any(|name| {
|
|
let name = name.as_str();
|
|
REPLICATION_SSE_TRANSPORT_PREFIXES
|
|
.iter()
|
|
.any(|prefix| super::starts_with_ignore_ascii_case(name, prefix))
|
|
|| name.eq_ignore_ascii_case(REPLICATION_ENCRYPTED_MULTIPART_HEADER)
|
|
})
|
|
}
|
|
|
|
/// Restores the stored SSE-C metadata keys from their replication transport
|
|
/// names. Returns None when the request carries no transport headers. When the
|
|
/// customer algorithm is present, the AES256 SSE marker is re-added so the
|
|
/// restored metadata matches the shape `encryption_material_to_metadata`
|
|
/// persists (SSE-C Direct writes both IV twins; each travels under its own
|
|
/// transport name, so the 1:1 reverse mapping restores the dual-key pair).
|
|
pub fn ssec_transport_to_stored_metadata(headers: &http::HeaderMap) -> Option<std::collections::HashMap<String, String>> {
|
|
let mut restored = std::collections::HashMap::new();
|
|
for (stored, transport) in SSEC_REPLICATION_TRANSPORT_HEADERS {
|
|
if let Some(value) = headers.get(*transport).and_then(|value| value.to_str().ok()) {
|
|
restored.insert((*stored).to_string(), value.to_string());
|
|
}
|
|
}
|
|
if restored.is_empty() {
|
|
return None;
|
|
}
|
|
if restored.contains_key(SSEC_ALGORITHM_HEADER) {
|
|
restored.insert("x-amz-server-side-encryption".to_string(), "AES256".to_string());
|
|
}
|
|
Some(restored)
|
|
}
|
|
|
|
/// Maps a stored SSE-C metadata key to its replication transport name.
|
|
pub fn ssec_replication_transport_header(stored_key: &str) -> Option<&'static str> {
|
|
SSEC_REPLICATION_TRANSPORT_HEADERS
|
|
.iter()
|
|
.find(|(stored, _)| stored.eq_ignore_ascii_case(stored_key))
|
|
.map(|(_, transport)| *transport)
|
|
}
|
|
|
|
/// Returns true for metadata keys that must never leave the source site as
|
|
/// plain replication metadata: encryption envelopes, SSE intent headers, and
|
|
/// SSE-C material. SSE-C passthrough re-adds its keys through the transport
|
|
/// mapping instead.
|
|
pub fn is_replication_stripped_encryption_key(key: &str) -> bool {
|
|
// The x-rustfs-internal- SSE prefix is a reserved name family with no
|
|
// writer today (see RUSTFS_INTERNAL_ENCRYPTION_PREFIX); cover it here so
|
|
// this predicate is safe to use standalone, without an is_internal_key
|
|
// backstop.
|
|
super::is_encryption_metadata_key(key)
|
|
|| super::is_sse_header(key)
|
|
|| key.eq_ignore_ascii_case(SSEC_ORIGINAL_SIZE_HEADER)
|
|
|| super::starts_with_ignore_ascii_case(key, RUSTFS_INTERNAL_ENCRYPTION_PREFIX)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn ssec_transport_projection_retains_only_redacted_reader_headers() {
|
|
let mut headers = http::HeaderMap::new();
|
|
headers.insert(SSEC_ALGORITHM_HEADER, http::HeaderValue::from_static("AES256"));
|
|
headers.insert(SSEC_KEY_HEADER, http::HeaderValue::from_static("secret-key"));
|
|
headers.insert(SSEC_KEY_MD5_HEADER, http::HeaderValue::from_static("key-md5"));
|
|
headers.insert(http::header::AUTHORIZATION, http::HeaderValue::from_static("credential"));
|
|
|
|
let projected = project_ssec_transport_headers(&headers);
|
|
|
|
assert_eq!(projected.len(), 3);
|
|
assert!(projected.values().all(http::HeaderValue::is_sensitive));
|
|
assert!(projected.get(http::header::AUTHORIZATION).is_none());
|
|
assert!(!format!("{projected:?}").contains("secret-key"));
|
|
}
|
|
|
|
#[test]
|
|
fn transport_metadata_roundtrip_restores_stored_keys() {
|
|
let mut headers = http::HeaderMap::new();
|
|
headers.insert(
|
|
http::HeaderName::from_static("x-rustfs-replication-ssec-algorithm"),
|
|
http::HeaderValue::from_static("AES256"),
|
|
);
|
|
headers.insert(
|
|
http::HeaderName::from_static("x-rustfs-replication-encryption-iv"),
|
|
http::HeaderValue::from_static("iv-direct"),
|
|
);
|
|
headers.insert(
|
|
http::HeaderName::from_static("x-rustfs-replication-server-side-encryption-iv"),
|
|
http::HeaderValue::from_static("iv-minio"),
|
|
);
|
|
|
|
assert!(has_ssec_transport_headers(&headers));
|
|
let restored = ssec_transport_to_stored_metadata(&headers).expect("transport headers must restore");
|
|
// Restore MUST use the exact lowercase stored key: the read-path SSE-C
|
|
// check is case-sensitive, so a TitleCase key would classify the
|
|
// replica as managed-SSE and reject SSE-C GETs.
|
|
assert_eq!(
|
|
restored
|
|
.get("x-amz-server-side-encryption-customer-algorithm")
|
|
.map(String::as_str),
|
|
Some("AES256")
|
|
);
|
|
assert!(!restored.keys().any(|k| k != "x-amz-server-side-encryption-customer-algorithm"
|
|
&& k.eq_ignore_ascii_case("x-amz-server-side-encryption-customer-algorithm")));
|
|
assert_eq!(restored.get(INTERNAL_ENCRYPTION_IV_HEADER).map(String::as_str), Some("iv-direct"));
|
|
assert_eq!(restored.get(MINIO_INTERNAL_ENCRYPTION_IV_HEADER).map(String::as_str), Some("iv-minio"));
|
|
// The SSE marker is re-added to match the stored SSE-C shape.
|
|
assert_eq!(restored.get("x-amz-server-side-encryption").map(String::as_str), Some("AES256"));
|
|
|
|
let plain = http::HeaderMap::new();
|
|
assert!(!has_ssec_transport_headers(&plain));
|
|
assert!(ssec_transport_to_stored_metadata(&plain).is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn transport_lookup_is_case_insensitive() {
|
|
assert_eq!(
|
|
ssec_replication_transport_header("X-AMZ-SERVER-SIDE-ENCRYPTION-CUSTOMER-ALGORITHM"),
|
|
Some(REPLICATION_SSEC_ALGORITHM_HEADER)
|
|
);
|
|
assert_eq!(
|
|
ssec_replication_transport_header("x-minio-internal-server-side-encryption-sealed-key"),
|
|
Some(REPLICATION_SSE_SEALED_KEY_HEADER)
|
|
);
|
|
assert_eq!(ssec_replication_transport_header("x-rustfs-encryption-key"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn stripped_predicate_covers_envelopes_intents_and_ssec_material() {
|
|
// Managed-SSE envelope material (x-rustfs-encryption-* prefix).
|
|
assert!(is_replication_stripped_encryption_key(INTERNAL_ENCRYPTION_KEY_HEADER));
|
|
assert!(is_replication_stripped_encryption_key(INTERNAL_ENCRYPTION_KEY_ID_HEADER));
|
|
assert!(is_replication_stripped_encryption_key(INTERNAL_ENCRYPTION_CONTEXT_HEADER));
|
|
// MinIO-internal sealed material, including the managed rio-v2 keys
|
|
// that only a non-default feature build ever writes — pinning them
|
|
// here keeps the default CI honest about the full key population.
|
|
assert!(is_replication_stripped_encryption_key(MINIO_INTERNAL_ENCRYPTION_SSEC_SEALED_KEY_HEADER));
|
|
assert!(is_replication_stripped_encryption_key(MINIO_INTERNAL_ENCRYPTION_S3_SEALED_KEY_HEADER));
|
|
assert!(is_replication_stripped_encryption_key(MINIO_INTERNAL_ENCRYPTION_KMS_SEALED_KEY_HEADER));
|
|
assert!(is_replication_stripped_encryption_key(MINIO_INTERNAL_ENCRYPTION_KMS_KEY_ID_HEADER));
|
|
assert!(is_replication_stripped_encryption_key(MINIO_INTERNAL_ENCRYPTION_KMS_DATA_KEY_HEADER));
|
|
assert!(is_replication_stripped_encryption_key(MINIO_INTERNAL_ENCRYPTION_KMS_CONTEXT_HEADER));
|
|
assert!(is_replication_stripped_encryption_key(MINIO_INTERNAL_ENCRYPTION_MULTIPART_HEADER));
|
|
// The dual-key invariant's rustfs-internal twin must be covered
|
|
// standalone, without relying on an is_internal_key backstop.
|
|
assert!(is_replication_stripped_encryption_key(
|
|
"x-rustfs-internal-server-side-encryption-sealed-key"
|
|
));
|
|
// SSE intent headers, including the KMS key id.
|
|
assert!(is_replication_stripped_encryption_key("x-amz-server-side-encryption"));
|
|
assert!(is_replication_stripped_encryption_key("x-amz-server-side-encryption-aws-kms-key-id"));
|
|
assert!(is_replication_stripped_encryption_key(SSEC_ALGORITHM_HEADER));
|
|
// is_sse_header does not cover the SSE-C original-size key; the
|
|
// predicate must add it explicitly.
|
|
assert!(is_replication_stripped_encryption_key(SSEC_ORIGINAL_SIZE_HEADER));
|
|
assert!(is_replication_stripped_encryption_key(
|
|
"X-Amz-Server-Side-Encryption-Customer-Original-Size"
|
|
));
|
|
// Ordinary user metadata passes through.
|
|
assert!(!is_replication_stripped_encryption_key("x-amz-meta-app"));
|
|
assert!(!is_replication_stripped_encryption_key("content-type"));
|
|
}
|
|
|
|
#[test]
|
|
fn transport_prefixes_cover_every_transport_value_key() {
|
|
// Every transport key that carries material must match a redaction
|
|
// prefix; the multipart flag is a boolean marker and is exempt.
|
|
for (_, transport) in SSEC_REPLICATION_TRANSPORT_HEADERS {
|
|
if transport.eq_ignore_ascii_case(REPLICATION_ENCRYPTED_MULTIPART_HEADER) {
|
|
continue;
|
|
}
|
|
let lower = transport.to_lowercase();
|
|
assert!(
|
|
REPLICATION_SSE_TRANSPORT_PREFIXES
|
|
.iter()
|
|
.any(|prefix| lower.starts_with(prefix)),
|
|
"transport key {transport} is not covered by a redaction prefix"
|
|
);
|
|
}
|
|
}
|
|
}
|