Skip to main content

sozu_command_lib/
certificate.rs

1use std::{fmt, str::FromStr};
2
3use hex::{FromHex, FromHexError};
4use serde::de::{self, Visitor};
5use sha2::{Digest, Sha256};
6use x509_parser::{
7    certificate::X509Certificate,
8    extensions::{GeneralName, ParsedExtension},
9    oid_registry::{OID_X509_COMMON_NAME, OID_X509_EXT_SUBJECT_ALT_NAME},
10    parse_x509_certificate, parse_x509_crl,
11    pem::{Pem, parse_x509_pem},
12};
13
14use crate::{
15    config::{Config, ConfigError},
16    proto::command::{CertificateAndKey, TlsVersion},
17};
18
19/// Byte length of a SHA-256 digest. Every fingerprint Sōzu *computes*
20/// (as opposed to one it *parses* from operator hex, which may be any
21/// length) is a SHA-256 hash and therefore exactly this many bytes.
22///
23/// Intentionally NOT `#[cfg(debug_assertions)]`-gated: `debug_assert!`
24/// compiles its arguments in every profile (it gates execution, not
25/// compilation), so a cfg-gated const referenced inside one would fail
26/// the release build with E0425. Ungated, it is dead code in release and
27/// dropped by the optimizer.
28#[allow(dead_code)]
29const SHA256_FINGERPRINT_LEN: usize = 32;
30
31// -----------------------------------------------------------------------------
32// CertificateError
33
34#[derive(thiserror::Error, Debug)]
35pub enum CertificateError {
36    #[error("Could not parse PEM certificate from bytes: {0}")]
37    ParsePEMCertificate(String),
38    #[error("Could not parse X509 certificate from bytes: {0}")]
39    ParseX509Certificate(String),
40    #[error("failed to parse tls version '{0}'")]
41    InvalidTlsVersion(String),
42    #[error("failed to parse fingerprint, {0}")]
43    InvalidFingerprint(FromHexError),
44    // `ConfigError` is boxed to keep `CertificateError` small: this enum is
45    // embedded by value in `sozu-lib`'s `ListenerError`/`ProxyError`, and an
46    // unboxed `ConfigError` (which grows with every new config-validation
47    // variant) pushed those past clippy's `result_large_err` 128-byte
48    // threshold at 42 call sites. Same pattern as
49    // `ProxyError::WrongInputFrontend { front: Box<RequestHttpFrontend> }`.
50    #[error("could not load file on path {path}: {error}")]
51    LoadFile {
52        path: String,
53        error: Box<ConfigError>,
54    },
55    #[error("Failed at decoding the hex encoded certificate: {0}")]
56    DecodeError(FromHexError),
57}
58
59// -----------------------------------------------------------------------------
60// parse
61
62/// parse a pem file encoded as binary and convert it into the right structure
63/// (a.k.a [`Pem`])
64pub fn parse_pem(certificate: &[u8]) -> Result<Pem, CertificateError> {
65    let (_, pem) = parse_x509_pem(certificate)
66        .map_err(|err| CertificateError::ParsePEMCertificate(err.to_string()))?;
67
68    Ok(pem)
69}
70
71/// parse x509 certificate from PEM bytes
72pub fn parse_x509(pem_bytes: &[u8]) -> Result<X509Certificate<'_>, CertificateError> {
73    parse_x509_certificate(pem_bytes)
74        .map_err(|nom_e| CertificateError::ParseX509Certificate(nom_e.to_string()))
75        .map(|t| t.1)
76}
77
78// -----------------------------------------------------------------------------
79// get_cn_and_san_attributes
80
81/// Retrieve the certificate's authoritative DNS identities for routing.
82///
83/// Per RFC 6125 §6.4.4: when the SubjectAlternativeName extension contains
84/// at least one `dNSName` entry, the SAN entries are the sole authoritative
85/// identities and the Common Name is ignored. The CN is only honoured as a
86/// fallback when the certificate omits the SAN extension entirely or
87/// declares it without a `dNSName` (e.g. SAN with only `iPAddress` /
88/// `rfc822Name` / `directoryName` entries — uncommon, but legal).
89///
90/// Aligns Sōzu's coalescing trust boundary with browser implementations
91/// (Firefox / Chrome both stopped honouring CN for hostname verification
92/// circa 2017) so a cert with `CN=tenant-b.example` and `SAN=tenant-a.example`
93/// cannot smuggle `tenant-b.example` into the routing authority list.
94pub fn get_cn_and_san_attributes(x509: &X509Certificate) -> Vec<String> {
95    let mut names: Vec<String> = Vec::new();
96    let mut san_dns_seen = false;
97
98    for extension in x509.extensions() {
99        if extension.oid == OID_X509_EXT_SUBJECT_ALT_NAME
100            && let ParsedExtension::SubjectAlternativeName(san) = extension.parsed_extension()
101        {
102            for name in &san.general_names {
103                if let GeneralName::DNSName(name) = name {
104                    san_dns_seen = true;
105                    names.push(name.to_string());
106                }
107            }
108        }
109    }
110
111    // POST: a dNSName SAN entry was observed iff at least one name has been
112    // collected from the SAN branch. `san_dns_seen` and a non-empty `names`
113    // must agree before the CN fallback runs — otherwise the RFC 6125
114    // §6.4.4 trust boundary (SAN dNSName is authoritative when present) is
115    // broken and the CN could smuggle an extra identity below.
116    debug_assert_eq!(
117        san_dns_seen,
118        !names.is_empty(),
119        "SAN dNSName presence must match the collected-names state before CN fallback"
120    );
121
122    if !san_dns_seen {
123        for name in x509.subject().iter_by_oid(&OID_X509_COMMON_NAME) {
124            names.push(
125                name.as_str()
126                    .map(String::from)
127                    .unwrap_or_else(|_| String::from_utf8_lossy(name.as_slice()).to_string()),
128            );
129        }
130    }
131    let before_dedup = names.len();
132    names.dedup();
133    // POST: dedup only removes *consecutive* equal entries, so it can never
134    // grow the list; the deduped length is an invariant upper bound.
135    debug_assert!(
136        names.len() <= before_dedup,
137        "dedup must not grow the identity list"
138    );
139    names
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    /// RFC 6125 §6.4.4: when SAN contains at least one dNSName, the CN is
147    /// ignored. A cert with `CN=tenant-b.example` and SAN `tenant-a.example`
148    /// is authoritative for `tenant-a.example` only.
149    #[test]
150    fn san_dns_present_excludes_cn() {
151        let pem = parse_pem(include_str!("../../lib/assets/cn-ne-san-cert.pem").as_bytes())
152            .expect("parse PEM");
153        let x509 = parse_x509(&pem.contents).expect("parse x509");
154        let names = get_cn_and_san_attributes(&x509);
155        assert_eq!(names, vec![String::from("tenant-a.example")]);
156    }
157
158    /// Fallback: SAN extension absent (no dNSName entries) ⇒ CN is honoured.
159    /// `lib/assets/certificate.pem` (CN=lolcatho.st, no SAN extension) is the
160    /// canonical fixture for this branch.
161    #[test]
162    fn cn_used_when_san_absent() {
163        let pem = parse_pem(include_str!("../../lib/assets/certificate.pem").as_bytes())
164            .expect("parse PEM");
165        let x509 = parse_x509(&pem.contents).expect("parse x509");
166        let names = get_cn_and_san_attributes(&x509);
167        assert_eq!(names, vec![String::from("lolcatho.st")]);
168    }
169
170    /// SAN dNSName present and CN ∈ SAN ⇒ the resulting list is the SAN
171    /// dNSName set verbatim (dedup removes the duplicate CN entry from the
172    /// pre-fix code path; the post-fix code never inserts the CN at all,
173    /// so the same list is observed but via a tighter path).
174    #[test]
175    fn san_dns_present_cn_is_san_member() {
176        let pem = parse_pem(include_str!("../../lib/assets/multi-sni-cert.pem").as_bytes())
177            .expect("parse PEM");
178        let x509 = parse_x509(&pem.contents).expect("parse x509");
179        let names = get_cn_and_san_attributes(&x509);
180        assert!(names.contains(&String::from("foo.example.com")));
181        assert!(names.contains(&String::from("bar.example.com")));
182        assert!(names.contains(&String::from("baz.example.com")));
183        assert!(names.contains(&String::from("localhost")));
184        assert_eq!(names.len(), 4);
185    }
186}
187
188// -----------------------------------------------------------------------------
189// TlsVersion
190
191impl FromStr for TlsVersion {
192    type Err = CertificateError;
193
194    fn from_str(s: &str) -> Result<Self, Self::Err> {
195        match s {
196            "SSL_V2" => Ok(TlsVersion::SslV2),
197            "SSL_V3" => Ok(TlsVersion::SslV3),
198            "TLSv1" => Ok(TlsVersion::TlsV10),
199            "TLS_V11" => Ok(TlsVersion::TlsV11),
200            "TLS_V12" => Ok(TlsVersion::TlsV12),
201            "TLS_V13" => Ok(TlsVersion::TlsV13),
202            _ => Err(CertificateError::InvalidTlsVersion(s.to_string())),
203        }
204    }
205}
206
207// -----------------------------------------------------------------------------
208// Fingerprint
209
210//FIXME: make fixed size depending on hash algorithm
211/// A TLS certificates, encoded in bytes
212#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
213pub struct Fingerprint(pub Vec<u8>);
214
215impl FromStr for Fingerprint {
216    type Err = CertificateError;
217
218    fn from_str(s: &str) -> Result<Self, Self::Err> {
219        hex::decode(s)
220            .map_err(CertificateError::InvalidFingerprint)
221            .map(Fingerprint)
222    }
223}
224
225impl fmt::Debug for Fingerprint {
226    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
227        write!(f, "CertificateFingerprint({})", hex::encode(&self.0))
228    }
229}
230
231impl fmt::Display for Fingerprint {
232    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
233        write!(f, "{}", hex::encode(&self.0))
234    }
235}
236
237impl serde::Serialize for Fingerprint {
238    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
239    where
240        S: serde::Serializer,
241    {
242        serializer.serialize_str(&hex::encode(&self.0))
243    }
244}
245
246struct FingerprintVisitor;
247
248impl Visitor<'_> for FingerprintVisitor {
249    type Value = Fingerprint;
250
251    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
252        formatter.write_str("the certificate fingerprint must be in hexadecimal format")
253    }
254
255    fn visit_str<E>(self, value: &str) -> Result<Fingerprint, E>
256    where
257        E: de::Error,
258    {
259        FromHex::from_hex(value)
260            .map_err(|e| E::custom(format!("could not deserialize hex: {e:?}")))
261            .map(Fingerprint)
262    }
263}
264
265impl<'de> serde::Deserialize<'de> for Fingerprint {
266    fn deserialize<D>(deserializer: D) -> Result<Fingerprint, D::Error>
267    where
268        D: serde::de::Deserializer<'de>,
269    {
270        deserializer.deserialize_str(FingerprintVisitor {})
271    }
272}
273
274/// Compute fingerprint from decoded pem as binary value
275/// The `nextUpdate` of a DER-encoded certificate revocation list, in seconds
276/// since the Unix epoch. `None` when the CRL does not parse or carries no
277/// `nextUpdate`.
278pub fn crl_next_update(crl_der: &[u8]) -> Option<i64> {
279    let (_, crl) = parse_x509_crl(crl_der).ok()?;
280    crl.next_update().map(|next_update| next_update.timestamp())
281}
282
283pub fn calculate_fingerprint_from_der(certificate: &[u8]) -> Vec<u8> {
284    let fingerprint: Vec<u8> = Sha256::digest(certificate).iter().cloned().collect();
285    // POST: a SHA-256 digest is unconditionally 32 bytes. Anything else
286    // means the digest collection went wrong and downstream fingerprint
287    // comparison / map keying would silently mismatch.
288    debug_assert_eq!(
289        fingerprint.len(),
290        SHA256_FINGERPRINT_LEN,
291        "SHA-256 fingerprint must be exactly 32 bytes"
292    );
293    fingerprint
294}
295
296/// Compute fingerprint from a certificate that is encoded in pem format
297pub fn calculate_fingerprint(certificate: &[u8]) -> Result<Vec<u8>, CertificateError> {
298    let parsed_certificate = parse_pem(certificate)?;
299    let fingerprint = calculate_fingerprint_from_der(&parsed_certificate.contents);
300    // POST: the result is a SHA-256 digest and recomputing it over the same
301    // DER bytes yields the same value — the fingerprint is a pure function of
302    // the parsed certificate contents, never of the surrounding PEM framing.
303    debug_assert_eq!(
304        fingerprint.len(),
305        SHA256_FINGERPRINT_LEN,
306        "PEM fingerprint must be a 32-byte SHA-256 digest"
307    );
308    debug_assert_eq!(
309        fingerprint,
310        calculate_fingerprint_from_der(&parsed_certificate.contents),
311        "fingerprint must be a deterministic function of the DER contents"
312    );
313    Ok(fingerprint)
314}
315
316pub fn split_certificate_chain(mut chain: String) -> Vec<String> {
317    let mut v = Vec::new();
318
319    let end = "-----END CERTIFICATE-----";
320    // PRE: the loop consumes exactly one END marker per iteration, so the
321    // final chain length must equal the number of markers present on entry.
322    // The leaf certificate is, by PEM convention, the first block — it lands
323    // at index 0 because we drain from the front. (`matches().count()` is
324    // overlap-free; the END marker cannot overlap itself.)
325    let expected_certs = chain.matches(end).count();
326    loop {
327        if let Some(sz) = chain.find(end) {
328            let cert: String = chain.drain(..sz + end.len()).collect();
329            // INV: every emitted block carries exactly the END marker that
330            // terminated it — the drain range includes `end.len()` bytes past
331            // the match, so a non-terminated trailing block is impossible.
332            debug_assert!(
333                cert.contains(end),
334                "each split block must contain its END CERTIFICATE marker"
335            );
336            v.push(cert.trim().to_string());
337            continue;
338        }
339
340        break;
341    }
342
343    // POST: one block per END marker, leaf at index 0.
344    debug_assert_eq!(
345        v.len(),
346        expected_certs,
347        "split must yield exactly one certificate per END marker"
348    );
349    v
350}
351
352pub fn get_fingerprint_from_certificate_path(
353    certificate_path: &str,
354) -> Result<Fingerprint, CertificateError> {
355    let bytes =
356        Config::load_file_bytes(certificate_path).map_err(|e| CertificateError::LoadFile {
357            path: certificate_path.to_string(),
358            error: Box::new(e),
359        })?;
360
361    let parsed_bytes = calculate_fingerprint(&bytes)?;
362
363    // POST: a computed fingerprint is always a 32-byte SHA-256 digest. (This
364    // is distinct from a *parsed* fingerprint — see `decode_fingerprint` /
365    // `Fingerprint::from_str` — which may be any operator-supplied length.)
366    debug_assert_eq!(
367        parsed_bytes.len(),
368        SHA256_FINGERPRINT_LEN,
369        "fingerprint loaded from a certificate path must be 32 bytes"
370    );
371    Ok(Fingerprint(parsed_bytes))
372}
373
374pub fn decode_fingerprint(fingerprint: &str) -> Result<Fingerprint, CertificateError> {
375    let bytes = hex::decode(fingerprint).map_err(CertificateError::DecodeError)?;
376    Ok(Fingerprint(bytes))
377}
378
379pub fn load_full_certificate(
380    certificate_path: &str,
381    certificate_chain_path: &str,
382    key_path: &str,
383    versions: Vec<TlsVersion>,
384    names: Vec<String>,
385) -> Result<CertificateAndKey, CertificateError> {
386    let certificate =
387        Config::load_file(certificate_path).map_err(|e| CertificateError::LoadFile {
388            path: certificate_path.to_string(),
389            error: Box::new(e),
390        })?;
391
392    let certificate_chain = Config::load_file(certificate_chain_path)
393        .map(split_certificate_chain)
394        .map_err(|e| CertificateError::LoadFile {
395            path: certificate_chain_path.to_string(),
396            error: Box::new(e),
397        })?;
398
399    let key = Config::load_file(key_path).map_err(|e| CertificateError::LoadFile {
400        path: key_path.to_string(),
401        error: Box::new(e),
402    })?;
403
404    let versions_len = versions.len();
405    let names_len = names.len();
406    let versions: Vec<i32> = versions.iter().map(|v| *v as i32).collect();
407
408    // POST: the i32-encoded TLS-version list is a 1:1 map of the input — no
409    // version is dropped or duplicated by the `as i32` projection.
410    debug_assert_eq!(
411        versions.len(),
412        versions_len,
413        "version encoding must preserve the input cardinality"
414    );
415
416    let built = CertificateAndKey {
417        certificate,
418        certificate_chain,
419        key,
420        versions,
421        names,
422    };
423
424    // POST: the routing-name list is carried through verbatim — the builder
425    // does not synthesize or drop names (overriding names are derived later
426    // via `apply_overriding_names`, never here).
427    debug_assert_eq!(
428        built.names.len(),
429        names_len,
430        "names must be carried through the builder unchanged"
431    );
432    Ok(built)
433}
434
435impl CertificateAndKey {
436    pub fn fingerprint(&self) -> Result<Fingerprint, CertificateError> {
437        let pem = parse_pem(self.certificate.as_bytes())?;
438        let fingerprint = Fingerprint(Sha256::digest(&pem.contents).iter().cloned().collect());
439        // POST: the certificate fingerprint is a 32-byte SHA-256 digest of the
440        // DER contents and agrees with the free-function recompute over the
441        // same bytes — the two fingerprint paths must never diverge or a cert
442        // would key into two different map slots.
443        debug_assert_eq!(
444            fingerprint.0.len(),
445            SHA256_FINGERPRINT_LEN,
446            "CertificateAndKey fingerprint must be 32 bytes"
447        );
448        debug_assert_eq!(
449            fingerprint.0,
450            calculate_fingerprint_from_der(&pem.contents),
451            "method and free-function fingerprints must agree on the same DER"
452        );
453        Ok(fingerprint)
454    }
455
456    pub fn get_overriding_names(&self) -> Result<Vec<String>, CertificateError> {
457        if self.names.is_empty() {
458            let pem = parse_pem(self.certificate.as_bytes())?;
459            let x509 = parse_x509(&pem.contents)?;
460
461            let overriding_names = get_cn_and_san_attributes(&x509);
462
463            Ok(overriding_names.into_iter().collect())
464        } else {
465            let names = self.names.to_owned();
466            // POST: when explicit names are set, they are returned verbatim —
467            // the cert is NOT consulted, so the operator's intent is the sole
468            // authority. (`to_owned` preserves both length and order.)
469            debug_assert_eq!(
470                names, self.names,
471                "explicit names must be returned unchanged when present"
472            );
473            Ok(names)
474        }
475    }
476
477    pub fn apply_overriding_names(&mut self) -> Result<(), CertificateError> {
478        let resolved = self.get_overriding_names()?;
479        self.names = resolved.clone();
480        // POST: after applying, the stored names are exactly the resolved set
481        // and re-resolving is idempotent — a second `apply_overriding_names`
482        // would now hit the "explicit names present" branch and be a no-op.
483        debug_assert_eq!(
484            self.names, resolved,
485            "applied names must equal the resolved set"
486        );
487        Ok(())
488    }
489}