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,
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
275pub fn calculate_fingerprint_from_der(certificate: &[u8]) -> Vec<u8> {
276    let fingerprint: Vec<u8> = Sha256::digest(certificate).iter().cloned().collect();
277    // POST: a SHA-256 digest is unconditionally 32 bytes. Anything else
278    // means the digest collection went wrong and downstream fingerprint
279    // comparison / map keying would silently mismatch.
280    debug_assert_eq!(
281        fingerprint.len(),
282        SHA256_FINGERPRINT_LEN,
283        "SHA-256 fingerprint must be exactly 32 bytes"
284    );
285    fingerprint
286}
287
288/// Compute fingerprint from a certificate that is encoded in pem format
289pub fn calculate_fingerprint(certificate: &[u8]) -> Result<Vec<u8>, CertificateError> {
290    let parsed_certificate = parse_pem(certificate)?;
291    let fingerprint = calculate_fingerprint_from_der(&parsed_certificate.contents);
292    // POST: the result is a SHA-256 digest and recomputing it over the same
293    // DER bytes yields the same value — the fingerprint is a pure function of
294    // the parsed certificate contents, never of the surrounding PEM framing.
295    debug_assert_eq!(
296        fingerprint.len(),
297        SHA256_FINGERPRINT_LEN,
298        "PEM fingerprint must be a 32-byte SHA-256 digest"
299    );
300    debug_assert_eq!(
301        fingerprint,
302        calculate_fingerprint_from_der(&parsed_certificate.contents),
303        "fingerprint must be a deterministic function of the DER contents"
304    );
305    Ok(fingerprint)
306}
307
308pub fn split_certificate_chain(mut chain: String) -> Vec<String> {
309    let mut v = Vec::new();
310
311    let end = "-----END CERTIFICATE-----";
312    // PRE: the loop consumes exactly one END marker per iteration, so the
313    // final chain length must equal the number of markers present on entry.
314    // The leaf certificate is, by PEM convention, the first block — it lands
315    // at index 0 because we drain from the front. (`matches().count()` is
316    // overlap-free; the END marker cannot overlap itself.)
317    let expected_certs = chain.matches(end).count();
318    loop {
319        if let Some(sz) = chain.find(end) {
320            let cert: String = chain.drain(..sz + end.len()).collect();
321            // INV: every emitted block carries exactly the END marker that
322            // terminated it — the drain range includes `end.len()` bytes past
323            // the match, so a non-terminated trailing block is impossible.
324            debug_assert!(
325                cert.contains(end),
326                "each split block must contain its END CERTIFICATE marker"
327            );
328            v.push(cert.trim().to_string());
329            continue;
330        }
331
332        break;
333    }
334
335    // POST: one block per END marker, leaf at index 0.
336    debug_assert_eq!(
337        v.len(),
338        expected_certs,
339        "split must yield exactly one certificate per END marker"
340    );
341    v
342}
343
344pub fn get_fingerprint_from_certificate_path(
345    certificate_path: &str,
346) -> Result<Fingerprint, CertificateError> {
347    let bytes =
348        Config::load_file_bytes(certificate_path).map_err(|e| CertificateError::LoadFile {
349            path: certificate_path.to_string(),
350            error: Box::new(e),
351        })?;
352
353    let parsed_bytes = calculate_fingerprint(&bytes)?;
354
355    // POST: a computed fingerprint is always a 32-byte SHA-256 digest. (This
356    // is distinct from a *parsed* fingerprint — see `decode_fingerprint` /
357    // `Fingerprint::from_str` — which may be any operator-supplied length.)
358    debug_assert_eq!(
359        parsed_bytes.len(),
360        SHA256_FINGERPRINT_LEN,
361        "fingerprint loaded from a certificate path must be 32 bytes"
362    );
363    Ok(Fingerprint(parsed_bytes))
364}
365
366pub fn decode_fingerprint(fingerprint: &str) -> Result<Fingerprint, CertificateError> {
367    let bytes = hex::decode(fingerprint).map_err(CertificateError::DecodeError)?;
368    Ok(Fingerprint(bytes))
369}
370
371pub fn load_full_certificate(
372    certificate_path: &str,
373    certificate_chain_path: &str,
374    key_path: &str,
375    versions: Vec<TlsVersion>,
376    names: Vec<String>,
377) -> Result<CertificateAndKey, CertificateError> {
378    let certificate =
379        Config::load_file(certificate_path).map_err(|e| CertificateError::LoadFile {
380            path: certificate_path.to_string(),
381            error: Box::new(e),
382        })?;
383
384    let certificate_chain = Config::load_file(certificate_chain_path)
385        .map(split_certificate_chain)
386        .map_err(|e| CertificateError::LoadFile {
387            path: certificate_chain_path.to_string(),
388            error: Box::new(e),
389        })?;
390
391    let key = Config::load_file(key_path).map_err(|e| CertificateError::LoadFile {
392        path: key_path.to_string(),
393        error: Box::new(e),
394    })?;
395
396    let versions_len = versions.len();
397    let names_len = names.len();
398    let versions: Vec<i32> = versions.iter().map(|v| *v as i32).collect();
399
400    // POST: the i32-encoded TLS-version list is a 1:1 map of the input — no
401    // version is dropped or duplicated by the `as i32` projection.
402    debug_assert_eq!(
403        versions.len(),
404        versions_len,
405        "version encoding must preserve the input cardinality"
406    );
407
408    let built = CertificateAndKey {
409        certificate,
410        certificate_chain,
411        key,
412        versions,
413        names,
414    };
415
416    // POST: the routing-name list is carried through verbatim — the builder
417    // does not synthesize or drop names (overriding names are derived later
418    // via `apply_overriding_names`, never here).
419    debug_assert_eq!(
420        built.names.len(),
421        names_len,
422        "names must be carried through the builder unchanged"
423    );
424    Ok(built)
425}
426
427impl CertificateAndKey {
428    pub fn fingerprint(&self) -> Result<Fingerprint, CertificateError> {
429        let pem = parse_pem(self.certificate.as_bytes())?;
430        let fingerprint = Fingerprint(Sha256::digest(&pem.contents).iter().cloned().collect());
431        // POST: the certificate fingerprint is a 32-byte SHA-256 digest of the
432        // DER contents and agrees with the free-function recompute over the
433        // same bytes — the two fingerprint paths must never diverge or a cert
434        // would key into two different map slots.
435        debug_assert_eq!(
436            fingerprint.0.len(),
437            SHA256_FINGERPRINT_LEN,
438            "CertificateAndKey fingerprint must be 32 bytes"
439        );
440        debug_assert_eq!(
441            fingerprint.0,
442            calculate_fingerprint_from_der(&pem.contents),
443            "method and free-function fingerprints must agree on the same DER"
444        );
445        Ok(fingerprint)
446    }
447
448    pub fn get_overriding_names(&self) -> Result<Vec<String>, CertificateError> {
449        if self.names.is_empty() {
450            let pem = parse_pem(self.certificate.as_bytes())?;
451            let x509 = parse_x509(&pem.contents)?;
452
453            let overriding_names = get_cn_and_san_attributes(&x509);
454
455            Ok(overriding_names.into_iter().collect())
456        } else {
457            let names = self.names.to_owned();
458            // POST: when explicit names are set, they are returned verbatim —
459            // the cert is NOT consulted, so the operator's intent is the sole
460            // authority. (`to_owned` preserves both length and order.)
461            debug_assert_eq!(
462                names, self.names,
463                "explicit names must be returned unchanged when present"
464            );
465            Ok(names)
466        }
467    }
468
469    pub fn apply_overriding_names(&mut self) -> Result<(), CertificateError> {
470        let resolved = self.get_overriding_names()?;
471        self.names = resolved.clone();
472        // POST: after applying, the stored names are exactly the resolved set
473        // and re-resolving is idempotent — a second `apply_overriding_names`
474        // would now hit the "explicit names present" branch and be a no-op.
475        debug_assert_eq!(
476            self.names, resolved,
477            "applied names must equal the resolved set"
478        );
479        Ok(())
480    }
481}