use crate::error::TargetError;
use rustfs_tls_runtime::{load_certs, load_private_key};
pub fn validate_cert_key_pairing(cert_path: &str, key_path: &str) -> Result<(), TargetError> {
if cert_path.is_empty() && key_path.is_empty() {
return Ok(());
}
if cert_path.is_empty() || key_path.is_empty() {
return Err(TargetError::Configuration(
"Client certificate and key must both be specified or both be empty".to_string(),
));
}
let certs = load_certs(cert_path)
.map_err(|e| TargetError::Configuration(format!("Invalid client certificate '{cert_path}': {e}")))?;
let key =
load_private_key(key_path).map_err(|e| TargetError::Configuration(format!("Invalid client key '{key_path}': {e}")))?;
let signing_key = rustls::crypto::aws_lc_rs::sign::any_supported_type(&key)
.map_err(|e| TargetError::Configuration(format!("Unsupported client private key '{key_path}': {e:?}")))?;
let certified = rustls::sign::CertifiedKey::new(certs, signing_key);
match certified.keys_match() {
Ok(()) => Ok(()),
Err(rustls::Error::InconsistentKeys(rustls::InconsistentKeys::Unknown)) => Ok(()),
Err(e) => Err(TargetError::Configuration(format!(
"Client certificate '{cert_path}' and key '{key_path}' do not match: {e}"
))),
}
}
pub fn validate_ca_file(ca_path: &str) -> Result<(), TargetError> {
if ca_path.is_empty() {
return Ok(());
}
load_certs(ca_path).map_err(|e| TargetError::Configuration(format!("Invalid CA certificate '{ca_path}': {e}")))?;
Ok(())
}
pub fn validate_tls_material(ca_path: &str, cert_path: &str, key_path: &str) -> Result<(), TargetError> {
validate_ca_file(ca_path)?;
validate_cert_key_pairing(cert_path, key_path)
}
#[cfg(test)]
mod tests {
use super::validate_cert_key_pairing;
use std::path::Path;
use tempfile::TempDir;
struct Pair {
cert_pem: String,
key_pem: String,
}
fn generate_pair() -> Pair {
let rcgen::CertifiedKey { cert, signing_key } =
rcgen::generate_simple_self_signed(vec!["example.com".to_string()]).expect("cert should generate");
Pair {
cert_pem: cert.pem(),
key_pem: signing_key.serialize_pem(),
}
}
fn write(dir: &Path, name: &str, contents: &str) -> String {
let path = dir.join(name);
std::fs::write(&path, contents).expect("write pem");
path.to_string_lossy().into_owned()
}
#[test]
fn empty_paths_are_ok() {
assert!(validate_cert_key_pairing("", "").is_ok());
}
#[test]
fn one_empty_path_is_rejected() {
assert!(validate_cert_key_pairing("/some/cert.pem", "").is_err());
assert!(validate_cert_key_pairing("", "/some/key.pem").is_err());
}
#[test]
fn matching_cert_and_key_pass() {
let dir = TempDir::new().expect("tempdir");
let pair = generate_pair();
let cert_path = write(dir.path(), "cert.pem", &pair.cert_pem);
let key_path = write(dir.path(), "key.pem", &pair.key_pem);
validate_cert_key_pairing(&cert_path, &key_path).expect("matching cert/key must validate");
}
#[test]
fn mismatched_cert_and_key_are_rejected() {
let dir = TempDir::new().expect("tempdir");
let a = generate_pair();
let b = generate_pair();
let cert_path = write(dir.path(), "cert.pem", &a.cert_pem);
let key_path = write(dir.path(), "key.pem", &b.key_pem);
let err = validate_cert_key_pairing(&cert_path, &key_path).expect_err("mismatched cert/key must be rejected");
assert!(err.to_string().contains("do not match"), "unexpected error: {err}");
}
}