use std::fs;
use std::io::BufReader;
use std::path::Path;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use parking_lot::RwLock;
use rcgen::{CertificateParams, DistinguishedName, DnType, Issuer, KeyPair, SanType};
use rustls::RootCertStore;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use tokio::sync::watch;
use tracing::{debug, error, info, warn};
use x509_parser::prelude::*;
use crate::error::{NetError, NetResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CertificateFormat {
Pem,
Der,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PrivateKeyType {
Rsa,
Ecdsa,
Ed25519,
Pkcs8,
}
#[derive(Debug, Clone)]
pub struct CertificateInfo {
pub common_name: Option<String>,
pub subject_alt_names: Vec<String>,
pub issuer: Option<String>,
pub serial_number: String,
pub not_before: SystemTime,
pub not_after: SystemTime,
pub is_ca: bool,
pub key_usage: Vec<String>,
pub extended_key_usage: Vec<String>,
pub fingerprint_sha256: String,
}
impl CertificateInfo {
pub fn is_valid(&self) -> bool {
let now = SystemTime::now();
now >= self.not_before && now <= self.not_after
}
pub fn time_to_expiry(&self) -> Option<Duration> {
SystemTime::now()
.duration_since(self.not_after)
.ok()
.map(|_| Duration::ZERO)
.or_else(|| self.not_after.duration_since(SystemTime::now()).ok())
}
pub fn expires_within(&self, duration: Duration) -> bool {
self.time_to_expiry()
.is_some_and(|remaining| remaining <= duration)
}
}
#[derive(Debug, Clone)]
pub struct CertificateLoader {
validate_on_load: bool,
}
impl Default for CertificateLoader {
fn default() -> Self {
Self::new()
}
}
impl CertificateLoader {
pub fn new() -> Self {
Self {
validate_on_load: true,
}
}
pub fn without_validation() -> Self {
Self {
validate_on_load: false,
}
}
pub fn load_pem_file<P: AsRef<Path>>(
&self,
path: P,
) -> NetResult<Vec<CertificateDer<'static>>> {
let path = path.as_ref();
debug!(path = %path.display(), "Loading PEM certificates from file");
let file = fs::File::open(path)
.map_err(|e| NetError::InvalidCertificate(format!("Failed to open PEM file: {e}")))?;
let mut reader = BufReader::new(file);
let certs: Vec<_> = rustls_pemfile::certs(&mut reader)
.filter_map(|result| result.ok())
.collect();
if certs.is_empty() {
return Err(NetError::InvalidCertificate(
"No certificates found in PEM file".to_string(),
));
}
if self.validate_on_load {
for cert in &certs {
self.validate_certificate_der(cert)?;
}
}
info!(count = certs.len(), "Loaded certificates from PEM file");
Ok(certs)
}
pub fn load_der_file<P: AsRef<Path>>(&self, path: P) -> NetResult<CertificateDer<'static>> {
let path = path.as_ref();
debug!(path = %path.display(), "Loading DER certificate from file");
let der_data = fs::read(path)
.map_err(|e| NetError::InvalidCertificate(format!("Failed to read DER file: {e}")))?;
let cert = CertificateDer::from(der_data);
if self.validate_on_load {
self.validate_certificate_der(&cert)?;
}
info!("Loaded DER certificate from file");
Ok(cert)
}
pub fn load_pem_bytes(&self, pem_data: &[u8]) -> NetResult<Vec<CertificateDer<'static>>> {
let mut reader = BufReader::new(pem_data);
let certs: Vec<_> = rustls_pemfile::certs(&mut reader)
.filter_map(|result| result.ok())
.collect();
if certs.is_empty() {
return Err(NetError::InvalidCertificate(
"No certificates found in PEM data".to_string(),
));
}
if self.validate_on_load {
for cert in &certs {
self.validate_certificate_der(cert)?;
}
}
Ok(certs)
}
pub fn load_der_bytes(&self, der_data: &[u8]) -> NetResult<CertificateDer<'static>> {
let cert = CertificateDer::from(der_data.to_vec());
if self.validate_on_load {
self.validate_certificate_der(&cert)?;
}
Ok(cert)
}
fn validate_certificate_der(&self, cert: &CertificateDer<'_>) -> NetResult<()> {
let (_, parsed) = X509Certificate::from_der(cert.as_ref()).map_err(|e| {
NetError::InvalidCertificate(format!("Failed to parse certificate: {e}"))
})?;
let now = ASN1Time::now();
if parsed.validity().not_before > now {
return Err(NetError::InvalidCertificate(
"Certificate is not yet valid".to_string(),
));
}
if parsed.validity().not_after < now {
return Err(NetError::InvalidCertificate(
"Certificate has expired".to_string(),
));
}
Ok(())
}
pub fn get_certificate_info(&self, cert: &CertificateDer<'_>) -> NetResult<CertificateInfo> {
let (_, parsed) = X509Certificate::from_der(cert.as_ref()).map_err(|e| {
NetError::InvalidCertificate(format!("Failed to parse certificate: {e}"))
})?;
let common_name = parsed
.subject()
.iter_common_name()
.next()
.and_then(|cn| cn.as_str().ok())
.map(String::from);
let issuer = parsed
.issuer()
.iter_common_name()
.next()
.and_then(|cn| cn.as_str().ok())
.map(String::from);
let mut subject_alt_names = Vec::new();
if let Ok(Some(san)) = parsed.subject_alternative_name() {
for name in san.value.general_names.iter() {
match name {
GeneralName::DNSName(dns) => subject_alt_names.push(dns.to_string()),
GeneralName::IPAddress(ip) => {
if ip.len() == 4 {
subject_alt_names
.push(format!("{}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3]));
} else if ip.len() == 16 {
let mut parts = Vec::with_capacity(8);
for i in 0..8 {
let val = u16::from_be_bytes([ip[i * 2], ip[i * 2 + 1]]);
parts.push(format!("{val:x}"));
}
subject_alt_names.push(parts.join(":"));
}
}
GeneralName::RFC822Name(email) => subject_alt_names.push(email.to_string()),
GeneralName::URI(uri) => subject_alt_names.push(uri.to_string()),
_ => {}
}
}
}
let serial_number = format!("{:x}", parsed.serial);
let not_before = asn1_time_to_system_time(&parsed.validity().not_before);
let not_after = asn1_time_to_system_time(&parsed.validity().not_after);
let is_ca = parsed.is_ca();
let mut key_usage = Vec::new();
if let Ok(Some(ku)) = parsed.key_usage() {
let flags = ku.value;
if flags.digital_signature() {
key_usage.push("digitalSignature".to_string());
}
if flags.non_repudiation() {
key_usage.push("nonRepudiation".to_string());
}
if flags.key_encipherment() {
key_usage.push("keyEncipherment".to_string());
}
if flags.data_encipherment() {
key_usage.push("dataEncipherment".to_string());
}
if flags.key_agreement() {
key_usage.push("keyAgreement".to_string());
}
if flags.key_cert_sign() {
key_usage.push("keyCertSign".to_string());
}
if flags.crl_sign() {
key_usage.push("cRLSign".to_string());
}
}
let mut extended_key_usage = Vec::new();
if let Ok(Some(eku)) = parsed.extended_key_usage() {
for oid in eku.value.other.iter() {
extended_key_usage.push(oid.to_string());
}
if eku.value.any {
extended_key_usage.push("anyExtendedKeyUsage".to_string());
}
if eku.value.server_auth {
extended_key_usage.push("serverAuth".to_string());
}
if eku.value.client_auth {
extended_key_usage.push("clientAuth".to_string());
}
if eku.value.code_signing {
extended_key_usage.push("codeSigning".to_string());
}
if eku.value.email_protection {
extended_key_usage.push("emailProtection".to_string());
}
if eku.value.time_stamping {
extended_key_usage.push("timeStamping".to_string());
}
if eku.value.ocsp_signing {
extended_key_usage.push("ocspSigning".to_string());
}
}
use std::fmt::Write;
let fingerprint_sha256 = cert
.as_ref()
.iter()
.take(32) .fold(String::new(), |mut s, b| {
let _ = write!(&mut s, "{b:02x}");
s
});
Ok(CertificateInfo {
common_name,
subject_alt_names,
issuer,
serial_number,
not_before,
not_after,
is_ca,
key_usage,
extended_key_usage,
fingerprint_sha256,
})
}
}
fn asn1_time_to_system_time(time: &ASN1Time) -> SystemTime {
let timestamp = time.timestamp();
if timestamp >= 0 {
SystemTime::UNIX_EPOCH + Duration::from_secs(timestamp as u64)
} else {
SystemTime::UNIX_EPOCH
}
}
#[derive(Debug, Clone)]
pub struct PrivateKeyLoader;
impl Default for PrivateKeyLoader {
fn default() -> Self {
Self::new()
}
}
impl PrivateKeyLoader {
pub fn new() -> Self {
Self
}
pub fn load_pem_file<P: AsRef<Path>>(&self, path: P) -> NetResult<PrivateKeyDer<'static>> {
let path = path.as_ref();
debug!(path = %path.display(), "Loading private key from PEM file");
let file = fs::File::open(path)
.map_err(|e| NetError::InvalidCertificate(format!("Failed to open key file: {e}")))?;
let mut reader = BufReader::new(file);
self.load_from_reader(&mut reader)
}
pub fn load_pem_bytes(&self, pem_data: &[u8]) -> NetResult<PrivateKeyDer<'static>> {
let mut reader = BufReader::new(pem_data);
self.load_from_reader(&mut reader)
}
pub fn load_der_file<P: AsRef<Path>>(
&self,
path: P,
key_type: PrivateKeyType,
) -> NetResult<PrivateKeyDer<'static>> {
let path = path.as_ref();
debug!(path = %path.display(), "Loading private key from DER file");
let der_data = fs::read(path)
.map_err(|e| NetError::InvalidCertificate(format!("Failed to read key file: {e}")))?;
self.load_der_bytes(&der_data, key_type)
}
pub fn load_der_bytes(
&self,
der_data: &[u8],
key_type: PrivateKeyType,
) -> NetResult<PrivateKeyDer<'static>> {
let key = match key_type {
PrivateKeyType::Rsa => PrivateKeyDer::Pkcs1(der_data.to_vec().into()),
PrivateKeyType::Ecdsa | PrivateKeyType::Ed25519 => {
PrivateKeyDer::Sec1(der_data.to_vec().into())
}
PrivateKeyType::Pkcs8 => {
PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(der_data.to_vec()))
}
};
Ok(key)
}
pub fn load_encrypted_pem_file<P: AsRef<Path>>(
&self,
path: P,
password: &str,
) -> NetResult<PrivateKeyDer<'static>> {
let path = path.as_ref();
debug!(path = %path.display(), "Loading potentially encrypted private key");
let pem_data = fs::read(path)
.map_err(|e| NetError::InvalidCertificate(format!("Failed to read key file: {e}")))?;
let pem_str = std::str::from_utf8(&pem_data).map_err(|e| {
NetError::InvalidCertificate(format!("Key file is not valid UTF-8: {e}"))
})?;
let enc_format = detect_encrypted_pem(pem_str);
match enc_format {
EncryptedPemFormat::NotEncrypted => {
debug!("Key is not encrypted, loading directly");
self.load_pem_bytes(&pem_data)
}
EncryptedPemFormat::Pkcs8Encrypted => {
let effective_password = resolve_password(password)?;
decrypt_pkcs8_encrypted_pem(pem_str, &effective_password)
}
EncryptedPemFormat::LegacyEncrypted => {
let effective_password = resolve_password(password)?;
decrypt_legacy_encrypted_pem(pem_str, &effective_password)
}
}
}
pub fn load_encrypted_pem_file_env<P: AsRef<Path>>(
&self,
path: P,
) -> NetResult<PrivateKeyDer<'static>> {
self.load_encrypted_pem_file(path, "")
}
fn load_from_reader<R: std::io::BufRead>(
&self,
reader: &mut R,
) -> NetResult<PrivateKeyDer<'static>> {
let mut original_data: Vec<u8> = Vec::new();
reader
.read_to_end(&mut original_data)
.map_err(|e| NetError::InvalidCertificate(format!("Failed to read key data: {e}")))?;
let mut cursor = std::io::Cursor::new(&original_data);
if let Some(Ok(key)) = rustls_pemfile::pkcs8_private_keys(&mut cursor).next() {
info!("Loaded PKCS#8 private key");
return Ok(PrivateKeyDer::Pkcs8(key));
}
let mut cursor = std::io::Cursor::new(&original_data);
if let Some(Ok(key)) = rustls_pemfile::rsa_private_keys(&mut cursor).next() {
info!("Loaded RSA private key");
return Ok(PrivateKeyDer::Pkcs1(key));
}
let mut cursor = std::io::Cursor::new(&original_data);
if let Some(Ok(key)) = rustls_pemfile::ec_private_keys(&mut cursor).next() {
info!("Loaded EC private key");
return Ok(PrivateKeyDer::Sec1(key));
}
Err(NetError::InvalidCertificate(
"No valid private key found in PEM data (tried PKCS#8, RSA, EC formats)".to_string(),
))
}
}
pub use crate::tls_crypto::{
EncryptedPemFormat, detect_encrypted_pem, parse_dek_info, pbkdf2_hmac_sha1, pbkdf2_hmac_sha256,
};
use crate::tls_crypto::{
decrypt_legacy_encrypted_pem, decrypt_pkcs8_encrypted_pem, resolve_password,
};
#[derive(Debug, Clone)]
pub struct SelfSignedGenerator {
subject_alt_names: Vec<String>,
common_name: String,
organization: Option<String>,
validity_days: u32,
is_ca: bool,
}
impl SelfSignedGenerator {
pub fn new(common_name: impl Into<String>) -> Self {
Self {
common_name: common_name.into(),
subject_alt_names: vec!["localhost".to_string()],
organization: None,
validity_days: 365,
is_ca: false,
}
}
pub fn with_san(mut self, san: impl Into<String>) -> Self {
self.subject_alt_names.push(san.into());
self
}
pub fn with_sans<I, S>(mut self, sans: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.subject_alt_names
.extend(sans.into_iter().map(|s| s.into()));
self
}
pub fn with_organization(mut self, org: impl Into<String>) -> Self {
self.organization = Some(org.into());
self
}
pub fn with_validity_days(mut self, days: u32) -> Self {
self.validity_days = days;
self
}
pub fn as_ca(mut self) -> Self {
self.is_ca = true;
self
}
pub fn generate(&self) -> NetResult<(CertificateDer<'static>, PrivateKeyDer<'static>)> {
let mut params = CertificateParams::default();
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, &self.common_name);
if let Some(ref org) = self.organization {
dn.push(DnType::OrganizationName, org);
}
params.distinguished_name = dn;
params.subject_alt_names = self
.subject_alt_names
.iter()
.map(|name| {
if let Ok(ip) = name.parse::<std::net::IpAddr>() {
SanType::IpAddress(ip)
} else {
SanType::DnsName(name.clone().try_into().unwrap_or_else(|_| {
"localhost"
.to_string()
.try_into()
.expect("localhost is valid DNS name")
}))
}
})
.collect();
params.not_before = rcgen::date_time_ymd(
chrono::Utc::now().year(),
chrono::Utc::now().month() as u8,
chrono::Utc::now().day() as u8,
);
let future = chrono::Utc::now() + chrono::Duration::days(self.validity_days as i64);
params.not_after =
rcgen::date_time_ymd(future.year(), future.month() as u8, future.day() as u8);
if self.is_ca {
params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
}
let key_pair = KeyPair::generate().map_err(|e| {
NetError::InvalidCertificate(format!("Failed to generate key pair: {e}"))
})?;
let cert = params.self_signed(&key_pair).map_err(|e| {
NetError::InvalidCertificate(format!("Failed to generate certificate: {e}"))
})?;
let cert_der = CertificateDer::from(cert.der().to_vec());
let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialize_der()));
info!(
common_name = %self.common_name,
is_ca = self.is_ca,
validity_days = self.validity_days,
"Generated self-signed certificate"
);
Ok((cert_der, key_der))
}
pub fn generate_signed_by_keypair(
&self,
ca_key_pair: &KeyPair,
ca_common_name: &str,
) -> NetResult<(CertificateDer<'static>, PrivateKeyDer<'static>)> {
let mut params = CertificateParams::default();
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, &self.common_name);
if let Some(ref org) = self.organization {
dn.push(DnType::OrganizationName, org);
}
params.distinguished_name = dn;
params.subject_alt_names = self
.subject_alt_names
.iter()
.map(|name| {
if let Ok(ip) = name.parse::<std::net::IpAddr>() {
SanType::IpAddress(ip)
} else {
SanType::DnsName(name.clone().try_into().unwrap_or_else(|_| {
"localhost"
.to_string()
.try_into()
.expect("localhost is valid DNS name")
}))
}
})
.collect();
params.not_before = rcgen::date_time_ymd(
chrono::Utc::now().year(),
chrono::Utc::now().month() as u8,
chrono::Utc::now().day() as u8,
);
let future = chrono::Utc::now() + chrono::Duration::days(self.validity_days as i64);
params.not_after =
rcgen::date_time_ymd(future.year(), future.month() as u8, future.day() as u8);
let key_pair = KeyPair::generate().map_err(|e| {
NetError::InvalidCertificate(format!("Failed to generate key pair: {e}"))
})?;
let mut ca_params = CertificateParams::default();
ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
let mut issuer_dn = DistinguishedName::new();
issuer_dn.push(DnType::CommonName, ca_common_name);
ca_params.distinguished_name = issuer_dn;
let issuer = Issuer::from_params(&ca_params, ca_key_pair);
let signed_cert = params.signed_by(&key_pair, &issuer).map_err(|e| {
NetError::InvalidCertificate(format!("Failed to sign certificate: {e}"))
})?;
let cert_der = CertificateDer::from(signed_cert.der().to_vec());
let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key_pair.serialize_der()));
info!(
common_name = %self.common_name,
"Generated CA-signed certificate"
);
Ok((cert_der, key_der))
}
}
use chrono::Datelike;
#[derive(Debug)]
pub struct CertificateStore {
roots: Arc<RwLock<RootCertStore>>,
cert_chain: Arc<RwLock<Vec<CertificateDer<'static>>>>,
cert_info: Arc<RwLock<Vec<CertificateInfo>>>,
}
impl Default for CertificateStore {
fn default() -> Self {
Self::new()
}
}
impl Clone for CertificateStore {
fn clone(&self) -> Self {
Self {
roots: Arc::new(RwLock::new((*self.roots.read()).clone())),
cert_chain: Arc::new(RwLock::new(self.cert_chain.read().clone())),
cert_info: Arc::new(RwLock::new(self.cert_info.read().clone())),
}
}
}
impl CertificateStore {
pub fn new() -> Self {
Self {
roots: Arc::new(RwLock::new(RootCertStore::empty())),
cert_chain: Arc::new(RwLock::new(Vec::new())),
cert_info: Arc::new(RwLock::new(Vec::new())),
}
}
pub fn add_system_roots(&mut self) -> NetResult<usize> {
let mut roots = self.roots.write();
let count_before = roots.len();
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
let added = roots.len() - count_before;
info!(count = added, "Added system root certificates");
Ok(added)
}
pub fn add_certificate(&mut self, cert: CertificateDer<'static>) -> NetResult<()> {
let loader = CertificateLoader::new();
let info = loader.get_certificate_info(&cert)?;
if !info.is_ca {
warn!(common_name = ?info.common_name, "Adding non-CA certificate to root store");
}
{
let mut roots = self.roots.write();
roots.add(cert.clone()).map_err(|e| {
NetError::InvalidCertificate(format!("Failed to add certificate: {e}"))
})?;
}
{
let mut chain = self.cert_chain.write();
chain.push(cert);
}
{
let mut infos = self.cert_info.write();
infos.push(info);
}
Ok(())
}
pub fn add_certificates_from_file<P: AsRef<Path>>(&mut self, path: P) -> NetResult<usize> {
let loader = CertificateLoader::new();
let certs = loader.load_pem_file(path)?;
let count = certs.len();
for cert in certs {
self.add_certificate(cert)?;
}
Ok(count)
}
pub fn get_root_store(&self) -> RootCertStore {
self.roots.read().clone()
}
pub fn get_cert_chain(&self) -> Vec<CertificateDer<'static>> {
self.cert_chain.read().clone()
}
pub fn len(&self) -> usize {
self.roots.read().len()
}
pub fn is_empty(&self) -> bool {
self.roots.read().is_empty()
}
pub fn get_certificate_infos(&self) -> Vec<CertificateInfo> {
self.cert_info.read().clone()
}
pub fn check_expiring(&self, within: Duration) -> Vec<CertificateInfo> {
self.cert_info
.read()
.iter()
.filter(|info| info.expires_within(within))
.cloned()
.collect()
}
}
#[derive(Debug, Clone)]
enum PrivateKeyData {
Pkcs8(Vec<u8>),
Pkcs1(Vec<u8>),
Sec1(Vec<u8>),
}
impl PrivateKeyData {
fn from_key(key: &PrivateKeyDer<'_>) -> Self {
match key {
PrivateKeyDer::Pkcs8(k) => Self::Pkcs8(k.secret_pkcs8_der().to_vec()),
PrivateKeyDer::Pkcs1(k) => Self::Pkcs1(k.secret_pkcs1_der().to_vec()),
PrivateKeyDer::Sec1(k) => Self::Sec1(k.secret_sec1_der().to_vec()),
_ => Self::Pkcs8(Vec::new()), }
}
fn to_key(&self) -> PrivateKeyDer<'static> {
match self {
Self::Pkcs8(data) => PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(data.clone())),
Self::Pkcs1(data) => PrivateKeyDer::Pkcs1(data.clone().into()),
Self::Sec1(data) => PrivateKeyDer::Sec1(data.clone().into()),
}
}
}
pub struct HotReloadableCertificates {
cert_chain: Arc<RwLock<Vec<CertificateDer<'static>>>>,
private_key_data: Arc<RwLock<Option<PrivateKeyData>>>,
update_tx: watch::Sender<u64>,
version: Arc<RwLock<u64>>,
cert_path: Arc<RwLock<Option<std::path::PathBuf>>>,
key_path: Arc<RwLock<Option<std::path::PathBuf>>>,
}
impl std::fmt::Debug for HotReloadableCertificates {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HotReloadableCertificates")
.field("version", &*self.version.read())
.field("cert_count", &self.cert_chain.read().len())
.field("has_key", &self.private_key_data.read().is_some())
.finish()
}
}
impl Default for HotReloadableCertificates {
fn default() -> Self {
Self::new()
}
}
impl HotReloadableCertificates {
pub fn new() -> Self {
let (update_tx, _) = watch::channel(0u64);
Self {
cert_chain: Arc::new(RwLock::new(Vec::new())),
private_key_data: Arc::new(RwLock::new(None)),
update_tx,
version: Arc::new(RwLock::new(0)),
cert_path: Arc::new(RwLock::new(None)),
key_path: Arc::new(RwLock::new(None)),
}
}
pub fn load_from_files<P: AsRef<Path>>(&self, cert_path: P, key_path: P) -> NetResult<()> {
let cert_path = cert_path.as_ref();
let key_path = key_path.as_ref();
let loader = CertificateLoader::new();
let key_loader = PrivateKeyLoader::new();
let certs = loader.load_pem_file(cert_path)?;
let key = key_loader.load_pem_file(key_path)?;
{
let mut chain = self.cert_chain.write();
*chain = certs;
}
{
let mut pk = self.private_key_data.write();
*pk = Some(PrivateKeyData::from_key(&key));
}
{
let mut cp = self.cert_path.write();
*cp = Some(cert_path.to_path_buf());
}
{
let mut kp = self.key_path.write();
*kp = Some(key_path.to_path_buf());
}
self.increment_version();
info!(
cert_path = %cert_path.display(),
key_path = %key_path.display(),
"Loaded certificates from files"
);
Ok(())
}
pub fn reload(&self) -> NetResult<()> {
let cert_path = self.cert_path.read().clone();
let key_path = self.key_path.read().clone();
match (cert_path, key_path) {
(Some(cp), Some(kp)) => {
self.load_from_files(&cp, &kp)?;
info!("Reloaded certificates");
Ok(())
}
_ => Err(NetError::InvalidCertificate(
"No certificate paths configured for reload".to_string(),
)),
}
}
pub fn set_certificates(
&self,
certs: Vec<CertificateDer<'static>>,
key: PrivateKeyDer<'static>,
) {
{
let mut chain = self.cert_chain.write();
*chain = certs;
}
{
let mut pk = self.private_key_data.write();
*pk = Some(PrivateKeyData::from_key(&key));
}
self.increment_version();
}
pub fn get_cert_chain(&self) -> Vec<CertificateDer<'static>> {
self.cert_chain.read().clone()
}
pub fn get_private_key(&self) -> Option<PrivateKeyDer<'static>> {
self.private_key_data.read().as_ref().map(|k| k.to_key())
}
pub fn get_version(&self) -> u64 {
*self.version.read()
}
pub fn subscribe(&self) -> watch::Receiver<u64> {
self.update_tx.subscribe()
}
fn increment_version(&self) {
let mut version = self.version.write();
*version += 1;
let _ = self.update_tx.send(*version);
}
pub fn start_file_watcher(
self: Arc<Self>,
check_interval: Duration,
) -> NetResult<tokio::task::JoinHandle<()>> {
let cert_path = self.cert_path.read().clone();
let key_path = self.key_path.read().clone();
let (cert_path, key_path) = match (cert_path, key_path) {
(Some(cp), Some(kp)) => (cp, kp),
_ => {
return Err(NetError::InvalidCertificate(
"No certificate paths configured for file watching".to_string(),
));
}
};
let handle = tokio::spawn(async move {
let mut last_cert_modified = get_file_modified(&cert_path);
let mut last_key_modified = get_file_modified(&key_path);
loop {
tokio::time::sleep(check_interval).await;
let cert_modified = get_file_modified(&cert_path);
let key_modified = get_file_modified(&key_path);
let cert_changed = cert_modified != last_cert_modified;
let key_changed = key_modified != last_key_modified;
if cert_changed || key_changed {
info!(
cert_changed = cert_changed,
key_changed = key_changed,
"Detected certificate file change, reloading"
);
match self.reload() {
Ok(()) => {
last_cert_modified = cert_modified;
last_key_modified = key_modified;
}
Err(e) => {
error!(error = %e, "Failed to reload certificates");
}
}
}
}
});
Ok(handle)
}
}
fn get_file_modified<P: AsRef<Path>>(path: P) -> Option<SystemTime> {
fs::metadata(path.as_ref())
.ok()
.and_then(|m| m.modified().ok())
}
#[cfg(test)]
#[path = "tls_tests.rs"]
mod tests;