#[cfg(feature = "experimental-rust-tls")]
use std::io;
#[cfg(feature = "experimental-rust-tls")]
use std::path::Path;
#[cfg(feature = "experimental-rust-tls")]
use std::sync::Arc;
#[cfg(feature = "experimental-rust-tls")]
use rustls_pki_types::{CertificateDer, PrivateKeyDer};
#[cfg(feature = "experimental-rust-tls")]
pub use tokio_rustls::rustls::ServerConfig;
#[cfg(feature = "experimental-rust-tls")]
use tokio_rustls::rustls::{ClientConfig, RootCertStore};
#[cfg(feature = "experimental-rust-tls")]
pub mod ca_secure;
#[cfg(feature = "experimental-rust-tls")]
#[derive(Clone)]
pub enum TlsConfig {
Server(Arc<ServerConfig>),
Client(Arc<ClientConfig>),
}
#[cfg(feature = "experimental-rust-tls")]
impl TlsConfig {
pub fn server_from_pem(
cert_chain: Vec<CertificateDer<'static>>,
key: PrivateKeyDer<'static>,
) -> Result<Self, TlsError> {
let _ = rustls::crypto::ring::default_provider().install_default();
let cfg = ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(cert_chain, key)
.map_err(|e| TlsError::Build(e.to_string()))?;
Ok(TlsConfig::Server(Arc::new(cfg)))
}
pub fn server_mtls_from_pem(
cert_chain: Vec<CertificateDer<'static>>,
key: PrivateKeyDer<'static>,
client_ca_roots: RootCertStore,
) -> Result<Self, TlsError> {
use tokio_rustls::rustls::server::WebPkiClientVerifier;
let _ = rustls::crypto::ring::default_provider().install_default();
let verifier = WebPkiClientVerifier::builder(Arc::new(client_ca_roots))
.build()
.map_err(|e| TlsError::Build(e.to_string()))?;
let cfg = ServerConfig::builder()
.with_client_cert_verifier(verifier)
.with_single_cert(cert_chain, key)
.map_err(|e| TlsError::Build(e.to_string()))?;
Ok(TlsConfig::Server(Arc::new(cfg)))
}
pub fn client_from_roots(roots: RootCertStore) -> Self {
let _ = rustls::crypto::ring::default_provider().install_default();
let cfg = ClientConfig::builder()
.with_root_certificates(roots)
.with_no_client_auth();
TlsConfig::Client(Arc::new(cfg))
}
pub fn client_mtls(
roots: RootCertStore,
client_cert: Vec<CertificateDer<'static>>,
client_key: PrivateKeyDer<'static>,
) -> Result<Self, TlsError> {
let _ = rustls::crypto::ring::default_provider().install_default();
let cfg = ClientConfig::builder()
.with_root_certificates(roots)
.with_client_auth_cert(client_cert, client_key)
.map_err(|e| TlsError::Build(e.to_string()))?;
Ok(TlsConfig::Client(Arc::new(cfg)))
}
}
#[cfg(feature = "experimental-rust-tls")]
pub fn load_certs(path: impl AsRef<Path>) -> io::Result<Vec<CertificateDer<'static>>> {
let mut reader = std::io::BufReader::new(std::fs::File::open(path)?);
rustls_pemfile::certs(&mut reader).collect::<io::Result<Vec<_>>>()
}
#[cfg(feature = "experimental-rust-tls")]
pub fn load_private_key(path: impl AsRef<Path>) -> io::Result<PrivateKeyDer<'static>> {
let mut reader = std::io::BufReader::new(std::fs::File::open(&path)?);
if let Some(key) = rustls_pemfile::pkcs8_private_keys(&mut reader)
.next()
.transpose()?
{
return Ok(PrivateKeyDer::Pkcs8(key));
}
let mut reader = std::io::BufReader::new(std::fs::File::open(&path)?);
if let Some(key) = rustls_pemfile::rsa_private_keys(&mut reader)
.next()
.transpose()?
{
return Ok(PrivateKeyDer::Pkcs1(key));
}
let mut reader = std::io::BufReader::new(std::fs::File::open(&path)?);
if let Some(key) = rustls_pemfile::ec_private_keys(&mut reader)
.next()
.transpose()?
{
return Ok(PrivateKeyDer::Sec1(key));
}
Err(io::Error::new(
io::ErrorKind::InvalidData,
"no PKCS8/PKCS1/EC private key found in file",
))
}
#[cfg(feature = "experimental-rust-tls")]
pub fn load_root_store(path: impl AsRef<Path>) -> io::Result<RootCertStore> {
let mut store = RootCertStore::empty();
for cert in load_certs(path)? {
store
.add(cert)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
}
Ok(store)
}
#[cfg(feature = "experimental-rust-tls")]
pub fn server_from_env() -> Result<Option<TlsConfig>, TlsError> {
let cert_path = epics_base_rs::runtime::env::get("EPICS_CAS_TLS_CERT_FILE");
let key_path = epics_base_rs::runtime::env::get("EPICS_CAS_TLS_KEY_FILE");
let client_ca_path = epics_base_rs::runtime::env::get("EPICS_CAS_TLS_CLIENT_CA_FILE");
match (cert_path, key_path) {
(None, None) => Ok(None),
(Some(cert), Some(key)) => {
let chain = load_certs(&cert)?;
let priv_key = load_private_key(&key)?;
let cfg = if let Some(client_ca) = client_ca_path {
let roots = load_root_store(&client_ca)?;
TlsConfig::server_mtls_from_pem(chain, priv_key, roots)?
} else {
TlsConfig::server_from_pem(chain, priv_key)?
};
Ok(Some(cfg))
}
_ => Err(TlsError::Build(
"EPICS_CAS_TLS_CERT_FILE and EPICS_CAS_TLS_KEY_FILE must both be set or both unset"
.into(),
)),
}
}
#[cfg(feature = "experimental-rust-tls")]
pub fn client_from_env() -> Result<Option<TlsConfig>, TlsError> {
let Some(roots_path) = epics_base_rs::runtime::env::get("EPICS_CA_TLS_ROOTS_FILE") else {
return Ok(None);
};
let roots = load_root_store(&roots_path)?;
let client_cert = epics_base_rs::runtime::env::get("EPICS_CA_TLS_CLIENT_CERT");
let client_key = epics_base_rs::runtime::env::get("EPICS_CA_TLS_CLIENT_KEY");
match (client_cert, client_key) {
(None, None) => Ok(Some(TlsConfig::client_from_roots(roots))),
(Some(cert), Some(key)) => {
let chain = load_certs(&cert)?;
let priv_key = load_private_key(&key)?;
Ok(Some(TlsConfig::client_mtls(roots, chain, priv_key)?))
}
_ => Err(TlsError::Build(
"EPICS_CA_TLS_CLIENT_CERT and EPICS_CA_TLS_CLIENT_KEY must both be set or both unset"
.into(),
)),
}
}
#[derive(Debug)]
pub enum TlsError {
Io(std::io::Error),
Build(String),
}
impl std::fmt::Display for TlsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TlsError::Io(e) => write!(f, "TLS I/O: {e}"),
TlsError::Build(s) => write!(f, "TLS build: {s}"),
}
}
}
impl std::error::Error for TlsError {}
impl From<std::io::Error> for TlsError {
fn from(e: std::io::Error) -> Self {
TlsError::Io(e)
}
}
#[cfg(feature = "experimental-rust-tls")]
pub fn identity_from_cert(cert: &CertificateDer<'_>) -> String {
use std::sync::OnceLock;
static FALLBACK_PREFIX: OnceLock<&'static str> = OnceLock::new();
let _ = FALLBACK_PREFIX.get_or_init(|| "sha256:");
if let Some(name) = parse_san_dns_or_cn(cert.as_ref()) {
return name;
}
use sha2::Digest;
let digest = sha2::Sha256::digest(cert.as_ref());
let mut s = String::with_capacity(7 + 64);
s.push_str("sha256:");
for b in digest.iter() {
s.push_str(&format!("{b:02x}"));
}
s
}
#[cfg(feature = "experimental-rust-tls")]
fn parse_san_dns_or_cn(der: &[u8]) -> Option<String> {
let (_, cert) = x509_parser::parse_x509_certificate(der).ok()?;
if let Ok(Some(san_ext)) = cert.tbs_certificate.subject_alternative_name() {
for name in &san_ext.value.general_names {
match name {
x509_parser::extensions::GeneralName::DNSName(s) => return Some(s.to_string()),
x509_parser::extensions::GeneralName::URI(s) => return Some(s.to_string()),
_ => continue,
}
}
}
cert.subject()
.iter_common_name()
.next()
.and_then(|cn| cn.as_str().ok())
.map(|s| s.to_string())
}
#[cfg(feature = "experimental-rust-tls")]
pub use rustls_pki_types::CertificateDer as Cert;
#[cfg(feature = "experimental-rust-tls")]
pub use rustls_pki_types::PrivateKeyDer as Key;
#[cfg(feature = "experimental-rust-tls")]
pub use tokio_rustls::rustls::RootCertStore as Roots;
#[cfg(feature = "experimental-rust-tls")]
mod rustls {
pub use tokio_rustls::rustls::*;
}