use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
use rustls::server::WebPkiClientVerifier;
use rustls::{
ClientConfig, ClientConnection, RootCertStore, ServerConfig, ServerConnection, StreamOwned,
};
use std::io;
use std::net::TcpStream;
use std::sync::Arc;
use std::sync::OnceLock;
pub type TlsStream = StreamOwned<ClientConnection, TcpStream>;
static EXTRA_CA: OnceLock<Vec<CertificateDer<'static>>> = OnceLock::new();
pub fn install_extra_ca(ca_pem: &[u8]) -> io::Result<usize> {
validate_ca_pem(ca_pem)?;
let certs: Vec<CertificateDer<'static>> = rustls_pemfile::certs(&mut io::Cursor::new(ca_pem))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| io::Error::other(format!("tls: bad CA PEM: {e}")))?;
let n = certs.len();
match EXTRA_CA.set(certs) {
Ok(()) => Ok(n),
Err(rejected) => {
if EXTRA_CA.get().map(Vec::as_slice) == Some(rejected.as_slice()) {
Ok(n) } else {
Err(io::Error::new(
io::ErrorKind::AlreadyExists,
"tls: extra CA already installed with a different bundle (set-once; restart to change trust anchors)",
))
}
}
}
}
pub fn extra_ca_count() -> usize {
EXTRA_CA.get().map_or(0, Vec::len)
}
pub fn validate_ca_pem(ca_pem: &[u8]) -> io::Result<usize> {
roots_from_pem(ca_pem).map(|r| r.len())
}
fn extend_with_extra_ca(roots: &mut RootCertStore) {
for cert in EXTRA_CA.get().into_iter().flatten() {
let _ = roots.add(cert.clone());
}
}
pub type ServerTlsStream = StreamOwned<ServerConnection, TcpStream>;
#[derive(Clone)]
pub struct ClientIdentity {
certs: Vec<CertificateDer<'static>>,
key: Arc<PrivateKeyDer<'static>>,
}
impl std::fmt::Debug for ClientIdentity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"ClientIdentity {{ certs: {}, key: <redacted> }}",
self.certs.len()
)
}
}
impl ClientIdentity {
pub fn from_pem(cert_pem: &[u8], key_pem: &[u8]) -> io::Result<ClientIdentity> {
let certs = rustls_pemfile::certs(&mut io::Cursor::new(cert_pem))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| io::Error::other(format!("mtls: bad client cert PEM: {e}")))?;
if certs.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"mtls: no CERTIFICATE in client cert PEM",
));
}
let key = rustls_pemfile::private_key(&mut io::Cursor::new(key_pem))
.map_err(|e| io::Error::other(format!("mtls: bad client key PEM: {e}")))?
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"mtls: no PRIVATE KEY in key PEM",
)
})?;
Ok(ClientIdentity {
certs,
key: Arc::new(key),
})
}
}
pub fn connect(
tcp: TcpStream,
host: &str,
identity: Option<&ClientIdentity>,
) -> io::Result<TlsStream> {
let config = match identity {
None => client_config(),
Some(id) => mtls_config(id)?,
};
connect_with_config(tcp, host, config)
}
pub fn connect_with_ca(
tcp: TcpStream,
host: &str,
ca_pem: &[u8],
identity: Option<&ClientIdentity>,
) -> io::Result<TlsStream> {
let roots = roots_from_pem(ca_pem)?;
let builder =
ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_safe_default_protocol_versions()
.expect("ring provides TLS 1.2 + 1.3")
.with_root_certificates(roots);
let config = match identity {
None => Arc::new(builder.with_no_client_auth()),
Some(id) => Arc::new(
builder
.with_client_auth_cert(id.certs.clone(), id.key.clone_key())
.map_err(|e| io::Error::other(format!("mtls: bad client identity: {e}")))?,
),
};
connect_with_config(tcp, host, config)
}
fn connect_with_config(
tcp: TcpStream,
host: &str,
config: Arc<ClientConfig>,
) -> io::Result<TlsStream> {
let sni = host
.strip_suffix('.')
.filter(|h| !h.is_empty())
.unwrap_or(host);
let server_name = ServerName::try_from(sni)
.map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("invalid TLS server name: {host}"),
)
})?
.to_owned();
let conn = ClientConnection::new(config, server_name)
.map_err(|e| io::Error::other(format!("tls: {e}")))?;
Ok(StreamOwned::new(conn, tcp))
}
fn roots_from_pem(ca_pem: &[u8]) -> io::Result<RootCertStore> {
let mut roots = RootCertStore::empty();
let mut added = 0usize;
for cert in rustls_pemfile::certs(&mut io::Cursor::new(ca_pem)) {
let cert = cert.map_err(|e| io::Error::other(format!("tls: bad CA PEM: {e}")))?;
roots
.add(cert)
.map_err(|e| io::Error::other(format!("tls: bad CA certificate: {e}")))?;
added += 1;
}
if added == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"tls: no CERTIFICATE in CA PEM",
));
}
Ok(roots)
}
pub struct ServerIdentity {
certs: Vec<CertificateDer<'static>>,
key: PrivateKeyDer<'static>,
}
impl std::fmt::Debug for ServerIdentity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"ServerIdentity {{ certs: {}, key: <redacted> }}",
self.certs.len()
)
}
}
impl ServerIdentity {
pub fn from_pem(cert_pem: &[u8], key_pem: &[u8]) -> io::Result<ServerIdentity> {
let certs = rustls_pemfile::certs(&mut io::Cursor::new(cert_pem))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| io::Error::other(format!("tls: bad server cert PEM: {e}")))?;
if certs.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"tls: no CERTIFICATE in server cert PEM",
));
}
let key = rustls_pemfile::private_key(&mut io::Cursor::new(key_pem))
.map_err(|e| io::Error::other(format!("tls: bad server key PEM: {e}")))?
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"tls: no PRIVATE KEY in key PEM",
)
})?;
Ok(ServerIdentity { certs, key })
}
}
pub struct TlsAcceptor {
config: Mutex<Arc<ServerConfig>>,
client_auth: bool,
reload: Option<ReloadSource>,
}
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::time::{Duration, Instant, SystemTime};
const RELOAD_CHECK_TTL: Duration = Duration::from_secs(1);
struct ReloadSource {
cert: PathBuf,
key: PathBuf,
client_ca: Option<PathBuf>,
state: Mutex<ReloadState>,
}
struct ReloadState {
checked_at: Instant,
mtimes: (SystemTime, SystemTime, Option<SystemTime>),
generation: u64,
last_error: Option<String>,
}
impl std::fmt::Debug for TlsAcceptor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"TlsAcceptor {{ client_auth: {}, live: {} }}",
self.client_auth,
self.reload.is_some()
)
}
}
fn build_server_config(
identity: ServerIdentity,
client_ca_pem: Option<&[u8]>,
) -> io::Result<Arc<ServerConfig>> {
let provider = Arc::new(rustls::crypto::ring::default_provider());
let builder = ServerConfig::builder_with_provider(provider.clone())
.with_safe_default_protocol_versions()
.expect("ring provides TLS 1.2 + 1.3");
let builder = match client_ca_pem {
Some(ca) => {
let roots = roots_from_pem(ca)?;
let verifier = WebPkiClientVerifier::builder_with_provider(Arc::new(roots), provider)
.build()
.map_err(|e| io::Error::other(format!("tls: bad client CA: {e}")))?;
builder.with_client_cert_verifier(verifier)
}
None => builder.with_no_client_auth(),
};
let config = builder
.with_single_cert(identity.certs, identity.key)
.map_err(|e| io::Error::other(format!("tls: bad server identity: {e}")))?;
Ok(Arc::new(config))
}
fn mtime(path: &Path) -> io::Result<SystemTime> {
std::fs::metadata(path)?.modified()
}
impl TlsAcceptor {
pub fn new(identity: ServerIdentity, client_ca_pem: Option<&[u8]>) -> io::Result<TlsAcceptor> {
let client_auth = client_ca_pem.is_some();
Ok(TlsAcceptor {
config: Mutex::new(build_server_config(identity, client_ca_pem)?),
client_auth,
reload: None,
})
}
pub fn from_paths(
cert: &Path,
key: &Path,
client_ca: Option<&Path>,
) -> io::Result<TlsAcceptor> {
let identity = ServerIdentity::from_pem(&std::fs::read(cert)?, &std::fs::read(key)?)?;
let ca_pem = client_ca.map(std::fs::read).transpose()?;
let config = build_server_config(identity, ca_pem.as_deref())?;
let mtimes = (mtime(cert)?, mtime(key)?, client_ca.map(mtime).transpose()?);
Ok(TlsAcceptor {
config: Mutex::new(config),
client_auth: client_ca.is_some(),
reload: Some(ReloadSource {
cert: cert.to_path_buf(),
key: key.to_path_buf(),
client_ca: client_ca.map(Path::to_path_buf),
state: Mutex::new(ReloadState {
checked_at: Instant::now(),
mtimes,
generation: 0,
last_error: None,
}),
}),
})
}
pub fn requires_client_auth(&self) -> bool {
self.client_auth
}
pub fn reload_generation(&self) -> u64 {
self.reload.as_ref().map_or(0, |r| {
r.state.lock().unwrap_or_else(|e| e.into_inner()).generation
})
}
pub fn last_reload_error(&self) -> Option<String> {
self.reload.as_ref().and_then(|r| {
r.state
.lock()
.unwrap_or_else(|e| e.into_inner())
.last_error
.clone()
})
}
pub fn force_reload_check(&self) {
self.maybe_reload(Duration::ZERO);
}
fn maybe_reload(&self, ttl: Duration) {
let Some(src) = &self.reload else { return };
let mut st = src.state.lock().unwrap_or_else(|e| e.into_inner());
if st.checked_at.elapsed() < ttl {
return;
}
st.checked_at = Instant::now();
let stat = (|| -> io::Result<_> {
Ok((
mtime(&src.cert)?,
mtime(&src.key)?,
src.client_ca.as_deref().map(mtime).transpose()?,
))
})();
let mtimes = match stat {
Ok(m) => m,
Err(e) => {
st.last_error = Some(format!("stat: {e}"));
return;
}
};
if mtimes == st.mtimes {
return;
}
let rebuilt = (|| -> io::Result<Arc<ServerConfig>> {
let identity =
ServerIdentity::from_pem(&std::fs::read(&src.cert)?, &std::fs::read(&src.key)?)?;
let ca_pem = src.client_ca.as_deref().map(std::fs::read).transpose()?;
build_server_config(identity, ca_pem.as_deref())
})();
match rebuilt {
Ok(config) => {
*self.config.lock().unwrap_or_else(|e| e.into_inner()) = config;
st.mtimes = mtimes;
st.generation += 1;
st.last_error = None;
}
Err(e) => {
st.last_error = Some(format!("reload: {e}"));
}
}
}
pub fn accept(&self, tcp: TcpStream) -> io::Result<ServerTlsStream> {
self.maybe_reload(RELOAD_CHECK_TTL);
let config = self
.config
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone();
let conn =
ServerConnection::new(config).map_err(|e| io::Error::other(format!("tls: {e}")))?;
let mut stream = StreamOwned::new(conn, tcp);
while stream.conn.is_handshaking() {
stream
.conn
.complete_io(&mut stream.sock)
.map_err(|e| io::Error::other(format!("tls handshake: {e}")))?;
}
Ok(stream)
}
pub fn server_config(&self) -> Arc<ServerConfig> {
self.maybe_reload(RELOAD_CHECK_TTL);
self.config
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
}
}
pub fn peer_presented_cert(stream: &ServerTlsStream) -> bool {
stream.conn.peer_certificates().is_some()
}
pub fn peer_identity(stream: &ServerTlsStream) -> Option<crate::x509::PeerIdentity> {
let certs = stream.conn.peer_certificates()?;
let leaf = certs.first()?;
Some(crate::x509::parse(leaf.as_ref()))
}
fn mtls_config(id: &ClientIdentity) -> io::Result<Arc<ClientConfig>> {
let mut roots = RootCertStore::empty();
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
extend_with_extra_ca(&mut roots);
let key = id.key.clone_key();
let config =
ClientConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_safe_default_protocol_versions()
.expect("ring provides TLS 1.2 + 1.3")
.with_root_certificates(roots)
.with_client_auth_cert(id.certs.clone(), key)
.map_err(|e| io::Error::other(format!("mtls: bad client identity: {e}")))?;
Ok(Arc::new(config))
}
fn client_config() -> Arc<ClientConfig> {
static CONFIG: OnceLock<Arc<ClientConfig>> = OnceLock::new();
CONFIG
.get_or_init(|| {
let mut roots = RootCertStore::empty();
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
extend_with_extra_ca(&mut roots);
let config = ClientConfig::builder_with_provider(Arc::new(
rustls::crypto::ring::default_provider(),
))
.with_safe_default_protocol_versions()
.expect("ring provides TLS 1.2 + 1.3")
.with_root_certificates(roots)
.with_no_client_auth();
Arc::new(config)
})
.clone()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn install_extra_ca_lifecycle() {
let ca = include_bytes!("../tests/fixtures/ca.pem");
assert_eq!(
install_extra_ca(b"not a pem").unwrap_err().kind(),
io::ErrorKind::InvalidInput
);
assert_eq!(
install_extra_ca(b"").unwrap_err().kind(),
io::ErrorKind::InvalidInput
);
assert_eq!(extra_ca_count(), 0);
let n = install_extra_ca(ca).expect("fixture CA installs");
assert!(n >= 1);
assert_eq!(extra_ca_count(), n);
assert_eq!(install_extra_ca(ca).expect("idempotent"), n);
let mut doubled = ca.to_vec();
doubled.extend_from_slice(ca);
assert_eq!(
install_extra_ca(&doubled).unwrap_err().kind(),
io::ErrorKind::AlreadyExists
);
let mut roots = RootCertStore::empty();
extend_with_extra_ca(&mut roots);
assert_eq!(roots.len(), n);
}
#[test]
fn client_identity_rejects_pem_without_cert_or_key() {
let err = ClientIdentity::from_pem(b"not a pem", b"not a pem").unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
let cert = "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----\n";
let err = ClientIdentity::from_pem(cert.as_bytes(), b"").unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
}
}