mod ca;
mod date;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use rustls::client::WebPkiServerVerifier;
use rustls::client::danger::ServerCertVerifier as _;
use rustls::crypto::CryptoProvider;
use rustls::pki_types::pem::PemObject as _;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer, ServerName, UnixTime};
use rustls::sign::CertifiedKey;
use rustls::{ClientConfig, ClientConnection, RootCertStore, ServerConfig, ServerConnection};
use zeroize::Zeroizing;
pub(crate) use self::date::Day;
pub const RULE_TLS: &str = "proxy-tls";
const HTTP_1_1: &[u8] = b"http/1.1";
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum TlsError {
#[error("no secret sets `tls: terminate`, so there is no host to make a CA for")]
NoHosts,
#[error("the system clock is before 1970 or past the year 9000")]
Clock,
#[error("the CA key is not the ECDSA P-256 key `openmoat proxy ca init` writes")]
Key,
#[error("the CA certificate file holds no PEM certificate")]
Cert,
#[error("{0} is not a name a certificate can carry")]
Host(String),
#[error("the OpenMoat proxy CA does not cover {host} today ({why})")]
Uncovered {
host: String,
why: String,
},
#[error("no trusted root certificates found on this system to verify upstream hosts")]
NoRoots,
#[error("making a certificate: {0}")]
Certificate(String),
#[error("TLS configuration: {0}")]
Config(String),
}
impl TlsError {
fn rcgen(e: &rcgen::Error) -> Self {
Self::Certificate(e.to_string())
}
fn config(e: &rustls::Error) -> Self {
Self::Config(e.to_string())
}
}
pub struct CaFiles {
pub cert_pem: String,
pub key_pem: Zeroizing<String>,
}
impl std::fmt::Debug for CaFiles {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CaFiles").finish_non_exhaustive()
}
}
pub fn new_ca(hosts: &[String]) -> Result<CaFiles, TlsError> {
let made = ca::generate(hosts, Day::today()?)?;
Ok(CaFiles {
cert_pem: made.cert,
key_pem: made.key,
})
}
pub const CA_VALID_DAYS: i64 = ca::CA_DAYS;
#[derive(Debug, Clone)]
pub enum Roots {
System,
Pem(String),
}
pub struct Tls {
provider: Arc<CryptoProvider>,
signer: ca::Signer,
ca: CertificateDer<'static>,
upstream: Arc<ClientConfig>,
leaves: Mutex<HashMap<String, (Day, Arc<ServerConfig>)>>,
}
impl std::fmt::Debug for Tls {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Tls").finish_non_exhaustive()
}
}
impl Tls {
pub fn new(
cert_pem: &str,
key_pem: &str,
roots: &Roots,
hosts: &[String],
) -> Result<Self, TlsError> {
let provider = Arc::new(rustls::crypto::ring::default_provider());
let ca = CertificateDer::from_pem_slice(cert_pem.as_bytes()).map_err(|_| TlsError::Cert)?;
let tls = Self {
signer: ca::Signer::new(key_pem)?,
upstream: Arc::new(upstream_config(roots, &provider)?),
provider,
ca,
leaves: Mutex::new(HashMap::new()),
};
for host in hosts {
tls.check(host)?;
}
Ok(tls)
}
fn check(&self, host: &str) -> Result<(), TlsError> {
let uncovered = |why: String| TlsError::Uncovered {
host: host.to_owned(),
why,
};
let mut roots = RootCertStore::empty();
roots
.add(self.ca.clone())
.map_err(|e| uncovered(e.to_string()))?;
let verifier =
WebPkiServerVerifier::builder_with_provider(Arc::new(roots), self.provider.clone())
.build()
.map_err(|e| uncovered(e.to_string()))?;
let (leaf, _) = self.signer.leaf(host, Day::today()?)?;
let name = server_name(host)?;
verifier
.verify_server_cert(
&CertificateDer::from(leaf),
&[],
&name,
&[],
UnixTime::now(),
)
.map(drop)
.map_err(|e| uncovered(e.to_string()))
}
pub(crate) fn accept(&self, host: &str) -> Result<ServerConnection, TlsError> {
let config = self.server_config(host)?;
ServerConnection::new(config).map_err(|e| TlsError::config(&e))
}
pub(crate) fn connect(&self, host: &str) -> Result<ClientConnection, TlsError> {
ClientConnection::new(self.upstream.clone(), server_name(host)?)
.map_err(|e| TlsError::config(&e))
}
fn server_config(&self, host: &str) -> Result<Arc<ServerConfig>, TlsError> {
let today = Day::today()?;
let mut leaves = self
.leaves
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some((day, config)) = leaves.get(host)
&& *day == today
{
return Ok(config.clone());
}
let (cert, key) = self.signer.leaf(host, today)?;
let key = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(key.to_vec()));
let certified =
CertifiedKey::from_der(vec![CertificateDer::from(cert)], key, &self.provider)
.map_err(|e| TlsError::config(&e))?;
let mut config = ServerConfig::builder_with_provider(self.provider.clone())
.with_safe_default_protocol_versions()
.map_err(|e| TlsError::config(&e))?
.with_no_client_auth()
.with_cert_resolver(Arc::new(rustls::sign::SingleCertAndKey::from(certified)));
config.alpn_protocols = vec![HTTP_1_1.to_vec()];
let config = Arc::new(config);
leaves.insert(host.to_owned(), (today, config.clone()));
Ok(config)
}
}
fn server_name(host: &str) -> Result<ServerName<'static>, TlsError> {
ServerName::try_from(host.to_owned()).map_err(|_| TlsError::Host(host.to_owned()))
}
fn upstream_config(
roots: &Roots,
provider: &Arc<CryptoProvider>,
) -> Result<ClientConfig, TlsError> {
let mut store = RootCertStore::empty();
match roots {
Roots::System => {
let found = rustls_native_certs::load_native_certs();
store.add_parsable_certificates(found.certs);
}
Roots::Pem(pem) => {
let certs = CertificateDer::pem_slice_iter(pem.as_bytes())
.collect::<Result<Vec<_>, _>>()
.map_err(|_| TlsError::Cert)?;
store.add_parsable_certificates(certs);
}
}
if store.is_empty() {
return Err(TlsError::NoRoots);
}
let mut config = ClientConfig::builder_with_provider(provider.clone())
.with_safe_default_protocol_versions()
.map_err(|e| TlsError::config(&e))?
.with_root_certificates(store)
.with_no_client_auth();
config.alpn_protocols = vec![HTTP_1_1.to_vec()];
Ok(config)
}
#[cfg(test)]
mod tests;