mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-12 16:16:55 +00:00
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
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user