use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use rcgen::{
BasicConstraints, CertificateParams, DnType, ExtendedKeyUsagePurpose, IsCa, Issuer, KeyPair,
KeyUsagePurpose, SanType,
};
use rustls::pki_types::pem::PemObject;
use rustls::pki_types::{CertificateDer, PrivateKeyDer};
use crate::error::{Error, Result};
const CA_CN: &str = "isb control plane CA";
pub const CLIENT_CN: &str = "isb-control-plane";
const CA_YEARS: i64 = 10;
pub const LEAF_DAYS: i64 = 397;
fn err(step: &str, e: impl std::fmt::Display) -> Error {
Error::invalid(format!("servers TLS: {step}: {e}"))
}
fn ca_params() -> CertificateParams {
let mut p = CertificateParams::default();
p.is_ca = IsCa::Ca(BasicConstraints::Constrained(0));
p.distinguished_name = rcgen::DistinguishedName::new();
p.distinguished_name.push(DnType::CommonName, CA_CN);
p.key_usages = vec![
KeyUsagePurpose::KeyCertSign,
KeyUsagePurpose::CrlSign,
KeyUsagePurpose::DigitalSignature,
];
p
}
#[doc(hidden)]
pub fn write_private(path: &Path, text: &str) -> Result<()> {
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let tmp = path.with_extension("tmp");
let mut f = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&tmp)?;
f.write_all(text.as_bytes())?;
f.sync_all()?;
std::fs::rename(&tmp, path)?;
Ok(())
}
#[derive(Debug, Clone)]
pub struct Leaf {
pub cert: String,
pub key: String,
}
impl Leaf {
pub fn fingerprint(&self) -> Result<String> {
fingerprint_pem(&self.cert)
}
}
pub fn fingerprint_pem(pem: &str) -> Result<String> {
let der = CertificateDer::from_pem_slice(pem.as_bytes()).map_err(|e| err("read a cert", e))?;
Ok(hex(ring::digest::digest(
&ring::digest::SHA256,
der.as_ref(),
)
.as_ref()))
}
pub(crate) fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
pub struct Ca {
dir: PathBuf,
key: KeyPair,
pub cert_pem: String,
}
impl std::fmt::Debug for Ca {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Ca").field("dir", &self.dir).finish()
}
}
impl Ca {
pub fn open(dir: &Path) -> Result<Ca> {
std::fs::create_dir_all(dir)?;
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;
}
let (key_path, cert_path) = (dir.join("ca.key"), dir.join("ca.crt"));
let now = time::OffsetDateTime::now_utc();
let key = match std::fs::read_to_string(&key_path) {
Ok(pem) => KeyPair::from_pem(&pem).map_err(|e| err("read the CA key", e))?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
let k = KeyPair::generate().map_err(|e| err("generate the CA key", e))?;
let mut p = ca_params();
p.not_before = now - time::Duration::days(1);
p.not_after = now + time::Duration::days(365 * CA_YEARS);
let c = p
.self_signed(&k)
.map_err(|e| err("sign the CA certificate", e))?;
write_private(&key_path, &k.serialize_pem())?;
std::fs::write(&cert_path, c.pem())?;
let _ = std::fs::remove_file(dir.join("client.crt"));
k
}
Err(e) => return Err(e.into()),
};
let cert_pem = std::fs::read_to_string(&cert_path)?;
let ca = Ca {
dir: dir.to_path_buf(),
key,
cert_pem,
};
if !(dir.join("client.crt").exists() && dir.join("client.key").exists()) {
let l = ca.issue_client()?;
write_private(&dir.join("client.key"), &l.key)?;
std::fs::write(dir.join("client.crt"), &l.cert)?;
}
Ok(ca)
}
fn leaf(&self, params: CertificateParams, what: &str) -> Result<Leaf> {
let issuer = Issuer::new(ca_params(), &self.key);
let k = KeyPair::generate().map_err(|e| err(&format!("generate the {what} key"), e))?;
let c = params
.signed_by(&k, &issuer)
.map_err(|e| err(&format!("sign the {what} certificate"), e))?;
Ok(Leaf {
cert: c.pem(),
key: k.serialize_pem(),
})
}
fn leaf_params(cn: &str) -> CertificateParams {
let now = time::OffsetDateTime::now_utc();
let mut p = CertificateParams::default();
p.distinguished_name = rcgen::DistinguishedName::new();
p.distinguished_name.push(DnType::CommonName, cn);
p.key_usages = vec![KeyUsagePurpose::DigitalSignature];
p.not_before = now - time::Duration::days(1);
p.not_after = now + time::Duration::days(LEAF_DAYS);
p
}
pub fn issue_server(&self, name: &str, address: &str) -> Result<Leaf> {
let mut p = Self::leaf_params(&format!("isb agent {name}"));
p.subject_alt_names = vec![match address.parse::<IpAddr>() {
Ok(ip) => SanType::IpAddress(ip),
Err(_) => SanType::DnsName(
address
.to_string()
.try_into()
.map_err(|e| err("the address as a DNS name", e))?,
),
}];
p.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
self.leaf(p, "agent")
}
pub fn issue_client(&self) -> Result<Leaf> {
let mut p = Self::leaf_params(CLIENT_CN);
p.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
self.leaf(p, "client")
}
pub fn client(&self) -> Result<Leaf> {
Ok(Leaf {
cert: std::fs::read_to_string(self.dir.join("client.crt"))?,
key: std::fs::read_to_string(self.dir.join("client.key"))?,
})
}
pub fn client_config(&self) -> Result<Arc<rustls::ClientConfig>> {
client_config(&self.cert_pem, &self.client()?)
}
}
fn provider() -> Arc<rustls::crypto::CryptoProvider> {
Arc::new(rustls::crypto::ring::default_provider())
}
fn certs(pem: &str) -> Result<Vec<CertificateDer<'static>>> {
let v: Vec<_> = CertificateDer::pem_slice_iter(pem.as_bytes())
.collect::<std::result::Result<_, _>>()
.map_err(|e| err("read certificates", e))?;
if v.is_empty() {
return Err(err("read certificates", "no certificate in the PEM"));
}
Ok(v)
}
fn key(pem: &str) -> Result<PrivateKeyDer<'static>> {
PrivateKeyDer::from_pem_slice(pem.as_bytes()).map_err(|e| err("read a private key", e))
}
fn roots(ca_pem: &str) -> Result<rustls::RootCertStore> {
let mut r = rustls::RootCertStore::empty();
for c in certs(ca_pem)? {
r.add(c).map_err(|e| err("add the CA", e))?;
}
Ok(r)
}
pub fn client_config(ca_pem: &str, leaf: &Leaf) -> Result<Arc<rustls::ClientConfig>> {
Ok(Arc::new(
rustls::ClientConfig::builder_with_provider(provider())
.with_safe_default_protocol_versions()
.map_err(|e| err("TLS versions", e))?
.with_root_certificates(roots(ca_pem)?)
.with_client_auth_cert(certs(&leaf.cert)?, key(&leaf.key)?)
.map_err(|e| err("the client certificate", e))?,
))
}
pub fn server_config(ca_pem: &str, leaf: &Leaf) -> Result<Arc<rustls::ServerConfig>> {
let verifier = rustls::server::WebPkiClientVerifier::builder_with_provider(
Arc::new(roots(ca_pem)?),
provider(),
)
.build()
.map_err(|e| err("the client verifier", e))?;
Ok(Arc::new(
rustls::ServerConfig::builder_with_provider(provider())
.with_safe_default_protocol_versions()
.map_err(|e| err("TLS versions", e))?
.with_client_cert_verifier(verifier)
.with_single_cert(certs(&leaf.cert)?, key(&leaf.key)?)
.map_err(|e| err("the server certificate", e))?,
))
}
pub const AGENT_CA: &str = "ca.crt";
pub const AGENT_CERT: &str = "tls.crt";
pub const AGENT_KEY: &str = "tls.key";
pub fn agent_server_config(dir: &Path) -> Result<Arc<rustls::ServerConfig>> {
let read = |f: &str| {
std::fs::read_to_string(dir.join(f))
.map_err(|e| Error::invalid(format!("{}: {e}", dir.join(f).display())))
};
server_config(
&read(AGENT_CA)?,
&Leaf {
cert: read(AGENT_CERT)?,
key: read(AGENT_KEY)?,
},
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ca_is_made_once_and_kept_private() {
let d = tempfile::tempdir().unwrap();
let a = Ca::open(d.path()).unwrap();
let b = Ca::open(d.path()).unwrap();
assert_eq!(a.cert_pem, b.cert_pem);
assert_eq!(a.client().unwrap().cert, b.client().unwrap().cert);
use std::os::unix::fs::PermissionsExt;
for f in ["ca.key", "client.key"] {
let m = std::fs::metadata(d.path().join(f)).unwrap().permissions();
assert_eq!(m.mode() & 0o777, 0o600, "{f}");
}
let m = std::fs::metadata(d.path()).unwrap().permissions();
assert_eq!(m.mode() & 0o777, 0o700);
}
#[test]
fn leaves_chain_to_the_ca_and_build_configs() {
let d = tempfile::tempdir().unwrap();
let ca = Ca::open(d.path()).unwrap();
let s = ca.issue_server("box", "203.0.113.7").unwrap();
let n = ca.issue_server("box", "agent.example.com").unwrap();
assert_ne!(s.fingerprint().unwrap(), n.fingerprint().unwrap());
server_config(&ca.cert_pem, &s).unwrap();
ca.client_config().unwrap();
}
}