use crate::error::{ProxyError, Result};
use rcgen::{
BasicConstraints, CertificateParams, DistinguishedName, DnType, IsCa, KeyPair, KeyUsagePurpose,
PKCS_ECDSA_P256_SHA256,
};
use std::time::{Duration, SystemTime};
use time::OffsetDateTime;
use zeroize::Zeroizing;
const CA_VALIDITY: Duration = Duration::from_secs(24 * 60 * 60);
pub struct EphemeralCa {
key_pair: KeyPair,
#[allow(dead_code)]
key_pkcs8_der: Zeroizing<Vec<u8>>,
ca_cert: rcgen::Certificate,
cert_pem: String,
}
impl EphemeralCa {
pub fn generate() -> Result<Self> {
let key_pair = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).map_err(|e| {
ProxyError::Config(format!("failed to generate ephemeral CA key pair: {}", e))
})?;
let key_pkcs8_der = Zeroizing::new(key_pair.serialize_der());
let mut params = CertificateParams::default();
params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
params.key_usages = vec![
KeyUsagePurpose::KeyCertSign,
KeyUsagePurpose::CrlSign,
KeyUsagePurpose::DigitalSignature,
];
let now = SystemTime::now();
params.not_before = system_time_to_offset(now)?;
params.not_after = system_time_to_offset(now + CA_VALIDITY)?;
let mut dn = DistinguishedName::new();
dn.push(DnType::CommonName, "nono-session-ca");
params.distinguished_name = dn;
let ca_cert = params
.self_signed(&key_pair)
.map_err(|e| ProxyError::Config(format!("failed to self-sign ephemeral CA: {}", e)))?;
let cert_pem = ca_cert.pem();
Ok(Self {
key_pair,
key_pkcs8_der,
ca_cert,
cert_pem,
})
}
#[must_use]
pub fn cert_pem(&self) -> &str {
&self.cert_pem
}
pub(super) fn ca_cert(&self) -> &rcgen::Certificate {
&self.ca_cert
}
pub(super) fn key_pair(&self) -> &KeyPair {
&self.key_pair
}
}
impl std::fmt::Debug for EphemeralCa {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("EphemeralCa")
.field("subject", &"CN=nono-session-ca")
.field("key_pair", &"[REDACTED]")
.field("key_pkcs8_der", &"[REDACTED]")
.field("cert_pem_len", &self.cert_pem.len())
.finish()
}
}
fn system_time_to_offset(t: SystemTime) -> Result<OffsetDateTime> {
OffsetDateTime::from_unix_timestamp(
t.duration_since(SystemTime::UNIX_EPOCH)
.map_err(|e| ProxyError::Config(format!("system time before unix epoch: {}", e)))?
.as_secs()
.try_into()
.map_err(|_| ProxyError::Config("system time exceeds i64::MAX".to_string()))?,
)
.map_err(|e| ProxyError::Config(format!("invalid system time for cert validity: {}", e)))
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use rustls::pki_types::CertificateDer;
use rustls::pki_types::pem::PemObject;
#[test]
fn generate_produces_valid_pem() {
let ca = EphemeralCa::generate().unwrap();
assert!(ca.cert_pem().contains("BEGIN CERTIFICATE"));
assert!(ca.cert_pem().contains("END CERTIFICATE"));
let der = CertificateDer::from_pem_slice(ca.cert_pem().as_bytes()).unwrap();
assert!(!der.as_ref().is_empty());
}
#[test]
fn each_call_produces_distinct_keys() {
let a = EphemeralCa::generate().unwrap();
let b = EphemeralCa::generate().unwrap();
assert_ne!(
a.cert_pem(),
b.cert_pem(),
"ephemeral CAs must not reuse key material across sessions"
);
}
#[test]
fn debug_redacts_key_material() {
let ca = EphemeralCa::generate().unwrap();
let dbg = format!("{:?}", ca);
assert!(dbg.contains("[REDACTED]"));
assert!(!dbg.contains("BEGIN PRIVATE KEY"));
}
}