#![allow(unused)]
use std::{fs::{self, create_dir_all}, sync::LazyLock};
use anyhow::{Context, Result, bail};
use camino::{Utf8Path, Utf8PathBuf};
use fslock::LockFile;
use pingora_boringssl::{
pkey::{PKey, Private},
x509::X509,
};
use rcgen::{
BasicConstraints, CertificateParams, DistinguishedName, DnType, IsCa, Issuer, KeyPair,
KeyUsagePurpose, PKCS_ECDSA_P256_SHA256,
};
use rustls::{RootCertStore, pki_types::{CertificateDer, pem::{PemObject, SectionKind}}};
pub const CERT_BASE: &str = "target/certs";
pub static CERT_DIR: LazyLock<Utf8PathBuf> = LazyLock::new(|| Utf8PathBuf::from(CERT_BASE));
pub struct TestCerts {
pub caroot: CaCert,
pub localhost: LocalCert,
pub www_example: LocalCert,
pub test_example: LocalCert,
pub wildcard_example: LocalCert,
pub snakeoil_1: LocalCert,
pub snakeoil_2: LocalCert,
}
impl TestCerts {
fn new() -> Result<Self> {
create_dir_all(CERT_DIR.as_path())?;
let caroot = get_root_ca()?;
let localhost = get_default_cert("localhost", &caroot)?;
let www_example = get_default_cert("www.example.com", &caroot)?;
let test_example = get_default_cert("test.example.com", &caroot)?;
let wildcard_example = get_cert("*.example.com", "_.example.com", None, None, &caroot)?;
let snakeoil_1 = {
let not_before = time::OffsetDateTime::now_utc();
let not_after = not_before
.checked_add(time::Duration::days(365));
let host = "snakeoil.example.com";
let name = "snakeoil-1";
get_cert(host, name, Some(not_before), not_after, &caroot)?
};
let snakeoil_2 = {
let host = "snakeoil.example.com";
let name = "snakeoil-2";
let not_before = time::OffsetDateTime::now_utc();
let not_after = not_before
.checked_add(time::Duration::days(720));
get_cert(host, name, Some(not_before), not_after, &caroot)?
};
Ok(Self {
caroot,
localhost,
www_example,
test_example,
wildcard_example,
snakeoil_1,
snakeoil_2,
})
}
}
pub static TEST_CERTS: LazyLock<TestCerts> = LazyLock::new(|| TestCerts::new().unwrap());
fn lock_dir(dir: &Utf8Path) -> Result<LockFile> {
let _ = create_dir_all(dir); let lockfile = dir.with_added_extension("lock");
Ok(LockFile::open(lockfile.as_os_str())?)
}
pub struct CaCert {
pub issuer: Issuer<'static, KeyPair>,
pub reqcert: reqwest::Certificate,
pub store: rustls::RootCertStore,
}
pub struct LocalCert {
pub keyfile: Utf8PathBuf,
pub key: PKey<Private>,
pub certfile: Utf8PathBuf,
pub certs: Vec<X509>,
}
fn gen_ca() -> Result<()> {
let mut params = CertificateParams::default();
params.distinguished_name = DistinguishedName::new();
params.distinguished_name.push(DnType::CountryName, "AU");
params.distinguished_name.push(DnType::OrganizationName, "Haltcondition CA");
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
params.key_usages.push(KeyUsagePurpose ::DigitalSignature);
params.key_usages.push(KeyUsagePurpose::KeyCertSign);
params.key_usages.push(KeyUsagePurpose::CrlSign);
let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256)?;
let key = key_pair.serialize_pem();
let cert = params.self_signed(&key_pair)?;
let issuer = Issuer::new(params, key_pair);
let certfile = CERT_DIR.join("CA.crt");
let cakey = CERT_DIR.join("CA.key");
let certpem = cert.pem();
fs::write(&certfile, &certpem)?;
fs::write(&cakey, &key)?;
Ok(())
}
fn load_ca(cacert: Utf8PathBuf, cakey: Utf8PathBuf) -> Result<CaCert> {
let keypem = String::from_utf8(fs::read(&cakey)?)?;
let key = KeyPair::from_pem(&keypem)?;
let capem = String::from_utf8(fs::read(&cacert)?)?;
let issuer = Issuer::from_ca_cert_pem(&capem, key)?;
let reqcert = reqwest::Certificate::from_pem(capem.as_bytes())?;
let rust_certs = PemObject::pem_reader_iter(capem.as_bytes());
let mut store = RootCertStore::empty();
for cert in rust_certs {
store.add(cert?).unwrap();
}
let cacert = CaCert {
issuer,
reqcert,
store,
};
Ok(cacert)
}
fn get_root_ca() -> Result<CaCert> {
let cafile = CERT_DIR.join("CA.crt");
let cakey = CERT_DIR.join("CA.key");
let mut lock = lock_dir(&CERT_DIR)?;
lock.lock()?;
if !(cafile.exists() && cakey.exists()) {
gen_ca()?;
};
lock.unlock()?;
let ca = load_ca(cafile, cakey)?;
Ok(ca)
}
fn gen_default_cert(host: &str, ca: &CaCert) -> Result<()>
{
gen_cert(host, host, None, None, ca)
}
fn gen_cert(host: &str,
name: &str,
not_before: Option<time::OffsetDateTime>,
not_after: Option<time::OffsetDateTime>,
ca: &CaCert,)
-> Result<()>
{
let keyfile = CERT_DIR.join(name).with_added_extension("key");
let certfile = CERT_DIR.join(name).with_added_extension("crt");
let sans = vec![host.to_string()];
let keypair = KeyPair::generate()?;
let mut params = CertificateParams::new(sans)?;
params.distinguished_name = DistinguishedName::new();
if let Some(not_before) = not_before {
params.not_before = not_before;
}
if let Some(not_after) = not_after {
params.not_after = not_after;
}
let cert = params.signed_by(&keypair, &ca.issuer)?;
let cert_pem = cert.pem();
let key_pem = keypair.serialize_pem();
std::fs::write(&keyfile, &key_pem)?;
std::fs::write(&certfile, &cert_pem)?;
Ok(())
}
fn load_cert(keyfile: Utf8PathBuf, certfile: Utf8PathBuf) -> Result<LocalCert> {
let kdata = fs::read(&keyfile)
.context("Failed to load keyfile {keyfile}")?;
let cdata = fs::read(&certfile)
.context("Failed to load certfile {certfile}")?;
let key = PKey::private_key_from_pem(&kdata)?;
let certs = X509::stack_from_pem(&cdata)?;
if certs.is_empty() {
bail!("No certificates found in TLS .crt file");
}
let lc = LocalCert {
keyfile,
certfile,
key,
certs,
};
Ok(lc)
}
fn get_default_cert(host: &str, ca: &CaCert) -> Result<LocalCert>
{
get_cert(host, host, None, None, ca)
}
fn get_cert(host: &str,
name: &str,
not_before: Option<time::OffsetDateTime>,
not_after: Option<time::OffsetDateTime>,
ca: &CaCert)
-> Result<LocalCert>
{
let certfile = CERT_DIR.join(name).with_added_extension("crt");
let keyfile = CERT_DIR.join(name).with_added_extension("key");
let mut lock = lock_dir(&CERT_DIR)?;
lock.lock()?;
let localcert = if certfile.exists() && keyfile.exists() {
lock.unlock()?;
load_cert(keyfile, certfile)?
} else {
gen_cert(host, name, not_before, not_after, ca)?;
load_cert(keyfile, certfile)?
};
Ok(localcert)
}