Skip to main content

pingap_certificate/
lib.rs

1// Copyright 2024-2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
41/// Parses a byte slice into an IP address (IPv4 or IPv6)
42///
43/// # Arguments
44/// * `data` - A byte slice containing the IP address data
45///
46/// # Returns
47/// * `Result<IpAddr>` - The parsed IP address or an error if invalid
48fn parse_ip_addr(data: &[u8]) -> Result<IpAddr> {
49    Ok(match data.len() {
50        4 => IpAddr::V4(Ipv4Addr::from(
51            // Should not fail due to len check
52            TryInto::<[u8; 4]>::try_into(data).map_err(|e| Error::Invalid {
53                category: "ip_parse".to_string(),
54                // 这个错误在逻辑上不应该发生,但我们还是处理它
55                message: format!(
56                    "internal slice conversion error (4 bytes): {e}"
57                ),
58            })?,
59        )),
60        16 => IpAddr::V6(Ipv6Addr::from(
61            // Should not fail due to len check
62            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
80// parse leaf certificate and chain certificates from pem and key
81pub 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        // get dns name and ip address of certificate
109        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/// Represents a X.509 certificate with associated metadata
154#[derive(Debug, Deserialize, Serialize, Default, Clone)]
155pub struct Certificate {
156    /// List of domain names and ip addresses that this certificate is valid for
157    pub domains: Vec<String>,
158    /// PEM-encoded certificate data as bytes
159    pub pem: Vec<u8>,
160    /// PEM-encoded private key data as bytes
161    pub key: Vec<u8>,
162    /// Optional ACME (Automated Certificate Management Environment) identifier
163    pub acme: Option<String>,
164    /// Unix timestamp when the certificate expires
165    pub not_after: i64,
166    /// Unix timestamp when the certificate becomes valid
167    pub not_before: i64,
168    /// Distinguished Name (DN) of the certificate issuer
169    pub issuer: String,
170}
171impl Certificate {
172    /// Extracts the Common Name (CN) from the certificate issuer field
173    ///
174    /// # Returns
175    /// * `String` - The issuer's Common Name or empty string if not found
176    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    /// Checks if the certificate is valid and not expiring within 48 hours
191    ///
192    /// # Returns
193    /// * `bool` - True if the certificate is valid, false otherwise
194    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    /// Returns the PEM-encoded certificate data
206    ///
207    /// # Returns
208    /// * `Vec<u8>` - The certificate data as bytes
209    pub fn get_cert(&self) -> Vec<u8> {
210        self.pem.clone()
211    }
212    /// Returns the PEM-encoded private key data
213    ///
214    /// # Returns
215    /// * `Vec<u8>` - The private key data as bytes
216    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
227// Type alias for storing certificates in a high-performance hash map
228pub 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        // spellchecker:off
260        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        // spellchecker:on
285        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}