use ahash::AHashMap;
use pingora::tls::x509::X509;
use serde::{Deserialize, Serialize};
use snafu::Snafu;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::sync::Arc;
use std::sync::LazyLock;
mod chain;
mod dynamic_certificate;
mod self_signed;
mod tls_certificate;
mod validity_checker;
pub static LOG_TARGET: &str = "pingap::certificate";
#[derive(Debug, Snafu)]
pub enum Error {
#[snafu(display("X509 error, category: {category}, {message}"))]
X509 { category: String, message: String },
#[snafu(display("Invalid error, category: {category}, {message}"))]
Invalid { message: String, category: String },
}
type Result<T, E = Error> = std::result::Result<T, E>;
fn parse_ip_addr(data: &[u8]) -> Result<IpAddr> {
Ok(match data.len() {
4 => IpAddr::V4(Ipv4Addr::from(
TryInto::<[u8; 4]>::try_into(data).map_err(|e| Error::Invalid {
category: "ip_parse".to_string(),
message: format!(
"internal slice conversion error (4 bytes): {e}"
),
})?,
)),
16 => IpAddr::V6(Ipv6Addr::from(
TryInto::<[u8; 16]>::try_into(data).map_err(|e| {
Error::Invalid {
category: "ip_parse".to_string(),
message: format!(
"internal slice conversion error (16 bytes): {e}"
),
}
})?,
)),
len => {
return Err(Error::Invalid {
category: "ip_parse".to_string(),
message: format!("invalid ip address length: {len}"),
});
},
})
}
pub fn parse_leaf_chain_certificates(
pem: &str,
key: &str,
) -> Result<(Certificate, Vec<X509>)> {
let pem_data_list = pingap_util::convert_certificate_bytes(Some(pem))
.ok_or_else(|| Error::Invalid {
category: "certificate".to_string(),
message: "invalid pem data".to_string(),
})?;
let key_data_list =
pingap_util::convert_certificate_bytes(Some(key)).unwrap_or_default();
let leaf_pem_data = &pem_data_list[0];
let (_, p) =
x509_parser::pem::parse_x509_pem(leaf_pem_data).map_err(|e| {
Error::X509 {
category: "parse_x509_pem".to_string(),
message: e.to_string(),
}
})?;
let x509 = p.parse_x509().map_err(|e| Error::X509 {
category: "parse_x509".to_string(),
message: e.to_string(),
})?;
let mut dns_names = vec![];
if let Ok(Some(subject_alternative_name)) = x509.subject_alternative_name()
{
for item in subject_alternative_name.value.general_names.iter() {
match item {
x509_parser::prelude::GeneralName::DNSName(name) => {
dns_names.push(name.to_string());
},
x509_parser::prelude::GeneralName::IPAddress(data) => {
if let Ok(addr) = parse_ip_addr(data) {
dns_names.push(addr.to_string());
}
},
_ => {},
};
}
};
dns_names.sort();
let validity = x509.validity();
let mut x509_certificates = vec![];
for pem in pem_data_list.iter() {
let cert = X509::from_pem(pem).map_err(|e| Error::Invalid {
category: "x509_from_pem".to_string(),
message: e.to_string(),
})?;
x509_certificates.push(cert);
}
let key = if key_data_list.is_empty() {
vec![]
} else {
key_data_list[0].clone()
};
let leaf_certificate = Certificate {
domains: dns_names,
pem: leaf_pem_data.clone(),
key,
not_after: validity.not_after.timestamp(),
not_before: validity.not_before.timestamp(),
issuer: x509.issuer.to_string(),
..Default::default()
};
Ok((leaf_certificate, x509_certificates))
}
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
pub struct Certificate {
pub domains: Vec<String>,
pub pem: Vec<u8>,
pub key: Vec<u8>,
pub acme: Option<String>,
pub not_after: i64,
pub not_before: i64,
pub issuer: String,
}
impl Certificate {
pub fn get_issuer_common_name(&self) -> String {
static CN_REGEX: LazyLock<Option<regex::Regex>> = LazyLock::new(|| {
regex::Regex::new(r"CN=(?P<CN>[\S ]+?)($|,)").ok()
});
let Some(regex) = CN_REGEX.as_ref() else {
return "".to_string();
};
regex
.captures(&self.issuer)
.and_then(|caps| caps.name("CN"))
.map(|m| m.as_str().to_string())
.unwrap_or_default()
}
pub fn valid(&self, buffer_days: u16) -> bool {
if self.not_after == 0 {
return false;
}
let ts = pingap_core::now_sec() as i64;
let mut days = buffer_days as i64;
if days == 0 {
days = 2;
}
self.not_after - ts > days * 24 * 3600
}
pub fn get_cert(&self) -> Vec<u8> {
self.pem.clone()
}
pub fn get_key(&self) -> Vec<u8> {
self.key.clone()
}
}
pub use dynamic_certificate::*;
pub use rcgen;
pub use self_signed::new_self_signed_certificate_validity_service;
pub use tls_certificate::TlsCertificate;
pub use validity_checker::new_certificate_validity_service;
pub type DynamicCertificates = AHashMap<String, Arc<TlsCertificate>>;
pub trait CertificateProvider: Send + Sync {
fn get(&self, sni: &str) -> Option<Arc<TlsCertificate>>;
fn list(&self) -> Arc<DynamicCertificates>;
fn store(&self, data: DynamicCertificates);
}
#[cfg(test)]
mod tests {
use super::{parse_ip_addr, parse_leaf_chain_certificates};
use pretty_assertions::assert_eq;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
#[test]
fn test_parse_ip_addr() {
assert_eq!(
parse_ip_addr(&[192, 168, 1, 1]).unwrap(),
IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))
);
assert_eq!(
parse_ip_addr(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1])
.unwrap(),
IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))
);
assert!(parse_ip_addr(&[192, 168, 1, 1, 1]).is_err());
}
#[test]
fn test_cert() {
let pem = r###"-----BEGIN CERTIFICATE-----
MIID/TCCAmWgAwIBAgIQJUGCkB1VAYha6fGExkx0KTANBgkqhkiG9w0BAQsFADBV
MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExFTATBgNVBAsMDHZpY2Fu
c29AdHJlZTEcMBoGA1UEAwwTbWtjZXJ0IHZpY2Fuc29AdHJlZTAeFw0yNDA3MDYw
MjIzMzZaFw0yNjEwMDYwMjIzMzZaMEAxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9w
bWVudCBjZXJ0aWZpY2F0ZTEVMBMGA1UECwwMdmljYW5zb0B0cmVlMIIBIjANBgkq
hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv5dbylSPQNARrpT/Rn7qZf6JmH3cueMp
YdOpctuPYeefT0Jdgp67bg17fU5pfyR2BWYdwyvHCNmKqLdYPx/J69hwTiVFMOcw
lVQJjbzSy8r5r2cSBMMsRaAZopRDnPy7Ls7Ji+AIT4vshUgL55eR7ACuIJpdtUYm
TzMx9PTA0BUDkit6z7bTMaEbjDmciIBDfepV4goHmvyBJoYMIjnAwnTFRGRs/QJN
d2ikFq999fRINzTDbRDP1K0Kk6+zYoFAiCMs9lEDymu3RmiWXBXpINR/Sv8CXtz2
9RTVwTkjyiMOPY99qBfaZTiy+VCjcwTGKPyus1axRMff4xjgOBewOwIDAQABo14w
XDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgw
FoAUhU5Igu3uLUabIqUhUpVXjk1JVtkwFAYDVR0RBA0wC4IJcGluZ2FwLmlvMA0G
CSqGSIb3DQEBCwUAA4IBgQDBimRKrqnEG65imKriM2QRCEfdB6F/eP9HYvPswuAP
tvQ6m19/74qbtkd6vjnf6RhMbj9XbCcAJIhRdnXmS0vsBrLDsm2q98zpg6D04F2E
L++xTiKU6F5KtejXcTHHe23ZpmD2XilwcVDeGFu5BEiFoRH9dmqefGZn3NIwnIeD
Yi31/cL7BoBjdWku5Qm2nCSWqy12ywbZtQCbgbzb8Me5XZajeGWKb8r6D0Nb+9I9
OG7dha1L3kxerI5VzVKSiAdGU0C+WcuxfsKAP8ajb1TLOlBaVyilfqmiF457yo/2
PmTYzMc80+cQWf7loJPskyWvQyfmAnSUX0DI56avXH8LlQ57QebllOtKgMiCo7cr
CCB2C+8hgRNG9ZmW1KU8rxkzoddHmSB8d6+vFqOajxGdyOV+aX00k3w6FgtHOoKD
Ztdj1N0eTfn02pibVcXXfwESPUzcjERaMAGg1hoH1F4Gxg0mqmbySAuVRqNLnXp5
CRVQZGgOQL6WDg3tUUDXYOs=
-----END CERTIFICATE-----"###;
let (cert, _) = parse_leaf_chain_certificates(pem, "").unwrap();
assert_eq!(
"O=mkcert development CA, OU=vicanso@tree, CN=mkcert vicanso@tree",
cert.issuer
);
assert_eq!(1720232616, cert.not_before);
assert_eq!(1791253416, cert.not_after);
assert_eq!("mkcert vicanso@tree", cert.get_issuer_common_name());
assert_eq!(true, cert.valid(2));
}
}