use rcgen::{BasicConstraints, CertificateParams, CertificateSigningRequestParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, SanType};
use rustls::pki_types::pem::PemObject;
use rustls::pki_types::{CertificateDer, PrivateKeyDer, ServerName};
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;
use std::time::Duration;
pub const BOOTSTRAP_CN: &str = "bootstrap";
pub const INSTALLER_CN: &str = "installer";
pub const VM_SERVER_NAME: &str = "bootstrap.installer";
fn vm_sans() -> Vec<SanType> {
match VM_SERVER_NAME.to_string().try_into() {
Ok(n) => vec![SanType::DnsName(n)],
Err(_) => Vec::new(),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MtlsError(pub String);
impl std::fmt::Display for MtlsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "REFUSED [pairing-mtls] {}", self.0)
}
}
impl std::error::Error for MtlsError {}
fn e(what: &str) -> impl Fn(rcgen::Error) -> MtlsError + '_ {
move |err| MtlsError(format!("{what}: {err}"))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Issued {
pub cert_pem: String,
pub key_pem: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InstallerCa {
pub cert_pem: String,
pub key_pem: String,
}
fn window(valid_for: Duration) -> (time::OffsetDateTime, time::OffsetDateTime) {
window_back(valid_for, time::Duration::minutes(5))
}
fn window_back(valid_for: Duration, back: time::Duration) -> (time::OffsetDateTime, time::OffsetDateTime) {
let now = time::OffsetDateTime::now_utc();
(now - back, now + time::Duration::seconds(valid_for.as_secs() as i64))
}
pub const DOOR_BACKDATE: Duration = Duration::from_secs(24 * 3600);
pub fn cert_left(pem: &str, now_unix: i64) -> Result<Duration, MtlsError> {
let (_, p) = x509_parser::pem::parse_x509_pem(pem.trim().as_bytes()).map_err(|err| MtlsError(format!("not a PEM certificate: {err}")))?;
let cert = p.parse_x509().map_err(|err| MtlsError(format!("not an X.509 certificate: {err}")))?;
let end = cert.validity().not_after.timestamp();
Ok(Duration::from_secs(end.saturating_sub(now_unix).max(0) as u64))
}
pub fn now_unix() -> i64 {
time::OffsetDateTime::now_utc().unix_timestamp()
}
impl InstallerCa {
pub fn mint(estate: &str, valid_for: Duration) -> Result<InstallerCa, MtlsError> {
let key = KeyPair::generate().map_err(e("the CA key"))?;
let mut p = CertificateParams::default();
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, format!("nordisk-pairing installer CA — {estate}"));
p.distinguished_name = dn;
p.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
p.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::DigitalSignature];
(p.not_before, p.not_after) = window(valid_for);
let cert = p.self_signed(&key).map_err(e("the CA certificate"))?;
Ok(InstallerCa { cert_pem: cert.pem(), key_pem: key.serialize_pem() })
}
fn issuer(&self) -> Result<(rcgen::Certificate, KeyPair), MtlsError> {
let key = KeyPair::from_pem(&self.key_pem).map_err(e("the CA key"))?;
let params = CertificateParams::from_ca_cert_pem(&self.cert_pem).map_err(e("the CA certificate"))?;
let cert = params.self_signed(&key).map_err(e("the CA as issuer"))?;
Ok((cert, key))
}
fn leaf(&self, cn: &str, sans: Vec<SanType>, purpose: Vec<ExtendedKeyUsagePurpose>, valid_for: Duration) -> Result<Issued, MtlsError> {
self.leaf_back(cn, sans, purpose, window(valid_for))
}
fn leaf_back(&self, cn: &str, sans: Vec<SanType>, purpose: Vec<ExtendedKeyUsagePurpose>, validity: (time::OffsetDateTime, time::OffsetDateTime)) -> Result<Issued, MtlsError> {
let (ca, ca_key) = self.issuer()?;
let key = KeyPair::generate().map_err(e("a leaf key"))?;
let mut p = CertificateParams::default();
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, cn);
p.distinguished_name = dn;
p.subject_alt_names = sans;
p.extended_key_usages = purpose;
(p.not_before, p.not_after) = validity;
let cert = p.signed_by(&key, &ca, &ca_key).map_err(e("a leaf certificate"))?;
Ok(Issued { cert_pem: cert.pem(), key_pem: key.serialize_pem() })
}
pub fn issue_door(&self, utility: Ipv4Addr, valid_for: Duration) -> Result<Issued, MtlsError> {
let back = time::Duration::seconds(DOOR_BACKDATE.as_secs() as i64);
self.leaf_back("nordisk-pairing door", vec![SanType::IpAddress(IpAddr::V4(utility))], vec![ExtendedKeyUsagePurpose::ServerAuth], window_back(valid_for, back))
}
pub fn issue_bootstrap(&self, valid_for: Duration) -> Result<Issued, MtlsError> {
self.leaf(BOOTSTRAP_CN, vm_sans(), vec![ExtendedKeyUsagePurpose::ServerAuth, ExtendedKeyUsagePurpose::ClientAuth], valid_for)
}
pub fn issue_installer(&self, valid_for: Duration) -> Result<Issued, MtlsError> {
let back = time::Duration::seconds(DOOR_BACKDATE.as_secs() as i64);
self.leaf_back(INSTALLER_CN, Vec::new(), vec![ExtendedKeyUsagePurpose::ClientAuth], window_back(valid_for, back))
}
pub fn sign_box(&self, csr_pem: &str, name: &str, valid_for: Duration) -> Result<String, MtlsError> {
if name == BOOTSTRAP_CN || name == INSTALLER_CN {
return Err(MtlsError(format!("`{name}` is not a box name")));
}
let (ca, ca_key) = self.issuer()?;
let mut csr = CertificateSigningRequestParams::from_pem(csr_pem).map_err(e("the box's signing request"))?;
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, name);
csr.params.distinguished_name = dn;
csr.params.subject_alt_names = vm_sans();
csr.params.is_ca = IsCa::NoCa;
csr.params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth, ExtendedKeyUsagePurpose::ClientAuth];
(csr.params.not_before, csr.params.not_after) = window(valid_for);
Ok(csr.signed_by(&ca, &ca_key).map_err(e("the box's certificate"))?.pem())
}
}
pub fn box_request() -> Result<(String, String), MtlsError> {
let key = KeyPair::generate().map_err(e("the box key"))?;
request_for(key)
}
pub fn box_request_for(key_pem: &str) -> Result<(String, String), MtlsError> {
request_for(KeyPair::from_pem(key_pem).map_err(e("the box key"))?)
}
fn request_for(key: KeyPair) -> Result<(String, String), MtlsError> {
let mut p = CertificateParams::default();
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, "box");
p.distinguished_name = dn;
let csr = p.serialize_request(&key).map_err(e("the signing request"))?;
Ok((csr.pem().map_err(e("the signing request as PEM"))?, key.serialize_pem()))
}
fn certs(pem: &str) -> Result<Vec<CertificateDer<'static>>, MtlsError> {
let v: Vec<_> = CertificateDer::pem_slice_iter(pem.as_bytes()).collect::<Result<_, _>>().map_err(|err| MtlsError(format!("a certificate PEM: {err:?}")))?;
if v.is_empty() {
return Err(MtlsError("a certificate PEM with no certificate in it".into()));
}
Ok(v)
}
fn key(pem: &str) -> Result<PrivateKeyDer<'static>, MtlsError> {
PrivateKeyDer::from_pem_slice(pem.as_bytes()).map_err(|err| MtlsError(format!("a private key PEM: {err:?}")))
}
fn provider() -> Arc<rustls::crypto::CryptoProvider> {
Arc::new(rustls::crypto::ring::default_provider())
}
fn roots(ca_pem: &str) -> Result<rustls::RootCertStore, MtlsError> {
let mut r = rustls::RootCertStore::empty();
for c in certs(ca_pem)? {
r.add(c).map_err(|err| MtlsError(format!("the installer CA as a root: {err}")))?;
}
Ok(r)
}
pub fn server_config(ca_pem: &str, door: &Issued) -> Result<Arc<rustls::ServerConfig>, MtlsError> {
let verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(Arc::new(roots(ca_pem)?), provider())
.build()
.map_err(|err| MtlsError(format!("the client verifier: {err}")))?;
let cfg = rustls::ServerConfig::builder_with_provider(provider())
.with_protocol_versions(&[&rustls::version::TLS13])
.map_err(|err| MtlsError(format!("TLS 1.3: {err}")))?
.with_client_cert_verifier(verifier)
.with_single_cert(certs(&door.cert_pem)?, key(&door.key_pem)?)
.map_err(|err| MtlsError(format!("the door's certificate: {err}")))?;
Ok(Arc::new(cfg))
}
pub fn client_config(ca_pem: &str, client: &Issued) -> Result<Arc<rustls::ClientConfig>, MtlsError> {
let cfg = rustls::ClientConfig::builder_with_provider(provider())
.with_protocol_versions(&[&rustls::version::TLS13])
.map_err(|err| MtlsError(format!("TLS 1.3: {err}")))?
.with_root_certificates(roots(ca_pem)?)
.with_client_auth_cert(certs(&client.cert_pem)?, key(&client.key_pem)?)
.map_err(|err| MtlsError(format!("the client certificate: {err}")))?;
Ok(Arc::new(cfg))
}
pub fn door_name(utility: Ipv4Addr) -> ServerName<'static> {
ServerName::IpAddress(IpAddr::V4(utility).into())
}
pub fn peer_cn(conn: &rustls::ServerConnection) -> Option<String> {
cn_of(conn.peer_certificates()?.first()?)
}
pub fn vm_name() -> ServerName<'static> {
ServerName::try_from(VM_SERVER_NAME).expect("VM_SERVER_NAME is a valid DNS name")
}
pub fn server_cn(conn: &rustls::ClientConnection) -> Option<String> {
cn_of(conn.peer_certificates()?.first()?)
}
fn cn_of(der: &CertificateDer<'_>) -> Option<String> {
let (_, cert) = x509_parser::parse_x509_certificate(der.as_ref()).ok()?;
let cn = cert.subject().iter_common_name().next()?.as_str().ok()?.to_string();
Some(cn)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::{Read, Write};
const H: Duration = Duration::from_secs(3600);
#[test]
fn a_box_joins_with_the_bootstrap_cert_then_speaks_as_itself_and_strangers_are_refused_both_ways() {
let ca = InstallerCa::mint("fleet.test", H).unwrap();
let door = ca.issue_door(Ipv4Addr::LOCALHOST, H).unwrap();
let boot = ca.issue_bootstrap(H).unwrap();
let (csr, box_key) = box_request().unwrap();
let mine = Issued { cert_pem: ca.sign_box(&csr, "main", H).unwrap(), key_pem: box_key };
let other = InstallerCa::mint("another", H).unwrap();
let stranger = other.issue_bootstrap(H).unwrap();
let serve = |client: Issued, pin: String| -> Result<String, String> {
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let addr = l.local_addr().unwrap();
let scfg = server_config(&ca.cert_pem, &door).unwrap();
let t = std::thread::spawn(move || {
let (s, _) = l.accept().unwrap();
let conn = rustls::ServerConnection::new(scfg).unwrap();
let mut tls = rustls::StreamOwned::new(conn, s);
let mut b = [0u8; 5];
match tls.read_exact(&mut b) {
Ok(()) => {
let cn = peer_cn(&tls.conn).unwrap_or_default();
let _ = tls.write_all(cn.as_bytes());
let _ = tls.conn.send_close_notify();
let _ = tls.flush();
Ok(cn)
}
Err(err) => Err(err.to_string()),
}
});
let ccfg = client_config(&pin, &client).map_err(|e| e.to_string())?;
let s = std::net::TcpStream::connect(addr).unwrap();
let conn = rustls::ClientConnection::new(ccfg, door_name(Ipv4Addr::LOCALHOST)).unwrap();
let mut tls = rustls::StreamOwned::new(conn, s);
let sent = tls.write_all(b"hello").and_then(|_| tls.flush());
let mut back = String::new();
let read = tls.read_to_string(&mut back);
let server = t.join().unwrap();
match (sent, read, server) {
(Ok(()), Ok(_), Ok(cn)) if back == cn => Ok(cn),
(s, r, sv) => Err(format!("sent {s:?} read {r:?} server {sv:?}")),
}
};
assert_eq!(serve(boot.clone(), ca.cert_pem.clone()), Ok(BOOTSTRAP_CN.to_string()));
assert_eq!(serve(mine, ca.cert_pem.clone()), Ok("main".to_string()), "the box speaks as the name the door gave it");
assert!(serve(stranger, ca.cert_pem.clone()).is_err(), "a certificate from another CA is refused by the door");
assert!(serve(boot, other.cert_pem.clone()).is_err(), "a box that pins the installer CA refuses a door it did not issue");
}
#[test]
fn the_door_certificate_is_back_dated_a_day_and_what_a_certificate_has_left_is_read_off_it() {
let ca = InstallerCa::mint("fleet.test", 90 * 24 * H).unwrap();
let now = now_unix();
let nb = |pem: &str| {
let der = certs(pem).unwrap().remove(0);
let (_, c) = x509_parser::parse_x509_certificate(der.as_ref()).unwrap();
c.validity().not_before.timestamp()
};
let door = ca.issue_door(Ipv4Addr::LOCALHOST, H).unwrap();
let back = now - nb(&door.cert_pem);
assert!((24 * 3600 - 60..=24 * 3600 + 60).contains(&back), "the door is back-dated a day, got {back} s");
let boot = ca.issue_bootstrap(H).unwrap();
assert!(now - nb(&boot.cert_pem) <= 5 * 60 + 60, "the bootstrap certificate keeps five minutes");
let left = cert_left(&ca.cert_pem, now).unwrap();
assert!(left > 89 * 24 * H && left <= 90 * 24 * H, "{left:?}");
assert_eq!(cert_left(&ca.cert_pem, now + 91 * 24 * 3600).unwrap(), Duration::ZERO, "ended");
assert!(cert_left("not a pem", now).is_err());
}
#[test]
fn the_box_request_is_signed_with_the_door_given_name_and_bootstrap_is_not_a_name() {
let ca = InstallerCa::mint("fleet.test", H).unwrap();
let (csr, _) = box_request().unwrap();
let pem = ca.sign_box(&csr, "twin", H).unwrap();
let der = certs(&pem).unwrap().remove(0);
let (_, cert) = x509_parser::parse_x509_certificate(der.as_ref()).unwrap();
assert_eq!(cert.subject().iter_common_name().next().unwrap().as_str().unwrap(), "twin");
assert!(ca.sign_box(&csr, BOOTSTRAP_CN, H).is_err());
assert!(ca.sign_box("not a csr", "twin", H).is_err());
}
}