#[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")]
pub fn issuer_from_cert(cert: &CertificateDer<'_>) -> Option<String> {
let (_, parsed) = x509_parser::parse_x509_certificate(cert.as_ref()).ok()?;
let dn = parsed.tbs_certificate.issuer.to_string();
if dn.is_empty() {
return None;
}
reject_unusable_identity(dn)
}
#[cfg(feature = "experimental-rust-tls")]
fn reject_unusable_identity(s: String) -> Option<String> {
if s.is_empty() || s.contains('\0') {
None
} else {
Some(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)
| x509_parser::extensions::GeneralName::URI(s) => {
if let Some(name) = reject_unusable_identity(s.to_string()) {
return Some(name);
}
}
_ => continue,
}
}
}
cert.subject()
.iter_common_name()
.next()
.and_then(|cn| cn.as_str().ok())
.map(|s| s.to_string())
.and_then(reject_unusable_identity)
}
#[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::*;
}
#[cfg(all(test, feature = "experimental-rust-tls"))]
mod nul_identity_tests {
use super::*;
fn self_signed_with_cn(cn: &str) -> CertificateDer<'static> {
let mut params = rcgen::CertificateParams::new(Vec::new()).expect("params");
params.distinguished_name = rcgen::DistinguishedName::new();
params
.distinguished_name
.push(rcgen::DnType::CommonName, cn);
let key = rcgen::KeyPair::generate().expect("key");
let cert = params.self_signed(&key).expect("self-signed");
CertificateDer::from(cert.der().to_vec())
}
#[test]
fn identity_rejects_embedded_nul_cn_falls_back_to_fingerprint() {
let cert = self_signed_with_cn("admin\0.evil");
assert_eq!(parse_san_dns_or_cn(cert.as_ref()), None);
let id = identity_from_cert(&cert);
assert!(
id.starts_with("sha256:"),
"expected fingerprint, got {id:?}"
);
assert!(!id.contains('\0'));
}
#[test]
fn identity_rejects_empty_cn_falls_back_to_fingerprint() {
let cert = self_signed_with_cn("");
assert_eq!(parse_san_dns_or_cn(cert.as_ref()), None);
let id = identity_from_cert(&cert);
assert!(
id.starts_with("sha256:"),
"expected fingerprint, got {id:?}"
);
assert!(!id.is_empty());
}
#[test]
fn identity_extracts_clean_cn() {
let cert = self_signed_with_cn("operator-bob");
assert_eq!(
parse_san_dns_or_cn(cert.as_ref()).as_deref(),
Some("operator-bob")
);
assert_eq!(identity_from_cert(&cert), "operator-bob");
}
#[test]
fn issuer_rejects_embedded_nul() {
let cert = self_signed_with_cn("ca\0.evil");
assert_eq!(issuer_from_cert(&cert), None);
}
#[test]
fn issuer_extracts_clean_dn() {
let cert = self_signed_with_cn("ops-ca");
let dn = issuer_from_cert(&cert).expect("issuer dn");
assert!(dn.contains("ops-ca"), "got {dn:?}");
assert!(!dn.contains('\0'));
}
}