Skip to main content

pingap_certificate/
dynamic_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::CertificateProvider;
16use super::DynamicCertificates;
17use super::{Error, LOG_TARGET, TlsCertificate};
18use ahash::AHashMap;
19use async_trait::async_trait;
20use pingap_config::CertificateConf;
21use pingora::listeners::tls::TlsSettings;
22use pingora::tls::ext;
23use pingora::tls::pkey::{PKey, Private};
24use pingora::tls::ssl::SslVersion;
25use pingora::tls::ssl::{NameType, SslRef};
26use pingora::tls::x509::X509;
27use std::borrow::Cow;
28use std::collections::HashMap;
29use std::sync::Arc;
30use tracing::{debug, error, info};
31
32type Result<T, E = Error> = std::result::Result<T, E>;
33
34// Fallback server name used when:
35// - No SNI (Server Name Indication) is provided in TLS handshake
36// - No matching certificate is found for the requested domain
37pub static DEFAULT_SERVER_NAME: &str = "*";
38
39// Parses certificate configurations and builds the certificate store
40// Parameters:
41// - certificate_configs: Map of certificate names to their configurations
42// Returns:
43// - DynamicCertificates: Map of domain names to parsed certificates
44// - Vec<(String, String)>: List of (certificate_name, error_message) for failed parsing
45pub fn parse_certificates(
46    certificate_configs: &HashMap<String, CertificateConf>,
47) -> (DynamicCertificates, Vec<(String, String)>) {
48    let mut dynamic_certs = AHashMap::new();
49    let mut errors = vec![];
50
51    // Use a temporary map to avoid cloning the Arc<TlsCertificate> for each domain.
52    let mut cert_cache: AHashMap<String, Arc<TlsCertificate>> = AHashMap::new();
53
54    for (name, conf) in certificate_configs.iter() {
55        if conf.tls_cert.is_none() || conf.tls_key.is_none() {
56            continue;
57        }
58
59        let cert_arc = match cert_cache.get(name) {
60            Some(cert) => cert.clone(),
61            None => match TlsCertificate::try_from(conf) {
62                Ok(mut cert) => {
63                    cert.name = Some(name.clone());
64                    let arc_cert = Arc::new(cert);
65                    cert_cache.insert(name.clone(), arc_cert.clone());
66                    arc_cert
67                },
68                Err(e) => {
69                    errors.push((name.clone(), e.to_string()));
70                    continue;
71                },
72            },
73        };
74
75        // Determine which domains this certificate should be served for.
76        let domains_to_serve: Cow<[String]> = if let Some(value) = &conf.domains
77        {
78            Cow::Owned(value.split(',').map(|s| s.trim().to_string()).collect())
79        } else {
80            Cow::Borrowed(&cert_arc.domains)
81        };
82
83        for domain in domains_to_serve.iter() {
84            dynamic_certs.insert(domain.to_string(), cert_arc.clone());
85        }
86
87        if conf.is_default.unwrap_or_default() {
88            dynamic_certs
89                .insert(DEFAULT_SERVER_NAME.to_string(), cert_arc.clone());
90        }
91    }
92    (dynamic_certs, errors)
93}
94
95/// Parameters for configuring TLS settings
96///
97/// Contains all the necessary configuration options for setting up TLS,
98/// including protocol versions, cipher suites, and HTTP/2 support.
99#[derive(Debug)]
100pub struct TlsSettingParams {
101    pub server_name: String,
102    pub enabled_h2: bool,            // Enable HTTP/2 support
103    pub cipher_list: Option<String>, // Legacy cipher list
104    pub cipher_suites: Option<String>, // Modern cipher suites
105    pub tls_min_version: Option<String>, // Minimum TLS version
106    pub tls_max_version: Option<String>, // Maximum TLS version
107}
108
109/// Applies certificate, private key and chain certificate to an SSL context
110///
111/// # Arguments
112/// * `ssl` - Reference to the SSL context to modify
113/// * `cert` - X509 certificate to apply
114/// * `key` - Private key for the certificate
115/// * `chain_certificate` - Optional chain certificate
116///
117/// # Side Effects
118/// Logs errors if any operation fails but continues execution
119#[inline]
120fn ssl_certificate(
121    ssl: &mut SslRef,
122    cert: &X509,
123    key: &PKey<Private>,
124    chain_certificates: &Option<Vec<X509>>,
125) {
126    // set tls certificate
127    if let Err(e) = ext::ssl_use_certificate(ssl, cert) {
128        error!(target: LOG_TARGET, error = %e, "ssl use certificate fail");
129    }
130    // set private key
131    if let Err(e) = ext::ssl_use_private_key(ssl, key) {
132        error!(target: LOG_TARGET, error = %e, "ssl use private key fail");
133    }
134    // set chain certificate
135    if let Some(chain_certificates) = chain_certificates {
136        for chain in chain_certificates.iter() {
137            if let Err(e) = ext::ssl_add_chain_cert(ssl, chain) {
138                error!(target: LOG_TARGET, error = %e, "ssl add chain cert fail");
139            }
140        }
141    }
142}
143
144fn convert_tls_version(version: &Option<String>) -> Option<SslVersion> {
145    if let Some(version) = &version {
146        let version = match version.to_lowercase().as_str() {
147            "tlsv1.1" => SslVersion::TLS1_1,
148            "tlsv1.3" => SslVersion::TLS1_3,
149            _ => SslVersion::TLS1_2,
150        };
151        return Some(version);
152    }
153    None
154}
155
156/// GlobalCertificate implements SNI-based dynamic certificate selection
157///
158/// Provides runtime certificate selection during TLS handshake based on the
159/// Server Name Indication (SNI). Supports:
160/// - Dynamic certificate updates
161/// - Wildcard certificates
162/// - Default fallback certificates
163/// - Self-signed CA certificates
164#[derive(Clone)]
165pub struct GlobalCertificate {
166    provider: Arc<dyn CertificateProvider>,
167}
168
169impl GlobalCertificate {
170    pub fn new(provider: Arc<dyn CertificateProvider>) -> Self {
171        Self { provider }
172    }
173    /// New a dynamic certificate from tls setting parameters
174    pub fn new_tls_settings(
175        &self,
176        params: &TlsSettingParams,
177    ) -> Result<TlsSettings> {
178        let name = params.server_name.clone();
179        let mut tls_settings = TlsSettings::with_callbacks(Box::new(
180            self.clone(),
181        ))
182        .map_err(|e| Error::Invalid {
183            category: "new_tls_settings".to_string(),
184            message: e.to_string(),
185        })?;
186        if params.enabled_h2 {
187            tls_settings.enable_h2();
188        }
189        if let Some(cipher_list) = &params.cipher_list
190            && let Err(e) = tls_settings.set_cipher_list(cipher_list)
191        {
192            error!(target: LOG_TARGET, error = %e, name, "set cipher list fail");
193        }
194        if let Some(cipher_suites) = &params.cipher_suites
195            && let Err(e) = tls_settings.set_ciphersuites(cipher_suites)
196        {
197            error!(target: LOG_TARGET, error = %e, name, "set cipher suites fail");
198        }
199        if let Some(version) = convert_tls_version(&params.tls_min_version) {
200            if let Err(e) = tls_settings.set_min_proto_version(Some(version)) {
201                error!(target: LOG_TARGET, error = %e, name, "set tls min proto version fail");
202            }
203            if version == pingora::tls::ssl::SslVersion::TLS1_1 {
204                tls_settings.set_security_level(0);
205                tls_settings
206                    .clear_options(pingora::tls::ssl::SslOptions::NO_TLSV1_1);
207            }
208        }
209        if let Err(e) = tls_settings
210            .set_max_proto_version(convert_tls_version(&params.tls_max_version))
211        {
212            error!(target: LOG_TARGET, error = %e, name, "set tls max proto version fail");
213        }
214
215        if let Some(min_version) = tls_settings.min_proto_version() {
216            info!(
217                target: LOG_TARGET,
218                name,
219                min_version = format!("{min_version:?}"),
220                "tls proto"
221            );
222        }
223        if let Some(max_version) = tls_settings.max_proto_version() {
224            info!(
225                target: LOG_TARGET,
226                name,
227                max_version = format!("{max_version:?}"),
228                "tls proto"
229            );
230        }
231
232        Ok(tls_settings)
233    }
234}
235
236#[async_trait]
237impl pingora::listeners::TlsAccept for GlobalCertificate {
238    async fn certificate_callback(&self, ssl: &mut SslRef) {
239        // Certificate selection process:
240        // 1. Extract SNI from TLS handshake
241        // 2. Try exact domain match (example.com)
242        // 3. Try wildcard domain match (*.example.com)
243        // 4. Fall back to default certificate (DEFAULT_SERVER_NAME)
244        // 5. Handle special case for CA certificates (self-signed)
245        // 6. Apply certificate, private key, and chain to SSL context
246
247        let sni = ssl
248            .servername(NameType::HOST_NAME)
249            .unwrap_or(DEFAULT_SERVER_NAME);
250        // TODO add more debug log
251        debug!(
252            target: LOG_TARGET,
253            ssl = format!("{ssl:?}"),
254            server_name = sni
255        );
256
257        // Optimized lookup sequence.
258        let dynamic_certificate = self.provider.get(sni);
259
260        let Some(d) = dynamic_certificate else {
261            error!(
262                target: LOG_TARGET,
263                sni,
264                ssl = format!("{ssl:?}"),
265                "no match certificate"
266            );
267            return;
268        };
269
270        // ca
271        if d.is_ca {
272            match d.get_self_signed_certificate(sni) {
273                Ok(result) => {
274                    ssl_certificate(
275                        ssl,
276                        &result.x509,
277                        &result.key,
278                        &d.chain_certificates,
279                    );
280                },
281                Err(err) => {
282                    error!(target: LOG_TARGET, error = %err, "get self signed cert fail");
283                },
284            };
285            return;
286        }
287
288        if let Some((cert, key)) = &d.certificate {
289            ssl_certificate(ssl, cert, key, &d.chain_certificates);
290        }
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297    use pingap_config::CertificateConf;
298    use pretty_assertions::assert_eq;
299
300    fn get_tls_pem() -> (String, String) {
301        // spellchecker:off
302        (
303            r###"-----BEGIN CERTIFICATE-----
304MIID/TCCAmWgAwIBAgIQJUGCkB1VAYha6fGExkx0KTANBgkqhkiG9w0BAQsFADBV
305MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExFTATBgNVBAsMDHZpY2Fu
306c29AdHJlZTEcMBoGA1UEAwwTbWtjZXJ0IHZpY2Fuc29AdHJlZTAeFw0yNDA3MDYw
307MjIzMzZaFw0yNjEwMDYwMjIzMzZaMEAxJzAlBgNVBAoTHm1rY2VydCBkZXZlbG9w
308bWVudCBjZXJ0aWZpY2F0ZTEVMBMGA1UECwwMdmljYW5zb0B0cmVlMIIBIjANBgkq
309hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv5dbylSPQNARrpT/Rn7qZf6JmH3cueMp
310YdOpctuPYeefT0Jdgp67bg17fU5pfyR2BWYdwyvHCNmKqLdYPx/J69hwTiVFMOcw
311lVQJjbzSy8r5r2cSBMMsRaAZopRDnPy7Ls7Ji+AIT4vshUgL55eR7ACuIJpdtUYm
312TzMx9PTA0BUDkit6z7bTMaEbjDmciIBDfepV4goHmvyBJoYMIjnAwnTFRGRs/QJN
313d2ikFq999fRINzTDbRDP1K0Kk6+zYoFAiCMs9lEDymu3RmiWXBXpINR/Sv8CXtz2
3149RTVwTkjyiMOPY99qBfaZTiy+VCjcwTGKPyus1axRMff4xjgOBewOwIDAQABo14w
315XDAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgw
316FoAUhU5Igu3uLUabIqUhUpVXjk1JVtkwFAYDVR0RBA0wC4IJcGluZ2FwLmlvMA0G
317CSqGSIb3DQEBCwUAA4IBgQDBimRKrqnEG65imKriM2QRCEfdB6F/eP9HYvPswuAP
318tvQ6m19/74qbtkd6vjnf6RhMbj9XbCcAJIhRdnXmS0vsBrLDsm2q98zpg6D04F2E
319L++xTiKU6F5KtejXcTHHe23ZpmD2XilwcVDeGFu5BEiFoRH9dmqefGZn3NIwnIeD
320Yi31/cL7BoBjdWku5Qm2nCSWqy12ywbZtQCbgbzb8Me5XZajeGWKb8r6D0Nb+9I9
321OG7dha1L3kxerI5VzVKSiAdGU0C+WcuxfsKAP8ajb1TLOlBaVyilfqmiF457yo/2
322PmTYzMc80+cQWf7loJPskyWvQyfmAnSUX0DI56avXH8LlQ57QebllOtKgMiCo7cr
323CCB2C+8hgRNG9ZmW1KU8rxkzoddHmSB8d6+vFqOajxGdyOV+aX00k3w6FgtHOoKD
324Ztdj1N0eTfn02pibVcXXfwESPUzcjERaMAGg1hoH1F4Gxg0mqmbySAuVRqNLnXp5
325CRVQZGgOQL6WDg3tUUDXYOs=
326-----END CERTIFICATE-----"###
327                .to_string(),
328            r###"-----BEGIN PRIVATE KEY-----
329MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/l1vKVI9A0BGu
330lP9Gfupl/omYfdy54ylh06ly249h559PQl2CnrtuDXt9Tml/JHYFZh3DK8cI2Yqo
331t1g/H8nr2HBOJUUw5zCVVAmNvNLLyvmvZxIEwyxFoBmilEOc/LsuzsmL4AhPi+yF
332SAvnl5HsAK4gml21RiZPMzH09MDQFQOSK3rPttMxoRuMOZyIgEN96lXiCgea/IEm
333hgwiOcDCdMVEZGz9Ak13aKQWr3319Eg3NMNtEM/UrQqTr7NigUCIIyz2UQPKa7dG
334aJZcFekg1H9K/wJe3Pb1FNXBOSPKIw49j32oF9plOLL5UKNzBMYo/K6zVrFEx9/j
335GOA4F7A7AgMBAAECggEAWNDkx2XtxsDuAX2m3VpGdSPLS3rFURMCgwwpGEq6LEvA
336qXB9gujswHbVkWBBPaR8ZcJR98EaknquccoUyaaF56Q9Y6yZZ7M07XS4vREUs06T
3378wEX9Ec6BcjTOW/77BGpAGjyO7qOf7nA2oRsqF62Ua57CjglSryLU9nKxeCUZaEa
338HWbpn/AVieddIBdCSK1ANFgXb1ySA3Rh2IaMggql1n2+gk2s4qyAScarNSz0PDps
339v65iK1ZAABmQEItsklBE8XddIK0BE5ciaLShK+BLX/bnPjCle2QGdDOtbNKfn3Ab
3408gMmY9q4/isO0i8njeNWtgrmOKpL8ETxbzCDGwqdEQKBgQDxe3nuxeDJSXUaj4Vl
341LMJ+jln8AZTEegt5T0lm3kke4vJTyQAjwCtWrxB8xario5uWwf0Np/NvLvqJI7e4
342+KIJF/5Vy15QngUHJ0c5D8Fm0DufWI9btuZDG3EYeqs4NRbc1Vu+QBziwZXvemkU
3432hHwnVYn3lc2WKgiEXcLf2SAQwKBgQDLHAkc9JzWOnj6YIb/WWLGQxu7kVW6T3Fr
344f+c4IZN9IhbjxrRilMG0Z/kQDX8dD2b3suOD+QjBZ1rJR34xDVGPPhbHx+3j+2rK
345piUZLPAqk+vODHlx9ST9V7RklZnsitQpxZLI5OhylIKXkTk6I92jDUJNRF9ooeoV
346zi2FHQasqQKBgFJg0g7PeEiSg51k+peyNkNgInhivbJtA/8FOkAaco1T1GEav65y
347fxZaMGCwOgSI1aoPUVlYQyZZu2QPSDyUrQo3Ii94ahtMXOC82IIxysNdJAnO91DN
348Sy33bZRxPHm3Oq5pJpv3WSNN8O06MCDJ57bSpbKCGfRTOEAu/xJwCgPrAoGBALtv
349GN3WwvFTrpboA0yb8XIjNfGHMkSn0XQx6W+8VH5SuirjEU40FvnkRUzSF676qrwF
350Ir6ET9cjCP3ccxDTSKPW2XDuCJOuTaPLZUrxVIUGUsKocl5+qu78Q+XaxNwsVZRi
3511o176SLr+APlKZmExaEVuEzTvvQxD3Ol/A3udl1ZAoGBAKztzGZc2YG5nw62kJ8J
3521XBrQG1rWuAMgrVbo/aDnPs04E31tPEOrZ2m7pKr/uGmf74OQeQrUaQ0+A5YZxrD
353vmkKQHwfyX6cFGxuXwyCZa7q1E83qFNLPSZ0ZF8DHiJqeunLchxYm4uA4Y8BO1jK
354aqcrKJfS+xaKWxXPiNlpBMG5
355-----END PRIVATE KEY-----"###
356                .to_string(),
357        )
358        // spellchecker:on
359    }
360
361    #[test]
362    fn test_convert_tls_version() {
363        assert_eq!(
364            SslVersion::TLS1_1,
365            convert_tls_version(&Some("tlsv1.1".to_string())).unwrap()
366        );
367        assert_eq!(
368            SslVersion::TLS1_2,
369            convert_tls_version(&Some("tlsv1.2".to_string())).unwrap()
370        );
371        assert_eq!(
372            SslVersion::TLS1_3,
373            convert_tls_version(&Some("tlsv1.3".to_string())).unwrap()
374        );
375    }
376
377    #[test]
378    fn test_parse_certificate() {
379        let (tls_cert, tls_key) = get_tls_pem();
380        let cert_info = CertificateConf {
381            tls_cert: Some(tls_cert),
382            tls_key: Some(tls_key),
383            is_default: Some(true),
384            ..Default::default()
385        };
386        let dynamic_certificate: TlsCertificate =
387            (&cert_info).try_into().unwrap();
388        let info = dynamic_certificate.info.unwrap_or_default();
389
390        assert_eq!("pingap.io", dynamic_certificate.domains.join(","));
391        assert_eq!(
392            "O=mkcert development CA, OU=vicanso@tree, CN=mkcert vicanso@tree",
393            info.issuer
394        );
395        assert_eq!(1720232616, info.not_before);
396        assert_eq!(1791253416, info.not_after);
397        assert_eq!(true, dynamic_certificate.certificate.is_some());
398    }
399}