Skip to main content

pingap_certificate/
tls_certificate.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 super::chain::get_lets_encrypt_chain_certificate;
16use super::self_signed::{
17    SelfSignedCertificate, add_self_signed_certificate,
18    get_self_signed_certificate,
19};
20use super::{
21    Certificate, Error, LOG_TARGET, Result, parse_leaf_chain_certificates,
22};
23use pingap_config::CertificateConf;
24use pingap_config::Hashable;
25use pingora::tls::pkey::{PKey, Private};
26use pingora::tls::x509::X509;
27use std::sync::Arc;
28use tracing::info;
29
30// Constants for categorizing different types of certificates and errors
31const LETS_ENCRYPT: &str = "lets_encrypt";
32const ERROR_CERTIFICATE: &str = "certificate";
33const ERROR_X509: &str = "x509_from_pem";
34const ERROR_PRIVATE_KEY: &str = "private_key_from_pem";
35const ERROR_CA: &str = "ca";
36
37/// Represents a TLS certificate with its associated data
38#[derive(Debug, Clone, Default)]
39pub struct TlsCertificate {
40    // Optional name identifier for the certificate
41    pub name: Option<String>,
42    // Optional chain certificate (intermediate CA)
43    pub chain_certificates: Option<Vec<X509>>,
44    // Optional tuple containing the certificate and its private key
45    pub certificate: Option<(X509, PKey<Private>)>,
46    // List of domain names this certificate is valid for
47    pub domains: Vec<String>,
48    // Additional certificate information
49    pub info: Option<Certificate>,
50    // Unique hash key for identifying the certificate
51    pub hash_key: String,
52    // Indicates if this certificate is a Certificate Authority
53    pub is_ca: bool,
54    // Buffer days for certificate renewal
55    pub buffer_days: u16,
56}
57
58impl TryFrom<&CertificateConf> for TlsCertificate {
59    type Error = Error;
60    fn try_from(value: &CertificateConf) -> Result<Self, Self::Error> {
61        // parse certificate
62        let (info, x509_certificates) = parse_leaf_chain_certificates(
63            value.tls_cert.clone().unwrap_or_default().as_str(),
64            value.tls_key.clone().unwrap_or_default().as_str(),
65        )
66        .map_err(|e| Error::Invalid {
67            message: e.to_string(),
68            category: ERROR_CERTIFICATE.to_string(),
69        })?;
70        let category = if value.acme.is_some() {
71            LETS_ENCRYPT
72        } else {
73            ""
74        };
75        let hash_key = value.hash_key();
76        if x509_certificates.is_empty() {
77            return Err(Error::Invalid {
78                message: "x509 certificates is empty".to_string(),
79                category: ERROR_CERTIFICATE.to_string(),
80            });
81        }
82        let cert = x509_certificates[0].clone();
83        let mut chain_certificates = None;
84        if x509_certificates.len() > 1 {
85            chain_certificates = Some(x509_certificates[1..].to_vec());
86        } else if category == LETS_ENCRYPT
87            && let Some(chain_certificate) = get_lets_encrypt_chain_certificate(
88                info.get_issuer_common_name().as_str(),
89            )
90        {
91            chain_certificates = Some(vec![chain_certificate]);
92        }
93
94        let key = PKey::private_key_from_pem(&info.get_key()).map_err(|e| {
95            Error::Invalid {
96                category: ERROR_PRIVATE_KEY.to_string(),
97                message: e.to_string(),
98            }
99        })?;
100        Ok(TlsCertificate {
101            hash_key,
102            chain_certificates,
103            domains: info.domains.clone(),
104            certificate: Some((cert, key)),
105            info: Some(info),
106            is_ca: value.is_ca.unwrap_or_default(),
107            buffer_days: value.buffer_days.unwrap_or_default(),
108            ..Default::default()
109        })
110    }
111}
112
113/// Creates a new certificate signed by the given CA certificate
114///
115/// # Arguments
116/// * `root_ca` - The CA certificate to sign with
117/// * `cn` - The common name for the new certificate
118///
119/// # Returns
120/// A tuple containing the new certificate, private key, and expiration timestamp
121fn new_certificate_with_ca(
122    root_ca: &TlsCertificate,
123    cn: &str,
124) -> Result<(X509, PKey<Private>, i64)> {
125    let Some(info) = &root_ca.info else {
126        return Err(Error::Invalid {
127            message: "root ca is invalid".to_string(),
128            category: ERROR_CA.to_string(),
129        });
130    };
131    let binding = info.get_cert();
132    let ca_pem = std::string::String::from_utf8_lossy(&binding);
133
134    let ca_params = rcgen::CertificateParams::from_ca_cert_pem(&ca_pem)
135        .map_err(|e| Error::Invalid {
136            message: e.to_string(),
137            category: ERROR_CA.to_string(),
138        })?;
139
140    let binding = info.get_key();
141    let ca_key = std::string::String::from_utf8_lossy(&binding);
142
143    let ca_kp =
144        rcgen::KeyPair::from_pem(&ca_key).map_err(|e| Error::Invalid {
145            message: e.to_string(),
146            category: ERROR_CA.to_string(),
147        })?;
148    let not_before = time::OffsetDateTime::now_utc() - time::Duration::days(1);
149    let two_years_from_now =
150        time::OffsetDateTime::now_utc() + time::Duration::days(365 * 2);
151    let not_after = ca_params.not_after.min(two_years_from_now);
152    let ca_cert =
153        ca_params.self_signed(&ca_kp).map_err(|e| Error::Invalid {
154            message: e.to_string(),
155            category: ERROR_CA.to_string(),
156        })?;
157
158    let mut params = rcgen::CertificateParams::new(vec![cn.to_string()])
159        .map_err(|e| Error::Invalid {
160            message: e.to_string(),
161            category: ERROR_CA.to_string(),
162        })?;
163    let mut dn = rcgen::DistinguishedName::new();
164    dn.push(rcgen::DnType::CommonName, cn.to_string());
165    if let Some(organ) = ca_cert
166        .params()
167        .distinguished_name
168        .get(&rcgen::DnType::OrganizationName)
169    {
170        dn.push(rcgen::DnType::OrganizationName, organ.clone());
171    };
172    if let Some(unit) = ca_cert
173        .params()
174        .distinguished_name
175        .get(&rcgen::DnType::OrganizationalUnitName)
176    {
177        dn.push(rcgen::DnType::OrganizationalUnitName, unit.clone());
178    };
179
180    params.distinguished_name = dn;
181    params.not_before = not_before;
182    params.not_after = not_after;
183
184    let cert_key = rcgen::KeyPair::generate().map_err(|e| Error::Invalid {
185        message: e.to_string(),
186        category: ERROR_CA.to_string(),
187    })?;
188
189    let cert = params.signed_by(&cert_key, &ca_cert, &ca_kp).map_err(|e| {
190        Error::Invalid {
191            message: e.to_string(),
192            category: ERROR_CA.to_string(),
193        }
194    })?;
195
196    let cert =
197        X509::from_pem(cert.pem().as_bytes()).map_err(|e| Error::Invalid {
198            category: ERROR_X509.to_string(),
199            message: e.to_string(),
200        })?;
201
202    let key = PKey::private_key_from_pem(cert_key.serialize_pem().as_bytes())
203        .map_err(|e| Error::Invalid {
204        category: ERROR_PRIVATE_KEY.to_string(),
205        message: e.to_string(),
206    })?;
207
208    Ok((cert, key, not_after.unix_timestamp()))
209}
210
211impl TlsCertificate {
212    /// Gets or creates a self-signed certificate for the given server name.
213    /// If a cached certificate exists for the server name, returns that.
214    /// Otherwise creates a new certificate signed by this CA.
215    ///
216    /// # Arguments
217    /// * `server_name` - The server name to create the certificate for
218    ///
219    /// # Returns
220    /// An Arc containing the self-signed certificate
221    pub fn get_self_signed_certificate(
222        &self,
223        server_name: &str,
224    ) -> Result<Arc<SelfSignedCertificate>> {
225        // Format the common name (converts subdomain.example.com to *.example.com)
226        let cn = Self::format_common_name(server_name);
227        // Create a unique cache key using the certificate name and common name
228        let cache_key = format!("{:?}:{}", self.name, cn);
229
230        // Try to get existing certificate from cache
231        if let Some(cert) = get_self_signed_certificate(&cache_key) {
232            return Ok(cert);
233        }
234
235        // Generate new certificate if not found in cache
236        let (cert, key, not_after) = new_certificate_with_ca(self, &cn)?;
237        info!(
238            target: LOG_TARGET,
239            ca_common_name = self.name,
240            common_name = cn,
241            "create new self signed certificate"
242        );
243        Ok(add_self_signed_certificate(cache_key, cert, key, not_after))
244    }
245
246    /// Formats a server name into a common name by converting subdomain patterns
247    /// For example, converts "subdomain.example.com" into "*.example.com"
248    ///
249    /// # Arguments
250    /// * `server_name` - The server name to format
251    ///
252    /// # Returns
253    /// The formatted common name as a String
254    fn format_common_name(server_name: &str) -> String {
255        let parts: Vec<&str> = server_name.split('.').collect();
256        // If there are more than 2 parts (e.g., sub.example.com),
257        // convert to wildcard format (*.example.com)
258        if parts.len() > 2 {
259            format!("*.{}", parts[1..].join("."))
260        } else {
261            server_name.to_string()
262        }
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::TlsCertificate;
269    use pingap_config::CertificateConf;
270    use pretty_assertions::assert_eq;
271
272    #[test]
273    fn test_format_common_name() {
274        assert_eq!(
275            "*.example.com",
276            TlsCertificate::format_common_name("subdomain.example.com")
277        );
278        assert_eq!(
279            "example.com",
280            TlsCertificate::format_common_name("example.com")
281        );
282    }
283
284    #[test]
285    fn test_get_self_signed_certificate() {
286        // spellchecker:off
287        let pem = r#"-----BEGIN CERTIFICATE-----
288MIIENzCCAp+gAwIBAgIRALESVNFwfk4BBxPnZLHdLaMwDQYJKoZIhvcNAQELBQAw
289bTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSEwHwYDVQQLDBh0cmVl
290QGFub255bW91cyAoVHJlZVhpZSkxKDAmBgNVBAMMH21rY2VydCB0cmVlQGFub255
291bW91cyAoVHJlZVhpZSkwHhcNMjUwMTI4MDczODE4WhcNMjcwNDI4MDczODE4WjBd
292MScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxMjAwBgNV
293BAsMKXRyZWVAVHJlZVhpZXMtTWFjQm9vay1Qcm8ubG9jYWwgKFRyZWVYaWUpMIIB
294IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv/Wt6HfiFrIOnW3eZx7A+iF7
295tywiyqRYX0CoecStk4n0H2s0V2nk6zmPwEvF1Qxd4OjUkwrVtIWCNyC3SXzoG+62
296dYMMCRmDZGqiUPaZjKcpObsxIcWIt1lO6mqZaf7hPNPtAb3gO4lLOgy4Ipv7q0Oy
297BY2myg7X9xOTzXmI6va8XSdGHsoilpic/mF95BE3D7FINx3j12HAwnMGY5/xPAFp
298QTa/3zoE22TCUpZHb1v9X3N2olPCUWRNbgCFWl5vKpfvqLlP19th1jhr2DkUVeWs
299RXYaB2ULwkNKkdhO0ka3hZipu6C3qDmfssfkm+lVhUvgUYWElaKDZG88ia26rQID
300AQABo2IwYDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYD
301VR0jBBgwFoAU210uBCmUnt7NVBOP3t3kjNq6XlEwGAYDVR0RBBEwD4INKi5leGFt
302cGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAYEATOEErFNYpxuxFkO/fDoUvuD9c9n3
303UetdrQ3u1E5EeYy+LaCWjEtDLf8t2NYKfuqQGxWgkdQYU6GIF4pbuZeARrReoind
3044SRSaA4Zwc8BvmA+UeCgm0uGAY2B3FQ6oUK7sY+wtIr0ob6nGLUtstZVesvA3elG
305xVcUM5tmBlm2rjLjvumIsfNK7VdKUY6yV2Z50nXNDkpT+achL1sJVMUIRokUezNB
306Pn1UjgblgpjuIA0A+e1XIm0Co/1JtJv8FOfUWGFE0oYJNSqp0sX51ZZ+5flV/3nS
307inJvDyFSfTsSNlgEeFb0Ek8XmFpQqFXd8O20owgkcO/XFCkovFzuPdJwQ0hxTAzU
308yOFUVc2HLxISKWJmyZ2XCoSrZgHjnOxdqY187J9Xv2T7P59H4JYvSB90iwqKLKTW
309GEVKsWU+0lbeWGwpAe46HfSg3xl/zoL62SCNsC0ruoJofprLDF0e6vxJQy9s4Dp6
310DiHunXjaGjAc2C1GAdLdkLDokENUTFp9nZJv
311-----END CERTIFICATE-----"#;
312        let key = r#"-----BEGIN PRIVATE KEY-----
313MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC/9a3od+IWsg6d
314bd5nHsD6IXu3LCLKpFhfQKh5xK2TifQfazRXaeTrOY/AS8XVDF3g6NSTCtW0hYI3
315ILdJfOgb7rZ1gwwJGYNkaqJQ9pmMpyk5uzEhxYi3WU7qaplp/uE80+0BveA7iUs6
316DLgim/urQ7IFjabKDtf3E5PNeYjq9rxdJ0YeyiKWmJz+YX3kETcPsUg3HePXYcDC
317cwZjn/E8AWlBNr/fOgTbZMJSlkdvW/1fc3aiU8JRZE1uAIVaXm8ql++ouU/X22HW
318OGvYORRV5axFdhoHZQvCQ0qR2E7SRreFmKm7oLeoOZ+yx+Sb6VWFS+BRhYSVooNk
319bzyJrbqtAgMBAAECggEBAJIME8KY43UtB52TZ/DBH0Wvj/bvJ5FRtMLT6NqsXvuv
320rALzh6EyOi8VXl+JxvyvKgXiX0l4pttv8ICM7aaF1/rYhg2mJNQPiz4tO02qMW0o
321CV+ZImp1Ze1Jj5cef5Z7i1bCTsJSenYRoSCLaNU8JCBLovhCq7Fz1bBwPrXIT/mj
322aoDF68eWuxefM0EiUh0xqNSF/eglyXOIt6Fz6p3gMvTKwYsSM02CXaui6rNbU5aN
323YJ5M6Sem/FqtwIyb48UHMvI26ajVwtMSiNwR2XXV6gxg8xWFpjU+Uh3u1Qxacj2K
324aW5jBFiLQegcZetGvL+z65VYV/cKHkYl4PhULep3CmkCgYEA45BpCntOVP/2ThUP
325oXTDJnRJ8YfiYgP41zPAfK8daeOCuJRNhGXUr8fI/PiAmLIRbs8hJ8TeZTa8nzSE
3260zBJ7CEbjSXbKR6UDCvm85QPGnog7fDpkjk2qcFRbXFZbolerMzHuQfUvFH/t48Q
327SScn64aSq/Ymjkz9O0jPJ4xrK7sCgYEA1/JQEkUdbFsOO/mRA+YEtS1C359UD5Au
328n413sI/D/C8dbTveQ4lNnd5s/JLZEvBCIX6+mczvm7ZAKLv4k9uYHw6mzdEQbUNR
329uf6BNbAeRUxoN90qWzSVqkK9S4vs8x3zWkREqSJeSwW7Kh2DAz8HM4u36gxQhz2V
330+eHz0a1Q6LcCgYEAoQOV/y+eDjCJ81edlq0KQ9Q2Waq++IE8+fAJO2+gTUMIRFfS
331vWJb6gBfavbd7qzX/uKZ4AzBGzZuoetELDXXqDcIyodFmcOkFzSdFi3lveM6F4HF
332kove7J/3YIu6LqcOERBYJMiwsosGd7fHWytUaKbwcrIZN8iryN3MjXwifG8CgYEA
333qt0oq/wR1t2JOr0yF+KVUQGaCzSXH6VWrpoR3Rsz2EMzRm37ZHasekA2/fX3WjvO
334J5CQoUL9R7iBtXldqygyikhehTVpiPqeHMuaUu+iU/Sr9Z/CVt4ZmdkqzC7P8mF9
335Xqvro+P0tem3+Q/WzOe++/MON1s9EHUTSN+Wuw4mmasCgYBqzSZro05J6U+74g5X
3361QRl97OzlCgzaIWLHlv9nZzHivIrQxPtK1QStFiJOeQTq5XqdLywkGHWHsgBYQhl
337iama6sNZgokeRWVL1QJBaC2q0312AG8xeOZ7oWqfAtfxGpjhvNpgPJfZi8NA7+WE
338kknq2XUsBMCyIW1BqgLVEyeNxg==
339-----END PRIVATE KEY-----"#;
340        // spellchecker:on
341        let cert = TlsCertificate::try_from(&CertificateConf {
342            tls_cert: Some(pem.to_string()),
343            tls_key: Some(key.to_string()),
344            ..Default::default()
345        })
346        .unwrap();
347
348        let server_name = format!("{}.test.example.com", nanoid::nanoid!(10));
349        let cert = cert.get_self_signed_certificate(&server_name).unwrap();
350        assert_eq!(
351            r#"[commonName = "*.test.example.com", organizationName = "mkcert development certificate", organizationalUnitName = "tree@TreeXies-MacBook-Pro.local (TreeXie)"]"#,
352            format!("{:?}", cert.x509.subject_name())
353        );
354    }
355}