use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, RwLock};
use std::time::Instant;
use rcgen::{
BasicConstraints, CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose, IsCa,
Issuer, KeyPair, KeyUsagePurpose,
};
use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
use rustls::server::{ClientHello, ResolvesServerCert};
use rustls::sign::CertifiedKey;
use rustls::ServerConfig;
use time::{Duration, OffsetDateTime};
use super::host_match::{self, HostRule};
use crate::error::{OlError, ERR_MODEL_RELAY_IO};
pub const CA_VALIDITY_DAYS: i64 = 365;
pub const LEAF_VALIDITY_DAYS: i64 = 90;
pub const CA_WARN_DAYS: i64 = 30;
pub const CA_RELEASE_DAYS: i64 = 7;
const LEAF_REMINT_DAYS: i64 = 30;
const BACKDATE: Duration = Duration::hours(1);
pub(crate) const CA_COMMON_NAME: &str = "OpenLatch Model Relay CA";
const PATTERN_LEAF_CAP: usize = 64;
const PATTERN_MINTS_PER_WINDOW: usize = 8;
const PATTERN_MINT_WINDOW: std::time::Duration = std::time::Duration::from_secs(60);
pub fn ca_dir(openlatch_dir: &Path) -> PathBuf {
openlatch_dir.join("model-relay").join("ca")
}
pub fn ca_key_path(dir: &Path) -> PathBuf {
dir.join("ca.key")
}
pub fn ca_pem_path(dir: &Path) -> PathBuf {
dir.join("ca.pem")
}
fn sibling_dir(dir: &Path, name: &str) -> PathBuf {
dir.with_file_name(name)
}
fn io_err(path: &Path, e: std::io::Error) -> OlError {
OlError::new(
ERR_MODEL_RELAY_IO,
format!("model relay CA {}: {e}", path.display()),
)
}
fn rcgen_err(path: &Path, e: rcgen::Error) -> OlError {
OlError::new(
ERR_MODEL_RELAY_IO,
format!("model relay CA {}: {e}", path.display()),
)
}
fn loadable(dir: &Path) -> bool {
ca_key_path(dir).exists() && ca_pem_path(dir).exists() && LocalCa::load(dir).is_ok()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CaInfo {
pub sha256_hex: String,
pub not_after: OffsetDateTime,
}
pub struct LocalCa {
issuer: Issuer<'static, KeyPair>,
sha256_hex: String,
not_after: OffsetDateTime,
dir: PathBuf,
}
impl LocalCa {
pub fn load_or_generate(dir: &Path) -> Result<LocalCa, OlError> {
let next_dir = sibling_dir(dir, "ca.next");
if !loadable(dir) && loadable(&next_dir) {
let prev_dir = sibling_dir(dir, "ca.prev");
if dir.exists() && !prev_dir.exists() {
std::fs::rename(dir, &prev_dir).map_err(|e| io_err(dir, e))?;
}
std::fs::rename(&next_dir, dir).map_err(|e| io_err(&next_dir, e))?;
}
if ca_key_path(dir).exists() && ca_pem_path(dir).exists() {
match Self::load(dir) {
Ok(ca) => return Ok(ca),
Err(e) => {
tracing::warn!(
error = %e.message,
"model relay CA unparseable; regenerating"
);
}
}
}
Self::generate_into(dir)
}
pub fn generate_into(dir: &Path) -> Result<LocalCa, OlError> {
let now = OffsetDateTime::now_utc();
Self::build_and_persist(dir, now - BACKDATE, now + Duration::days(CA_VALIDITY_DAYS))
}
#[cfg(test)]
fn generate_with_validity(
dir: &Path,
not_before: OffsetDateTime,
not_after: OffsetDateTime,
) -> Result<LocalCa, OlError> {
Self::build_and_persist(dir, not_before, not_after)
}
fn build_and_persist(
dir: &Path,
not_before: OffsetDateTime,
not_after: OffsetDateTime,
) -> Result<LocalCa, OlError> {
let mut params =
CertificateParams::new(Vec::<String>::new()).map_err(|e| rcgen_err(dir, e))?;
let mut name = DistinguishedName::new();
name.push(DnType::CommonName, CA_COMMON_NAME);
name.push(DnType::OrganizationName, "OpenLatch");
params.distinguished_name = name;
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
params.key_usages = vec![
KeyUsagePurpose::KeyCertSign,
KeyUsagePurpose::CrlSign,
KeyUsagePurpose::DigitalSignature,
];
params.not_before = not_before;
params.not_after = not_after;
let key = KeyPair::generate().map_err(|e| rcgen_err(dir, e))?;
let cert = params.self_signed(&key).map_err(|e| rcgen_err(dir, e))?;
crate::fs_secure::create_dir_owner_only(dir).map_err(|e| io_err(dir, e))?;
crate::fs_secure::write_owner_only(&ca_key_path(dir), &key.serialize_pem())
.map_err(|e| io_err(&ca_key_path(dir), e))?;
crate::fs_secure::write_readable(&ca_pem_path(dir), &cert.pem())
.map_err(|e| io_err(&ca_pem_path(dir), e))?;
Self::load(dir)
}
fn load(dir: &Path) -> Result<LocalCa, OlError> {
let key_path = ca_key_path(dir);
let pem_path = ca_pem_path(dir);
let key_pem = std::fs::read_to_string(&key_path).map_err(|e| io_err(&key_path, e))?;
let pem = std::fs::read_to_string(&pem_path).map_err(|e| io_err(&pem_path, e))?;
let key = KeyPair::from_pem(&key_pem).map_err(|e| rcgen_err(&key_path, e))?;
let issuer = Issuer::from_ca_cert_pem(&pem, key).map_err(|e| rcgen_err(&pem_path, e))?;
let cert_der = CertificateDer::from_pem_slice(pem.as_bytes()).map_err(|e| {
OlError::new(
ERR_MODEL_RELAY_IO,
format!("model relay CA {}: {e}", pem_path.display()),
)
})?;
let (sha256_hex, not_after) = cert_facts(&cert_der)?;
Ok(LocalCa {
issuer,
sha256_hex,
not_after,
dir: dir.to_path_buf(),
})
}
pub fn sha256_hex(&self) -> String {
self.sha256_hex.clone()
}
pub fn not_after(&self) -> OffsetDateTime {
self.not_after
}
pub fn pem_path(&self) -> PathBuf {
ca_pem_path(&self.dir)
}
fn mint(&self, host: &str) -> Result<Leaf, OlError> {
if host.contains('*') {
return Err(OlError::new(
ERR_MODEL_RELAY_IO,
format!("model relay never mints a wildcard leaf: {host}"),
));
}
let mut params =
CertificateParams::new(vec![host.to_string()]).map_err(|e| rcgen_err(&self.dir, e))?;
let mut name = DistinguishedName::new();
name.push(DnType::CommonName, host);
params.distinguished_name = name;
params.is_ca = IsCa::NoCa;
params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
params.key_usages = vec![KeyUsagePurpose::DigitalSignature];
params.use_authority_key_identifier_extension = true;
let now = OffsetDateTime::now_utc();
params.not_before = now - BACKDATE;
let wanted = now + Duration::days(LEAF_VALIDITY_DAYS);
params.not_after = if wanted < self.not_after {
wanted
} else {
self.not_after
};
let key = KeyPair::generate().map_err(|e| rcgen_err(&self.dir, e))?;
let not_after = params.not_after;
let cert = params
.signed_by(&key, &self.issuer)
.map_err(|e| rcgen_err(&self.dir, e))?;
Ok(Leaf {
cert,
key,
not_after,
})
}
}
fn cert_facts(der: &[u8]) -> Result<(String, OffsetDateTime), OlError> {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(der);
let sha256_hex = hex::encode(hasher.finalize());
let (_, x509) = x509_parser::parse_x509_certificate(der).map_err(|e| {
OlError::new(
ERR_MODEL_RELAY_IO,
format!("model relay CA certificate could not be parsed: {e}"),
)
})?;
let not_after = x509.validity().not_after.to_datetime();
Ok((sha256_hex, not_after))
}
pub fn inspect(dir: &Path) -> Option<CaInfo> {
let pem = std::fs::read_to_string(ca_pem_path(dir)).ok()?;
let der = CertificateDer::from_pem_slice(pem.as_bytes()).ok()?;
let (sha256_hex, not_after) = cert_facts(&der).ok()?;
Some(CaInfo {
sha256_hex,
not_after,
})
}
pub fn windows_thumbprint(ca_pem: &Path) -> Option<String> {
use sha1::{Digest, Sha1};
let pem = std::fs::read(ca_pem).ok()?;
let der = CertificateDer::from_pem_slice(&pem).ok()?;
let hex = hex::encode_upper(Sha1::digest(der.as_ref()));
let groups: Vec<&str> = hex
.as_bytes()
.chunks(8)
.filter_map(|group| std::str::from_utf8(group).ok())
.collect();
Some(groups.join(" "))
}
pub fn remove(dir: &Path) -> Result<bool, OlError> {
match std::fs::remove_dir_all(dir) {
Ok(()) => Ok(true),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(io_err(dir, e)),
}
}
struct Leaf {
cert: rcgen::Certificate,
key: KeyPair,
not_after: OffsetDateTime,
}
#[derive(Debug)]
struct LeafEntry {
key: Arc<CertifiedKey>,
not_after: OffsetDateTime,
}
struct PatternLeaf {
config: Arc<ServerConfig>,
not_after: OffsetDateTime,
}
#[derive(Default)]
struct PatternLeaves {
entries: BTreeMap<String, PatternLeaf>,
order: VecDeque<String>,
mints: VecDeque<Instant>,
}
impl PatternLeaves {
fn get(
&mut self,
host: &str,
now: OffsetDateTime,
ca_not_after: OffsetDateTime,
) -> Option<Arc<ServerConfig>> {
let fresh = self.entries.get(host).map(|e| {
e.not_after > now
&& (e.not_after - now > Duration::days(LEAF_REMINT_DAYS)
|| e.not_after >= ca_not_after)
})?;
if !fresh {
self.remove(host);
return None;
}
self.touch(host);
self.entries.get(host).map(|e| e.config.clone())
}
fn touch(&mut self, host: &str) {
if let Some(pos) = self.order.iter().position(|h| h == host) {
if let Some(h) = self.order.remove(pos) {
self.order.push_back(h);
}
}
}
fn remove(&mut self, host: &str) {
self.entries.remove(host);
self.order.retain(|h| h != host);
}
fn try_reserve_mint(&mut self, now: Instant) -> bool {
while self
.mints
.front()
.is_some_and(|t| now.saturating_duration_since(*t) >= PATTERN_MINT_WINDOW)
{
self.mints.pop_front();
}
if self.mints.len() >= PATTERN_MINTS_PER_WINDOW {
return false;
}
self.mints.push_back(now);
true
}
fn insert(&mut self, host: String, leaf: PatternLeaf) {
self.order.retain(|h| h != &host);
self.order.push_back(host.clone());
self.entries.insert(host, leaf);
while self.entries.len() > PATTERN_LEAF_CAP {
match self.order.pop_front() {
Some(oldest) => {
self.entries.remove(&oldest);
}
None => break,
}
}
}
fn retain(&mut self, keep: impl Fn(&str) -> bool) {
self.entries.retain(|h, _| keep(h));
let entries = &self.entries;
self.order.retain(|h| entries.contains_key(h));
}
}
#[derive(Debug)]
struct OlAuthority {
leaves: Arc<RwLock<BTreeMap<String, LeafEntry>>>,
pin: Option<String>,
}
impl ResolvesServerCert for OlAuthority {
fn resolve(&self, hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> {
let want = match (hello.server_name(), &self.pin) {
(Some(sni), Some(pin)) if !sni.eq_ignore_ascii_case(pin) => return None,
(Some(sni), _) => sni.to_ascii_lowercase(),
(None, Some(pin)) => pin.clone(),
(None, None) => return None,
};
let leaves = self.leaves.read().unwrap_or_else(|e| e.into_inner());
leaves.get(&want).map(|l| l.key.clone())
}
}
pub struct Interceptor {
ca: LocalCa,
leaves: Arc<RwLock<BTreeMap<String, LeafEntry>>>,
pinned: RwLock<BTreeMap<String, (bool, Arc<ServerConfig>)>>,
any_host: Arc<ServerConfig>,
patterns: RwLock<Vec<HostRule>>,
pattern_leaves: Mutex<PatternLeaves>,
pattern_mint_capped: AtomicU64,
}
impl Interceptor {
pub fn new(
dir: &Path,
hosts: impl IntoIterator<Item = impl Into<String>>,
h2_hosts: impl IntoIterator<Item = impl Into<String>>,
) -> Result<Interceptor, OlError> {
let ca = LocalCa::load_or_generate(dir)?;
let leaves: Arc<RwLock<BTreeMap<String, LeafEntry>>> =
Arc::new(RwLock::new(BTreeMap::new()));
let any_host = server_config_with(&leaves, None, false)?;
let interceptor = Interceptor {
ca,
leaves,
pinned: RwLock::new(BTreeMap::new()),
any_host,
patterns: RwLock::new(Vec::new()),
pattern_leaves: Mutex::new(PatternLeaves::default()),
pattern_mint_capped: AtomicU64::new(0),
};
interceptor.set_hosts(hosts, h2_hosts)?;
Ok(interceptor)
}
pub fn set_hosts(
&self,
hosts: impl IntoIterator<Item = impl Into<String>>,
h2_hosts: impl IntoIterator<Item = impl Into<String>>,
) -> Result<(), OlError> {
let want = normalize_hosts(hosts);
let h2 = normalize_hosts(h2_hosts);
let mut minted: BTreeMap<String, LeafEntry> = BTreeMap::new();
{
let existing = self.leaves.read().unwrap_or_else(|e| e.into_inner());
let now = OffsetDateTime::now_utc();
for h in &want {
if let Some(entry) = existing.get(h) {
if entry.not_after - now > Duration::days(LEAF_REMINT_DAYS) {
minted.insert(
h.clone(),
LeafEntry {
key: entry.key.clone(),
not_after: entry.not_after,
},
);
}
}
}
}
let to_mint: Vec<&String> = want.iter().filter(|h| !minted.contains_key(*h)).collect();
for h in to_mint {
minted.insert(h.clone(), self.certified_leaf(h)?);
}
let mut pinned_new: BTreeMap<String, (bool, Arc<ServerConfig>)> = BTreeMap::new();
{
let existing_pinned = self.pinned.read().unwrap_or_else(|e| e.into_inner());
for h in &want {
let is_h2 = h2.contains(h);
let cfg = match existing_pinned.get(h) {
Some((was_h2, cfg)) if *was_h2 == is_h2 => cfg.clone(),
_ => server_config_with(&self.leaves, Some(h), is_h2)?,
};
pinned_new.insert(h.clone(), (is_h2, cfg));
}
}
*self.leaves.write().unwrap_or_else(|e| e.into_inner()) = minted;
*self.pinned.write().unwrap_or_else(|e| e.into_inner()) = pinned_new;
Ok(())
}
fn certified_leaf(&self, host: &str) -> Result<LeafEntry, OlError> {
let leaf = self.ca.mint(host)?;
let signing_key = rustls::crypto::ring::sign::any_ecdsa_type(&PrivateKeyDer::Pkcs8(
leaf.key.serialize_der().into(),
))
.map_err(|e| {
OlError::new(
ERR_MODEL_RELAY_IO,
format!("model relay leaf key for {host}: {e}"),
)
})?;
let certified = CertifiedKey::new(vec![leaf.cert.der().clone()], signing_key);
Ok(LeafEntry {
key: Arc::new(certified),
not_after: leaf.not_after,
})
}
pub fn set_patterns(&self, rules: impl IntoIterator<Item = HostRule>) {
let rules: Vec<HostRule> = rules.into_iter().collect();
self.pattern_leaves
.lock()
.unwrap_or_else(|e| e.into_inner())
.retain(|h| host_match::matches(&rules, h));
*self.patterns.write().unwrap_or_else(|e| e.into_inner()) = rules;
}
fn matches_pattern(&self, host: &str) -> bool {
host_match::matches(
&self.patterns.read().unwrap_or_else(|e| e.into_inner()),
host,
)
}
fn is_exact(&self, host: &str) -> bool {
self.leaves
.read()
.unwrap_or_else(|e| e.into_inner())
.contains_key(host)
}
pub fn intercepts(&self, host: &str) -> bool {
let host = host_match::normalize(host);
self.is_exact(&host) || self.matches_pattern(&host)
}
pub fn ensure_leaf(&self, host: &str) -> bool {
self.connect_config(host).is_some()
}
pub(crate) fn connect_config(&self, host: &str) -> Option<Arc<ServerConfig>> {
let host = host_match::normalize(host);
if self.is_exact(&host) {
return self
.pinned
.read()
.unwrap_or_else(|e| e.into_inner())
.get(host.as_ref())
.map(|(_, cfg)| cfg.clone());
}
if !self.matches_pattern(&host) {
return None;
}
{
let mut cache = self
.pattern_leaves
.lock()
.unwrap_or_else(|e| e.into_inner());
if let Some(cfg) = cache.get(&host, OffsetDateTime::now_utc(), self.ca.not_after()) {
return Some(cfg);
}
if !cache.try_reserve_mint(Instant::now()) {
drop(cache);
self.pattern_mint_capped.fetch_add(1, Ordering::Relaxed);
tracing::debug!(
host = %host,
"model relay pattern leaf mint cap reached; CONNECT tunneled untouched"
);
return None;
}
}
let built = self.certified_leaf(&host).and_then(|entry| {
let not_after = entry.not_after;
let one: Arc<RwLock<BTreeMap<String, LeafEntry>>> =
Arc::new(RwLock::new(BTreeMap::from([(host.to_string(), entry)])));
server_config_with(&one, Some(&host), false)
.map(|config| PatternLeaf { config, not_after })
});
match built {
Ok(leaf) => {
let config = leaf.config.clone();
self.pattern_leaves
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(host.into_owned(), leaf);
Some(config)
}
Err(e) => {
tracing::warn!(
code = %e.code,
host = %host,
"model relay pattern leaf could not be minted; CONNECT tunneled untouched"
);
None
}
}
}
pub fn pattern_mint_capped(&self) -> u64 {
self.pattern_mint_capped.load(Ordering::Relaxed)
}
pub fn server_config(&self) -> Arc<ServerConfig> {
self.any_host.clone()
}
#[cfg(test)]
pub(crate) fn pinned_config(&self, host: &str) -> Option<Arc<ServerConfig>> {
let host = host_match::normalize(host);
let exact = self
.pinned
.read()
.unwrap_or_else(|e| e.into_inner())
.get(host.as_ref())
.map(|(_, cfg)| cfg.clone());
if exact.is_some() || !self.matches_pattern(&host) {
return exact;
}
self.pattern_leaves
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&host, OffsetDateTime::now_utc(), self.ca.not_after())
}
pub fn is_h2_host(&self, host: &str) -> bool {
let host = host_match::normalize(host);
self.pinned
.read()
.unwrap_or_else(|e| e.into_inner())
.get(host.as_ref())
.is_some_and(|(h2, _)| *h2)
}
pub fn mint_leaf_pem(&self, host: &str) -> Result<String, OlError> {
if !self.intercepts(host) {
return Err(OlError::new(
ERR_MODEL_RELAY_IO,
format!("not a declared intercept host: {host}"),
));
}
Ok(self.ca.mint(&host_match::normalize(host))?.cert.pem())
}
pub fn ca(&self) -> &LocalCa {
&self.ca
}
}
fn normalize_hosts(hosts: impl IntoIterator<Item = impl Into<String>>) -> BTreeSet<String> {
hosts
.into_iter()
.map(|h| host_match::normalize(h.into().trim()).into_owned())
.filter(|h| !h.is_empty())
.filter(|h| {
let wildcard = h.contains('*');
if wildcard {
tracing::warn!(host = %h, "model relay ignores a wildcard intercept host");
}
!wildcard
})
.collect()
}
fn server_config_with(
leaves: &Arc<RwLock<BTreeMap<String, LeafEntry>>>,
pin: Option<&str>,
h2: bool,
) -> Result<Arc<ServerConfig>, OlError> {
let mut config =
ServerConfig::builder_with_provider(Arc::new(rustls::crypto::ring::default_provider()))
.with_safe_default_protocol_versions()
.map_err(|e| {
OlError::new(
ERR_MODEL_RELAY_IO,
format!("model relay TLS server config: {e}"),
)
})?
.with_no_client_auth()
.with_cert_resolver(Arc::new(OlAuthority {
leaves: leaves.clone(),
pin: pin.map(str::to_string),
}));
config.alpn_protocols = if h2 {
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
} else {
vec![b"http/1.1".to_vec()]
};
Ok(Arc::new(config))
}
#[cfg(test)]
mod tests {
use super::*;
use x509_parser::extensions::GeneralName;
const FIXTURE_PEM: &str = "-----BEGIN CERTIFICATE-----
MIIBVDCB+gIJAJapbvI+vytBMAoGCCqGSM49BAMCMDIxMDAuBgNVBAMMJ09wZW5M
YXRjaCBNb2RlbCBSZWxheSBDQSAodGVzdCBmaXh0dXJlKTAeFw0yNjEwMTIyMTI5
MjRaFw0zNjEwMDkyMTI5MjRaMDIxMDAuBgNVBAMMJ09wZW5MYXRjaCBNb2RlbCBS
ZWxheSBDQSAodGVzdCBmaXh0dXJlKTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA
BEw1f96hzUHBgHWgq8MArvBeMhEvGJwuq/WddWb0q1nNRSo5c7IjIk/fNQY3PNwQ
Y21qN/aDosIP5qyjvdBO3u8wCgYIKoZIzj0EAwIDSQAwRgIhAIh0EGybuU3vtaEG
myr9qVGNaEFoqRFYvYFdqQwBF2vPAiEAxVPs54aHF5vH9g60KMQkHIJaxchUeywm
pdQ2kqiYF64=
-----END CERTIFICATE-----
";
const FIXTURE_WINDOWS_THUMBPRINT: &str = "BE095A3B F3CE7BDE 40E88C58 C3F82B51 311943F6";
#[test]
fn windows_thumbprint_matches_openssl_in_the_dialog_layout() {
let tmp = tempfile::tempdir().expect("tempdir");
let pem = tmp.path().join("ca.pem");
std::fs::write(&pem, FIXTURE_PEM).expect("write fixture");
assert_eq!(
windows_thumbprint(&pem).as_deref(),
Some(FIXTURE_WINDOWS_THUMBPRINT)
);
}
#[test]
fn windows_thumbprint_is_none_for_a_missing_or_non_pem_file() {
let tmp = tempfile::tempdir().expect("tempdir");
assert_eq!(windows_thumbprint(&tmp.path().join("absent.pem")), None);
let junk = tmp.path().join("junk.pem");
std::fs::write(&junk, "not a certificate").expect("write junk");
assert_eq!(windows_thumbprint(&junk), None);
}
#[test]
fn the_ca_is_generated_once_and_reused() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
let first = LocalCa::load_or_generate(&dir).expect("first generate");
let second = LocalCa::load_or_generate(&dir).expect("second load");
assert_eq!(first.sha256_hex(), second.sha256_hex());
let days = (second.not_after() - OffsetDateTime::now_utc()).whole_days();
assert!(
(CA_VALIDITY_DAYS - 1..=CA_VALIDITY_DAYS).contains(&days),
"expected not_after within a day of {CA_VALIDITY_DAYS}, got {days}"
);
std::fs::remove_file(ca_key_path(&dir)).expect("remove key");
let third = LocalCa::load_or_generate(&dir).expect("regenerate after missing key");
assert_ne!(second.sha256_hex(), third.sha256_hex());
std::fs::write(ca_pem_path(&dir), b"not a certificate").expect("corrupt pem");
let fourth = LocalCa::load_or_generate(&dir).expect("regenerate after garbage pem");
assert_ne!(third.sha256_hex(), fourth.sha256_hex());
}
#[test]
fn an_expired_ca_is_loaded_not_regenerated() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
let now = OffsetDateTime::now_utc();
let expired = LocalCa::generate_with_validity(
&dir,
now - Duration::days(400),
now - Duration::days(1),
)
.expect("generate an already-expired CA");
let loaded = LocalCa::load_or_generate(&dir).expect("load, never regenerate an expired CA");
assert_eq!(expired.sha256_hex(), loaded.sha256_hex());
assert!(loaded.not_after() < now);
}
#[test]
#[cfg(unix)]
fn the_ca_key_is_owner_only() {
use std::os::unix::fs::PermissionsExt;
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
LocalCa::load_or_generate(&dir).expect("generate");
let key_mode = std::fs::metadata(ca_key_path(&dir))
.expect("key metadata")
.permissions()
.mode();
assert_eq!(key_mode & 0o777, 0o600);
let pem_mode = std::fs::metadata(ca_pem_path(&dir))
.expect("pem metadata")
.permissions()
.mode();
assert_eq!(pem_mode & 0o777, 0o644);
let dir_mode = std::fs::metadata(&dir)
.expect("dir metadata")
.permissions()
.mode();
assert_eq!(dir_mode & 0o777, 0o700);
}
#[test]
#[cfg(not(unix))]
fn the_ca_key_is_owner_only() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
LocalCa::load_or_generate(&dir).expect("generate");
assert!(ca_key_path(&dir).exists());
assert!(ca_pem_path(&dir).exists());
}
#[test]
fn leaves_use_their_own_keys() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
let ca = LocalCa::load_or_generate(&dir).expect("generate");
let leaf1 = ca.mint("a.test").expect("mint 1");
let leaf2 = ca.mint("a.test").expect("mint 2");
let spki = |der: &[u8]| -> Vec<u8> {
x509_parser::parse_x509_certificate(der)
.expect("parse leaf")
.1
.tbs_certificate
.subject_pki
.subject_public_key
.data
.to_vec()
};
let ca_pem = std::fs::read_to_string(ca.pem_path()).expect("read ca.pem");
let ca_der = CertificateDer::from_pem_slice(ca_pem.as_bytes()).expect("parse ca pem");
let spki_ca = spki(&ca_der);
let spki1 = spki(leaf1.cert.der());
let spki2 = spki(leaf2.cert.der());
assert_ne!(
spki1, spki2,
"two mints for one host must use distinct keys"
);
assert_ne!(spki1, spki_ca);
assert_ne!(spki2, spki_ca);
}
#[test]
fn a_leaf_names_its_host_and_chains_to_the_ca() {
use rustls::client::danger::ServerCertVerifier;
use rustls::client::WebPkiServerVerifier;
use rustls::pki_types::{ServerName, UnixTime};
use rustls::RootCertStore;
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
let ca = LocalCa::load_or_generate(&dir).expect("generate");
let ca_pem = std::fs::read_to_string(ca.pem_path()).expect("read ca.pem");
let ca_der = CertificateDer::from_pem_slice(ca_pem.as_bytes()).expect("parse ca pem");
let mut roots = RootCertStore::empty();
roots.add(ca_der).expect("trust the CA");
let verifier = WebPkiServerVerifier::builder_with_provider(
Arc::new(roots),
Arc::new(rustls::crypto::ring::default_provider()),
)
.build()
.expect("build verifier");
let leaf = ca.mint("a.test").expect("mint a.test");
let leaf_der = leaf.cert.der().clone();
let ok_name = ServerName::try_from("a.test").expect("server name");
verifier
.verify_server_cert(&leaf_der, &[], &ok_name, &[], UnixTime::now())
.expect("a.test's leaf must verify for a.test");
let bad_name = ServerName::try_from("b.test").expect("server name");
verifier
.verify_server_cert(&leaf_der, &[], &bad_name, &[], UnixTime::now())
.expect_err("a.test's leaf must not verify for b.test");
let ip_leaf = ca.mint("127.0.0.1").expect("mint IP leaf");
let ip_der = ip_leaf.cert.der().clone();
let ip_name = ServerName::try_from("127.0.0.1").expect("IP server name");
verifier
.verify_server_cert(&ip_der, &[], &ip_name, &[], UnixTime::now())
.expect("an IP leaf must verify for its own IP SAN");
}
#[test]
fn a_leaf_never_outlives_its_ca() {
let tmp = tempfile::tempdir().expect("tempdir");
let short_dir = tmp.path().join("short");
let now = OffsetDateTime::now_utc();
let short_ca =
LocalCa::generate_with_validity(&short_dir, now - BACKDATE, now + Duration::days(10))
.expect("generate a short-lived CA");
let short_leaf = short_ca.mint("a.test").expect("mint under a short CA");
assert!(short_leaf.not_after <= short_ca.not_after());
let long_dir = tmp.path().join("long");
let long_ca = LocalCa::load_or_generate(&long_dir).expect("generate a full-validity CA");
let long_leaf = long_ca
.mint("a.test")
.expect("mint under a full-validity CA");
let days = (long_leaf.not_after - OffsetDateTime::now_utc()).whole_days();
assert!(
(LEAF_VALIDITY_DAYS - 1..=LEAF_VALIDITY_DAYS).contains(&days),
"expected leaf not_after within a day of {LEAF_VALIDITY_DAYS}, got {days}"
);
}
#[test]
fn inspect_never_generates() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
assert!(inspect(&dir).is_none());
assert!(!dir.exists(), "inspect must never create the directory");
let ca = LocalCa::load_or_generate(&dir).expect("generate");
let info = inspect(&dir).expect("inspect after generate");
assert_eq!(info.sha256_hex, ca.sha256_hex());
assert_eq!(info.not_after, ca.not_after());
}
#[test]
fn remove_deletes_the_directory_once() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
LocalCa::load_or_generate(&dir).expect("generate");
assert!(remove(&dir).expect("first remove"));
assert!(!dir.exists());
assert!(!remove(&dir).expect("second remove is not an error"));
}
#[test]
fn generate_into_replaces_the_ca() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
let first = LocalCa::load_or_generate(&dir).expect("first generate");
let second = LocalCa::generate_into(&dir).expect("replace");
assert_ne!(first.sha256_hex(), second.sha256_hex());
let third = LocalCa::load_or_generate(&dir).expect("load the replacement");
assert_eq!(second.sha256_hex(), third.sha256_hex());
}
#[test]
fn an_interrupted_promotion_recovers_from_ca_next() {
{
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
let next_dir = dir.with_file_name("ca.next");
let next_ca = LocalCa::generate_into(&next_dir).expect("generate ca.next");
assert!(!dir.exists());
let loaded = LocalCa::load_or_generate(&dir).expect("promote and load");
assert_eq!(loaded.sha256_hex(), next_ca.sha256_hex());
assert!(
!next_dir.exists(),
"ca.next must be consumed by the promotion"
);
}
{
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
let _old = LocalCa::load_or_generate(&dir).expect("generate the old CA");
std::fs::write(ca_pem_path(&dir), b"garbage").expect("corrupt the old pem");
let next_dir = dir.with_file_name("ca.next");
let next_ca = LocalCa::generate_into(&next_dir).expect("generate ca.next");
let prev_dir = dir.with_file_name("ca.prev");
assert!(!prev_dir.exists());
let loaded = LocalCa::load_or_generate(&dir).expect("promote over a corrupt ca/");
assert_eq!(loaded.sha256_hex(), next_ca.sha256_hex());
assert!(!next_dir.exists());
assert!(
prev_dir.exists(),
"the old ca/ must be preserved at ca.prev/"
);
}
{
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
let next_dir = dir.with_file_name("ca.next");
std::fs::create_dir_all(&next_dir).expect("create ca.next");
std::fs::write(next_dir.join("ca.key"), b"garbage-key").expect("write garbage key");
std::fs::write(next_dir.join("ca.pem"), b"garbage-pem").expect("write garbage pem");
let _generated = LocalCa::load_or_generate(&dir).expect("fresh generation");
assert!(dir.exists());
assert_eq!(
std::fs::read(next_dir.join("ca.key")).expect("ca.next/ca.key"),
b"garbage-key"
);
}
}
#[test]
fn set_hosts_mints_new_hosts_and_drops_removed_ones() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
let ic = Interceptor::new(&dir, ["a.test"], None::<&str>).expect("new");
ic.set_hosts(["b.test"], None::<&str>).expect("set_hosts");
assert!(ic.intercepts("b.test"));
assert!(ic.intercepts("B.TEST"));
assert!(!ic.intercepts("a.test"));
assert!(ic.pinned_config("a.test").is_none());
let key_before = ic
.leaves
.read()
.expect("read leaves")
.get("b.test")
.expect("b.test leaf")
.key
.clone();
let config_before = ic.pinned_config("b.test").expect("b.test config");
ic.set_hosts(["b.test"], None::<&str>)
.expect("set_hosts again");
let key_after = ic
.leaves
.read()
.expect("read leaves")
.get("b.test")
.expect("b.test leaf")
.key
.clone();
assert!(
Arc::ptr_eq(&key_before, &key_after),
"an unexpired leaf must be reused, not re-minted"
);
let config_after = ic.pinned_config("b.test").expect("b.test config");
assert!(
Arc::ptr_eq(&config_before, &config_after),
"the pinned config must be reused when its leaf is reused"
);
}
#[test]
fn non_flagged_host_keeps_http1_alpn() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
let ic = Interceptor::new(&dir, ["a.test", "h2.test"], ["H2.test"]).expect("new");
let plain = ic.pinned_config("a.test").expect("a.test config");
assert_eq!(plain.alpn_protocols, vec![b"http/1.1".to_vec()]);
assert!(!ic.is_h2_host("a.test"));
let flagged = ic.pinned_config("h2.test").expect("h2.test config");
assert_eq!(
flagged.alpn_protocols,
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
);
assert!(ic.is_h2_host("h2.TEST"));
assert_eq!(
ic.server_config().alpn_protocols,
vec![b"http/1.1".to_vec()]
);
ic.set_hosts(["a.test"], ["z.test"]).expect("set_hosts");
assert!(!ic.intercepts("z.test"));
assert!(!ic.is_h2_host("z.test"));
}
#[test]
fn pinned_config_is_rebuilt_when_h2_flips() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
let ic = Interceptor::new(&dir, ["h.test"], None::<&str>).expect("new");
let before = ic.pinned_config("h.test").expect("config");
assert_eq!(before.alpn_protocols, vec![b"http/1.1".to_vec()]);
ic.set_hosts(["h.test"], ["h.test"]).expect("flag h2");
let flagged = ic.pinned_config("h.test").expect("config");
assert!(
!Arc::ptr_eq(&before, &flagged),
"the config must be rebuilt"
);
assert_eq!(
flagged.alpn_protocols,
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
);
ic.set_hosts(["h.test"], ["h.test"])
.expect("same set again");
assert!(
Arc::ptr_eq(&flagged, &ic.pinned_config("h.test").expect("config")),
"an unchanged flag reuses the config"
);
ic.set_hosts(["h.test"], None::<&str>).expect("unflag");
assert_eq!(
ic.pinned_config("h.test").expect("config").alpn_protocols,
vec![b"http/1.1".to_vec()]
);
}
#[test]
fn mint_leaf_pem_refuses_an_undeclared_host() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
let ic = Interceptor::new(&dir, ["a.test"], None::<&str>).expect("new");
let pem = ic.mint_leaf_pem("a.test").expect("declared host mints");
let der = CertificateDer::from_pem_slice(pem.as_bytes()).expect("parse minted pem");
let (_, x509) = x509_parser::parse_x509_certificate(&der).expect("parse x509");
assert!(!x509.is_ca(), "a leaf must not itself be a CA");
let san = x509
.subject_alternative_name()
.expect("SAN parses")
.expect("leaf carries a SAN");
assert!(san
.value
.general_names
.iter()
.any(|n| matches!(n, GeneralName::DNSName(h) if *h == "a.test")));
assert!(format!("{}", x509.issuer()).contains(CA_COMMON_NAME));
let err = ic
.mint_leaf_pem("z.test")
.expect_err("an undeclared host must be refused");
assert_eq!(err.code, ERR_MODEL_RELAY_IO);
}
async fn one_handshake(
config: Arc<ServerConfig>,
ca_der: &CertificateDer<'static>,
sni: &str,
) -> (std::io::Result<()>, std::io::Result<()>) {
let (client_io, server_io) = tokio::io::duplex(8192);
let acceptor = tokio_rustls::TlsAcceptor::from(config);
let mut roots = rustls::RootCertStore::empty();
roots.add(ca_der.clone()).expect("trust the CA");
let client_config = rustls::ClientConfig::builder()
.with_root_certificates(roots)
.with_no_client_auth();
let connector = tokio_rustls::TlsConnector::from(Arc::new(client_config));
let server_name =
rustls::pki_types::ServerName::try_from(sni.to_string()).expect("server name");
let (server_res, client_res) = tokio::time::timeout(
std::time::Duration::from_secs(5),
futures_util::future::join(
acceptor.accept(server_io),
connector.connect(server_name, client_io),
),
)
.await
.expect("handshake did not hang");
(server_res.map(|_| ()), client_res.map(|_| ()))
}
#[tokio::test]
async fn the_pinned_resolver_refuses_a_foreign_sni() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
let ic = Interceptor::new(&dir, ["a.test", "c.test"], None::<&str>).expect("new");
let ca_pem = std::fs::read_to_string(ic.ca().pem_path()).expect("read ca.pem");
let ca_der = CertificateDer::from_pem_slice(ca_pem.as_bytes()).expect("parse ca pem");
let config = ic.pinned_config("a.test").expect("a.test is declared");
let (server_ok, client_ok) = one_handshake(config.clone(), &ca_der, "a.test").await;
server_ok.expect("server accepts a matching SNI");
client_ok.expect("client completes against a matching SNI");
let (server_err, client_err) = one_handshake(config, &ca_der, "c.test").await;
let server_err = server_err.expect_err("server must refuse a foreign SNI");
assert!(
server_err
.to_string()
.contains("no server certificate chain resolved"),
"unexpected server error: {server_err}"
);
let client_err = client_err.expect_err("client must see the refusal");
let inner = client_err
.get_ref()
.and_then(|e| e.downcast_ref::<rustls::Error>());
assert_eq!(
inner,
Some(&rustls::Error::AlertReceived(
rustls::AlertDescription::AccessDenied
)),
"unexpected client error: {client_err}"
);
}
const PAT: HostRule = HostRule::OneLabel {
prefix: "",
suffix: ".pat.test",
};
fn pattern_interceptor(dir: &Path) -> Interceptor {
let ic = Interceptor::new(dir, ["a.test"], None::<&str>).expect("new");
ic.set_patterns([PAT]);
ic
}
fn san_names(pem: &str) -> Vec<String> {
let der = CertificateDer::from_pem_slice(pem.as_bytes()).expect("parse pem");
let (_, x509) = x509_parser::parse_x509_certificate(&der).expect("parse x509");
let san = x509
.subject_alternative_name()
.expect("SAN parses")
.expect("leaf carries a SAN");
san.value
.general_names
.iter()
.filter_map(|n| match n {
GeneralName::DNSName(h) => Some(h.to_string()),
_ => None,
})
.collect()
}
#[test]
fn a_pattern_host_gets_its_own_leaf_at_connect() {
let tmp = tempfile::tempdir().expect("tempdir");
let ic = pattern_interceptor(&tmp.path().join("ca"));
assert!(ic.intercepts("us-east-1.pat.test"));
assert!(ic.intercepts("US-EAST-1.PAT.TEST."));
assert!(
ic.pinned_config("us-east-1.pat.test").is_none(),
"no mint before CONNECT"
);
assert!(ic.ensure_leaf("us-east-1.pat.test"));
let first = ic
.pinned_config("us-east-1.pat.test")
.expect("served after the mint");
assert!(
!ic.is_h2_host("us-east-1.pat.test"),
"a pattern leaf offers http/1.1 only"
);
assert_eq!(first.alpn_protocols, vec![b"http/1.1".to_vec()]);
assert!(ic.ensure_leaf("us-east-1.pat.test."));
let again = ic
.pinned_config("US-EAST-1.pat.test")
.expect("still served");
assert!(
Arc::ptr_eq(&first, &again),
"a cached leaf is reused, not re-minted"
);
assert!(ic.ensure_leaf("a.test"));
assert!(ic.pinned_config("a.test").is_some());
assert_eq!(ic.pattern_mint_capped(), 0);
}
#[test]
fn a_host_outside_the_rules_gets_no_leaf() {
let tmp = tempfile::tempdir().expect("tempdir");
let ic = pattern_interceptor(&tmp.path().join("ca"));
for host in [
"z.test",
"pat.test",
".pat.test",
"a.b.pat.test",
"*.pat.test",
"us-east-1.pat.test.evil",
] {
assert!(!ic.intercepts(host), "{host}");
assert!(!ic.ensure_leaf(host), "{host}");
assert!(ic.pinned_config(host).is_none(), "{host}");
assert!(ic.mint_leaf_pem(host).is_err(), "{host}");
}
assert_eq!(
ic.pattern_mint_capped(),
0,
"an undeclared host is not a capped mint"
);
}
#[test]
fn no_leaf_ever_carries_a_wildcard_name() {
let tmp = tempfile::tempdir().expect("tempdir");
let ic = pattern_interceptor(&tmp.path().join("ca"));
assert!(ic.ca.mint("*.pat.test").is_err());
assert!(ic.ca.mint("*").is_err());
ic.set_hosts(["a.test", "*.wild.test"], None::<&str>)
.expect("a wildcard entry is dropped, not fatal");
assert!(ic.intercepts("a.test"));
assert!(!ic.intercepts("*.wild.test"));
assert!(!ic.intercepts("x.wild.test"));
for host in ["a.test", "us-east-1.pat.test", "x.pat.test"] {
let pem = ic.mint_leaf_pem(host).expect("declared host mints");
let names = san_names(&pem);
assert_eq!(names, vec![host.to_string()]);
assert!(names.iter().all(|n| !n.contains('*')), "{names:?}");
}
}
#[test]
fn mints_past_the_per_minute_cap_tunnel_and_are_counted() {
let tmp = tempfile::tempdir().expect("tempdir");
let ic = pattern_interceptor(&tmp.path().join("ca"));
for i in 0..PATTERN_MINTS_PER_WINDOW {
assert!(ic.ensure_leaf(&format!("h{i}.pat.test")), "mint {i}");
}
assert!(
!ic.ensure_leaf("over.pat.test"),
"the ninth mint in a minute is capped"
);
assert!(ic.pinned_config("over.pat.test").is_none());
assert!(
ic.intercepts("over.pat.test"),
"still declared, just not minted"
);
assert_eq!(ic.pattern_mint_capped(), 1);
assert!(!ic.ensure_leaf("over2.pat.test"));
assert_eq!(ic.pattern_mint_capped(), 2);
assert!(ic.ensure_leaf("h0.pat.test"));
assert_eq!(ic.pattern_mint_capped(), 2);
}
#[test]
fn the_mint_window_rolls() {
let mut cache = PatternLeaves::default();
let t0 = Instant::now();
for _ in 0..PATTERN_MINTS_PER_WINDOW {
assert!(cache.try_reserve_mint(t0));
}
assert!(!cache.try_reserve_mint(t0));
assert!(
!cache.try_reserve_mint(t0 + PATTERN_MINT_WINDOW - std::time::Duration::from_millis(1))
);
let t1 = t0 + PATTERN_MINT_WINDOW;
for _ in 0..PATTERN_MINTS_PER_WINDOW {
assert!(cache.try_reserve_mint(t1));
}
assert!(!cache.try_reserve_mint(t1));
}
#[test]
fn the_pattern_cache_is_a_bounded_lru() {
let leaves: Arc<RwLock<BTreeMap<String, LeafEntry>>> = Arc::default();
let config = server_config_with(&leaves, Some("x.test"), false).expect("config");
let far = OffsetDateTime::now_utc() + Duration::days(LEAF_VALIDITY_DAYS);
let now = OffsetDateTime::now_utc();
let leaf = || PatternLeaf {
config: config.clone(),
not_after: far,
};
let mut cache = PatternLeaves::default();
for i in 0..PATTERN_LEAF_CAP {
cache.insert(format!("h{i}"), leaf());
}
assert_eq!(cache.entries.len(), PATTERN_LEAF_CAP);
assert!(cache.get("h0", now, far).is_some());
cache.insert("new".into(), leaf());
assert_eq!(cache.entries.len(), PATTERN_LEAF_CAP);
assert_eq!(cache.order.len(), PATTERN_LEAF_CAP);
assert!(
cache.get("h0", now, far).is_some(),
"a recently used leaf survives"
);
assert!(
cache.get("h1", now, far).is_none(),
"the least recently used leaf is evicted"
);
assert!(cache.get("new", now, far).is_some());
cache.insert(
"old".into(),
PatternLeaf {
config: config.clone(),
not_after: now + Duration::days(LEAF_REMINT_DAYS - 1),
},
);
assert!(cache.get("old", now, far).is_none());
assert!(!cache.entries.contains_key("old"));
assert!(!cache.order.iter().any(|h| h == "old"));
let ca_end = now + Duration::days(10);
cache.insert(
"clipped".into(),
PatternLeaf {
config: config.clone(),
not_after: ca_end,
},
);
assert!(cache.get("clipped", now, ca_end).is_some());
assert!(cache
.get("clipped", ca_end + Duration::seconds(1), ca_end)
.is_none());
}
fn handshakes(config: Arc<ServerConfig>, dir: &Path, host: &str) -> bool {
use rustls::pki_types::ServerName;
let pem = std::fs::read_to_string(ca_pem_path(dir)).expect("ca pem");
let mut roots = rustls::RootCertStore::empty();
roots
.add(CertificateDer::from_pem_slice(pem.as_bytes()).expect("parse ca"))
.expect("trust the CA");
let client_config = Arc::new(
rustls::ClientConfig::builder_with_provider(Arc::new(
rustls::crypto::ring::default_provider(),
))
.with_safe_default_protocol_versions()
.expect("versions")
.with_root_certificates(roots)
.with_no_client_auth(),
);
let name = ServerName::try_from(host.to_string()).expect("server name");
let mut client = rustls::ClientConnection::new(client_config, name).expect("client");
let mut server = rustls::ServerConnection::new(config).expect("server");
for _ in 0..8 {
let mut buf = Vec::new();
client.write_tls(&mut buf).expect("client write");
server.read_tls(&mut buf.as_slice()).expect("server read");
if server.process_new_packets().is_err() {
return false;
}
let mut buf = Vec::new();
server.write_tls(&mut buf).expect("server write");
client.read_tls(&mut buf.as_slice()).expect("client read");
if client.process_new_packets().is_err() {
return false;
}
if !client.is_handshaking() && !server.is_handshaking() {
return true;
}
}
false
}
#[test]
fn a_connect_config_survives_its_eviction_from_the_lru() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
let ic = pattern_interceptor(&dir);
let held = ic.connect_config("us-east-1.pat.test").expect("minted");
ic.pattern_leaves
.lock()
.unwrap_or_else(|e| e.into_inner())
.retain(|_| false);
assert!(ic.pinned_config("us-east-1.pat.test").is_none());
assert!(handshakes(held, &dir, "us-east-1.pat.test"));
assert!(handshakes(
ic.connect_config("a.test").expect("exact"),
&dir,
"a.test"
));
}
#[test]
fn a_pattern_leaf_under_a_near_expiry_ca_is_served_and_handshakes() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path().join("ca");
let now = OffsetDateTime::now_utc();
LocalCa::generate_with_validity(&dir, now - BACKDATE, now + Duration::days(10))
.expect("a CA ten days from expiry");
let ic = pattern_interceptor(&dir);
let config = ic.connect_config("x.pat.test").expect("minted");
assert!(
ic.pinned_config("x.pat.test").is_some(),
"the clipped leaf stays cached"
);
assert!(ic.ensure_leaf("x.pat.test"));
assert_eq!(ic.pattern_mint_capped(), 0);
assert!(handshakes(config, &dir, "x.pat.test"));
}
#[test]
fn set_patterns_withdraws_cached_leaves() {
let tmp = tempfile::tempdir().expect("tempdir");
let ic = pattern_interceptor(&tmp.path().join("ca"));
assert!(ic.ensure_leaf("x.pat.test"));
ic.set_patterns([]);
assert!(!ic.intercepts("x.pat.test"));
assert!(ic.pinned_config("x.pat.test").is_none());
assert!(!ic.ensure_leaf("x.pat.test"));
assert!(ic.intercepts("a.test"), "exact hosts are untouched");
}
#[tokio::test]
async fn a_pattern_leaf_serves_its_own_host_only() {
let tmp = tempfile::tempdir().expect("tempdir");
let ic = pattern_interceptor(&tmp.path().join("ca"));
let ca_pem = std::fs::read_to_string(ic.ca().pem_path()).expect("read ca.pem");
let ca_der = CertificateDer::from_pem_slice(ca_pem.as_bytes()).expect("parse ca pem");
assert!(ic.ensure_leaf("eu-west-1.pat.test"));
let config = ic.pinned_config("eu-west-1.pat.test").expect("minted");
let (server_ok, client_ok) =
one_handshake(config.clone(), &ca_der, "eu-west-1.pat.test").await;
server_ok.expect("server accepts its own SNI");
client_ok.expect("client trusts the pattern leaf");
let (server_err, client_err) = one_handshake(config, &ca_der, "us-east-1.pat.test").await;
server_err.expect_err("a sibling pattern host is a different leaf");
client_err.expect_err("client sees the refusal");
}
}