use std::path::PathBuf;
use rcgen::{CertificateParams, DnType, IsCa, Issuer, KeyPair, SanType};
use tracing::info;
fn tls_dir() -> PathBuf {
dirs::config_dir()
.unwrap_or_default()
.join("llm-manager")
.join("tls")
}
fn ca_cert_path() -> PathBuf {
tls_dir().join("ca.pem")
}
fn ca_key_path() -> PathBuf {
tls_dir().join("ca-key.pem")
}
fn server_cert_path() -> PathBuf {
tls_dir().join("server.pem")
}
fn server_key_path() -> PathBuf {
tls_dir().join("server-key.pem")
}
pub async fn load_tls_config(
cert_path: &str,
key_path: &str,
) -> Result<axum_server::tls_rustls::RustlsConfig, Box<dyn std::error::Error + Send + Sync>> {
info!("Loading TLS config from {} and {}", cert_path, key_path);
axum_server::tls_rustls::RustlsConfig::from_pem_file(cert_path, key_path)
.await
.map_err(|e| e.into())
}
fn generate_ca() -> Result<(String, String), Box<dyn std::error::Error + Send + Sync>> {
let key = KeyPair::generate()?;
let mut params = CertificateParams::default();
params.is_ca = IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
params
.distinguished_name
.push(DnType::CommonName, "llm-manager CA".to_string());
let cert = params.self_signed(&key)?;
let cert_pem = cert.pem();
let key_pem = key.serialize_pem();
Ok((cert_pem, key_pem))
}
fn parse_ca_from_pem(
ca_cert_pem: &str,
ca_key_pem: &str,
) -> Result<(Issuer<'static, KeyPair>, KeyPair), Box<dyn std::error::Error + Send + Sync>> {
let ca_key1 = KeyPair::from_pem(ca_key_pem)?;
let ca_issuer = Issuer::from_ca_cert_pem(ca_cert_pem, ca_key1)?;
let ca_key2 = KeyPair::from_pem(ca_key_pem)?;
Ok((ca_issuer, ca_key2))
}
fn generate_server_cert(
ca_cert_pem: &str,
ca_key_pem: &str,
) -> Result<(String, String), Box<dyn std::error::Error + Send + Sync>> {
let server_key = KeyPair::generate()?;
let (ca_issuer, _ca_key) = parse_ca_from_pem(ca_cert_pem, ca_key_pem)?;
let mut params = CertificateParams::default();
params.subject_alt_names = vec![
SanType::DnsName("localhost".try_into().unwrap()),
SanType::IpAddress([127, 0, 0, 1].into()),
SanType::IpAddress([0, 0, 0, 0].into()),
];
let cert = params.signed_by(&server_key, &ca_issuer)?;
let cert_pem = cert.pem();
let key_pem = server_key.serialize_pem();
Ok((cert_pem, key_pem))
}
pub fn ensure_tls_certs() -> Result<(PathBuf, PathBuf), Box<dyn std::error::Error + Send + Sync>> {
let ca_path = ca_cert_path();
let ca_key_path = ca_key_path();
let server_cert_path = server_cert_path();
let server_key_path = server_key_path();
if server_cert_path.exists() && server_key_path.exists() {
return Ok((server_cert_path, server_key_path));
}
std::fs::create_dir_all(tls_dir())?;
let (ca_cert_pem, ca_key_pem) = if ca_path.exists() && ca_key_path.exists() {
(
std::fs::read_to_string(&ca_path)?,
std::fs::read_to_string(&ca_key_path)?,
)
} else {
let (cert, key) = generate_ca()?;
std::fs::write(&ca_path, &cert)?;
std::fs::write(&ca_key_path, &key)?;
(cert, key)
};
let (server_cert, server_key) = generate_server_cert(&ca_cert_pem, &ca_key_pem)?;
std::fs::write(&server_cert_path, &server_cert)?;
std::fs::write(&server_key_path, &server_key)?;
info!(
"Generated self-signed TLS certificates in {}",
tls_dir().display()
);
info!(
"To trust this CA, install {} into your system trust store:",
ca_path.display()
);
info!(
" Linux (system-wide): sudo cp {} /usr/local/share/ca-certificates/ && sudo update-ca-certificates",
ca_path.display()
);
info!(
" macOS: sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain {}",
ca_path.display()
);
Ok((server_cert_path, server_key_path))
}
pub fn validate_tls_path(path: &str) -> Result<(), String> {
let p = PathBuf::from(path);
if !p.exists() {
return Err(format!("TLS file not found: {}", path));
}
if !p.is_file() {
return Err(format!("TLS path is not a file: {}", path));
}
Ok(())
}