use super::{ProtocolVersion, TlsOpts, VerifyCallback};
use crate::error::{Error, Result};
#[derive(Clone)]
pub(crate) struct TlsSettings {
pub(crate) verify: bool,
pub(crate) ca_bundle: Option<String>,
pub(crate) ca_path: Option<String>,
pub(crate) client_cert: Option<String>,
pub(crate) client_key: Option<String>,
pub(crate) client_key_pass: Option<String>,
pub(crate) cert_is_der: bool,
pub(crate) key_is_der: bool,
pub(crate) pinned_pubkey: Option<String>,
pub(crate) crl_file: Option<String>,
pub(crate) ciphers: Option<String>,
pub(crate) tls13_ciphers: Option<String>,
pub(crate) min_version: Option<ProtocolVersion>,
pub(crate) max_version: Option<ProtocolVersion>,
pub(crate) verify_callback: Option<VerifyCallback>,
}
impl Default for TlsSettings {
fn default() -> Self {
TlsSettings {
verify: true,
ca_bundle: None,
ca_path: None,
client_cert: None,
client_key: None,
client_key_pass: None,
cert_is_der: false,
key_is_der: false,
pinned_pubkey: None,
crl_file: None,
ciphers: None,
tls13_ciphers: None,
min_version: None,
max_version: None,
verify_callback: None,
}
}
}
impl std::fmt::Debug for TlsSettings {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TlsSettings")
.field("verify", &self.verify)
.field("ca_bundle", &self.ca_bundle)
.field("ca_path", &self.ca_path)
.field("client_cert", &self.client_cert)
.field("client_key", &self.client_key)
.field(
"client_key_pass",
&self.client_key_pass.as_ref().map(|_| "<redacted>"),
)
.field("cert_is_der", &self.cert_is_der)
.field("key_is_der", &self.key_is_der)
.field("pinned_pubkey", &self.pinned_pubkey)
.field("crl_file", &self.crl_file)
.field("ciphers", &self.ciphers)
.field("tls13_ciphers", &self.tls13_ciphers)
.field("min_version", &self.min_version)
.field("max_version", &self.max_version)
.field("verify_callback", &self.verify_callback)
.finish()
}
}
macro_rules! proxy_tls_builder_methods {
() => {
pub fn proxy_verify_tls(mut self, on: bool) -> Self {
self.proxy_tls.verify = on;
self
}
pub fn proxy_ca_bundle(mut self, path: &str) -> Self {
self.proxy_tls.ca_bundle = Some(path.to_string());
self
}
pub fn proxy_ca_path(mut self, dir: &str) -> Self {
self.proxy_tls.ca_path = Some(dir.to_string());
self
}
pub fn proxy_crl_file(mut self, path: &str) -> Self {
self.proxy_tls.crl_file = Some(path.to_string());
self
}
pub fn proxy_client_cert(mut self, path: &str) -> Self {
self.proxy_tls.client_cert = Some(path.to_string());
self
}
pub fn proxy_client_key(mut self, path: &str) -> Self {
self.proxy_tls.client_key = Some(path.to_string());
self
}
pub fn proxy_client_key_pass(mut self, pass: &str) -> Self {
self.proxy_tls.client_key_pass = Some(pass.to_string());
self
}
pub fn proxy_cert_type_der(mut self, der: bool) -> Self {
self.proxy_tls.cert_is_der = der;
self
}
pub fn proxy_key_type_der(mut self, der: bool) -> Self {
self.proxy_tls.key_is_der = der;
self
}
pub fn proxy_pinned_pubkey(mut self, spec: &str) -> Self {
self.proxy_tls.pinned_pubkey = Some(spec.to_string());
self
}
pub fn proxy_ciphers(mut self, list: &str) -> Self {
self.proxy_tls.ciphers = Some(list.to_string());
self
}
pub fn proxy_tls13_ciphers(mut self, list: &str) -> Self {
self.proxy_tls.tls13_ciphers = Some(list.to_string());
self
}
pub fn proxy_tls_min_version(mut self, v: crate::tls::ProtocolVersion) -> Self {
self.proxy_tls.min_version = Some(v);
self
}
pub fn proxy_tls_max_version(mut self, v: crate::tls::ProtocolVersion) -> Self {
self.proxy_tls.max_version = Some(v);
self
}
};
}
pub(crate) use proxy_tls_builder_methods;
impl TlsSettings {
pub(crate) fn to_opts(&self, alpn: &[&[u8]]) -> Result<TlsOpts> {
let mut opts = TlsOpts::verifying();
opts.alpn = alpn.iter().map(|p| p.to_vec()).collect();
opts.verify = self.verify;
opts.min_version = self.min_version;
opts.max_version = self.max_version;
if let Some(path) = &self.ca_bundle {
opts.roots = Some(super::load_roots_from_file(path)?);
}
if let Some(dir) = &self.ca_path {
opts.roots = Some(super::load_roots_from_dir(opts.roots.take(), dir)?);
}
if let Some(cert_path) = &self.client_cert {
opts.client_cert = Some(std::fs::read(cert_path).map_err(Error::Io)?);
opts.cert_is_der = self.cert_is_der;
opts.key_is_der = self.key_is_der;
opts.client_key_pass = self.client_key_pass.clone();
if let Some(key_path) = &self.client_key {
opts.client_key = Some(std::fs::read(key_path).map_err(Error::Io)?);
}
}
if let Some(spec) = &self.pinned_pubkey {
opts.pinned_spki_sha256 = super::parse_pinned_pubkey(spec)?;
}
if let Some(path) = &self.crl_file {
opts.crl_pem = Some(std::fs::read(path).map_err(Error::Io)?);
}
if let Some(spec) = &self.ciphers {
opts.cipher_suites.extend(super::cipher_names_to_ids(spec)?);
}
if let Some(spec) = &self.tls13_ciphers {
opts.cipher_suites.extend(super::cipher_names_to_ids(spec)?);
}
opts.verify_callback = self.verify_callback.clone();
Ok(opts)
}
pub(crate) fn connect<S: std::io::Read + std::io::Write>(
&self,
transport: S,
host: &str,
) -> Result<super::TlsStream<S>> {
super::connect_over_tls(transport, host, self.to_opts(&[])?)
}
pub(crate) fn connect_resuming<S: std::io::Read + std::io::Write>(
&self,
transport: S,
host: &str,
session: &super::TlsSessionCache,
) -> Result<super::TlsStream<S>> {
let mut opts = self.to_opts(&[])?;
opts.session_cache = Some(session.clone());
super::connect_over_tls(transport, host, opts)
}
pub(crate) fn verify_peer(
&self,
chain: &[Vec<u8>],
server_name: &str,
pins: &[[u8; 32]],
) -> Result<()> {
verify_peer_chain(
chain,
server_name,
self.verify,
self.verify_callback.as_ref(),
pins,
)
}
}
pub(crate) fn verify_peer_chain(
chain: &[Vec<u8>],
server_name: &str,
verify: bool,
callback: Option<&VerifyCallback>,
pins: &[[u8; 32]],
) -> Result<()> {
use super::client_auth::{enforce_pins_then_callback, leaf_has_san, PostHandshakeDecision};
let leaf = chain.first().map(Vec::as_slice);
if enforce_pins_then_callback(leaf, pins, server_name, chain, callback)?
== PostHandshakeDecision::CallbackAccepted
{
return Ok(());
}
if verify && leaf.is_some_and(|der| !leaf_has_san(der)) {
return Err(Error::BadResponse(
"server certificate has no Subject Alternative Name \
(CN fallback is not accepted)"
.into(),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::TlsSettings;
#[test]
fn default_matches_verifying_opts() {
let o = TlsSettings::default().to_opts(&[b"h2"]).unwrap();
assert!(o.verify);
assert!(o.roots.is_none());
assert!(o.pinned_spki_sha256.is_empty());
assert_eq!(o.alpn, vec![b"h2".to_vec()]);
}
#[test]
fn insecure_and_bad_pin_are_carried() {
let s = TlsSettings {
verify: false,
..TlsSettings::default()
};
assert!(!s.to_opts(&[]).unwrap().verify);
let s = TlsSettings {
pinned_pubkey: Some("not-a-pin".into()),
..TlsSettings::default()
};
assert!(s.to_opts(&[]).is_err(), "malformed pin must fail closed");
}
#[test]
fn missing_ca_file_is_an_error_before_connecting() {
let s = TlsSettings {
ca_bundle: Some("/nonexistent/rsurl-ca.pem".into()),
..TlsSettings::default()
};
assert!(s.to_opts(&[]).is_err());
}
}