use crate::config::{
MtlsConfig, MtlsMode, TlsConfig, TlsListenerConfig, TlsOptions, TlsVersion,
};
use anyhow::Context;
use arc_swap::ArcSwap;
use rustls::RootCertStore;
use rustls::ServerConfig;
use rustls::pki_types::{
CertificateDer, CertificateRevocationListDer, PrivateKeyDer,
PrivatePkcs8KeyDer,
};
use rustls::server::WebPkiClientVerifier;
use rustls::server::danger::ClientCertVerifier;
use rustls_pemfile::{certs, crls, private_key};
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::watch;
use tokio_rustls::TlsAcceptor;
pub struct CertPair {
pub chain: Vec<CertificateDer<'static>>,
pub key: PrivateKeyDer<'static>,
pub alpn_store: Option<crate::cert::acme_alpn::AlpnChallengeStore>,
pub ocsp: Vec<u8>,
}
#[derive(Clone)]
pub struct CertSource {
pub tls: Arc<ArcSwap<TlsAcceptor>>,
pub cert_rx: watch::Receiver<Arc<CertPair>>,
pub cert_tx: Arc<ArcSwap<watch::Sender<Arc<CertPair>>>>,
}
pub struct VhostAlpnMap {
pub default: Arc<rustls::ServerConfig>,
pub by_sni: std::collections::HashMap<String, Arc<rustls::ServerConfig>>,
}
impl VhostAlpnMap {
pub fn pick(&self, sni: Option<&str>) -> Arc<rustls::ServerConfig> {
if let Some(name) = sni
&& let Some(cfg) = self.by_sni.get(name)
{
return cfg.clone();
}
self.default.clone()
}
pub fn build(
pair: &CertPair,
opts: &TlsOptions,
listener_alpn: Option<&[String]>,
vhost_overrides: &[(String, Vec<String>)],
client_verifier: Option<Arc<dyn ClientCertVerifier>>,
) -> anyhow::Result<Self> {
let default = Arc::new(make_rustls_server_config(
pair,
opts,
listener_alpn,
client_verifier.clone(),
)?);
let mut by_sni = std::collections::HashMap::new();
for (sni, alpn) in vhost_overrides {
let cfg = Arc::new(make_rustls_server_config(
pair,
opts,
Some(alpn),
client_verifier.clone(),
)?);
by_sni.insert(sni.clone(), cfg);
}
Ok(VhostAlpnMap { default, by_sni })
}
}
pub fn make_rustls_server_config(
pair: &CertPair,
opts: &TlsOptions,
alpn: Option<&[String]>,
client_verifier: Option<Arc<dyn ClientCertVerifier>>,
) -> anyhow::Result<rustls::ServerConfig> {
let mut provider = rustls::crypto::aws_lc_rs::default_provider();
if !opts.ciphers.is_empty() {
provider.cipher_suites = resolve_ciphers(&opts.ciphers)?;
}
let versions = protocol_versions(opts.min_version);
let builder = ServerConfig::builder_with_provider(Arc::new(provider))
.with_protocol_versions(&versions)
.context("invalid TLS protocol version configuration")?;
let auth_stage = match client_verifier {
Some(v) => builder.with_client_cert_verifier(v),
None => builder.with_no_client_auth(),
};
let mut config = if let Some(store) = pair.alpn_store.as_ref() {
let signing_key =
rustls::crypto::aws_lc_rs::sign::any_supported_type(
&clone_key(&pair.key),
)
.map_err(|e| {
anyhow::anyhow!("loading TLS server key: {e}")
})?;
let mut ck = rustls::sign::CertifiedKey::new(
pair.chain.clone(),
signing_key,
);
if !pair.ocsp.is_empty() {
ck.ocsp = Some(pair.ocsp.clone());
}
let resolver = Arc::new(crate::cert::acme_alpn::AlpnAwareResolver {
store: store.clone(),
production: Arc::new(ck),
});
auth_stage.with_cert_resolver(resolver)
} else {
auth_stage
.with_single_cert_with_ocsp(
pair.chain.clone(),
clone_key(&pair.key),
pair.ocsp.clone(),
)
.context("building TLS ServerConfig")?
};
config.alpn_protocols = alpn
.map(|list| list.iter().map(|s| s.as_bytes().to_vec()).collect())
.unwrap_or_else(|| vec![b"h2".to_vec(), b"http/1.1".to_vec()]);
if pair.alpn_store.is_some()
&& !config
.alpn_protocols
.iter()
.any(|p| p.as_slice() == b"acme-tls/1")
{
config.alpn_protocols.push(b"acme-tls/1".to_vec());
}
Ok(config)
}
pub fn build_client_verifier(
mtls: &MtlsConfig,
) -> anyhow::Result<Arc<dyn ClientCertVerifier>> {
let mut roots = RootCertStore::empty();
for ca_path in &mtls.cas {
let mut rd = BufReader::new(File::open(ca_path).with_context(
|| format!("opening mtls CA file {ca_path}"),
)?);
let parsed: Vec<CertificateDer<'static>> = certs(&mut rd)
.collect::<Result<_, _>>()
.with_context(|| {
format!("reading mtls CA certificates from {ca_path}")
})?;
if parsed.is_empty() {
anyhow::bail!(
"no CA certificates found in mtls CA file {ca_path}"
);
}
let (added, _ignored) = roots.add_parsable_certificates(parsed);
if added == 0 {
anyhow::bail!(
"mtls CA file {ca_path} contained no usable trust anchors"
);
}
}
let crls = load_crls(&mtls.crls)?;
let mut builder = WebPkiClientVerifier::builder(Arc::new(roots));
if !crls.is_empty() {
builder = builder.with_crls(crls);
}
if mtls.mode == MtlsMode::Optional {
builder = builder.allow_unauthenticated();
}
builder
.build()
.context("building rustls WebPkiClientVerifier")
.map(|v| v as Arc<dyn ClientCertVerifier>)
}
pub fn load_crls(
paths: &[String],
) -> anyhow::Result<Vec<CertificateRevocationListDer<'static>>> {
let mut out = Vec::new();
for p in paths {
let mut rd = BufReader::new(File::open(p).with_context(|| {
format!("opening mtls revocation file {p}")
})?);
let mut got = 0;
for crl in crls(&mut rd) {
out.push(crl.with_context(|| {
format!("reading CRL entries from {p}")
})?);
got += 1;
}
if got == 0 {
anyhow::bail!("no CRL entries found in {p}");
}
}
Ok(out)
}
pub fn build_acceptor_with_pair_alpn(
tls: &TlsListenerConfig,
defaults: &TlsOptions,
alpn: Option<&[String]>,
) -> anyhow::Result<(TlsAcceptor, CertPair)> {
let opts = tls.options.resolve(defaults);
let verifier: Option<Arc<dyn ClientCertVerifier>> = None;
match &tls.cert {
TlsConfig::Files { cert, key } => {
let (chain, key) =
load_cert_and_key(Path::new(cert), Path::new(key))?;
let acc = make_acceptor_from_refs_with_alpn(
&chain, &key, &opts, alpn, verifier,
)?;
Ok((
acc,
CertPair {
chain,
key,
alpn_store: None,
ocsp: Vec::new(),
},
))
}
TlsConfig::SelfSigned => {
tracing::warn!(
"using ephemeral self-signed certificate -- \
not suitable for production"
);
let pair = build_self_signed_pair()?;
let acc = make_acceptor_from_refs_with_alpn(
&pair.chain, &pair.key, &opts, alpn, verifier,
)?;
Ok((acc, pair))
}
TlsConfig::Acme { .. } => {
unreachable!("Acme TLS handled by AcmeManager")
}
TlsConfig::Ref(_) => {
unreachable!("TlsConfig::Ref resolved before build_acceptor")
}
}
}
fn build_self_signed_pair() -> anyhow::Result<CertPair> {
use rcgen::{CertifiedKey, generate_simple_self_signed};
let CertifiedKey { cert, signing_key } =
generate_simple_self_signed(vec!["localhost".to_owned()])
.context("generating self-signed certificate")?;
let cert_der = CertificateDer::from(cert.der().to_vec());
let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(
signing_key.serialize_der(),
));
Ok(CertPair {
chain: vec![cert_der],
key: key_der,
alpn_store: None,
ocsp: Vec::new(),
})
}
pub fn make_acceptor_from_refs(
chain: &[CertificateDer<'static>],
key: &PrivateKeyDer<'static>,
opts: &TlsOptions,
) -> anyhow::Result<TlsAcceptor> {
make_acceptor_with_alpn(chain.to_vec(), clone_key(key), opts, None, None)
}
pub fn make_acceptor_from_refs_with_alpn(
chain: &[CertificateDer<'static>],
key: &PrivateKeyDer<'static>,
opts: &TlsOptions,
alpn: Option<&[String]>,
client_verifier: Option<Arc<dyn ClientCertVerifier>>,
) -> anyhow::Result<TlsAcceptor> {
make_acceptor_with_alpn(
chain.to_vec(),
clone_key(key),
opts,
alpn,
client_verifier,
)
}
pub fn clone_key(key: &PrivateKeyDer<'static>) -> PrivateKeyDer<'static> {
use rustls::pki_types::{
PrivatePkcs1KeyDer, PrivatePkcs8KeyDer, PrivateSec1KeyDer,
};
match key {
PrivateKeyDer::Pkcs1(k) => PrivateKeyDer::Pkcs1(
PrivatePkcs1KeyDer::from(k.secret_pkcs1_der().to_vec()),
),
PrivateKeyDer::Pkcs8(k) => PrivateKeyDer::Pkcs8(
PrivatePkcs8KeyDer::from(k.secret_pkcs8_der().to_vec()),
),
PrivateKeyDer::Sec1(k) => PrivateKeyDer::Sec1(
PrivateSec1KeyDer::from(k.secret_sec1_der().to_vec()),
),
_ => unreachable!("unhandled PrivateKeyDer variant"),
}
}
pub fn make_acceptor(
chain: Vec<CertificateDer<'static>>,
key: PrivateKeyDer<'static>,
opts: &TlsOptions,
) -> anyhow::Result<TlsAcceptor> {
make_acceptor_with_alpn(chain, key, opts, None, None)
}
pub fn make_acceptor_with_alpn(
chain: Vec<CertificateDer<'static>>,
key: PrivateKeyDer<'static>,
opts: &TlsOptions,
alpn: Option<&[String]>,
client_verifier: Option<Arc<dyn ClientCertVerifier>>,
) -> anyhow::Result<TlsAcceptor> {
let pair =
CertPair { chain, key, alpn_store: None, ocsp: Vec::new() };
let config =
make_rustls_server_config(&pair, opts, alpn, client_verifier)?;
Ok(TlsAcceptor::from(Arc::new(config)))
}
fn build_quic_rustls_config(
pair: &CertPair,
opts: &TlsOptions,
alpn: Option<&[String]>,
transport: Option<&crate::config::QuicTransport>,
client_verifier: Option<Arc<dyn ClientCertVerifier>>,
) -> anyhow::Result<ServerConfig> {
let mut provider = rustls::crypto::aws_lc_rs::default_provider();
if !opts.ciphers.is_empty() {
provider.cipher_suites = resolve_ciphers(&opts.ciphers)?;
}
let versions = protocol_versions(opts.min_version);
let builder = ServerConfig::builder_with_provider(Arc::new(provider))
.with_protocol_versions(&versions)
.context("invalid TLS protocol version configuration")?;
let mut rustls_cfg = match client_verifier {
Some(v) => builder.with_client_cert_verifier(v),
None => builder.with_no_client_auth(),
}
.with_single_cert_with_ocsp(
pair.chain.clone(),
clone_key(&pair.key),
pair.ocsp.clone(),
)
.context("building rustls ServerConfig for QUIC")?;
rustls_cfg.alpn_protocols = alpn
.map(|list| list.iter().map(|s| s.as_bytes().to_vec()).collect())
.unwrap_or_else(|| vec![b"h3".to_vec()]);
if let Some(t) = transport
&& t.zero_rtt_enabled
{
rustls_cfg.max_early_data_size = u32::MAX;
}
Ok(rustls_cfg)
}
pub fn build_quic_server_config(
pair: &CertPair,
opts: &TlsOptions,
alpn: Option<&[String]>,
transport: Option<&crate::config::QuicTransport>,
client_verifier: Option<Arc<dyn ClientCertVerifier>>,
) -> anyhow::Result<quinn::ServerConfig> {
let rustls_cfg = build_quic_rustls_config(
pair, opts, alpn, transport, client_verifier,
)?;
let quic = quinn::crypto::rustls::QuicServerConfig::try_from(rustls_cfg)
.context("wrapping rustls config as QuicServerConfig")?;
let mut server = quinn::ServerConfig::with_crypto(Arc::new(quic));
if let Some(t) = transport {
let mut tc = quinn::TransportConfig::default();
if let Some(n) = t.max_concurrent_bidi_streams {
tc.max_concurrent_bidi_streams(quinn::VarInt::from_u64(n)
.context("max-concurrent-bidi-streams out of range")?);
}
if let Some(secs) = t.max_idle_timeout_secs {
let dur = std::time::Duration::from_secs(secs);
let it: quinn::IdleTimeout = dur
.try_into()
.context("max-idle-timeout out of range")?;
tc.max_idle_timeout(Some(it));
}
if let Some(secs) = t.keep_alive_interval_secs {
tc.keep_alive_interval(if secs == 0 {
None
} else {
Some(std::time::Duration::from_secs(secs))
});
}
server.transport_config(Arc::new(tc));
if let Some(secs) = t.retry_token_lifetime_secs {
server.retry_token_lifetime(std::time::Duration::from_secs(
secs,
));
}
}
Ok(server)
}
fn protocol_versions(
min: Option<TlsVersion>,
) -> Vec<&'static rustls::SupportedProtocolVersion> {
match min {
None | Some(TlsVersion::Tls12) => {
vec![&rustls::version::TLS12, &rustls::version::TLS13]
}
Some(TlsVersion::Tls13) => vec![&rustls::version::TLS13],
}
}
fn resolve_ciphers(
names: &[String],
) -> anyhow::Result<Vec<rustls::SupportedCipherSuite>> {
let provider = rustls::crypto::aws_lc_rs::default_provider();
names
.iter()
.map(|name| {
let cs = name_to_cipher_suite(name)?;
provider
.cipher_suites
.iter()
.find(|s| s.suite() == cs)
.copied()
.ok_or_else(|| {
anyhow::anyhow!(
"cipher suite '{name}' is not available \
with the current provider"
)
})
})
.collect()
}
fn name_to_cipher_suite(name: &str) -> anyhow::Result<rustls::CipherSuite> {
use rustls::CipherSuite::*;
Ok(match name {
"TLS13_AES_256_GCM_SHA384" => TLS13_AES_256_GCM_SHA384,
"TLS13_AES_128_GCM_SHA256" => TLS13_AES_128_GCM_SHA256,
"TLS13_CHACHA20_POLY1305_SHA256" => TLS13_CHACHA20_POLY1305_SHA256,
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" => {
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
}
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" => {
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
}
"TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" => {
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
}
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" => {
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
}
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" => {
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
}
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" => {
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
}
other => anyhow::bail!("unknown cipher suite '{other}'"),
})
}
pub fn load_cert_and_key(
cert_path: &Path,
key_path: &Path,
) -> anyhow::Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>)> {
let cert_chain: Vec<CertificateDer<'static>> =
certs(&mut BufReader::new(File::open(cert_path).with_context(
|| format!("opening cert file {}", cert_path.display()),
)?))
.collect::<Result<_, _>>()
.context("reading certificate PEM")?;
if cert_chain.is_empty() {
anyhow::bail!("no certificates found in {}", cert_path.display());
}
let key =
private_key(&mut BufReader::new(File::open(key_path).with_context(
|| format!("opening key file {}", key_path.display()),
)?))
.context("reading private key PEM")?
.with_context(|| {
format!("no private key found in {}", key_path.display())
})?;
Ok((cert_chain, key))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::TlsVersion;
fn install_provider() {
rustls::crypto::aws_lc_rs::default_provider()
.install_default()
.ok();
}
#[test]
fn known_tls13_cipher_names_parse() {
for name in &[
"TLS13_AES_256_GCM_SHA384",
"TLS13_AES_128_GCM_SHA256",
"TLS13_CHACHA20_POLY1305_SHA256",
] {
assert!(name_to_cipher_suite(name).is_ok(), "{name} should parse");
}
}
#[test]
fn known_tls12_cipher_names_parse() {
for name in &[
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
] {
assert!(name_to_cipher_suite(name).is_ok(), "{name} should parse");
}
}
#[test]
fn unknown_cipher_name_is_error() {
assert!(name_to_cipher_suite("RC4_MD5").is_err());
assert!(name_to_cipher_suite("").is_err());
}
#[test]
fn no_min_version_includes_tls12_and_tls13() {
let versions = protocol_versions(None);
assert_eq!(versions.len(), 2);
}
#[test]
fn min_tls12_includes_both_versions() {
let versions = protocol_versions(Some(TlsVersion::Tls12));
assert_eq!(versions.len(), 2);
}
#[test]
fn min_tls13_includes_only_tls13() {
let versions = protocol_versions(Some(TlsVersion::Tls13));
assert_eq!(versions.len(), 1);
assert_eq!(versions[0].version, rustls::ProtocolVersion::TLSv1_3);
}
#[tokio::test]
async fn self_signed_acceptor_builds_without_error() {
install_provider();
let opts = TlsOptions::default();
let pair = build_self_signed_pair().unwrap();
let result =
make_acceptor_from_refs(&pair.chain, &pair.key, &opts);
assert!(result.is_ok());
}
#[tokio::test]
async fn quic_zero_rtt_uses_rfc9001_sentinel_value() {
install_provider();
let pair = build_self_signed_pair().unwrap();
let opts = TlsOptions::default();
let transport = crate::config::QuicTransport {
zero_rtt_enabled: true,
..Default::default()
};
let cfg =
build_quic_rustls_config(&pair, &opts, None, Some(&transport), None)
.unwrap();
assert_eq!(cfg.max_early_data_size, u32::MAX);
}
#[tokio::test]
async fn quic_zero_rtt_disabled_leaves_default_zero() {
install_provider();
let pair = build_self_signed_pair().unwrap();
let opts = TlsOptions::default();
let transport = crate::config::QuicTransport::default();
let cfg =
build_quic_rustls_config(&pair, &opts, None, Some(&transport), None)
.unwrap();
assert_eq!(cfg.max_early_data_size, 0);
let cfg_none =
build_quic_rustls_config(&pair, &opts, None, None, None).unwrap();
assert_eq!(cfg_none.max_early_data_size, 0);
}
#[tokio::test]
async fn vhost_alpn_map_picks_per_sni() {
install_provider();
let pair = build_self_signed_pair().unwrap();
let opts = TlsOptions::default();
let overrides = vec![(
"alpha.example.com".to_string(),
vec!["http/1.1".to_string()],
)];
let map = VhostAlpnMap::build(
&pair,
&opts,
Some(&["h2".to_string(), "http/1.1".to_string()]),
&overrides,
None,
)
.unwrap();
let picked = map.pick(Some("alpha.example.com"));
assert_eq!(picked.alpn_protocols, vec![b"http/1.1".to_vec()]);
let fallback = map.pick(Some("beta.example.com"));
assert_eq!(
fallback.alpn_protocols,
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
);
let no_sni = map.pick(None);
assert_eq!(
no_sni.alpn_protocols,
vec![b"h2".to_vec(), b"http/1.1".to_vec()]
);
}
use crate::config::{MtlsConfig, MtlsMode};
fn write_tmp(pem: &str, suffix: &str) -> std::path::PathBuf {
let path = std::env::temp_dir().join(format!(
"hypershunt-mtls-{}-{}.pem",
std::process::id(),
suffix
));
std::fs::write(&path, pem).unwrap();
path
}
fn make_ca_pem() -> String {
use rcgen::{
CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa,
KeyPair, KeyUsagePurpose,
};
let mut params = CertificateParams::new(Vec::<String>::new()).unwrap();
params.is_ca = IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
params
.distinguished_name
.push(DnType::CommonName, "hypershunt test CA");
params.key_usages.push(KeyUsagePurpose::KeyCertSign);
params.key_usages.push(KeyUsagePurpose::CrlSign);
params
.extended_key_usages
.push(ExtendedKeyUsagePurpose::ClientAuth);
let kp = KeyPair::generate().unwrap();
params.self_signed(&kp).unwrap().pem()
}
#[test]
fn build_client_verifier_required_loads_ca() {
install_provider();
let ca = make_ca_pem();
let ca_path = write_tmp(&ca, "ca-required");
let mtls = MtlsConfig {
cas: vec![ca_path.to_string_lossy().into_owned()],
mode: MtlsMode::Required,
crls: vec![],
crl_refresh_secs: 0,
};
let v = build_client_verifier(&mtls).unwrap();
assert!(v.client_auth_mandatory());
assert!(!v.root_hint_subjects().is_empty());
let _ = std::fs::remove_file(ca_path);
}
#[test]
fn build_client_verifier_optional_allows_anon() {
install_provider();
let ca = make_ca_pem();
let ca_path = write_tmp(&ca, "ca-optional");
let mtls = MtlsConfig {
cas: vec![ca_path.to_string_lossy().into_owned()],
mode: MtlsMode::Optional,
crls: vec![],
crl_refresh_secs: 0,
};
let v = build_client_verifier(&mtls).unwrap();
assert!(!v.client_auth_mandatory());
let _ = std::fs::remove_file(ca_path);
}
#[test]
fn build_client_verifier_rejects_missing_ca_file() {
install_provider();
let mtls = MtlsConfig {
cas: vec!["/no/such/path-hypershunt-mtls-test.pem".to_string()],
mode: MtlsMode::Required,
crls: vec![],
crl_refresh_secs: 0,
};
let err = build_client_verifier(&mtls).unwrap_err().to_string();
assert!(err.contains("mtls CA file"), "got: {err}");
}
#[test]
fn build_client_verifier_rejects_empty_ca_pem() {
install_provider();
let ca_path = write_tmp("", "ca-empty");
let mtls = MtlsConfig {
cas: vec![ca_path.to_string_lossy().into_owned()],
mode: MtlsMode::Required,
crls: vec![],
crl_refresh_secs: 0,
};
let err = build_client_verifier(&mtls).unwrap_err().to_string();
assert!(err.contains("no CA certificates"), "got: {err}");
let _ = std::fs::remove_file(ca_path);
}
#[tokio::test]
async fn missing_client_cert_classifies_as_bad_client_cert() {
use tokio_rustls::TlsConnector;
install_provider();
let ca_path = write_tmp(&make_ca_pem(), "ca-loopback");
let mtls = MtlsConfig {
cas: vec![ca_path.to_string_lossy().into_owned()],
mode: MtlsMode::Required,
crls: vec![],
crl_refresh_secs: 0,
};
let verifier = build_client_verifier(&mtls).unwrap();
let pair = build_self_signed_pair().unwrap();
let server_cfg = make_rustls_server_config(
&pair,
&TlsOptions::default(),
None,
Some(verifier),
)
.unwrap();
let acceptor = TlsAcceptor::from(Arc::new(server_cfg));
#[derive(Debug)]
struct NoVerify;
impl rustls::client::danger::ServerCertVerifier for NoVerify {
fn verify_server_cert(
&self,
_: &rustls::pki_types::CertificateDer<'_>,
_: &[rustls::pki_types::CertificateDer<'_>],
_: &rustls::pki_types::ServerName<'_>,
_: &[u8],
_: rustls::pki_types::UnixTime,
) -> Result<
rustls::client::danger::ServerCertVerified,
rustls::Error,
> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_: &[u8],
_: &rustls::pki_types::CertificateDer<'_>,
_: &rustls::DigitallySignedStruct,
) -> Result<
rustls::client::danger::HandshakeSignatureValid,
rustls::Error,
> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_: &[u8],
_: &rustls::pki_types::CertificateDer<'_>,
_: &rustls::DigitallySignedStruct,
) -> Result<
rustls::client::danger::HandshakeSignatureValid,
rustls::Error,
> {
Ok(rustls::client::danger::HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(
&self,
) -> Vec<rustls::SignatureScheme> {
rustls::crypto::aws_lc_rs::default_provider()
.signature_verification_algorithms
.supported_schemes()
}
}
let client_cfg = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(NoVerify))
.with_no_client_auth();
let connector = TlsConnector::from(Arc::new(client_cfg));
let (client_io, server_io) = tokio::io::duplex(16 * 1024);
let server = tokio::spawn(async move { acceptor.accept(server_io).await });
let client = tokio::spawn(async move {
let name = rustls::pki_types::ServerName::try_from("localhost")
.unwrap();
let _ = connector.connect(name, client_io).await;
});
let server_err = server
.await
.unwrap()
.expect_err("required mTLS must reject a client with no cert");
let _ = client.await;
assert_eq!(
crate::security::client_cert_rejection(&server_err),
Some("no-cert"),
"server handshake error did not classify as a client-cert rejection"
);
let _ = std::fs::remove_file(ca_path);
}
}