Files
rustfs/crates/tls-runtime/src/material.rs
T
houseme d74e6eb042 refactor(tls): centralize runtime foundation (#3065)
* refactor(targets): move notify net helpers from utils

* refactor(tls): centralize runtime foundation

* refactor(targets): move notify net helpers from utils

* refactor(tls): centralize runtime foundation

* feat(tls-runtime): add TLS debug state and admin handler

* refactor(tls-runtime): unify TLS debug consumer status view

* fix(tls): address PR3065 review feedback

* refactor(tls): align debug status payload types

* refactor(targets): harden TLS hot reload paths

* fix(targets): resolve review-4348251652 findings

* fix(targets): finalize tls runtime review follow-ups

* fix(targets): harden tls reload and review follow-ups

* fix(targets): align tls reload handling across targets

* fix(targets): finalize tls reload state and metrics updates

* chore(deps): trim unused TLS deps

* style(targets): normalize TLS reload formatting

* refactor(targets): introduce tls runtime adapter path

* chore: update workspace manifests for tls refactor

* fix(tls): stabilize material reload and audit workflow

* fix(targets): refresh tls fingerprint flow across sinks

* fix(tls): align runtime coordinator and http reader updates

* fix(sftp): simplify protocol error mapping

* fix(tls): harmonize material loading behavior

* fix(server): finalize tls material wiring in startup flow

* fix(protos): tighten tls generation cache and deps
2026-05-24 06:41:15 +00:00

201 lines
7.7 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.
use crate::certs::{CertDirectoryLoadOptions, load_all_certs_from_directory, load_certs, load_private_key};
use crate::error::TlsRuntimeError;
use crate::fingerprint::TlsFingerprint;
use crate::source::TlsSource;
use rustfs_common::MtlsIdentityPem;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, pem::PemObject};
use std::collections::HashMap;
use std::io::Cursor;
use std::io::ErrorKind;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct OutboundTlsMaterial {
pub root_ca_pem: Vec<u8>,
pub mtls_identity: Option<MtlsIdentityPem>,
}
#[derive(Debug)]
pub enum ServerTlsMaterial {
SingleCert {
certs: Vec<CertificateDer<'static>>,
key: PrivateKeyDer<'static>,
},
MultiCert {
cert_key_pairs: HashMap<String, (Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)>,
},
}
#[derive(Debug)]
pub struct TlsMaterialSnapshot {
pub source: TlsSource,
pub server: Option<ServerTlsMaterial>,
pub outbound: OutboundTlsMaterial,
pub fingerprint: TlsFingerprint,
}
impl TlsMaterialSnapshot {
pub async fn load(source: &TlsSource) -> Result<Self, TlsRuntimeError> {
let base_dir = source.validate_directory()?.to_path_buf();
let server = load_server_material(source, &base_dir)?;
let public_ca_path = base_dir.join(&source.layout.public_ca_filename);
let client_ca_path = base_dir.join(&source.fallback_ca_filename);
let client_cert_path = base_dir.join(&source.layout.client_cert_filename);
let client_key_path = base_dir.join(&source.layout.client_key_filename);
let public_ca_pem = tokio::fs::read(&public_ca_path).await.ok();
let client_ca_pem = tokio::fs::read(&client_ca_path).await.ok();
let root_ca_pem = combine_optional_pem(public_ca_pem.as_deref(), client_ca_pem.as_deref());
let mtls_identity = match (
tokio::fs::read(&client_cert_path).await.ok(),
tokio::fs::read(&client_key_path).await.ok(),
) {
(Some(cert_pem), Some(key_pem)) => {
let mut cert_reader = Cursor::new(&cert_pem);
if CertificateDer::pem_reader_iter(&mut cert_reader).next().is_none() {
return Err(TlsRuntimeError::Material("no valid certificate in client cert PEM".to_string()));
}
let mut key_reader = Cursor::new(&key_pem);
PrivateKeyDer::from_pem_reader(&mut key_reader)
.map_err(|e| TlsRuntimeError::Material(format!("invalid client key PEM: {e}")))?;
Some(MtlsIdentityPem { cert_pem, key_pem })
}
_ => None,
};
let outbound = OutboundTlsMaterial {
root_ca_pem: root_ca_pem.clone(),
mtls_identity: mtls_identity.clone(),
};
let server_fingerprint_bytes = server.as_ref().map(serialize_server_material_for_fingerprint);
let fingerprint = TlsFingerprint::from_optional_bytes(
server_fingerprint_bytes.as_deref(),
public_ca_pem.as_deref(),
client_ca_pem.as_deref(),
mtls_identity.as_ref().map(|identity| identity.cert_pem.as_slice()),
mtls_identity.as_ref().map(|identity| identity.key_pem.as_slice()),
);
Ok(Self {
source: source.clone(),
server,
outbound,
fingerprint,
})
}
}
fn load_server_material(source: &TlsSource, base_dir: &Path) -> Result<Option<ServerTlsMaterial>, TlsRuntimeError> {
let root_cert = base_dir.join(&source.layout.server_cert_filename);
let root_key = base_dir.join(&source.layout.server_key_filename);
let has_root_pair = root_cert.exists() && root_key.exists();
let cert_key_pairs = load_all_certs_from_directory(
CertDirectoryLoadOptions::builder(base_dir, &source.layout.server_cert_filename, &source.layout.server_key_filename)
.build(),
);
match cert_key_pairs {
Ok(cert_key_pairs) if cert_key_pairs.len() > 1 || cert_key_pairs.keys().any(|key| key != "default") => {
Ok(Some(ServerTlsMaterial::MultiCert { cert_key_pairs }))
}
Ok(cert_key_pairs) if !cert_key_pairs.is_empty() => {
if let Some((certs, key)) = cert_key_pairs.get("default") {
return Ok(Some(ServerTlsMaterial::SingleCert {
certs: certs.clone(),
key: key.clone_key(),
}));
}
Ok(Some(ServerTlsMaterial::MultiCert { cert_key_pairs }))
}
Ok(_) => Ok(None),
Err(_err) if has_root_pair => {
let root_cert_path = path_to_utf8_str(&root_cert, "root TLS certificate")?;
let root_key_path = path_to_utf8_str(&root_key, "root TLS private key")?;
let certs = load_certs(root_cert_path)
.map_err(|e| TlsRuntimeError::Material(format!("load root TLS certificate {}: {e}", root_cert.display())))?;
let key = load_private_key(root_key_path)
.map_err(|e| TlsRuntimeError::Material(format!("load root TLS private key {}: {e}", root_key.display())))?;
Ok(Some(ServerTlsMaterial::SingleCert { certs, key }))
}
Err(err) if err.kind() == ErrorKind::NotFound => Ok(None),
Err(err) => Err(TlsRuntimeError::Material(format!(
"discover server TLS certificates under '{}': {err}",
base_dir.display()
))),
}
}
fn path_to_utf8_str<'a>(path: &'a Path, description: &str) -> Result<&'a str, TlsRuntimeError> {
path.to_str()
.ok_or_else(|| TlsRuntimeError::Material(format!("{description} path '{}' is not valid UTF-8", path.display())))
}
fn combine_optional_pem(primary: Option<&[u8]>, fallback: Option<&[u8]>) -> Vec<u8> {
let mut combined = Vec::new();
for pem in [primary, fallback].into_iter().flatten() {
if pem.iter().all(|&b| b.is_ascii_whitespace()) {
continue;
}
combined.extend_from_slice(pem);
if !combined.ends_with(b"\n") {
combined.push(b'\n');
}
}
combined
}
fn serialize_server_material_for_fingerprint(material: &ServerTlsMaterial) -> Vec<u8> {
match material {
ServerTlsMaterial::SingleCert { certs, key } => {
let mut bytes = Vec::new();
for cert in certs {
bytes.extend_from_slice(cert.as_ref());
}
bytes.extend_from_slice(key.secret_der());
bytes
}
ServerTlsMaterial::MultiCert { cert_key_pairs } => {
let mut entries = cert_key_pairs.iter().collect::<Vec<_>>();
entries.sort_by_key(|(left, _)| *left);
let mut bytes = Vec::new();
for (domain, (certs, key)) in entries {
bytes.extend_from_slice(domain.as_bytes());
for cert in certs {
bytes.extend_from_slice(cert.as_ref());
}
bytes.extend_from_slice(key.secret_der());
}
bytes
}
}
}
pub(crate) fn server_material_fingerprint(material: &ServerTlsMaterial) -> TlsFingerprint {
let bytes = serialize_server_material_for_fingerprint(material);
TlsFingerprint::from_optional_bytes(Some(&bytes), None, None, None, None)
}