1use ahash::AHashMap;
16use pingora::tls::x509::X509;
17use serde::{Deserialize, Serialize};
18use snafu::Snafu;
19use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
20use std::sync::Arc;
21use std::sync::LazyLock;
22
23mod chain;
24mod dynamic_certificate;
25mod self_signed;
26mod tls_certificate;
27mod validity_checker;
28
29pub static LOG_TARGET: &str = "pingap::certificate";
30
31#[derive(Debug, Snafu)]
32pub enum Error {
33 #[snafu(display("X509 error, category: {category}, {message}"))]
34 X509 { category: String, message: String },
35 #[snafu(display("Invalid error, category: {category}, {message}"))]
36 Invalid { message: String, category: String },
37}
38
39type Result<T, E = Error> = std::result::Result<T, E>;
40
41fn parse_ip_addr(data: &[u8]) -> Result<IpAddr> {
49 Ok(match data.len() {
50 4 => IpAddr::V4(Ipv4Addr::from(
51 TryInto::<[u8; 4]>::try_into(data).map_err(|e| Error::Invalid {
53 category: "ip_parse".to_string(),
54 message: format!(
56 "internal slice conversion error (4 bytes): {e}"
57 ),
58 })?,
59 )),
60 16 => IpAddr::V6(Ipv6Addr::from(
61 TryInto::<[u8; 16]>::try_into(data).map_err(|e| {
63 Error::Invalid {
64 category: "ip_parse".to_string(),
65 message: format!(
66 "internal slice conversion error (16 bytes): {e}"
67 ),
68 }
69 })?,
70 )),
71 len => {
72 return Err(Error::Invalid {
73 category: "ip_parse".to_string(),
74 message: format!("invalid ip address length: {len}"),
75 });
76 },
77 })
78}
79
80pub fn parse_leaf_chain_certificates(
82 pem: &str,
83 key: &str,
84) -> Result<(Certificate, Vec<X509>)> {
85 let pem_data_list = pingap_util::convert_certificate_bytes(Some(pem))
86 .ok_or_else(|| Error::Invalid {
87 category: "certificate".to_string(),
88 message: "invalid pem data".to_string(),
89 })?;
90 let key_data_list =
91 pingap_util::convert_certificate_bytes(Some(key)).unwrap_or_default();
92 let leaf_pem_data = &pem_data_list[0];
93 let (_, p) =
94 x509_parser::pem::parse_x509_pem(leaf_pem_data).map_err(|e| {
95 Error::X509 {
96 category: "parse_x509_pem".to_string(),
97 message: e.to_string(),
98 }
99 })?;
100
101 let x509 = p.parse_x509().map_err(|e| Error::X509 {
102 category: "parse_x509".to_string(),
103 message: e.to_string(),
104 })?;
105 let mut dns_names = vec![];
106 if let Ok(Some(subject_alternative_name)) = x509.subject_alternative_name()
107 {
108 for item in subject_alternative_name.value.general_names.iter() {
110 match item {
111 x509_parser::prelude::GeneralName::DNSName(name) => {
112 dns_names.push(name.to_string());
113 },
114 x509_parser::prelude::GeneralName::IPAddress(data) => {
115 if let Ok(addr) = parse_ip_addr(data) {
116 dns_names.push(addr.to_string());
117 }
118 },
119 _ => {},
120 };
121 }
122 };
123 dns_names.sort();
124 let validity = x509.validity();
125
126 let mut x509_certificates = vec![];
127 for pem in pem_data_list.iter() {
128 let cert = X509::from_pem(pem).map_err(|e| Error::Invalid {
129 category: "x509_from_pem".to_string(),
130 message: e.to_string(),
131 })?;
132 x509_certificates.push(cert);
133 }
134 let key = if key_data_list.is_empty() {
135 vec![]
136 } else {
137 key_data_list[0].clone()
138 };
139
140 let leaf_certificate = Certificate {
141 domains: dns_names,
142 pem: leaf_pem_data.clone(),
143 key,
144 not_after: validity.not_after.timestamp(),
145 not_before: validity.not_before.timestamp(),
146 issuer: x509.issuer.to_string(),
147 ..Default::default()
148 };
149
150 Ok((leaf_certificate, x509_certificates))
151}
152
153#[derive(Debug, Deserialize, Serialize, Default, Clone)]
155pub struct Certificate {
156 pub domains: Vec<String>,
158 pub pem: Vec<u8>,
160 pub key: Vec<u8>,
162 pub acme: Option<String>,
164 pub not_after: i64,
166 pub not_before: i64,
168 pub issuer: String,
170}
171impl Certificate {
172 pub fn get_issuer_common_name(&self) -> String {
177 static CN_REGEX: LazyLock<Option<regex::Regex>> = LazyLock::new(|| {
178 regex::Regex::new(r"CN=(?P<CN>[\S ]+?)($|,)").ok()
179 });
180 let Some(regex) = CN_REGEX.as_ref() else {
181 return "".to_string();
182 };
183
184 regex
185 .captures(&self.issuer)
186 .and_then(|caps| caps.name("CN"))
187 .map(|m| m.as_str().to_string())
188 .unwrap_or_default()
189 }
190 pub fn valid(&self, buffer_days: u16) -> bool {
195 if self.not_after == 0 {
196 return false;
197 }
198 let ts = pingap_core::now_sec() as i64;
199 let mut days = buffer_days as i64;
200 if days == 0 {
201 days = 2;
202 }
203 self.not_after - ts > days * 24 * 3600
204 }
205 pub fn get_cert(&self) -> Vec<u8> {
210 self.pem.clone()
211 }
212 pub fn get_key(&self) -> Vec<u8> {
217 self.key.clone()
218 }
219}
220
221pub use dynamic_certificate::*;
222pub use rcgen;
223pub use self_signed::new_self_signed_certificate_validity_service;
224pub use tls_certificate::TlsCertificate;
225pub use validity_checker::new_certificate_validity_service;
226
227pub type DynamicCertificates = AHashMap<String, Arc<TlsCertificate>>;
229
230pub trait CertificateProvider: Send + Sync {
231 fn get(&self, sni: &str) -> Option<Arc<TlsCertificate>>;
232 fn list(&self) -> Arc<DynamicCertificates>;
233 fn store(&self, data: DynamicCertificates);
234}
235
236#[cfg(test)]
237mod tests {
238 use super::{parse_ip_addr, parse_leaf_chain_certificates};
239 use pretty_assertions::assert_eq;
240 use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
241
242 #[test]
243 fn test_parse_ip_addr() {
244 assert_eq!(
245 parse_ip_addr(&[192, 168, 1, 1]).unwrap(),
246 IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))
247 );
248
249 assert_eq!(
250 parse_ip_addr(&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1])
251 .unwrap(),
252 IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))
253 );
254 assert!(parse_ip_addr(&[192, 168, 1, 1, 1]).is_err());
255 }
256
257 #[test]
258 fn test_cert() {
259 let pem = r###"-----BEGIN CERTIFICATE-----
261MIID/TCCAmWgAwIBAgIQJUGCkB1VAYha6fGExkx0KTANBgkqhkiG9w0BAQsFADBV
262MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExFTATBgNVBAsMDHZpY2Fu
263c29AdHJlZTEcMBoGA1UEAwwTbWtjZXJ0IHZpY2Fuc29AdHJlZTAeFw0yNDA3MDYw
264MjIzMzZaFw0yNjEwMDYwMjIzMzZaMEAxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9w
265bWVudCBjZXJ0aWZpY2F0ZTEVMBMGA1UECwwMdmljYW5zb0B0cmVlMIIBIjANBgkq
266hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv5dbylSPQNARrpT/Rn7qZf6JmH3cueMp
267YdOpctuPYeefT0Jdgp67bg17fU5pfyR2BWYdwyvHCNmKqLdYPx/J69hwTiVFMOcw
268lVQJjbzSy8r5r2cSBMMsRaAZopRDnPy7Ls7Ji+AIT4vshUgL55eR7ACuIJpdtUYm
269TzMx9PTA0BUDkit6z7bTMaEbjDmciIBDfepV4goHmvyBJoYMIjnAwnTFRGRs/QJN
270d2ikFq999fRINzTDbRDP1K0Kk6+zYoFAiCMs9lEDymu3RmiWXBXpINR/Sv8CXtz2
2719RTVwTkjyiMOPY99qBfaZTiy+VCjcwTGKPyus1axRMff4xjgOBewOwIDAQABo14w
272XDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgw
273FoAUhU5Igu3uLUabIqUhUpVXjk1JVtkwFAYDVR0RBA0wC4IJcGluZ2FwLmlvMA0G
274CSqGSIb3DQEBCwUAA4IBgQDBimRKrqnEG65imKriM2QRCEfdB6F/eP9HYvPswuAP
275tvQ6m19/74qbtkd6vjnf6RhMbj9XbCcAJIhRdnXmS0vsBrLDsm2q98zpg6D04F2E
276L++xTiKU6F5KtejXcTHHe23ZpmD2XilwcVDeGFu5BEiFoRH9dmqefGZn3NIwnIeD
277Yi31/cL7BoBjdWku5Qm2nCSWqy12ywbZtQCbgbzb8Me5XZajeGWKb8r6D0Nb+9I9
278OG7dha1L3kxerI5VzVKSiAdGU0C+WcuxfsKAP8ajb1TLOlBaVyilfqmiF457yo/2
279PmTYzMc80+cQWf7loJPskyWvQyfmAnSUX0DI56avXH8LlQ57QebllOtKgMiCo7cr
280CCB2C+8hgRNG9ZmW1KU8rxkzoddHmSB8d6+vFqOajxGdyOV+aX00k3w6FgtHOoKD
281Ztdj1N0eTfn02pibVcXXfwESPUzcjERaMAGg1hoH1F4Gxg0mqmbySAuVRqNLnXp5
282CRVQZGgOQL6WDg3tUUDXYOs=
283-----END CERTIFICATE-----"###;
284 let (cert, _) = parse_leaf_chain_certificates(pem, "").unwrap();
286
287 assert_eq!(
288 "O=mkcert development CA, OU=vicanso@tree, CN=mkcert vicanso@tree",
289 cert.issuer
290 );
291 assert_eq!(1720232616, cert.not_before);
292 assert_eq!(1791253416, cert.not_after);
293 assert_eq!("mkcert vicanso@tree", cert.get_issuer_common_name());
294 assert_eq!(true, cert.valid(2));
295 }
296}