use crate::crypto;
use rustls::pki_types::pem::PemObject;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use std::{fs, io};
#[cfg(all(
any(feature = "quinn", feature = "noq", feature = "quiche"),
any(feature = "aws-lc-rs", feature = "ring")
))]
use rustls::pki_types::PrivatePkcs8KeyDer;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("failed to open certificate file")]
Open(#[source] std::io::Error),
#[error("failed to read file")]
ReadFile(#[source] std::io::Error),
#[error("failed to read certificates")]
Read(#[source] rustls::pki_types::pem::Error),
#[error("failed to parse private key")]
Key(#[source] rustls::pki_types::pem::Error),
#[error("no certificates found")]
Empty,
#[error("no roots found in {}", .0.display())]
EmptyRoots(PathBuf),
#[error(
"no trusted roots: provide --client-tls-root, enable --client-tls-system-roots, or use --client-tls-fingerprint / --client-tls-disable-verify"
)]
NoRoots,
#[error("invalid TLS fingerprint (expected hex-encoded SHA-256)")]
Fingerprint(#[source] hex::FromHexError),
#[error("invalid TLS fingerprint length: expected 32 bytes (SHA-256), got {0}")]
FingerprintLength(usize),
#[error(
"--client-tls-fingerprint cannot be combined with --client-tls-root or --client-tls-system-roots: fingerprint pinning bypasses CA verification"
)]
FingerprintWithRoots,
#[error(
"--client-tls-disable-verify cannot be combined with --client-tls-fingerprint, --client-tls-root or --client-tls-system-roots: it accepts every certificate, so the trust material would be ignored"
)]
DisableVerifyWithTrust,
#[error("failed to add root certificate")]
AddRoot(#[source] rustls::Error),
#[cfg(target_os = "android")]
#[error("failed to initialize the Android platform verifier")]
AndroidInit(#[source] jni::errors::Error),
#[error("failed to configure client certificate")]
ClientAuth(#[source] rustls::Error),
#[error("both --client-tls-cert and --client-tls-key must be provided")]
IncompleteClientAuth,
#[error("must provide both cert and key")]
CertKeyCountMismatch,
#[error("must provide at least one cert/key pair or generate entry")]
NoCertSource,
#[error("private key {} doesn't match certificate {}", key.display(), cert.display())]
KeyMismatch {
key: PathBuf,
cert: PathBuf,
#[source]
source: rustls::Error,
},
#[error(transparent)]
Rustls(#[from] rustls::Error),
#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
#[error("failed to build client certificate verifier")]
ClientVerifier(#[source] rustls::server::VerifierBuilderError),
#[error("failed to build server certificate verifier")]
ServerVerifier(#[source] rustls::client::VerifierBuilderError),
#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
#[error(transparent)]
Rcgen(#[from] rcgen::Error),
#[error("no crypto provider available; enable aws-lc-rs or ring feature")]
NoCryptoProvider,
}
pub type Result<T> = std::result::Result<T, Error>;
pub fn parse_fingerprint(value: &str) -> Result<[u8; 32]> {
let bytes = hex::decode(value.trim()).map_err(Error::Fingerprint)?;
bytes.try_into().map_err(|v: Vec<u8>| Error::FingerprintLength(v.len()))
}
pub(crate) fn read_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>> {
let file = fs::File::open(path).map_err(Error::Open)?;
let mut reader = io::BufReader::new(file);
CertificateDer::pem_reader_iter(&mut reader)
.collect::<std::result::Result<_, _>>()
.map_err(Error::Read)
}
fn read_roots(paths: &[PathBuf]) -> Result<Vec<CertificateDer<'static>>> {
let mut roots = Vec::new();
for path in paths {
let certs = read_certs(path)?;
if certs.is_empty() {
return Err(Error::EmptyRoots(path.clone()));
}
roots.extend(certs);
}
Ok(roots)
}
#[serde_with::serde_as]
#[derive(Clone, Default, Debug, clap::Args, serde::Serialize, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
#[group(id = "tls-client")]
#[non_exhaustive]
pub struct Client {
#[serde(skip_serializing_if = "Vec::is_empty")]
#[arg(id = "client-tls-root", long = "client-tls-root", env = "MOQ_CLIENT_TLS_ROOT")]
#[serde_as(as = "serde_with::OneOrMany<_>")]
pub root: Vec<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(
id = "client-tls-system-roots",
long = "client-tls-system-roots",
env = "MOQ_CLIENT_TLS_SYSTEM_ROOTS",
default_missing_value = "true",
num_args = 0..=1,
require_equals = true,
value_parser = clap::value_parser!(bool),
)]
pub system_roots: Option<bool>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[arg(
id = "client-tls-fingerprint",
long = "client-tls-fingerprint",
env = "MOQ_CLIENT_TLS_FINGERPRINT"
)]
#[serde_as(as = "serde_with::OneOrMany<_>")]
pub fingerprint: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(id = "client-tls-cert", long = "client-tls-cert", env = "MOQ_CLIENT_TLS_CERT")]
pub cert: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(id = "client-tls-key", long = "client-tls-key", env = "MOQ_CLIENT_TLS_KEY")]
pub key: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(
id = "client-tls-disable-verify",
long = "client-tls-disable-verify",
env = "MOQ_CLIENT_TLS_DISABLE_VERIFY",
default_missing_value = "true",
num_args = 0..=1,
require_equals = true,
value_parser = clap::value_parser!(bool),
)]
pub disable_verify: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[arg(
id = "client-tls-host-name",
long = "client-tls-host-name",
env = "MOQ_CLIENT_TLS_HOST_NAME"
)]
pub host_name: Option<String>,
#[command(flatten)]
#[serde(skip)]
deprecated: Deprecated,
}
#[derive(Clone, Default, Debug, clap::Args)]
struct Deprecated {
#[arg(long = "tls-root", hide = true)]
root: Vec<PathBuf>,
#[arg(
long = "tls-system-roots",
hide = true,
default_missing_value = "true",
num_args = 0..=1,
require_equals = true,
value_parser = clap::value_parser!(bool),
)]
system_roots: Option<bool>,
#[arg(long = "tls-fingerprint", hide = true)]
fingerprint: Vec<String>,
#[arg(
long = "tls-disable-verify",
hide = true,
default_missing_value = "true",
num_args = 0..=1,
require_equals = true,
value_parser = clap::value_parser!(bool),
)]
disable_verify: Option<bool>,
}
#[derive(Clone)]
pub(crate) struct CustomRoots {
paths: Vec<PathBuf>,
current: Arc<RwLock<Vec<CertificateDer<'static>>>>,
}
impl CustomRoots {
fn new(paths: Vec<PathBuf>) -> Result<Self> {
let current = read_roots(&paths)?;
Ok(Self {
paths,
current: Arc::new(RwLock::new(current)),
})
}
fn load(&self) -> Result<Vec<CertificateDer<'static>>> {
read_roots(&self.paths)
}
fn replace(&self, roots: Vec<CertificateDer<'static>>) {
*self.current.write().unwrap_or_else(std::sync::PoisonError::into_inner) = roots;
}
pub(crate) fn current(&self) -> Vec<CertificateDer<'static>> {
self.current
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
#[cfg(feature = "quiche")]
pub(crate) fn refresh(&self) -> Vec<CertificateDer<'static>> {
self.refresh_with(|| self.load())
}
#[cfg(feature = "quiche")]
fn refresh_with(
&self,
load: impl FnOnce() -> Result<Vec<CertificateDer<'static>>>,
) -> Vec<CertificateDer<'static>> {
let mut current = self.current.write().unwrap_or_else(std::sync::PoisonError::into_inner);
match load().and_then(|roots| {
root_store(&roots)?;
Ok(roots)
}) {
Ok(roots) => {
*current = roots.clone();
roots
}
Err(err) => {
tracing::warn!(%err, "failed to reload client root certificates; retaining previous roots");
current.clone()
}
}
}
}
#[cfg(feature = "watch")]
struct ReloadState<T: ?Sized + Send + Sync + 'static> {
current: RwLock<Arc<T>>,
build: Box<dyn Fn() -> Result<Arc<T>> + Send + Sync>,
role: &'static str,
}
#[cfg(feature = "watch")]
impl<T: ?Sized + Send + Sync + 'static> ReloadState<T> {
fn reload(&self) {
match (self.build)() {
Ok(next) => {
*self.current.write().unwrap_or_else(std::sync::PoisonError::into_inner) = next;
tracing::info!(role = self.role, "reloaded TLS root certificates");
}
Err(err) => {
tracing::warn!(%err, role = self.role, "failed to reload TLS root certificates; retaining previous roots");
}
}
}
fn current(&self) -> Arc<T> {
self.current
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
}
}
#[cfg(feature = "watch")]
struct Reloading<T: ?Sized + Send + Sync + 'static> {
state: Arc<ReloadState<T>>,
_watcher: Option<notify::RecommendedWatcher>,
}
#[cfg(feature = "watch")]
impl<T: ?Sized + Send + Sync + 'static> Reloading<T> {
fn new(
paths: &[PathBuf],
initial: Arc<T>,
role: &'static str,
build: impl Fn() -> Result<Arc<T>> + Send + Sync + 'static,
) -> Self {
let state = Arc::new(ReloadState {
current: RwLock::new(initial),
build: Box::new(build),
role,
});
let reload = state.clone();
let watcher = match crate::watch::callback(paths, move || reload.reload()) {
Ok(watcher) => Some(watcher),
Err(err) => {
tracing::error!(%err, role, "failed to watch TLS root certificates; hot reload disabled");
None
}
};
Self {
state,
_watcher: watcher,
}
}
fn current(&self) -> Arc<T> {
self.state.current()
}
}
#[cfg(feature = "watch")]
impl<T: ?Sized + Send + Sync + 'static> std::fmt::Debug for Reloading<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Reloading").field("role", &self.state.role).finish()
}
}
#[derive(Clone)]
pub(crate) enum Verification {
Disabled,
Fingerprints(Vec<[u8; 32]>),
Roots { custom: CustomRoots, system: bool },
}
impl Client {
pub(crate) fn warn_deprecated(&self) {
if !self.deprecated.root.is_empty() {
tracing::warn!("--tls-root is deprecated; use --client-tls-root");
}
if self.deprecated.system_roots.is_some() {
tracing::warn!("--tls-system-roots is deprecated; use --client-tls-system-roots");
}
if !self.deprecated.fingerprint.is_empty() {
tracing::warn!("--tls-fingerprint is deprecated; use --client-tls-fingerprint");
}
if self.deprecated.disable_verify.is_some() {
tracing::warn!("--tls-disable-verify is deprecated; use --client-tls-disable-verify");
}
}
pub(crate) fn effective_root(&self) -> Vec<PathBuf> {
let mut root = self.root.clone();
root.extend(self.deprecated.root.iter().cloned());
root
}
pub(crate) fn effective_fingerprint(&self) -> Vec<String> {
let mut fp = self.fingerprint.clone();
fp.extend(self.deprecated.fingerprint.iter().cloned());
fp
}
pub(crate) fn effective_system_roots(&self) -> Option<bool> {
self.system_roots.or(self.deprecated.system_roots)
}
pub(crate) fn effective_disable_verify(&self) -> Option<bool> {
self.disable_verify.or(self.deprecated.disable_verify)
}
pub(crate) fn verification(&self) -> Result<Verification> {
self.warn_deprecated();
let fingerprints = self.fingerprints()?;
let roots = self.effective_root();
let system_roots = self.effective_system_roots();
if self.effective_disable_verify().unwrap_or_default() {
if !fingerprints.is_empty() || !roots.is_empty() || system_roots == Some(true) {
return Err(Error::DisableVerifyWithTrust);
}
return Ok(Verification::Disabled);
}
if !fingerprints.is_empty() {
if !roots.is_empty() || system_roots == Some(true) {
return Err(Error::FingerprintWithRoots);
}
return Ok(Verification::Fingerprints(fingerprints));
}
let system = system_roots.unwrap_or(roots.is_empty());
let custom = CustomRoots::new(roots)?;
if !system && custom.current().is_empty() {
return Err(Error::NoRoots);
}
Ok(Verification::Roots { custom, system })
}
#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
pub(crate) fn allows_http_bootstrap(&self) -> bool {
self.effective_fingerprint().is_empty() && !self.effective_disable_verify().unwrap_or_default()
}
fn fingerprints(&self) -> Result<Vec<[u8; 32]>> {
self.effective_fingerprint()
.iter()
.map(|fp| parse_fingerprint(fp))
.collect()
}
pub fn build(&self) -> Result<rustls::ClientConfig> {
let provider = crypto::provider();
let verification = self.verification()?;
let reloadable_roots = cfg!(feature = "watch")
&& matches!(&verification, Verification::Roots { custom, .. } if !custom.paths.is_empty());
let builder = rustls::ClientConfig::builder_with_provider(provider.clone())
.with_protocol_versions(&[&rustls::version::TLS13, &rustls::version::TLS12])?;
let verifier: Arc<dyn rustls::client::danger::ServerCertVerifier> = match verification {
Verification::Disabled => {
tracing::warn!(
"TLS server certificate verification is disabled; A man-in-the-middle attack is possible."
);
Arc::new(NoCertificateVerification(provider))
}
Verification::Fingerprints(fingerprints) => {
let fingerprints = fingerprints.into_iter().map(|fp| fp.to_vec()).collect();
Arc::new(FingerprintVerifier::new(provider, fingerprints))
}
Verification::Roots { custom, system } => Self::root_server_verifier(custom, system, provider)?,
};
let builder = builder.dangerous().with_custom_certificate_verifier(verifier);
let mut tls = self.with_client_auth(builder)?;
if reloadable_roots {
tls.resumption = rustls::client::Resumption::disabled();
}
Ok(tls)
}
fn root_server_verifier(
custom: CustomRoots,
system: bool,
provider: crypto::Provider,
) -> Result<Arc<dyn rustls::client::danger::ServerCertVerifier>> {
let initial = Self::build_root_server_verifier(&custom.current(), system, &provider)?;
#[cfg(feature = "watch")]
if !custom.paths.is_empty() {
let paths = custom.paths.clone();
let reload = custom.clone();
let reload_provider = provider.clone();
let verifier = ReloadingServerVerifier::new(&paths, initial, move || {
let roots = reload.load()?;
let verifier = Self::build_root_server_verifier(&roots, system, &reload_provider)?;
reload.replace(roots);
Ok(verifier)
});
return Ok(Arc::new(verifier));
}
Ok(initial)
}
fn build_root_server_verifier(
custom: &[CertificateDer<'static>],
system: bool,
provider: &crypto::Provider,
) -> Result<Arc<dyn rustls::client::danger::ServerCertVerifier>> {
if !system {
let roots = root_store(custom)?;
let verifier =
rustls::client::WebPkiServerVerifier::builder_with_provider(Arc::new(roots), provider.clone())
.build()
.map_err(Error::ServerVerifier)?;
return Ok(verifier);
}
#[cfg(target_os = "android")]
{
if ANDROID_INITIALIZED.load(std::sync::atomic::Ordering::Acquire) && custom.is_empty() {
let verifier = rustls_platform_verifier::Verifier::new(provider.clone())?;
return Ok(Arc::new(verifier));
}
let mut roots = rustls::RootCertStore::empty();
roots.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
for cert in custom {
roots.add(cert.clone()).map_err(Error::AddRoot)?;
}
let verifier =
rustls::client::WebPkiServerVerifier::builder_with_provider(Arc::new(roots), provider.clone())
.build()
.map_err(Error::ServerVerifier)?;
Ok(verifier)
}
#[cfg(not(target_os = "android"))]
{
let verifier = if custom.is_empty() {
rustls_platform_verifier::Verifier::new(provider.clone())?
} else {
rustls_platform_verifier::Verifier::new_with_extra_roots(custom.iter().cloned(), provider.clone())?
};
Ok(Arc::new(verifier))
}
}
fn with_client_auth(
&self,
builder: rustls::ConfigBuilder<rustls::ClientConfig, rustls::client::WantsClientCert>,
) -> Result<rustls::ClientConfig> {
Ok(match (&self.cert, &self.key) {
(Some(cert_path), Some(key_path)) => {
let cert_pem = fs::read(cert_path).map_err(Error::ReadFile)?;
let chain: Vec<CertificateDer<'static>> = CertificateDer::pem_slice_iter(&cert_pem)
.collect::<std::result::Result<_, _>>()
.map_err(Error::Read)?;
if chain.is_empty() {
return Err(Error::Empty);
}
let key_pem = fs::read(key_path).map_err(Error::ReadFile)?;
let key = PrivateKeyDer::from_pem_slice(&key_pem).map_err(Error::Key)?;
builder.with_client_auth_cert(chain, key).map_err(Error::ClientAuth)?
}
(None, None) => builder.with_no_client_auth(),
_ => return Err(Error::IncompleteClientAuth),
})
}
}
fn root_store(custom: &[CertificateDer<'static>]) -> Result<rustls::RootCertStore> {
let mut roots = rustls::RootCertStore::empty();
for cert in custom {
roots.add(cert.clone()).map_err(Error::AddRoot)?;
}
Ok(roots)
}
#[cfg(target_os = "android")]
static ANDROID_INITIALIZED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
#[cfg(target_os = "android")]
pub fn init_android(env: &mut jni::Env, context: jni::objects::JObject) -> Result<()> {
rustls_platform_verifier::android::init_with_env(env, context).map_err(Error::AndroidInit)?;
ANDROID_INITIALIZED.store(true, std::sync::atomic::Ordering::Release);
Ok(())
}
#[serde_with::serde_as]
#[derive(clap::Args, Clone, Default, Debug, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
#[group(id = "tls-server")]
#[non_exhaustive]
pub struct Server {
#[arg(long = "tls-cert", id = "tls-cert", env = "MOQ_SERVER_TLS_CERT")]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde_as(as = "serde_with::OneOrMany<_>")]
pub cert: Vec<PathBuf>,
#[arg(long = "tls-key", id = "tls-key", env = "MOQ_SERVER_TLS_KEY")]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde_as(as = "serde_with::OneOrMany<_>")]
pub key: Vec<PathBuf>,
#[arg(
long = "tls-generate",
id = "tls-generate",
value_delimiter = ',',
env = "MOQ_SERVER_TLS_GENERATE"
)]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde_as(as = "serde_with::OneOrMany<_>")]
pub generate: Vec<String>,
#[arg(
long = "server-tls-root",
id = "server-tls-root",
value_delimiter = ',',
env = "MOQ_SERVER_TLS_ROOT"
)]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde_as(as = "serde_with::OneOrMany<_>")]
pub root: Vec<PathBuf>,
}
impl Server {
#[cfg(feature = "watch")]
pub(crate) fn disable_resumption(&self, tls: &mut rustls::ServerConfig) {
if !self.root.is_empty() {
tls.session_storage = Arc::new(rustls::server::NoServerSessionStorage {});
tls.send_tls13_tickets = 0;
}
}
pub fn load_roots(&self) -> Result<rustls::RootCertStore> {
root_store(&read_roots(&self.root)?)
}
#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
pub(crate) fn client_verifier(
&self,
provider: crypto::Provider,
) -> Result<Arc<dyn rustls::server::danger::ClientCertVerifier>> {
let initial = Self::build_client_verifier(&self.root, &provider)?;
#[cfg(feature = "watch")]
{
let paths = self.root.clone();
let reload_paths = paths.clone();
let reload_provider = provider.clone();
let verifier = ReloadingClientVerifier::new(&paths, initial, move || {
Self::build_client_verifier(&reload_paths, &reload_provider)
});
Ok(Arc::new(verifier))
}
#[cfg(not(feature = "watch"))]
Ok(initial)
}
#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
fn build_client_verifier(
paths: &[PathBuf],
provider: &crypto::Provider,
) -> Result<Arc<dyn rustls::server::danger::ClientCertVerifier>> {
let roots = root_store(&read_roots(paths)?)?;
rustls::server::WebPkiClientVerifier::builder_with_provider(Arc::new(roots), provider.clone())
.allow_unauthenticated()
.build()
.map_err(Error::ClientVerifier)
}
#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
pub fn server_config(&self, alpn: Vec<Vec<u8>>) -> Result<Arc<rustls::ServerConfig>> {
server_config(self, alpn)
}
}
#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
fn server_config(config: &Server, alpn: Vec<Vec<u8>>) -> Result<Arc<rustls::ServerConfig>> {
let provider = crypto::provider();
let certs = ServeCerts::new(provider.clone());
certs.load_certs(config)?;
let certs = Arc::new(certs);
let builder =
rustls::ServerConfig::builder_with_provider(provider.clone()).with_safe_default_protocol_versions()?;
let mut tls = if config.root.is_empty() {
builder.with_no_client_auth().with_cert_resolver(certs)
} else {
let verifier = config.client_verifier(provider)?;
builder.with_client_cert_verifier(verifier).with_cert_resolver(certs)
};
tls.alpn_protocols = alpn;
config.disable_resumption(&mut tls);
Ok(Arc::new(tls))
}
#[derive(Clone)]
pub struct PeerIdentity {
chain: Vec<CertificateDer<'static>>,
}
impl PeerIdentity {
#[cfg(any(feature = "quinn", feature = "noq"))]
pub(crate) fn from_any(identity: Option<Box<dyn std::any::Any>>) -> Option<Self> {
let chain = identity?.downcast::<Vec<CertificateDer<'static>>>().ok()?;
Some(Self { chain: *chain })
}
#[cfg(feature = "quiche")]
pub(crate) fn from_chain(chain: Vec<CertificateDer<'static>>) -> Self {
Self { chain }
}
pub fn chain(&self) -> &[CertificateDer<'static>] {
&self.chain
}
pub fn expiry(&self) -> Option<std::time::SystemTime> {
use std::time::{Duration, UNIX_EPOCH};
let leaf = self.chain.first()?;
let (_, cert) = x509_parser::parse_x509_certificate(leaf).ok()?;
let secs = u64::try_from(cert.validity().not_after.timestamp()).ok()?;
Some(UNIX_EPOCH + Duration::from_secs(secs))
}
}
#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
#[derive(Debug, Default)]
pub(crate) struct Info {
pub(crate) certs: Vec<Arc<rustls::sign::CertifiedKey>>,
pub(crate) fingerprints: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct Certificates {
#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
info: Arc<RwLock<Info>>,
}
impl Certificates {
#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
pub(crate) fn new(info: Arc<RwLock<Info>>) -> Self {
Self { info }
}
pub(crate) fn empty() -> Self {
Self {
#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
info: Arc::new(RwLock::new(Info::default())),
}
}
pub fn fingerprints(&self) -> Vec<String> {
#[cfg(any(feature = "noq", feature = "quinn", feature = "quiche"))]
{
let info = self.info.read().unwrap_or_else(std::sync::PoisonError::into_inner);
info.fingerprints.clone()
}
#[cfg(not(any(feature = "noq", feature = "quinn", feature = "quiche")))]
Vec::new()
}
}
#[cfg(feature = "watch")]
#[derive(Debug)]
struct ReloadingServerVerifier {
inner: Reloading<dyn rustls::client::danger::ServerCertVerifier>,
}
#[cfg(feature = "watch")]
impl ReloadingServerVerifier {
fn new(
paths: &[PathBuf],
initial: Arc<dyn rustls::client::danger::ServerCertVerifier>,
build: impl Fn() -> Result<Arc<dyn rustls::client::danger::ServerCertVerifier>> + Send + Sync + 'static,
) -> Self {
Self {
inner: Reloading::new(paths, initial, "client", build),
}
}
}
#[cfg(feature = "watch")]
impl rustls::client::danger::ServerCertVerifier for ReloadingServerVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
intermediates: &[CertificateDer<'_>],
server_name: &ServerName<'_>,
ocsp_response: &[u8],
now: UnixTime,
) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
self.inner
.current()
.verify_server_cert(end_entity, intermediates, server_name, ocsp_response, now)
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
self.inner.current().verify_tls12_signature(message, cert, dss)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
self.inner.current().verify_tls13_signature(message, cert, dss)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
self.inner.current().supported_verify_schemes()
}
fn requires_raw_public_keys(&self) -> bool {
self.inner.current().requires_raw_public_keys()
}
}
#[cfg(all(feature = "watch", any(feature = "quinn", feature = "noq", feature = "quiche")))]
#[derive(Debug)]
struct ReloadingClientVerifier {
inner: Reloading<dyn rustls::server::danger::ClientCertVerifier>,
}
#[cfg(all(feature = "watch", any(feature = "quinn", feature = "noq", feature = "quiche")))]
impl ReloadingClientVerifier {
fn new(
paths: &[PathBuf],
initial: Arc<dyn rustls::server::danger::ClientCertVerifier>,
build: impl Fn() -> Result<Arc<dyn rustls::server::danger::ClientCertVerifier>> + Send + Sync + 'static,
) -> Self {
Self {
inner: Reloading::new(paths, initial, "server", build),
}
}
}
#[cfg(all(feature = "watch", any(feature = "quinn", feature = "noq", feature = "quiche")))]
impl rustls::server::danger::ClientCertVerifier for ReloadingClientVerifier {
fn offer_client_auth(&self) -> bool {
self.inner.current().offer_client_auth()
}
fn client_auth_mandatory(&self) -> bool {
self.inner.current().client_auth_mandatory()
}
fn root_hint_subjects(&self) -> &[rustls::DistinguishedName] {
&[]
}
fn verify_client_cert(
&self,
end_entity: &CertificateDer<'_>,
intermediates: &[CertificateDer<'_>],
now: UnixTime,
) -> std::result::Result<rustls::server::danger::ClientCertVerified, rustls::Error> {
self.inner.current().verify_client_cert(end_entity, intermediates, now)
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
self.inner.current().verify_tls12_signature(message, cert, dss)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
self.inner.current().verify_tls13_signature(message, cert, dss)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
self.inner.current().supported_verify_schemes()
}
fn requires_raw_public_keys(&self) -> bool {
self.inner.current().requires_raw_public_keys()
}
}
#[derive(Debug)]
struct NoCertificateVerification(crypto::Provider);
impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp: &[u8],
_now: UnixTime,
) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls12_signature(message, cert, dss, &self.0.signature_verification_algorithms)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls13_signature(message, cert, dss, &self.0.signature_verification_algorithms)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
self.0.signature_verification_algorithms.supported_schemes()
}
}
#[derive(Debug)]
pub(crate) struct FingerprintVerifier {
provider: crypto::Provider,
fingerprints: Vec<Vec<u8>>,
}
impl FingerprintVerifier {
pub fn new(provider: crypto::Provider, fingerprints: Vec<Vec<u8>>) -> Self {
Self { provider, fingerprints }
}
}
impl rustls::client::danger::ServerCertVerifier for FingerprintVerifier {
fn verify_server_cert(
&self,
end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp: &[u8],
_now: UnixTime,
) -> std::result::Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
let fingerprint = crypto::sha256(&self.provider, end_entity);
if self.fingerprints.iter().any(|fp| fingerprint.as_ref() == fp.as_slice()) {
Ok(rustls::client::danger::ServerCertVerified::assertion())
} else {
Err(rustls::Error::General("fingerprint mismatch".into()))
}
}
fn verify_tls12_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls12_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
}
fn verify_tls13_signature(
&self,
message: &[u8],
cert: &CertificateDer<'_>,
dss: &rustls::DigitallySignedStruct,
) -> std::result::Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> {
rustls::crypto::verify_tls13_signature(message, cert, dss, &self.provider.signature_verification_algorithms)
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
self.provider.signature_verification_algorithms.supported_schemes()
}
}
#[cfg(test)]
#[cfg(all(any(feature = "quinn", feature = "noq", feature = "quiche"), feature = "aws-lc-rs"))]
mod tests {
#[test]
fn disable_verify_rejects_trust_material() {
let insecure = Client {
disable_verify: Some(true),
..Default::default()
};
assert!(matches!(insecure.verification(), Ok(Verification::Disabled)));
let with_fingerprint = Client {
disable_verify: Some(true),
fingerprint: vec!["ab".repeat(32)],
..Default::default()
};
assert!(matches!(
with_fingerprint.verification(),
Err(Error::DisableVerifyWithTrust)
));
let with_root = Client {
disable_verify: Some(true),
root: vec!["/tmp/root.pem".into()],
..Default::default()
};
assert!(matches!(with_root.verification(), Err(Error::DisableVerifyWithTrust)));
let with_system_roots = Client {
disable_verify: Some(true),
system_roots: Some(true),
..Default::default()
};
assert!(matches!(
with_system_roots.verification(),
Err(Error::DisableVerifyWithTrust)
));
let without_system_roots = Client {
disable_verify: Some(true),
system_roots: Some(false),
..Default::default()
};
assert!(matches!(
without_system_roots.verification(),
Ok(Verification::Disabled)
));
}
use super::*;
use rustls::client::danger::ServerCertVerifier;
use rustls::pki_types::ServerName;
#[cfg(feature = "watch")]
use rustls::server::danger::ClientCertVerifier;
fn self_signed() -> CertificateDer<'static> {
let key = rcgen::KeyPair::generate().unwrap();
let params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
params.self_signed(&key).unwrap().into()
}
#[cfg(any(feature = "quinn", feature = "noq"))]
#[test]
fn peer_identity_expiry_reads_not_after() {
let not_after = ::time::OffsetDateTime::from_unix_timestamp(2_000_000_000).unwrap();
let key = rcgen::KeyPair::generate().unwrap();
let mut params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
params.not_after = not_after;
let cert: CertificateDer<'static> = params.self_signed(&key).unwrap().into();
let identity: Box<dyn std::any::Any> = Box::new(vec![cert]);
let parsed = PeerIdentity::from_any(Some(identity)).expect("chain parsed");
let expiry = parsed.expiry().expect("expiry parsed");
assert_eq!(
expiry.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(),
2_000_000_000
);
}
#[cfg(any(feature = "quinn", feature = "noq"))]
#[test]
fn peer_identity_none_without_chain() {
assert!(PeerIdentity::from_any(None).is_none());
let bogus: Box<dyn std::any::Any> = Box::new(42u32);
assert!(PeerIdentity::from_any(Some(bogus)).is_none());
}
#[test]
fn fingerprint_verifier_matches_and_rejects() {
let provider = crypto::provider();
let cert = self_signed();
let fingerprint = crypto::sha256(&provider, cert.as_ref()).as_ref().to_vec();
let name = ServerName::try_from("localhost").unwrap();
let now = UnixTime::now();
let verifier = FingerprintVerifier::new(provider.clone(), vec![fingerprint]);
assert!(verifier.verify_server_cert(&cert, &[], &name, &[], now).is_ok());
let other = self_signed();
assert!(verifier.verify_server_cert(&other, &[], &name, &[], now).is_err());
}
#[test]
fn build_installs_fingerprint_verifier() {
let cert = self_signed();
let fingerprint = hex::encode(crypto::sha256(&crypto::provider(), cert.as_ref()));
let config = Client {
fingerprint: vec![fingerprint],
..Default::default()
};
assert!(config.build().is_ok());
}
#[test]
fn build_rejects_invalid_fingerprint_hex() {
let config = Client {
fingerprint: vec!["not-hex".to_string()],
..Default::default()
};
assert!(matches!(config.build(), Err(Error::Fingerprint(_))));
}
#[test]
fn build_rejects_wrong_length_fingerprint() {
let config = Client {
fingerprint: vec!["abcd".to_string()],
..Default::default()
};
assert!(matches!(config.build(), Err(Error::FingerprintLength(2))));
}
#[test]
fn build_rejects_no_roots() {
let config = Client {
system_roots: Some(false),
..Default::default()
};
assert!(matches!(config.build(), Err(Error::NoRoots)));
}
#[test]
fn build_allows_no_roots_when_verification_overridden() {
let config = Client {
system_roots: Some(false),
disable_verify: Some(true),
..Default::default()
};
assert!(config.build().is_ok());
let cert = self_signed();
let fingerprint = hex::encode(crypto::sha256(&crypto::provider(), cert.as_ref()));
let config = Client {
system_roots: Some(false),
fingerprint: vec![fingerprint],
..Default::default()
};
assert!(config.build().is_ok());
}
#[test]
fn build_rejects_fingerprint_with_roots() {
let cert = self_signed();
let fingerprint = hex::encode(crypto::sha256(&crypto::provider(), cert.as_ref()));
let with_system = Client {
fingerprint: vec![fingerprint.clone()],
system_roots: Some(true),
..Default::default()
};
assert!(matches!(with_system.build(), Err(Error::FingerprintWithRoots)));
let with_custom = Client {
fingerprint: vec![fingerprint],
root: vec![PathBuf::from("/does-not-exist.pem")],
..Default::default()
};
assert!(matches!(with_custom.build(), Err(Error::FingerprintWithRoots)));
}
fn self_signed_root() -> (tempfile::NamedTempFile, PathBuf) {
use std::io::Write;
let key = rcgen::KeyPair::generate().unwrap();
let params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
let cert = params.self_signed(&key).unwrap();
let mut file = tempfile::NamedTempFile::new().unwrap();
file.write_all(cert.pem().as_bytes()).unwrap();
let path = file.path().to_path_buf();
(file, path)
}
#[cfg(all(feature = "watch", any(feature = "quinn", feature = "noq", feature = "quiche")))]
fn signed_certificates() -> (
String,
CertificateDer<'static>,
PrivateKeyDer<'static>,
CertificateDer<'static>,
PrivateKeyDer<'static>,
) {
use rcgen::{BasicConstraints, ExtendedKeyUsagePurpose, IsCa, Issuer};
let ca_key = rcgen::KeyPair::generate().unwrap();
let mut ca_params = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap();
ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
let ca = ca_params.self_signed(&ca_key).unwrap();
let issuer = Issuer::from_params(&ca_params, &ca_key);
let server_key = rcgen::KeyPair::generate().unwrap();
let server_key_der = PrivatePkcs8KeyDer::from(server_key.serialize_der());
let mut server_params = rcgen::CertificateParams::new(vec!["localhost".to_string()]).unwrap();
server_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
let server = server_params.signed_by(&server_key, &issuer).unwrap();
let client_key = rcgen::KeyPair::generate().unwrap();
let client_key_der = PrivatePkcs8KeyDer::from(client_key.serialize_der());
let mut client_params = rcgen::CertificateParams::new(Vec::<String>::new()).unwrap();
client_params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
let client = client_params.signed_by(&client_key, &issuer).unwrap();
(
ca.pem(),
server.into(),
server_key_der.into(),
client.into(),
client_key_der.into(),
)
}
#[cfg(all(feature = "watch", any(feature = "quinn", feature = "noq", feature = "quiche")))]
fn handshake_kinds(
client: Arc<rustls::ClientConfig>,
server: Arc<rustls::ServerConfig>,
) -> std::result::Result<(rustls::HandshakeKind, rustls::HandshakeKind), rustls::Error> {
let name = ServerName::try_from("localhost").unwrap();
let mut client = rustls::ClientConnection::new(client, name).unwrap();
let mut server = rustls::ServerConnection::new(server).unwrap();
for _ in 0..100 {
let mut client_data = Vec::new();
client.write_tls(&mut client_data).unwrap();
if !client_data.is_empty() {
server.read_tls(&mut client_data.as_slice()).unwrap();
server.process_new_packets()?;
}
let mut server_data = Vec::new();
server.write_tls(&mut server_data).unwrap();
if !server_data.is_empty() {
client.read_tls(&mut server_data.as_slice()).unwrap();
client.process_new_packets()?;
}
if !client.is_handshaking() && !server.is_handshaking() && !client.wants_write() && !server.wants_write() {
return Ok((client.handshake_kind().unwrap(), server.handshake_kind().unwrap()));
}
}
panic!("TLS handshake did not settle");
}
#[cfg(all(feature = "watch", any(feature = "quinn", feature = "noq", feature = "quiche")))]
#[test]
fn reloadable_roots_disable_session_resumption() {
use std::io::Write;
let (ca, server_cert, server_key, _, _) = signed_certificates();
let mut root_file = tempfile::NamedTempFile::new().unwrap();
root_file.write_all(ca.as_bytes()).unwrap();
let roots = read_roots(&[root_file.path().to_path_buf()]).unwrap();
let provider = crypto::provider();
let control = rustls::ClientConfig::builder_with_provider(provider.clone())
.with_safe_default_protocol_versions()
.unwrap()
.with_root_certificates(root_store(&roots).unwrap())
.with_no_client_auth();
let reloadable = Client {
root: vec![root_file.path().to_path_buf()],
..Default::default()
}
.build()
.unwrap();
let server = rustls::ServerConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.unwrap()
.with_no_client_auth()
.with_single_cert(vec![server_cert], server_key)
.unwrap();
let server = Arc::new(server);
let control = Arc::new(control);
assert_eq!(
handshake_kinds(control.clone(), server.clone()).unwrap().0,
rustls::HandshakeKind::Full
);
assert_eq!(
handshake_kinds(control, server.clone()).unwrap().0,
rustls::HandshakeKind::Resumed
);
let reloadable = Arc::new(reloadable);
assert_eq!(
handshake_kinds(reloadable.clone(), server.clone()).unwrap().0,
rustls::HandshakeKind::Full
);
assert_eq!(
handshake_kinds(reloadable, server).unwrap().0,
rustls::HandshakeKind::Full
);
}
#[cfg(all(feature = "watch", any(feature = "quinn", feature = "noq", feature = "quiche")))]
#[test]
fn reloadable_client_roots_disable_server_resumption() {
use std::io::Write;
let (ca_a, server_cert, server_key, client_cert, client_key) = signed_certificates();
let (ca_b, _, _, _, _) = signed_certificates();
let mut root_file = tempfile::NamedTempFile::new().unwrap();
root_file.write_all(ca_a.as_bytes()).unwrap();
let paths = vec![root_file.path().to_path_buf()];
let provider = crypto::provider();
let build_client = |cert, key| {
rustls::ClientConfig::builder_with_provider(provider.clone())
.with_safe_default_protocol_versions()
.unwrap()
.dangerous()
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification(provider.clone())))
.with_client_auth_cert(vec![cert], key)
.unwrap()
};
let control_client = Arc::new(build_client(client_cert.clone(), client_key.clone_key()));
let reloadable_client = Arc::new(build_client(client_cert, client_key));
let verifier = Server::build_client_verifier(&paths, &provider).unwrap();
let control = rustls::ServerConfig::builder_with_provider(provider.clone())
.with_safe_default_protocol_versions()
.unwrap()
.with_client_cert_verifier(verifier)
.with_single_cert(vec![server_cert.clone()], server_key.clone_key())
.unwrap();
let initial = Server::build_client_verifier(&paths, &provider).unwrap();
let reload_paths = paths.clone();
let reload_provider = provider.clone();
let verifier = Arc::new(ReloadingClientVerifier::new(&paths, initial, move || {
Server::build_client_verifier(&reload_paths, &reload_provider)
}));
let reload = verifier.inner.state.clone();
let mut reloadable = rustls::ServerConfig::builder_with_provider(provider)
.with_safe_default_protocol_versions()
.unwrap()
.with_client_cert_verifier(verifier)
.with_single_cert(vec![server_cert], server_key)
.unwrap();
Server {
root: paths,
..Default::default()
}
.disable_resumption(&mut reloadable);
let control = Arc::new(control);
assert_eq!(
handshake_kinds(control_client.clone(), control.clone()).unwrap().1,
rustls::HandshakeKind::Full
);
assert_eq!(
handshake_kinds(control_client, control).unwrap().1,
rustls::HandshakeKind::Resumed
);
let reloadable = Arc::new(reloadable);
assert_eq!(
handshake_kinds(reloadable_client.clone(), reloadable.clone())
.unwrap()
.1,
rustls::HandshakeKind::Full
);
std::fs::write(root_file.path(), ca_b).unwrap();
reload.reload();
assert!(handshake_kinds(reloadable_client, reloadable).is_err());
}
#[cfg(all(feature = "watch", any(feature = "quinn", feature = "noq", feature = "quiche")))]
#[test]
fn custom_roots_reload_for_new_client_and_server_handshakes() {
use std::io::Write;
let (ca_a, server_a, _, client_a, _) = signed_certificates();
let (ca_b, server_b, _, client_b, _) = signed_certificates();
let mut root_file = tempfile::NamedTempFile::new().unwrap();
root_file.write_all(ca_a.as_bytes()).unwrap();
let paths = vec![root_file.path().to_path_buf()];
let provider = crypto::provider();
let custom = CustomRoots::new(paths.clone()).unwrap();
let initial = Client::build_root_server_verifier(&custom.current(), false, &provider).unwrap();
let reload_custom = custom.clone();
let reload_provider = provider.clone();
let server_verifier = ReloadingServerVerifier::new(&paths, initial, move || {
let roots = reload_custom.load()?;
let verifier = Client::build_root_server_verifier(&roots, false, &reload_provider)?;
reload_custom.replace(roots);
Ok(verifier)
});
let initial = Server::build_client_verifier(&paths, &provider).unwrap();
let reload_paths = paths.clone();
let reload_provider = provider.clone();
let client_verifier = ReloadingClientVerifier::new(&paths, initial, move || {
Server::build_client_verifier(&reload_paths, &reload_provider)
});
let name = ServerName::try_from("localhost").unwrap();
let now = UnixTime::now();
assert!(
server_verifier
.verify_server_cert(&server_a, &[], &name, &[], now)
.is_ok()
);
assert!(
server_verifier
.verify_server_cert(&server_b, &[], &name, &[], now)
.is_err()
);
assert!(client_verifier.verify_client_cert(&client_a, &[], now).is_ok());
assert!(client_verifier.verify_client_cert(&client_b, &[], now).is_err());
std::fs::write(root_file.path(), ca_b).unwrap();
server_verifier.inner.state.reload();
client_verifier.inner.state.reload();
assert!(
server_verifier
.verify_server_cert(&server_a, &[], &name, &[], now)
.is_err()
);
assert!(client_verifier.verify_client_cert(&client_a, &[], now).is_err());
std::fs::write(root_file.path(), "not a PEM certificate").unwrap();
server_verifier.inner.state.reload();
client_verifier.inner.state.reload();
assert!(
server_verifier
.verify_server_cert(&server_b, &[], &name, &[], now)
.is_ok()
);
assert!(client_verifier.verify_client_cert(&client_b, &[], now).is_ok());
}
#[cfg(all(feature = "quiche", feature = "watch"))]
#[test]
fn custom_root_refresh_retains_last_valid_bundle() {
use std::io::Write;
let (ca_a, _, _, _, _) = signed_certificates();
let (ca_b, _, _, _, _) = signed_certificates();
let mut root_file = tempfile::NamedTempFile::new().unwrap();
root_file.write_all(ca_a.as_bytes()).unwrap();
let roots = CustomRoots::new(vec![root_file.path().to_path_buf()]).unwrap();
let initial = roots.current();
std::fs::write(root_file.path(), "not a PEM certificate").unwrap();
assert_eq!(roots.refresh(), initial);
std::fs::write(
root_file.path(),
"-----BEGIN CERTIFICATE-----\nAQID\n-----END CERTIFICATE-----\n",
)
.unwrap();
assert_eq!(roots.refresh(), initial);
std::fs::write(root_file.path(), ca_b).unwrap();
let rotated = roots.refresh();
assert_ne!(rotated, initial);
assert_eq!(roots.current(), rotated);
}
#[cfg(all(feature = "quiche", feature = "watch"))]
#[test]
fn custom_root_refresh_serializes_cache_updates() {
let (ca_a, _, _, _, _) = signed_certificates();
let (ca_b, _, _, _, _) = signed_certificates();
let (ca_c, _, _, _, _) = signed_certificates();
let parse = |pem: &str| {
CertificateDer::pem_slice_iter(pem.as_bytes())
.collect::<std::result::Result<Vec<_>, _>>()
.unwrap()
};
let initial = parse(&ca_a);
let bundle_b = parse(&ca_b);
let bundle_c = parse(&ca_c);
let roots = CustomRoots {
paths: Vec::new(),
current: Arc::new(RwLock::new(initial)),
};
let (first_loaded_tx, first_loaded_rx) = std::sync::mpsc::sync_channel(0);
let (release_first_tx, release_first_rx) = std::sync::mpsc::sync_channel(0);
let first_roots = roots.clone();
let first = std::thread::spawn(move || {
first_roots.refresh_with(|| {
first_loaded_tx.send(()).unwrap();
release_first_rx.recv().unwrap();
Ok(bundle_b)
})
});
first_loaded_rx.recv().unwrap();
let (second_ready_tx, second_ready_rx) = std::sync::mpsc::sync_channel(0);
let (second_loaded_tx, second_loaded_rx) = std::sync::mpsc::channel();
let second_roots = roots.clone();
let expected = bundle_c.clone();
let second = std::thread::spawn(move || {
second_ready_tx.send(()).unwrap();
second_roots.refresh_with(|| {
second_loaded_tx.send(()).unwrap();
Ok(bundle_c)
})
});
second_ready_rx.recv().unwrap();
let overlapped = second_loaded_rx
.recv_timeout(std::time::Duration::from_millis(100))
.is_ok();
release_first_tx.send(()).unwrap();
first.join().unwrap();
second.join().unwrap();
assert!(!overlapped, "root cache refresh transactions must not overlap");
assert_eq!(roots.current(), expected);
}
#[test]
fn build_uses_platform_verifier_by_default() {
assert!(Client::default().build().is_ok());
}
#[test]
fn build_with_custom_roots_only() {
let (_keep, path) = self_signed_root();
let config = Client {
root: vec![path],
..Default::default()
};
assert!(config.build().is_ok());
}
#[test]
fn build_with_custom_and_system_roots() {
let (_keep, path) = self_signed_root();
let config = Client {
root: vec![path],
system_roots: Some(true),
..Default::default()
};
assert!(config.build().is_ok());
}
}
#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
#[derive(Debug)]
pub(crate) struct ServeCerts {
pub info: Arc<RwLock<Info>>,
provider: crypto::Provider,
}
#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
impl ServeCerts {
pub fn new(provider: crypto::Provider) -> Self {
Self {
info: Arc::new(RwLock::new(Info::default())),
provider,
}
}
pub fn load_certs(&self, config: &Server) -> Result<()> {
if config.cert.len() != config.key.len() {
return Err(Error::CertKeyCountMismatch);
}
if config.cert.is_empty() && config.generate.is_empty() {
return Err(Error::NoCertSource);
}
let mut certs = Vec::new();
for (cert, key) in config.cert.iter().zip(config.key.iter()) {
certs.push(Arc::new(self.load(cert, key)?));
}
if !config.generate.is_empty() {
certs.push(Arc::new(self.generate(&config.generate)?));
}
self.set_certs(certs);
Ok(())
}
fn load(&self, chain_path: &Path, key_path: &Path) -> Result<rustls::sign::CertifiedKey> {
let chain = read_certs(chain_path)?;
if chain.is_empty() {
return Err(Error::Empty);
}
let key = PrivateKeyDer::from_pem_file(key_path).map_err(Error::Key)?;
let key = self.provider.key_provider.load_private_key(key)?;
let certified_key = rustls::sign::CertifiedKey::new(chain, key);
certified_key.keys_match().map_err(|source| Error::KeyMismatch {
key: key_path.to_path_buf(),
cert: chain_path.to_path_buf(),
source,
})?;
Ok(certified_key)
}
#[cfg(any(feature = "aws-lc-rs", feature = "ring"))]
fn generate(&self, hostnames: &[String]) -> Result<rustls::sign::CertifiedKey> {
let key_pair = rcgen::KeyPair::generate()?;
let mut params = rcgen::CertificateParams::new(hostnames)?;
params.not_before = ::time::OffsetDateTime::now_utc() - ::time::Duration::days(1);
params.not_after = params.not_before + ::time::Duration::days(14);
let cert = params.self_signed(&key_pair)?;
let key_der = key_pair.serialized_der().to_vec();
let key_der = PrivatePkcs8KeyDer::from(key_der);
let key = self.provider.key_provider.load_private_key(key_der.into())?;
Ok(rustls::sign::CertifiedKey::new(vec![cert.into()], key))
}
#[cfg(not(any(feature = "aws-lc-rs", feature = "ring")))]
fn generate(&self, _hostnames: &[String]) -> Result<rustls::sign::CertifiedKey> {
Err(Error::NoCryptoProvider)
}
pub fn set_certs(&self, certs: Vec<Arc<rustls::sign::CertifiedKey>>) {
let fingerprints = certs
.iter()
.map(|ck| {
let fingerprint = crate::crypto::sha256(&self.provider, ck.cert[0].as_ref());
hex::encode(fingerprint)
})
.collect();
let mut info = self.info.write().expect("info write lock poisoned");
info.certs = certs;
info.fingerprints = fingerprints;
}
fn best_certificate(
&self,
client_hello: &rustls::server::ClientHello<'_>,
) -> Option<Arc<rustls::sign::CertifiedKey>> {
let server_name = client_hello.server_name()?;
let dns_name = rustls::pki_types::ServerName::try_from(server_name).ok()?;
for ck in self.info.read().expect("info read lock poisoned").certs.iter() {
let leaf: webpki::EndEntityCert = ck
.end_entity_cert()
.expect("missing certificate")
.try_into()
.expect("failed to parse certificate");
if leaf.verify_is_valid_for_subject_name(&dns_name).is_ok() {
return Some(ck.clone());
}
}
None
}
}
#[cfg(any(feature = "quinn", feature = "noq", feature = "quiche"))]
impl rustls::server::ResolvesServerCert for ServeCerts {
fn resolve(&self, client_hello: rustls::server::ClientHello<'_>) -> Option<Arc<rustls::sign::CertifiedKey>> {
if let Some(cert) = self.best_certificate(&client_hello) {
return Some(cert);
}
tracing::warn!(server_name = ?client_hello.server_name(), "no SNI certificate found");
self.info
.read()
.expect("info read lock poisoned")
.certs
.first()
.cloned()
}
}
#[cfg(any(feature = "quinn", feature = "noq"))]
pub(crate) async fn reload_certs(certs: Arc<ServeCerts>, tls_config: Server) {
let paths: Vec<PathBuf> = tls_config.cert.iter().chain(tls_config.key.iter()).cloned().collect();
if paths.is_empty() {
return;
}
let mut watcher = match crate::watch::FileWatcher::new(&paths) {
Ok(watcher) => watcher,
Err(err) => {
tracing::error!(%err, "failed to watch certificate files; hot reload disabled");
return;
}
};
loop {
watcher.changed().await;
tracing::info!("reloading server certificates");
if let Err(err) = certs.load_certs(&tls_config) {
tracing::warn!(%err, "failed to reload server certificates");
}
}
}