Skip to main content

cert_helper/
certificate.rs

1mod builder;
2mod common;
3mod csr;
4mod key;
5mod policy;
6mod usage;
7use builder::select_hash;
8pub use builder::{BuilderCommon, BuilderFields, HashAlg, UseesBuilderFields};
9use common::{ValidityBound, set_validity};
10pub use common::{X509Common, X509Parts};
11pub use csr::{Csr, CsrBuilder, CsrOptions, CsrX509Common};
12pub use key::KeyType;
13pub(crate) use key::is_digestless_key;
14#[cfg(feature = "pqc")]
15use key::reject_mlkem_signing;
16use key::{select_key, sign_certificate_digestless};
17use openssl::asn1::{Asn1Object, Asn1OctetString, Asn1Time};
18use openssl::bn::BigNum;
19use openssl::error::ErrorStack;
20use openssl::hash::{MessageDigest, hash};
21use openssl::nid::Nid;
22use openssl::pkey::{PKey, Private};
23use openssl::stack::Stack;
24use openssl::x509::extension::{AuthorityKeyIdentifier, BasicConstraints, SubjectAlternativeName};
25use openssl::x509::{
26    X509, X509Builder, X509Extension, X509NameBuilder, X509StoreContext, store::X509StoreBuilder,
27};
28pub use policy::CertificatePolicy;
29use policy::append_certificate_policies;
30use std::collections::{HashMap, HashSet};
31use std::marker::PhantomData;
32use std::net::IpAddr;
33use std::path::Path;
34pub use usage::Usage;
35use usage::get_key_usage;
36#[cfg(feature = "pqc")]
37use usage::validate_pqc_key_usage;
38use x509_parser::extensions::ParsedExtension;
39use x509_parser::parse_x509_certificate;
40
41/// Typestate marker for a [`CertBuilder`] with **no** path length set.
42///
43/// This is the default state ([`CertBuilder::new`] returns
44/// `CertBuilder<PathLenUnset>`). In this state the builder can self-sign or sign
45/// without a chain. Calling [`CertBuilder::pathlen`] transitions it to
46/// [`PathLenSet`].
47pub struct PathLenUnset;
48
49/// Typestate marker for a [`CertBuilder`] with a path length set via
50/// [`CertBuilder::pathlen`].
51///
52/// In this state the builder must be signed with
53/// [`CertBuilder::build_and_sign_with_chain`], so the requested path length can be
54/// validated against the signer's chain. The plain `build_and_sign` is not
55/// available, preventing a CA from being issued without that check.
56pub struct PathLenSet;
57
58/// Holds the generated X.509 certificate and its associated private key.
59///
60/// `Debug` is derived; the `pkey` field prints opaquely (openssl's `PKey`
61/// `Debug` does not expose key material), so it is safe to log a `Certificate`.
62#[derive(Clone, Debug)]
63pub struct Certificate {
64    /// The X.509 certificate.
65    pub x509: X509,
66    /// The private key used to generate or sign the certificate.
67    ///
68    /// This is optional to allow for cases where the key is stored or managed separately.
69    pub pkey: Option<PKey<Private>>,
70}
71
72impl X509Parts for Certificate {
73    fn get_pem(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
74        Ok(self.x509.to_pem()?)
75    }
76
77    fn get_private_key(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
78        match self.pkey {
79            Some(ref pkey) => Ok(pkey.private_key_to_pem_pkcs8()?),
80            _ => Err("No private key found".into()),
81        }
82    }
83    fn pem_extension(&self) -> &'static str {
84        "_cert.pem"
85    }
86}
87
88/// Helper trait to document that Certificate implements X509Common
89pub trait CertificateX509Common: X509Common {}
90impl CertificateX509Common for Certificate {}
91
92impl Certificate {
93    /// Loads a certificate and private key that are in PEM format from file
94    /// and creates an X509 and PKey object.
95    pub fn load_cert_and_key<C: AsRef<Path>, K: AsRef<Path>>(
96        cert_pem_file: C,
97        key_pem_file: K,
98    ) -> Result<Self, Box<dyn std::error::Error>> {
99        let cert_pem = std::fs::read(cert_pem_file)?;
100        let key_pem = std::fs::read(key_pem_file)?;
101        let cert = X509::from_pem(&cert_pem)?;
102        let pkey = PKey::private_key_from_pem(&key_pem)?;
103        Ok(Self {
104            x509: cert,
105            pkey: Some(pkey),
106        })
107    }
108
109    /// Loads an X.509 certificate (without a private key) from a PEM file.
110    ///
111    /// The returned [`Certificate`] has `pkey: None`. This is intended for chain
112    /// entries passed to [`CertBuilder::build_and_sign_with_chain`] or
113    /// [`CsrOptions::pathlen`], which only read the certificate, never the key.
114    ///
115    /// # Errors
116    /// Returns an error if the file cannot be read or does not contain a valid
117    /// PEM-encoded X.509 certificate.
118    pub fn load_cert<C: AsRef<Path>>(cert_pem_file: C) -> Result<Self, Box<dyn std::error::Error>> {
119        let cert_pem = std::fs::read(cert_pem_file)?;
120        let cert = X509::from_pem(&cert_pem)?;
121        Ok(Self {
122            x509: cert,
123            pkey: None,
124        })
125    }
126}
127
128/// Builder for creating a new certificate and private key
129pub struct CertBuilder<P = PathLenUnset> {
130    fields: BuilderFields,
131    valid_from: ValidityBound,
132    valid_to: ValidityBound,
133    policies: Vec<CertificatePolicy>,
134    ca: bool,
135    path_len: Option<u32>,
136    _marker: PhantomData<P>,
137}
138
139impl<P> UseesBuilderFields for CertBuilder<P> {
140    fn fields_mut(&mut self) -> &mut BuilderFields {
141        &mut self.fields
142    }
143}
144impl Default for CertBuilder {
145    fn default() -> Self {
146        Self::new()
147    }
148}
149
150impl<P> CertBuilder<P> {
151    /// Add optional certificate policies
152    ///
153    /// # Arguments
154    /// * `policies` - A list of certificate policies
155    pub fn certificate_policies(mut self, policies: Vec<CertificatePolicy>) -> Self {
156        self.policies = policies;
157        self
158    }
159    /// Sets the `yyyy-mm-dd` date from which the certificate is valid.
160    ///
161    /// The date is parsed when the certificate is built, so a malformed value is
162    /// reported as an error from the build step rather than a panic here.
163    pub fn valid_from(mut self, valid_from: &str) -> Self {
164        self.valid_from = ValidityBound::Date(valid_from.to_owned());
165        self
166    }
167    /// Sets the `yyyy-mm-dd` date after which the certificate is no longer valid.
168    ///
169    /// The date is parsed when the certificate is built, so a malformed value is
170    /// reported as an error from the build step rather than a panic here. When
171    /// the certificate is signed by a CA, a date later than the signer's
172    /// `notAfter` is rejected at build time; the default one-year window is
173    /// clamped to the signer's `notAfter` instead.
174    pub fn valid_to(mut self, valid_to: &str) -> Self {
175        self.valid_to = ValidityBound::Date(valid_to.to_owned());
176        self
177    }
178    /// Specifies whether the certificate should be a Certificate Authority (CA).
179    ///
180    /// # Arguments
181    /// * `ca` - `true` if the certificate should be a CA, `false` otherwise.
182    pub fn is_ca(mut self, ca: bool) -> Self {
183        if ca {
184            self.ca = ca;
185            self.fields
186                .set_key_usage(HashSet::from([Usage::certsign, Usage::crlsign]));
187        }
188        self
189    }
190    /// Add optional path length, it is the max number of **non-self-issued
191    /// intermediate CA certs** that may follow this cert in a chain
192    ///
193    /// # Arguments
194    /// * `path_len`- u32
195    pub fn pathlen(self, path_len: u32) -> CertBuilder<PathLenSet> {
196        CertBuilder {
197            fields: self.fields,
198            valid_from: self.valid_from,
199            valid_to: self.valid_to,
200            policies: self.policies,
201            ca: self.ca,
202            path_len: Some(path_len),
203            _marker: PhantomData,
204        }
205    }
206
207    /// Creates a self-signed X.509 certificate and its private key.
208    ///
209    /// # Errors
210    /// Returns an error if a `valid_from`/`valid_to` date is not `yyyy-mm-dd`,
211    /// if key generation fails, or if OpenSSL rejects the certificate contents.
212    pub fn build_and_self_sign(&self) -> Result<Certificate, Box<dyn std::error::Error>> {
213        let (mut builder, pkey) = self.prepare_x509_builder(None)?;
214
215        // ML-KEM keys cannot produce signatures, so a self-signed certificate is
216        // impossible — issue via build_and_sign() with a separate signing CA.
217        #[cfg(feature = "pqc")]
218        reject_mlkem_signing(
219            &pkey,
220            "ML-KEM (FIPS 203) is a key-encapsulation key and cannot produce \
221             signatures, so it cannot self-sign a certificate. Issue an ML-KEM \
222             certificate with CertBuilder::build_and_sign() using a signing CA \
223             (e.g. an ML-DSA or ECDSA CA) instead.",
224        )?;
225
226        let ca_cert: X509 = if is_digestless_key(&pkey) {
227            let build_cert = builder.build();
228            sign_certificate_digestless(&build_cert, &pkey)
229                .map_err(|e| format!("Failed to sign certificate with digestless key: {}", e))?;
230            build_cert
231        } else {
232            builder.sign(&pkey, select_hash(&self.fields.signature_alg))?;
233            builder.build()
234        };
235
236        Ok(Certificate {
237            x509: ca_cert,
238            pkey: Some(pkey),
239        })
240    }
241
242    fn prepare_x509_builder(
243        &self,
244        signer: Option<&Certificate>,
245    ) -> Result<(X509Builder, PKey<Private>), Box<dyn std::error::Error>> {
246        let mut name_builder = X509NameBuilder::new()?;
247        name_builder.append_entry_by_nid(Nid::COMMONNAME, &self.fields.common_name)?;
248        if !self.fields.country_name.trim().is_empty() {
249            name_builder.append_entry_by_nid(Nid::COUNTRYNAME, &self.fields.country_name)?;
250        }
251        if !self.fields.state_province.trim().is_empty() {
252            name_builder
253                .append_entry_by_nid(Nid::STATEORPROVINCENAME, &self.fields.state_province)?;
254        }
255        if !self.fields.locality_time.trim().is_empty() {
256            name_builder.append_entry_by_nid(Nid::LOCALITYNAME, &self.fields.locality_time)?;
257        }
258        if !self.fields.organization.trim().is_empty() {
259            name_builder.append_entry_by_nid(Nid::ORGANIZATIONNAME, &self.fields.organization)?;
260        }
261        if !self.fields.organization_unit.trim().is_empty() {
262            name_builder
263                .append_entry_by_nid(Nid::ORGANIZATIONALUNITNAME, &self.fields.organization_unit)?;
264        }
265
266        let name = name_builder.build();
267
268        let mut builder = X509::builder()?;
269        builder.set_version(2)?;
270
271        let serial_number = {
272            let mut serial = BigNum::new()?;
273            serial.rand(159, openssl::bn::MsbOption::MAYBE_ZERO, false)?;
274            serial.to_asn1_integer()?
275        };
276
277        let pkey = match &self.fields.existing_key {
278            Some(pkey) => pkey.clone(),
279            None => select_key(&self.fields.key_type)?,
280        };
281        builder.set_serial_number(&serial_number)?;
282        builder.set_subject_name(&name)?;
283        builder.set_pubkey(&pkey)?;
284        set_validity(
285            &mut builder,
286            &self.valid_from,
287            &self.valid_to,
288            signer.map(|s| &*s.x509),
289        )?;
290        match signer {
291            Some(signer) => builder.set_issuer_name(signer.x509.subject_name())?,
292            None => builder.set_issuer_name(&name)?,
293        }
294
295        let key_usage = self.fields.usage.clone().unwrap_or_default();
296        append_certificate_policies(&mut builder, &self.policies)?;
297        // Enforce post-quantum KeyUsage rules (signature-only ML-DSA/SLH-DSA vs
298        // keyEncipherment-only ML-KEM). See validate_pqc_key_usage.
299        #[cfg(feature = "pqc")]
300        validate_pqc_key_usage(&pkey, &key_usage)?;
301
302        if self.ca {
303            let result = ca_basic_constraints(self.path_len)?;
304            builder.append_extension(result)?;
305        } else {
306            builder.append_extension(BasicConstraints::new().build()?)?;
307        }
308
309        let (tracked_key_usage, tracked_extended_key_usage) = get_key_usage(&Some(key_usage));
310        if tracked_key_usage.is_used() {
311            builder.append_extension(tracked_key_usage.into_inner().build()?)?;
312        }
313        if tracked_extended_key_usage.is_used() {
314            builder.append_extension(tracked_extended_key_usage.into_inner().build()?)?;
315        }
316
317        // A CA is identified by its DN and key id, never by SAN — so don't invent one.
318        // End-entity certs get the CN copied in, since RFC 6125 verifiers ignore CN.
319        let names = san_names(&self.fields, !self.ca);
320        if !names.is_empty() {
321            // RFC 5280 §4.2.1.6 — never emit an empty SAN
322            let mut san = SubjectAlternativeName::new();
323            for n in &names {
324                add_san_entry(&mut san, n);
325            }
326            if let Some(signer_cert) = signer {
327                builder.append_extension(
328                    san.build(&builder.x509v3_context(Some(&signer_cert.x509), None))?,
329                )?;
330            } else {
331                builder.append_extension(san.build(&builder.x509v3_context(None, None))?)?;
332            }
333        }
334
335        if let Some(signer_cert) = signer {
336            if signer_cert.x509.subject_key_id().is_some() {
337                let aki = AuthorityKeyIdentifier::new()
338                    .keyid(true)
339                    .issuer(false)
340                    .build(&builder.x509v3_context(Some(&signer_cert.x509), None))?;
341                builder.append_extension(aki)?;
342            }
343        } else {
344            // add aki that is the same as ski for self signed
345            let oid = Asn1Object::from_str("2.5.29.35")?; // OID för Authority Key Identifier (AKI)
346            let pubkey_der = pkey.public_key_to_der()?;
347            let aki_hash = hash(MessageDigest::sha1(), &pubkey_der)?;
348            let der_encoded = yasna::construct_der(|writer| {
349                writer.write_sequence(|writer| {
350                    writer
351                        .next()
352                        .write_tagged_implicit(yasna::Tag::context(0), |writer| {
353                            writer.write_bytes(aki_hash.as_ref());
354                        })
355                })
356            });
357            let aki_asn1 = Asn1OctetString::new_from_bytes(&der_encoded)?;
358            let ext = X509Extension::new_from_der(oid.as_ref(), false, &aki_asn1)?;
359            builder.append_extension(ext)?;
360        }
361        // tried
362        // let ski = SubjectKeyIdentifier::new().build(&builder.x509v3_context(None, None))?;
363        // but got miss/match in hash values so I calculate the ski explicitly with sha1
364        // to verify with openssl cli
365        // RSA:
366        // openssl x509 -in mytestca_cert.pem -inform PEM -pubkey -noout | openssl rsa -pubin -outform DER | openssl dgst -c -sha1
367        // EC:
368        // openssl x509 -in mytestca_cert.pem -inform PEM -pubkey -noout| openssl pkey -pubin -outform DER| openssl dgst -c -sha1
369        let oid = Asn1Object::from_str("2.5.29.14")?; // OID för Subject Key Identifier (SKI)
370        let pubkey_der = pkey.public_key_to_der()?;
371        let ski_hash = hash(MessageDigest::sha1(), &pubkey_der)?;
372        let der_encoded = yasna::construct_der(|writer| {
373            writer.write_bytes(ski_hash.as_ref());
374        });
375        let ski_asn1 = Asn1OctetString::new_from_bytes(&der_encoded)?;
376        let ext = X509Extension::new_from_der(oid.as_ref(), false, &ski_asn1)?;
377        builder.append_extension(ext)?;
378
379        Ok((builder, pkey))
380    }
381}
382
383impl CertBuilder<PathLenUnset> {
384    /// Create a new CertBuilder with defaults and one year from now as valid date
385    pub fn new() -> Self {
386        Self {
387            fields: BuilderFields::default(),
388            valid_from: ValidityBound::DaysFromNow(0), // today, resolved at build
389            valid_to: ValidityBound::DaysFromNow(365), // one year from now
390            ca: false,
391            policies: Default::default(),
392            path_len: None,
393            _marker: PhantomData,
394        }
395    }
396    /// Creates a certificate and private key signed by `signer`.
397    ///
398    /// # Errors
399    /// Returns an error if `signer` is not a valid CA within its validity period
400    /// or has no private key, if a `valid_from`/`valid_to` date is not
401    /// `yyyy-mm-dd`, if an explicit `valid_to` is later than the signer's
402    /// `notAfter`, or if OpenSSL rejects the certificate contents.
403    pub fn build_and_sign(
404        &self,
405        signer: &Certificate,
406    ) -> Result<Certificate, Box<dyn std::error::Error>> {
407        let can_sign = can_sign_cert(signer)?;
408        if !can_sign {
409            let err = format!(
410                "Cannot sign with signer certificate {:?}: it must be a valid CA \
411             (BasicConstraints CA flag set, KeyUsage keyCertSign, within its validity \
412             period) and have an associated private key",
413                signer.x509.subject_name()
414            );
415            return Err(err.into());
416        }
417        let (mut builder, pkey) = self.prepare_x509_builder(Some(signer))?;
418        let signer_key = signer
419            .pkey
420            .as_ref()
421            .ok_or("signer certificate has no associated private key; cannot sign")?;
422        let cert: X509 = if is_digestless_key(signer_key) {
423            let build_cert = builder.build();
424            sign_certificate_digestless(&build_cert, signer_key)
425                .map_err(|e| format!("Failed to sign certificate with digestless key: {}", e))?;
426            build_cert
427        } else {
428            builder.sign(signer_key, select_hash(&self.fields.signature_alg))?;
429            builder.build()
430        };
431        Ok(Certificate {
432            x509: cert,
433            pkey: Some(pkey),
434        })
435    }
436}
437
438impl CertBuilder<PathLenSet> {
439    /// Creates a CA certificate signed by `signer`, enforcing its path length against `chain`.
440    ///
441    /// # Errors
442    /// Returns an error under the same conditions as [`CertBuilder::build_and_sign`],
443    /// and additionally if the requested path length does not fit under the
444    /// constraints already present in `chain`.
445    pub fn build_and_sign_with_chain(
446        &self,
447        signer: &Certificate,
448        chain: &[&Certificate],
449    ) -> Result<Certificate, Box<dyn std::error::Error>> {
450        let can_sign = can_sign_cert(signer)?;
451        if !can_sign {
452            let err = format!(
453                "Cannot sign with signer certificate {:?}: it must be a valid CA \
454             (BasicConstraints CA flag set, KeyUsage keyCertSign, within its validity \
455             period) and have an associated private key",
456                signer.x509.subject_name()
457            );
458            return Err(err.into());
459        }
460        let chain_x509: Vec<&X509> = chain.iter().map(|c| &c.x509).collect();
461        enforce_path_len(self.ca, self.path_len, signer, &chain_x509)?;
462
463        let (mut builder, pkey) = self.prepare_x509_builder(Some(signer))?;
464        let signer_key = signer
465            .pkey
466            .as_ref()
467            .ok_or("signer certificate has no associated private key; cannot sign")?;
468        let cert: X509 = if is_digestless_key(signer_key) {
469            let build_cert = builder.build();
470            sign_certificate_digestless(&build_cert, signer_key)
471                .map_err(|e| format!("Failed to sign certificate with digestless key: {}", e))?;
472            build_cert
473        } else {
474            builder.sign(signer_key, select_hash(&self.fields.signature_alg))?;
475            builder.build()
476        };
477        Ok(Certificate {
478            x509: cert,
479            pkey: Some(pkey),
480        })
481    }
482}
483fn ca_basic_constraints(path_len: Option<u32>) -> Result<X509Extension, ErrorStack> {
484    let mut bc = BasicConstraints::new();
485    bc.ca().critical();
486    if let Some(len) = path_len {
487        bc.pathlen(len);
488    }
489    bc.build()
490}
491
492/// Verifies a certificate against a root certificate and the intermediate
493/// chain leading up to it.
494/// Note: The root certificate should not be included in the chain.
495pub fn verify_cert(
496    cert: &X509,
497    ca: &X509,
498    cert_chain: Vec<&X509>,
499) -> Result<bool, Box<dyn std::error::Error>> {
500    // Build a certificate store and add the issuer
501    let mut store_builder = X509StoreBuilder::new()?;
502    store_builder.add_cert(ca.clone())?;
503    let store = store_builder.build();
504
505    // Create a verification context
506    let mut ctx = X509StoreContext::new()?;
507    let mut chain = Stack::new()?; // create an empty chain
508    cert_chain
509        .iter()
510        .try_for_each(|c| chain.push((*c).clone()))?;
511    ctx.init(&store, cert, &chain, |c| c.verify_cert())?;
512    let verified = ctx.error() == openssl::x509::X509VerifyResult::OK;
513    Ok(verified)
514}
515
516/// Takes a vector of certificates and returns a vector ordered
517/// from the root to the leaf, with the leaf certificate as the last element.
518///
519/// For example, if the input list contains `ca2`, `leaf`, and `ca1`,
520/// and the signing order is `ca1 -> ca2 -> leaf`,
521/// the returned vector will be `[ca1, ca2, leaf]`.
522///
523/// If multiple valid chains are possible, the longest one is returned.
524pub fn create_cert_chain_from_cert_list(
525    certs: Vec<X509>,
526) -> Result<Vec<X509>, Box<dyn std::error::Error>> {
527    let mut subject_map: HashMap<Vec<u8>, X509> = HashMap::new();
528    let mut issuer_map: HashMap<Vec<u8>, Vec<u8>> = HashMap::new();
529
530    for cert in &certs {
531        let subject = cert.subject_name().to_der()?;
532        let issuer = cert.issuer_name().to_der()?;
533        subject_map.insert(subject.clone(), cert.clone());
534        issuer_map.insert(subject, issuer);
535    }
536
537    // Find leaf certificates (those that are not issuers of any other cert)
538    let all_issuers: Vec<Vec<u8>> = issuer_map
539        .iter()
540        .filter(|(subject, issuer)| subject != issuer) // ignore the self-signed self-reference
541        .map(|(_, issuer)| issuer.clone())
542        .collect();
543    let leaf_certs: Vec<X509> = subject_map
544        .iter()
545        .filter(|(subject, _)| !all_issuers.contains(subject))
546        .map(|(_, cert)| cert.clone())
547        .collect();
548
549    // Try to build the longest chain from each leaf
550    let mut longest_chain = Vec::new();
551
552    for leaf in leaf_certs {
553        let mut chain = vec![leaf.clone()];
554        let mut current_cert = leaf;
555
556        while let Ok(issuer_der) = current_cert.issuer_name().to_der() {
557            if let Some(parent_cert) = subject_map.get(&issuer_der) {
558                if parent_cert.subject_name().to_der()? == current_cert.subject_name().to_der()? {
559                    break; // Self-signed, stop here
560                }
561                chain.push(parent_cert.clone());
562                current_cert = parent_cert.clone();
563            } else {
564                break; // Issuer not found in the list
565            }
566        }
567
568        if chain.len() > longest_chain.len() {
569            longest_chain = chain;
570        }
571    }
572
573    // Reverse to have root (or highest known CA) first
574    longest_chain.reverse();
575    Ok(longest_chain)
576}
577
578fn add_san_entry(san: &mut SubjectAlternativeName, value: &str) {
579    match value.parse::<IpAddr>() {
580        Ok(ip) => san.ip(&ip.to_string()),
581        Err(_) => san.dns(value),
582    };
583}
584
585/// The SAN entries for a certificate or signing request: everything the caller
586/// asked for, plus the common name when it belongs there.
587///
588/// `include_cn` is false for CA certificates — a CA is identified by its
589/// distinguished name and key identifier during path validation, so it carries
590/// no SAN at all. It is true for end-entity certificates and for signing
591/// requests, because RFC 6125 verifiers match on the SAN and ignore the CN.
592///
593/// The CN is skipped when the caller already listed it, so a name given both
594/// ways is emitted once rather than twice.
595///
596/// Callers must not emit an extension when this returns an empty list —
597/// RFC 5280 §4.2.1.6 requires a present SubjectAltName to hold at least one
598/// entry.
599pub(crate) fn san_names(fields: &BuilderFields, include_cn: bool) -> Vec<&str> {
600    let mut names: Vec<&str> = fields
601        .alternative_names
602        .iter()
603        .map(String::as_str)
604        .collect();
605    if include_cn
606        && !fields.common_name.trim().is_empty()
607        && !fields.alternative_names.contains(&fields.common_name)
608    {
609        names.push(&fields.common_name);
610    }
611    names
612}
613
614fn enforce_path_len(
615    is_ca: bool,
616    path_len: Option<u32>,
617    signer: &Certificate,
618    chain: &[&X509],
619) -> Result<(), Box<dyn std::error::Error>> {
620    // No pathLen set (or non-CA) → nothing to enforce, no chain required.
621    // Matches CertBuilder: a no-pathLen / unlimited CA isn't budget-checked.
622    if !is_ca || path_len.is_none() {
623        return Ok(());
624    }
625    let budget = verify_cert_path(signer, chain)?;
626    if let (Some(b), Some(m)) = (budget, path_len)
627        && m >= b
628    {
629        return Err("requested pathLen exceeds what the signer's chain permits".into());
630    }
631    Ok(())
632}
633
634fn verify_cert_path(
635    signer: &Certificate,
636    chain: &[&X509],
637) -> Result<Option<u32>, Box<dyn std::error::Error>> {
638    let mut owned: Vec<X509> = chain.iter().map(|c| (*c).clone()).collect();
639    owned.push(signer.x509.clone());
640    let ordered = create_cert_chain_from_cert_list(owned)?;
641    let root = ordered.first().unwrap();
642    let pubkey = root.public_key()?;
643    if !root.verify(&pubkey)? {
644        let err = format!(
645            "Could not find self signed root, found last ancestor {:?}",
646            root.subject_name()
647        );
648        return Err(err.into());
649    }
650    let signer = ordered.last().unwrap();
651    let intermediates: Vec<&X509> = ordered
652        .get(1..ordered.len().saturating_sub(1)) // 1..0 → None for len==1
653        .unwrap_or(&[])
654        .iter()
655        .collect();
656    match verify_cert(signer, root, intermediates) {
657        Ok(true) => {
658            let mut budget: Option<i64> = None;
659            for (d, ancestor) in ordered.iter().rev().enumerate() {
660                if let Some(p) = ancestor.pathlen() {
661                    // ancestor's declared pathLen
662                    let candidate = p as i64 - d as i64; // can be negative — that's fine
663                    budget = Some(budget.map_or(candidate, |b| b.min(candidate)));
664                }
665            }
666            match budget {
667                Some(b) if b < 1 => {
668                    Err("signer's path length budget is exhausted; cannot issue a CA".into())
669                }
670                Some(b) => Ok(Some(b as u32)), // b >= 1 here, cast is safe
671                None => Ok(None),              // no pathLen anywhere → unlimited
672            }
673        }
674        _ => {
675            let err = format!(
676                "Cannot verify the crtificate chain for {:?}",
677                signer.subject_name()
678            );
679            Err(err.into())
680        }
681    }
682}
683
684fn can_sign_cert(cert: &Certificate) -> Result<bool, Box<dyn std::error::Error>> {
685    if cert.pkey.is_none() {
686        return Ok(false);
687    }
688
689    let der = cert.x509.to_der()?;
690    let (_, parsed_cert) = parse_x509_certificate(&der)?;
691
692    let mut is_ca = false;
693    let mut can_sign = false;
694    let now = Asn1Time::days_from_now(0)?;
695    let valid_time = cert.x509.not_before().compare(&now)? != std::cmp::Ordering::Greater
696        && cert.x509.not_after().compare(&now)? == std::cmp::Ordering::Greater;
697
698    for ext in parsed_cert.tbs_certificate.extensions().iter() {
699        match &ext.parsed_extension() {
700            ParsedExtension::BasicConstraints(bc) => {
701                is_ca = bc.ca;
702            }
703            ParsedExtension::KeyUsage(ku) => {
704                can_sign = ku.key_cert_sign();
705            }
706            _ => {}
707        }
708    }
709    Ok(is_ca && can_sign && valid_time)
710}
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715
716    use std::io::Write;
717    use std::path::Path;
718    use tempfile::NamedTempFile;
719    use x509_parser::extensions::GeneralName;
720
721    #[test]
722    fn create_ca_with_path_len_set() {
723        let ca = CertBuilder::new()
724            .common_name("My Test Ca")
725            .is_ca(true)
726            .pathlen(0)
727            .build_and_self_sign()
728            .unwrap();
729        assert_eq!(ca.x509.pathlen(), Some(0));
730    }
731
732    #[test]
733    fn create_ca_with_no_path_len_set() {
734        let ca = CertBuilder::new()
735            .common_name("My Test Ca")
736            .is_ca(true)
737            .build_and_self_sign()
738            .unwrap();
739        assert_eq!(ca.x509.pathlen(), None);
740    }
741
742    #[test]
743    fn key_usage_is_marked_critical() {
744        let ca = CertBuilder::new()
745            .common_name("My Test Ca")
746            .is_ca(true)
747            .build_and_self_sign()
748            .unwrap();
749        let leaf = CertBuilder::new()
750            .common_name("leaf")
751            .key_usage([Usage::certsign].into_iter().collect())
752            .build_and_sign(&ca)
753            .unwrap();
754        let der = leaf.x509.to_der().unwrap();
755        let (_, parsed) = parse_x509_certificate(&der).unwrap();
756
757        let ku = parsed
758            .tbs_certificate
759            .extensions()
760            .iter()
761            .find(|ext| matches!(ext.parsed_extension(), ParsedExtension::KeyUsage(_)))
762            .expect("certificate should carry a KeyUsage extension");
763
764        assert!(ku.critical, "KeyUsage must be critical (RFC 5280 §4.2.1.3)");
765    }
766
767    #[test]
768    fn extended_key_usage_is_not_marked_critical() {
769        let ca = CertBuilder::new()
770            .common_name("My Test Ca")
771            .is_ca(true)
772            .build_and_self_sign()
773            .unwrap();
774        let leaf = CertBuilder::new()
775            .common_name("leaf")
776            .key_usage([Usage::serverauth].into_iter().collect())
777            .build_and_sign(&ca)
778            .unwrap();
779        let der = leaf.x509.to_der().unwrap();
780        let (_, parsed) = parse_x509_certificate(&der).unwrap();
781
782        let eku = parsed
783            .tbs_certificate
784            .extensions()
785            .iter()
786            .find(|ext| matches!(ext.parsed_extension(), ParsedExtension::ExtendedKeyUsage(_)))
787            .expect("certificate should carry a ExtendedKeyUsage extension");
788
789        assert!(!eku.critical, "Extened KeyUsage not critical by default");
790    }
791
792    #[test]
793    fn san_names_should_be_added_correctly() {
794        let ca = CertBuilder::new()
795            .common_name("My Test Ca")
796            .is_ca(true)
797            .build_and_self_sign()
798            .unwrap();
799        let leaf = CertBuilder::new()
800            .common_name("leaf")
801            .alternative_names(vec!["localhost", "127.0.0.1", "::1"])
802            .key_usage([Usage::serverauth].into_iter().collect())
803            .build_and_sign(&ca)
804            .unwrap();
805        let der = leaf.x509.to_der().unwrap();
806        let (_, parsed) = parse_x509_certificate(&der).unwrap();
807        let san = parsed
808            .subject_alternative_name()
809            .unwrap()
810            .expect("leaf should carry a SubjectAltName extension");
811
812        let mut dns: Vec<&str> = Vec::new();
813        let mut ips: Vec<IpAddr> = Vec::new();
814        for gn in &san.value.general_names {
815            match gn {
816                GeneralName::DNSName(name) => dns.push(name),
817                GeneralName::IPAddress(bytes) => ips.push(match bytes.len() {
818                    4 => IpAddr::from(<[u8; 4]>::try_from(*bytes).unwrap()),
819                    16 => IpAddr::from(<[u8; 16]>::try_from(*bytes).unwrap()),
820                    n => panic!("iPAddress SAN must be 4 or 16 octets, got {n}"),
821                }),
822                other => panic!("unexpected GeneralName in SAN: {other:?}"),
823            }
824        }
825
826        // `alternative_names` is a HashSet — sort before comparing or this flakes.
827        dns.sort_unstable();
828        ips.sort_unstable();
829
830        // The CN is copied into the SAN list, so "leaf" appears alongside the
831        // explicitly requested names.
832        assert_eq!(dns, ["leaf", "localhost"]);
833        assert_eq!(
834            ips,
835            [
836                "127.0.0.1".parse::<IpAddr>().unwrap(),
837                "::1".parse::<IpAddr>().unwrap()
838            ]
839        )
840    }
841
842    #[test]
843    fn common_name_listed_in_alternative_names_is_not_duplicated() {
844        let ca = CertBuilder::new()
845            .common_name("My Test Ca")
846            .is_ca(true)
847            .build_and_self_sign()
848            .unwrap();
849        // The CN is also given explicitly — it must still appear exactly once.
850        let leaf = CertBuilder::new()
851            .common_name("example.com")
852            .alternative_names(vec!["example.com", "www.example.com"])
853            .key_usage([Usage::serverauth].into_iter().collect())
854            .build_and_sign(&ca)
855            .unwrap();
856
857        let der = leaf.x509.to_der().unwrap();
858        let (_, parsed) = parse_x509_certificate(&der).unwrap();
859        let san = parsed
860            .subject_alternative_name()
861            .unwrap()
862            .expect("leaf should carry a SubjectAltName");
863
864        let mut dns: Vec<&str> = san
865            .value
866            .general_names
867            .iter()
868            .filter_map(|gn| match gn {
869                GeneralName::DNSName(n) => Some(*n),
870                _ => None,
871            })
872            .collect();
873        dns.sort_unstable();
874
875        assert_eq!(dns, ["example.com", "www.example.com"]);
876    }
877
878    #[test]
879    fn incomplete_chain_without_root_is_rejected() {
880        let ca = CertBuilder::new()
881            .common_name("Root")
882            .is_ca(true)
883            .pathlen(2)
884            .build_and_self_sign()
885            .unwrap();
886        let inter = CertBuilder::new()
887            .common_name("Inter")
888            .is_ca(true)
889            .pathlen(1)
890            .build_and_sign_with_chain(&ca, &[])
891            .unwrap();
892
893        // sign with `inter` but forget to pass its chain → top of chain is `inter`,
894        // which is not self-signed → reject
895        let err = CertBuilder::new()
896            .common_name("Sub")
897            .is_ca(true)
898            .pathlen(0)
899            .build_and_sign_with_chain(&inter, &[])
900            .expect_err("incomplete chain (missing root) must be rejected");
901        assert!(
902            err.to_string().contains("Could not find self signed root"),
903            "got: {err}"
904        );
905    }
906
907    #[test]
908    fn unlimited_root_can_issue_ca() {
909        let root = CertBuilder::new()
910            .common_name("Unlimited Root")
911            .is_ca(true) // note: no .pathlen() → None / unlimited
912            .build_and_self_sign()
913            .unwrap();
914        assert_eq!(root.x509.pathlen(), None);
915
916        // budget is None → no ceiling; even a large declared pathLen is fine
917        let inter = CertBuilder::new()
918            .common_name("Inter")
919            .is_ca(true)
920            .pathlen(5)
921            .build_and_sign_with_chain(&root, &[])
922            .unwrap();
923        assert_eq!(inter.x509.pathlen(), Some(5));
924    }
925
926    #[test]
927    fn create_cert_chain_and_verify() {
928        let ca = CertBuilder::new()
929            .common_name("My Test Ca")
930            .is_ca(true)
931            .pathlen(1)
932            .build_and_self_sign()
933            .unwrap();
934        let chain: Vec<&Certificate> = Vec::new();
935        let inter_ca = CertBuilder::new()
936            .common_name("My Test inter Ca")
937            .is_ca(true)
938            .pathlen(0)
939            .build_and_sign_with_chain(&ca, chain.as_slice())
940            .unwrap();
941        let leaf = CertBuilder::new()
942            .common_name("My Test leaf")
943            .build_and_sign(&inter_ca)
944            .unwrap();
945        assert_eq!(ca.x509.pathlen(), Some(1));
946        assert_eq!(leaf.x509.pathlen(), None);
947        assert!(verify_cert(&leaf.x509, &ca.x509, vec![&inter_ca.x509]).unwrap());
948    }
949
950    #[test]
951    fn create_ca_cert_chain_and_verify() {
952        let ca = CertBuilder::new()
953            .common_name("My Test Ca")
954            .is_ca(true)
955            .pathlen(2)
956            .build_and_self_sign()
957            .unwrap();
958        let chain: Vec<&Certificate> = Vec::new();
959        let inter_ca = CertBuilder::new()
960            .common_name("My Test inter Ca")
961            .is_ca(true)
962            .pathlen(1)
963            .build_and_sign_with_chain(&ca, chain.as_slice())
964            .unwrap();
965        let leaf = CertBuilder::new()
966            .common_name("leaf ca")
967            .is_ca(true)
968            .pathlen(0)
969            .build_and_sign_with_chain(&inter_ca, &[&ca])
970            .unwrap();
971
972        assert_eq!(leaf.x509.pathlen(), Some(0));
973    }
974
975    #[test]
976    fn can_not_create_ca_cert_chain_with_wrong_intermediate_ca_path() {
977        let ca = CertBuilder::new()
978            .common_name("My Test Ca")
979            .is_ca(true)
980            .pathlen(2)
981            .build_and_self_sign()
982            .unwrap();
983        let chain: Vec<&Certificate> = Vec::new();
984        let inter_ca = CertBuilder::new()
985            .common_name("My Test inter Ca")
986            .is_ca(true)
987            .pathlen(0)
988            .build_and_sign_with_chain(&ca, chain.as_slice())
989            .unwrap();
990        let err = CertBuilder::new()
991            .common_name("leaf ca")
992            .is_ca(true)
993            .pathlen(0)
994            .build_and_sign_with_chain(&inter_ca, &[&ca])
995            .expect_err("");
996
997        assert!(
998            err.to_string()
999                .contains("signer's path length budget is exhausted; cannot issue a CA"),
1000            "signer's path length budget is exhausted; cannot issue a CA got: {err}"
1001        );
1002    }
1003
1004    #[test]
1005    fn root_ca_have_one_path_length_and_can_not_sign_ca() {
1006        let ca = CertBuilder::new()
1007            .common_name("My Test Ca")
1008            .is_ca(true)
1009            .pathlen(1)
1010            .build_and_self_sign()
1011            .unwrap();
1012
1013        let chain: Vec<&Certificate> = Vec::new();
1014
1015        // A user mistake: an intermediate claiming pathlen(1) under a root that only
1016        // permits one CA below it. Must be rejected at issuance.
1017        let err = CertBuilder::new()
1018            .common_name("My Test inter Ca")
1019            .is_ca(true)
1020            .pathlen(1)
1021            .build_and_sign_with_chain(&ca, chain.as_slice())
1022            .expect_err("inter CA with pathlen(1) under root pathlen(1) must be rejected");
1023
1024        assert!(
1025            err.to_string()
1026                .contains("requested pathLen exceeds what the signer's chain permits"),
1027            "expected a pathLen-exceeds error, got: {err}"
1028        );
1029    }
1030
1031    #[test]
1032    fn build_and_sign_without_signer_private_key_errors_not_panics() {
1033        let ca = CertBuilder::new()
1034            .common_name("My Test Ca")
1035            .is_ca(true)
1036            .build_and_self_sign()
1037            .unwrap();
1038        let keyless_ca = Certificate {
1039            x509: ca.x509.clone(),
1040            pkey: None,
1041        };
1042
1043        let err = CertBuilder::new()
1044            .common_name("leaf")
1045            .build_and_sign(&keyless_ca)
1046            .expect_err("signing with a key-less CA must return an error, not panic");
1047        assert!(
1048            err.to_string().contains("private key"),
1049            "expected a missing-private-key error, got: {err}"
1050        );
1051    }
1052
1053    #[test]
1054    fn ensure_that_supplied_private_key_is_used() {
1055        let ca_one = CertBuilder::new()
1056            .common_name("My Test Ca1")
1057            .is_ca(true)
1058            .build_and_self_sign()
1059            .unwrap();
1060        let ca_two = CertBuilder::new()
1061            .common_name("My Test Ca2")
1062            .is_ca(true)
1063            .private_key(ca_one.pkey.unwrap())
1064            .build_and_self_sign()
1065            .unwrap();
1066        assert_eq!(
1067            ca_one
1068                .x509
1069                .public_key()
1070                .unwrap()
1071                .public_key_to_der()
1072                .unwrap(),
1073            ca_two
1074                .x509
1075                .public_key()
1076                .unwrap()
1077                .public_key_to_der()
1078                .unwrap()
1079        )
1080    }
1081    #[test]
1082    fn save_certificate() {
1083        let ca = CertBuilder::new().common_name("My Test Ca").is_ca(true);
1084        match ca.build_and_self_sign() {
1085            Ok(cert) => {
1086                let output_file = NamedTempFile::new().unwrap();
1087                let full_path = output_file.path();
1088                let parent_dir: &Path = full_path.parent().unwrap();
1089                let file_name: &str = full_path.file_name().unwrap().to_str().unwrap();
1090                cert.save(parent_dir, file_name)
1091                    .expect("Failed to save certificate and key");
1092                let written_file_path = parent_dir.join(file_name);
1093                assert!(written_file_path.exists(), "File was not created");
1094            }
1095            Err(_) => panic!("Failed to creat certificate"),
1096        }
1097    }
1098
1099    #[test]
1100    fn read_certificate_and_key_from_file() {
1101        let cert_pem = b"-----BEGIN CERTIFICATE-----
1102MIICiDCCAemgAwIBAgIUO3+y1WZPRRNs8dmZZTUHMj6TdiowCgYIKoZIzj0EAwQw
1103WzETMBEGA1UEAwwKTXkgVGVzdCBDYTELMAkGA1UEBhMCU0UxEjAQBgNVBAgMCVN0
1104b2NraG9sbTESMBAGA1UEBwwJU3RvY2tob2xtMQ8wDQYDVQQKDAZteSBvcmcwHhcN
1105MjUwNzA4MTExMzI2WhcNMjYwNzA4MTExMzI2WjBbMRMwEQYDVQQDDApNeSBUZXN0
1106IENhMQswCQYDVQQGEwJTRTESMBAGA1UECAwJU3RvY2tob2xtMRIwEAYDVQQHDAlT
1107dG9ja2hvbG0xDzANBgNVBAoMBm15IG9yZzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOB
1108hgAEADZXcQK2ihgVTJeGx5FKm1x+R+ivygIvMnkv03faq1LpLU3doKX38DEO/cSW
1109Ev5u+kcjspXeeDPhqJFC8rRAz4awAMk+D0mXEms7xpFPh0HmI6NNcJc5eJ/8ZsEJ
1110GH1a34y0Yn6259gqlwAh2Eh9Nx1579BAanRr8lr+n1tZ09T/9AQho0gwRjAMBgNV
1111HRMEBTADAQH/MAsGA1UdDwQEAwIBBjApBgNVHREEIjAgggZjYS5jb22CCnd3dy5j
1112YS5jb22CCk15IFRlc3QgQ2EwCgYIKoZIzj0EAwQDgYwAMIGIAkIB8NVUgRIuNXmJ
1113cLCQ74Ub7Dqo71S0+iCrZF1YyJA8/q65aqMCT54k5Yx7HRBUUVHbCEpDXRqGPsIH
1114frfe5OmS3qICQgDBn07o0CcyfoSEd+Xoj2+/RBuU0vo9lUP7TKj7tssBxzEQFoxX
1115eE1qT98UIe78FZ+zqjwZTN9MCSsatuim6pXvOA==
1116-----END CERTIFICATE-----";
1117
1118        let key_pem = b"-----BEGIN PRIVATE KEY-----
1119MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIAgvZTeQgGysadAX0r
1120aZB5Lk4vjHy5iVuKdvcGdYt9NBvYx+Ib3Uk7vqMag7M1jyHL0Xf9uNtT2mxBmzBG
11213CF+EgOhgYkDgYYABAA2V3ECtooYFUyXhseRSptcfkfor8oCLzJ5L9N32qtS6S1N
11223aCl9/AxDv3ElhL+bvpHI7KV3ngz4aiRQvK0QM+GsADJPg9JlxJrO8aRT4dB5iOj
1123TXCXOXif/GbBCRh9Wt+MtGJ+tufYKpcAIdhIfTcdee/QQGp0a/Ja/p9bWdPU//QE
1124IQ==
1125-----END PRIVATE KEY-----";
1126
1127        let mut cert_file = NamedTempFile::new().expect("Failed to create temp cert file");
1128        let mut key_file = NamedTempFile::new().expect("Failed to create temp key file");
1129
1130        cert_file.write_all(cert_pem).expect("Failed to write cert");
1131        key_file.write_all(key_pem).expect("Failed to write key");
1132
1133        let result = Certificate::load_cert_and_key(cert_file.path(), key_file.path());
1134        assert!(
1135            result.is_ok(),
1136            "Failed to load cert and key: {:?}",
1137            result.err()
1138        );
1139    }
1140
1141    #[test]
1142    fn load_cert_reads_cert_without_key() {
1143        use std::io::Write;
1144        // Setup: a self-signed cert written to a temp PEM file.
1145        let cert = CertBuilder::new()
1146            .common_name("load-cert-test")
1147            .is_ca(true)
1148            .build_and_self_sign()
1149            .unwrap();
1150        let mut file = tempfile::NamedTempFile::new().unwrap();
1151        file.write_all(&cert.x509.to_pem().unwrap()).unwrap();
1152
1153        // Invoke.
1154        let loaded = Certificate::load_cert(file.path()).unwrap();
1155
1156        // Expect: no private key, and the subject survived the round-trip.
1157        assert!(
1158            loaded.pkey.is_none(),
1159            "load_cert must not carry a private key"
1160        );
1161        assert_eq!(
1162            loaded
1163                .x509
1164                .subject_name()
1165                .try_cmp(cert.x509.subject_name())
1166                .unwrap(),
1167            std::cmp::Ordering::Equal,
1168        );
1169    }
1170
1171    #[test]
1172    fn load_cert_missing_file_errors() {
1173        let err = Certificate::load_cert("/no/such/file/here.pem").expect_err("msg");
1174        assert!(
1175            err.to_string().contains("No such file or directory"),
1176            "No such file or directory, got: {err}"
1177        );
1178    }
1179}