Skip to main content

cert_helper/certificate/
csr.rs

1use super::builder::{BuilderFields, UseesBuilderFields, select_hash};
2use super::common::{X509Common, X509Parts, create_asn1_time_from_date};
3use super::enforce_path_len;
4#[cfg(feature = "pqc")]
5use super::key::reject_mlkem_signing;
6use super::key::{
7    is_digestless_key, select_key, sign_certificate_digestless, sign_x509_req_digestless,
8};
9use super::policy::append_certificate_policies;
10use super::usage::get_key_usage;
11#[cfg(feature = "pqc")]
12use super::usage::validate_pqc_key_usage;
13use super::{Certificate, CertificatePolicy, Usage, ca_basic_constraints, can_sign_cert};
14use super::{add_san_entry, san_names};
15use openssl::asn1::{Asn1Object, Asn1OctetString, Asn1Time};
16use openssl::bn::BigNum;
17use openssl::hash::{MessageDigest, hash};
18use openssl::nid::Nid;
19use openssl::pkey::{PKey, Private};
20use openssl::stack::Stack;
21use openssl::x509::extension::{
22    AuthorityKeyIdentifier, BasicConstraints, KeyUsage, SubjectAlternativeName,
23};
24use openssl::x509::{X509, X509Builder, X509Extension, X509NameBuilder, X509Req, X509ReqBuilder};
25use std::collections::HashSet;
26use std::error::Error;
27use std::net::{Ipv4Addr, Ipv6Addr};
28use std::path::Path;
29use x509_parser::certification_request::X509CertificationRequest;
30use x509_parser::extensions::{GeneralName, ParsedExtension};
31use x509_parser::prelude::FromDer;
32
33/// Holds the generated Certificate Signing Request (CSR) and its associated private key.
34pub struct Csr {
35    /// The X.509 certificate signing request.
36    pub csr: X509Req,
37    /// The private key used to generate the CSR.
38    ///
39    /// This is optional to allow flexibility in cases where the key is managed or stored separately.
40    pub pkey: Option<PKey<Private>>,
41}
42
43impl X509Parts for Csr {
44    fn get_pem(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
45        Ok(self.csr.to_pem()?)
46    }
47
48    fn get_private_key(&self) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
49        match self.pkey {
50            Some(ref pkey) => Ok(pkey.private_key_to_pem_pkcs8()?),
51            _ => Err("No private key found".into()),
52        }
53    }
54    fn pem_extension(&self) -> &'static str {
55        "_csr.pem"
56    }
57}
58/// Helper trait to document that Csr implements X509Common
59pub trait CsrX509Common: X509Common {}
60impl CsrX509Common for Csr {}
61
62/// Holds configuration options for creating a certificate from a Certificate Signing Request (CSR).
63pub struct CsrOptions {
64    valid_to: Asn1Time,
65    valid_from: Asn1Time,
66    ca: bool,
67    policies: Vec<CertificatePolicy>,
68    path_len: Option<u32>,
69    chain: Vec<Certificate>,
70}
71impl Default for CsrOptions {
72    fn default() -> Self {
73        Self::new()
74    }
75}
76
77impl CsrOptions {
78    /// Creates a default `CsrOptions` instance:
79    /// - `valid_from` is set to today.
80    /// - `valid_to` is set to one year from today.
81    /// - `ca` is set to `false`.
82    pub fn new() -> Self {
83        Self {
84            ca: false,
85            valid_from: Asn1Time::days_from_now(0).unwrap(), // today
86            valid_to: Asn1Time::days_from_now(365).unwrap(), // one year from now
87            policies: Default::default(),
88            path_len: None,
89            chain: Vec::new(),
90        }
91    }
92
93    /// Sets the start date from which the certificate should be valid.
94    ///
95    /// # Arguments
96    /// * `valid_from` - A string in the format `yyyy-mm-dd`.
97    pub fn valid_from(mut self, valid_from: &str) -> Self {
98        self.valid_from =
99            create_asn1_time_from_date(valid_from).expect("Failed to parse valid_from date");
100        self
101    }
102
103    /// Sets the end date after which the certificate should no longer be valid.
104    ///
105    /// # Arguments
106    /// * `valid_to` - A string in the format `yyyy-mm-dd`.
107    pub fn valid_to(mut self, valid_to: &str) -> Self {
108        self.valid_to =
109            create_asn1_time_from_date(valid_to).expect("Failed to parse valid_to date");
110        self
111    }
112
113    /// Specifies whether the certificate should be a Certificate Authority (CA).
114    ///
115    /// # Arguments
116    /// * `ca` - `true` if the certificate should be a CA, `false` otherwise.
117    pub fn is_ca(mut self, ca: bool) -> Self {
118        self.ca = ca;
119        self
120    }
121    /// Add optional certificate policies
122    ///
123    /// # Arguments
124    /// * `policies` - A list of certificate policies
125    pub fn certificate_policies(mut self, policies: Vec<CertificatePolicy>) -> Self {
126        self.policies = policies;
127        self
128    }
129    /// Add optional path length, it is the max number of **non-self-issued
130    /// intermediate CA certs** that may follow this cert in a chain
131    ///
132    /// # Arguments
133    /// * `path_len`- u32
134    pub fn pathlen(mut self, path_len: u32, chain: Vec<Certificate>) -> Self {
135        self.path_len = Some(path_len);
136        self.chain = chain;
137        self
138    }
139}
140
141impl Csr {
142    /// Read a certificate signing request from file
143    pub fn load_csr<C: AsRef<Path>>(csr_pem_file: C) -> Result<Self, Box<dyn std::error::Error>> {
144        let cert_pem = std::fs::read(csr_pem_file)?;
145        let cs_req = X509Req::from_pem(&cert_pem)?;
146        Ok(Self {
147            csr: cs_req,
148            pkey: None,
149        })
150    }
151    /// Create a signed certificate from a certificate signing request(csr)
152    pub fn build_signed_certificate(
153        &self,
154        signer: &Certificate,
155        options: CsrOptions,
156    ) -> Result<Certificate, Box<dyn std::error::Error>> {
157        // Proof-of-possession: refuse to issue from a CSR whose self-signature
158        // does not verify against its own public key.
159        verify_csr_proof_of_possession(&self.csr)?;
160
161        let can_sign = can_sign_cert(signer)?;
162        if !can_sign {
163            let err = format!(
164                "Cannot sign with signer certificate {:?}: it must be a valid CA \
165                 (BasicConstraints CA flag set, KeyUsage keyCertSign, within its validity \
166                 period) and have an associated private key",
167                signer.x509.subject_name()
168            );
169            return Err(err.into());
170        }
171        let chain_refs: Vec<&X509> = options.chain.iter().map(|c| &c.x509).collect();
172        enforce_path_len(options.ca, options.path_len, signer, &chain_refs)?;
173        let signer_key = signer
174            .pkey
175            .as_ref()
176            .ok_or("signer certificate has no associated private key; cannot sign")?;
177        let mut builder = X509Builder::new()?;
178        builder.set_version(2)?;
179        builder.set_subject_name(self.csr.subject_name())?;
180        builder.set_issuer_name(signer.x509.subject_name())?;
181        let csr_public_key = self.csr.public_key()?;
182        builder.set_pubkey(&csr_public_key)?;
183
184        let der = self.csr.to_der()?;
185        let parsed_csr = X509CertificationRequest::from_der(&der)?;
186
187        let req_ext = parsed_csr.1.requested_extensions();
188        let mut any_key_used = false;
189        let mut requested: HashSet<Usage> = HashSet::new();
190        if let Some(exts) = req_ext {
191            for ext in exts {
192                match ext {
193                    ParsedExtension::KeyUsage(ku) => {
194                        any_key_used = true;
195                        let mut cert_sign_added = false;
196                        let mut crl_sign_added = false;
197                        let mut usage = openssl::x509::extension::KeyUsage::new();
198                        usage.critical();
199                        if ku.digital_signature() {
200                            usage.digital_signature();
201                            requested.insert(Usage::signature);
202                        }
203                        if ku.key_encipherment() {
204                            usage.key_encipherment();
205                            requested.insert(Usage::encipherment);
206                        }
207                        if ku.key_cert_sign() {
208                            cert_sign_added = true;
209                            usage.key_cert_sign();
210                            requested.insert(Usage::certsign);
211                        }
212                        if ku.non_repudiation() {
213                            usage.non_repudiation();
214                            requested.insert(Usage::contentcommitment);
215                        }
216                        if ku.crl_sign() {
217                            crl_sign_added = true;
218                            usage.crl_sign();
219                            requested.insert(Usage::crlsign);
220                        }
221
222                        if options.ca {
223                            if !cert_sign_added {
224                                usage.key_cert_sign();
225                            }
226                            if !crl_sign_added {
227                                usage.crl_sign();
228                            }
229                        }
230                        builder.append_extension(usage.build()?)?;
231                    }
232                    ParsedExtension::ExtendedKeyUsage(eku) => {
233                        let mut ext = openssl::x509::extension::ExtendedKeyUsage::new();
234                        if eku.server_auth {
235                            ext.server_auth();
236                        }
237                        if eku.client_auth {
238                            ext.client_auth();
239                        }
240                        if eku.code_signing {
241                            ext.code_signing();
242                        }
243                        if eku.email_protection {
244                            ext.email_protection();
245                        }
246                        builder.append_extension(ext.build()?)?;
247                    }
248                    ParsedExtension::SubjectAlternativeName(san) => {
249                        let mut openssl_san =
250                            openssl::x509::extension::SubjectAlternativeName::new();
251                        for name in &san.general_names {
252                            match name {
253                                GeneralName::DNSName(dns) => {
254                                    openssl_san.dns(dns);
255                                }
256                                GeneralName::IPAddress(bytes) => match bytes.len() {
257                                    4 => {
258                                        let octets = <[u8; 4]>::try_from(*bytes)
259                                            .map_err(|_| "unreachable: 4 byte slice")?;
260                                        openssl_san.ip(&Ipv4Addr::from(octets).to_string());
261                                    }
262                                    16 => {
263                                        let octets = <[u8; 16]>::try_from(*bytes)
264                                            .map_err(|_| "unreachable: 16 byte slice")?;
265                                        openssl_san.ip(&Ipv6Addr::from(octets).to_string());
266                                    }
267                                    len => {
268                                        return Err(format!(
269                                            "CSR contains a malformed iPAddress SubjectAltName: \
270                                             expected 4 or 16 octets, got {len}"
271                                        )
272                                        .into());
273                                    }
274                                },
275                                GeneralName::RFC822Name(email) => {
276                                    openssl_san.email(email);
277                                }
278                                GeneralName::URI(uri) => {
279                                    openssl_san.uri(uri);
280                                }
281                                GeneralName::RegisteredID(oid) => {
282                                    openssl_san.rid(&oid.to_id_string());
283                                }
284
285                                GeneralName::DirectoryName(_) => {
286                                    return Err(unsupported_san("directoryName"));
287                                }
288                                GeneralName::OtherName(..) => {
289                                    return Err(unsupported_san("otherName"));
290                                }
291                                GeneralName::X400Address(_) => {
292                                    return Err(unsupported_san("x400Address"));
293                                }
294                                GeneralName::EDIPartyName(_) => {
295                                    return Err(unsupported_san("ediPartyName"));
296                                }
297                            }
298                        }
299                        builder.append_extension(
300                            openssl_san.build(&builder.x509v3_context(None, None))?,
301                        )?;
302                    }
303                    _ => {
304                        println!("Unsupported extension: {:?}", ext);
305                    }
306                }
307            }
308        }
309        #[cfg(feature = "pqc")]
310        validate_pqc_key_usage(&csr_public_key, &requested)?;
311
312        if options.ca {
313            let result = ca_basic_constraints(options.path_len)?;
314            builder.append_extension(result)?;
315            if !any_key_used {
316                let key_usage = KeyUsage::new()
317                    .critical()
318                    .key_cert_sign()
319                    .crl_sign()
320                    .build()
321                    .unwrap();
322                builder.append_extension(key_usage)?;
323            }
324        } else {
325            builder.append_extension(BasicConstraints::new().build()?)?;
326        }
327        builder.set_not_before(&options.valid_from)?;
328        builder.set_not_after(&options.valid_to)?;
329        let serial_number = {
330            let mut serial = BigNum::new()?;
331            serial.rand(159, openssl::bn::MsbOption::MAYBE_ZERO, false)?;
332            serial.to_asn1_integer()?
333        };
334        builder.set_serial_number(&serial_number)?;
335        if signer.x509.subject_key_id().is_some() {
336            let aki = AuthorityKeyIdentifier::new()
337                .keyid(true)
338                .issuer(false)
339                .build(&builder.x509v3_context(Some(&signer.x509), None))?;
340            builder.append_extension(aki)?;
341        }
342        let oid = Asn1Object::from_str("2.5.29.14")?; // OID för Subject Key Identifier (SKI)
343        let pubkey_der = self.csr.public_key().unwrap().public_key_to_der()?;
344        let ski_hash = hash(MessageDigest::sha1(), &pubkey_der)?;
345        let der_encoded = yasna::construct_der(|writer| {
346            writer.write_bytes(ski_hash.as_ref());
347        });
348        let ski_asn1 = Asn1OctetString::new_from_bytes(&der_encoded)?;
349        let ext = X509Extension::new_from_der(oid.as_ref(), false, &ski_asn1)?;
350        builder.append_extension(ext)?;
351        append_certificate_policies(&mut builder, &options.policies)?;
352        let cert: X509 = if is_digestless_key(signer_key) {
353            let builder_cert = builder.build();
354            sign_certificate_digestless(&builder_cert, signer_key)
355                .map_err(|e| format!("Failed to sign certificate with digestless key: {}", e))?;
356            builder_cert
357        } else {
358            builder.sign(signer_key, MessageDigest::sha256())?;
359            builder.build()
360        };
361
362        Ok(Certificate {
363            x509: cert,
364            pkey: None,
365        })
366    }
367}
368
369/// Builder for creating a new certificate signing request and private key
370pub struct CsrBuilder {
371    fields: BuilderFields,
372}
373impl UseesBuilderFields for CsrBuilder {
374    fn fields_mut(&mut self) -> &mut BuilderFields {
375        &mut self.fields
376    }
377}
378impl Default for CsrBuilder {
379    fn default() -> Self {
380        Self::new()
381    }
382}
383
384impl CsrBuilder {
385    /// Create a new CsrBuilder with defaults
386    pub fn new() -> Self {
387        Self {
388            fields: BuilderFields::default(),
389        }
390    }
391
392    /// Builds and returns a Certificate Signing Request (CSR) based on the configured builder fields.
393    ///
394    /// This function constructs the subject name, sets the public key, and adds relevant X.509 extensions
395    /// such as Key Usage, Extended Key Usage, and Subject Alternative Names (SAN).
396    /// It supports signing with both traditional algorithms and Ed25519.
397    ///
398    /// # Returns
399    /// - `Ok(Csr)` if the CSR was successfully built and signed.
400    /// - `Err(Box<dyn std::error::Error>)` if any step in the CSR creation process fails.
401    ///
402    /// # Errors
403    /// This function may return errors in the following cases:
404    /// - Failure to initialize or build the X509 name or request.
405    /// - Failure to select or use the appropriate key type.
406    /// - Failure to build or add X.509 extensions.
407    /// - Failure to sign the CSR, especially with Ed25519.
408    ///
409    /// # Extensions Added
410    /// - **Key Usage** and **Extended Key Usage**: Based on the builder's `usage` field.
411    /// - **Subject Alternative Names (SAN)**: Includes all entries from `alternative_names`.
412    ///
413    /// # Signing Behavior
414    /// - If the key type is Ed25519, uses a custom signing function.
415    /// - Otherwise, signs using the selected hash algorithm.
416    ///
417    /// # Example
418    /// ```rust
419    /// use cert_helper::certificate::CsrBuilder;
420    /// use crate::cert_helper::certificate::UseesBuilderFields;
421    /// let builder = CsrBuilder::new().common_name("example.com");
422    /// let csr = builder.certificate_signing_request().unwrap();
423    /// ```
424    pub fn certificate_signing_request(self) -> Result<Csr, Box<dyn std::error::Error>> {
425        let mut name_builder = X509NameBuilder::new()?;
426        name_builder.append_entry_by_nid(Nid::COMMONNAME, &self.fields.common_name)?;
427        if !self.fields.country_name.trim().is_empty() {
428            name_builder.append_entry_by_nid(Nid::COUNTRYNAME, &self.fields.country_name)?;
429        }
430        if !self.fields.state_province.trim().is_empty() {
431            name_builder
432                .append_entry_by_nid(Nid::STATEORPROVINCENAME, &self.fields.state_province)?;
433        }
434        if !self.fields.locality_time.trim().is_empty() {
435            name_builder.append_entry_by_nid(Nid::LOCALITYNAME, &self.fields.locality_time)?;
436        }
437        if !self.fields.organization.trim().is_empty() {
438            name_builder.append_entry_by_nid(Nid::ORGANIZATIONNAME, &self.fields.organization)?;
439        }
440        let name = name_builder.build();
441        let mut builder = X509ReqBuilder::new()?;
442        builder.set_version(0)?;
443        builder.set_subject_name(&name)?;
444        let pkey = match &self.fields.existing_key {
445            Some(pkey) => pkey.clone(),
446            None => select_key(&self.fields.key_type)?,
447        };
448        builder.set_pubkey(&pkey)?;
449        let key_usage = self.fields.usage.clone().unwrap_or_default();
450
451        // Enforce post-quantum KeyUsage rules. Run before the can't-sign guard
452        // below so a contradictory KeyUsage is reported precisely.
453        #[cfg(feature = "pqc")]
454        validate_pqc_key_usage(&pkey, &key_usage)?;
455
456        // ML-KEM cannot sign, and a PKCS#10 CSR requires a self-signature for
457        // proof-of-possession — so an ML-KEM CSR cannot be produced here.
458        #[cfg(feature = "pqc")]
459        reject_mlkem_signing(
460            &pkey,
461            "ML-KEM (FIPS 203) is a key-encapsulation key and cannot sign a CSR \
462             (PKCS#10 requires a self-signature for proof-of-possession). Issue the \
463             ML-KEM certificate directly with CertBuilder::build_and_sign() using a \
464             signing CA instead.",
465        )?;
466
467        let mut extensions = Stack::new()?;
468
469        let (tracked_key_usage, tracked_extended_key_usage) = get_key_usage(&Some(key_usage));
470        if tracked_key_usage.is_used() {
471            extensions.push(tracked_key_usage.into_inner().build()?)?;
472        }
473        if tracked_extended_key_usage.is_used() {
474            extensions.push(tracked_extended_key_usage.into_inner().build()?)?;
475        }
476
477        // A CSR is a request for an end-entity certificate, so the common name
478        // belongs in the SAN — RFC 6125 verifiers match on SAN and ignore the CN.
479        let names = san_names(&self.fields, true);
480        if !names.is_empty() {
481            // RFC 5280 §4.2.1.6 — never emit an empty SAN
482            let mut san = SubjectAlternativeName::new();
483            for n in &names {
484                add_san_entry(&mut san, n);
485            }
486            extensions.push(san.build(&builder.x509v3_context(None))?)?;
487        }
488
489        builder.add_extensions(&extensions)?;
490        let csr: X509Req = if is_digestless_key(&pkey) {
491            let builder_csr = builder.build();
492            sign_x509_req_digestless(&builder_csr, &pkey)
493                .map_err(|e| format!("Failed to sign certificate with digestless key: {}", e))?;
494            builder_csr
495        } else {
496            builder.sign(&pkey, select_hash(&self.fields.signature_alg))?;
497            builder.build()
498        };
499        Ok(Csr {
500            csr,
501            pkey: Some(pkey),
502        })
503    }
504}
505
506/// Verify a CSR's proof-of-possession.
507///
508/// A PKCS#10 request is self-signed with the private key matching its own public
509/// key; a valid signature proves the requester possesses that private key. We
510/// verify the request signature against its embedded public key and refuse to
511/// issue a certificate from a request that fails
512fn verify_csr_proof_of_possession(csr: &X509Req) -> Result<(), Box<dyn std::error::Error>> {
513    let public_key = csr.public_key()?;
514    if !csr.verify(&public_key)? {
515        return Err(
516            "CSR proof-of-possession check failed: the request signature does \
517            not verify against its own public key"
518                .into(),
519        );
520    }
521    Ok(())
522}
523
524fn unsupported_san(kind: &str) -> Box<dyn Error> {
525    format!("CSR contains a SubjectAltName type this crate cannot reproduce: {kind}").into()
526}
527
528#[cfg(test)]
529mod tests {
530    use super::*;
531    use crate::certificate::CertBuilder;
532    #[cfg(feature = "pqc")]
533    use crate::certificate::key::generate_pqc_key;
534    #[cfg(feature = "pqc")]
535    use crate::certificate::key::sign_x509_req_digestless;
536    use openssl::ec::{EcGroup, EcKey};
537    use openssl::x509::X509Req;
538    #[cfg(feature = "pqc")]
539    use openssl::x509::X509ReqBuilder;
540    #[cfg(feature = "pqc")]
541    use openssl::x509::extension::KeyUsage;
542    use std::io::Write;
543    use tempfile::NamedTempFile;
544
545    #[test]
546    fn create_ca_cert_from_csr_with_path_len() {
547        let ca = CertBuilder::new()
548            .common_name("My Test Ca")
549            .is_ca(true)
550            .pathlen(2)
551            .build_and_self_sign()
552            .unwrap();
553
554        let csr = CsrBuilder::new()
555            .common_name("leaf")
556            .certificate_signing_request()
557            .unwrap();
558        let cert = csr
559            .build_signed_certificate(&ca, CsrOptions::new().pathlen(1, vec![]).is_ca(true))
560            .unwrap();
561        assert_eq!(cert.x509.pathlen(), Some(1));
562    }
563
564    #[test]
565    fn create_non_ca_cert_from_csr_with_path_len() {
566        let ca = CertBuilder::new()
567            .common_name("My Test Ca")
568            .is_ca(true)
569            .pathlen(2)
570            .build_and_self_sign()
571            .unwrap();
572
573        let csr = CsrBuilder::new()
574            .common_name("leaf")
575            .certificate_signing_request()
576            .unwrap();
577        let cert = csr
578            .build_signed_certificate(&ca, CsrOptions::new().pathlen(1, vec![]))
579            .unwrap();
580        assert_eq!(cert.x509.pathlen(), None);
581    }
582
583    #[test]
584    fn create_ca_cert_with_chain_from_csr_with_path_len() {
585        let ca = CertBuilder::new()
586            .common_name("My Test root Ca")
587            .is_ca(true)
588            .pathlen(3)
589            .build_and_self_sign()
590            .unwrap();
591        let interca1 = CertBuilder::new()
592            .common_name("My Test Ca1")
593            .is_ca(true)
594            .pathlen(2)
595            .build_and_sign_with_chain(&ca, &[])
596            .unwrap();
597        let interca2 = CertBuilder::new()
598            .common_name("My Test Ca2")
599            .is_ca(true)
600            .pathlen(1)
601            .build_and_sign_with_chain(&interca1, &[&ca])
602            .unwrap();
603        let csr = CsrBuilder::new()
604            .common_name("leaf")
605            .certificate_signing_request()
606            .unwrap();
607        let cert = csr
608            .build_signed_certificate(
609                &interca2,
610                CsrOptions::new().pathlen(0, vec![ca, interca1]).is_ca(true),
611            )
612            .unwrap();
613        assert_eq!(cert.x509.pathlen(), Some(0));
614    }
615
616    #[test]
617    fn create_ca_cert_with_chain_from_csr_with_budget_exhausted() {
618        let ca = CertBuilder::new()
619            .common_name("My Test root Ca")
620            .is_ca(true)
621            .pathlen(3)
622            .build_and_self_sign()
623            .unwrap();
624        let interca1 = CertBuilder::new()
625            .common_name("My Test Ca1")
626            .is_ca(true)
627            .pathlen(2)
628            .build_and_sign_with_chain(&ca, &[])
629            .unwrap();
630        let interca2 = CertBuilder::new()
631            .common_name("My Test Ca2")
632            .is_ca(true)
633            .pathlen(0)
634            .build_and_sign_with_chain(&interca1, &[&ca])
635            .unwrap();
636        let csr = CsrBuilder::new()
637            .common_name("leaf")
638            .certificate_signing_request()
639            .unwrap();
640        let err = csr
641            .build_signed_certificate(
642                &interca2,
643                CsrOptions::new().pathlen(0, vec![ca, interca1]).is_ca(true),
644            )
645            .err()
646            .expect("");
647        assert!(
648            err.to_string()
649                .contains("signer's path length budget is exhausted; cannot issue a CA"),
650            "signer's path length budget is exhausted; cannot issue a CA got: {err}"
651        );
652    }
653    #[test]
654    fn csr_inflated_pathlen_is_rejected() {
655        let ca = CertBuilder::new()
656            .common_name("root")
657            .is_ca(true)
658            .pathlen(1)
659            .build_and_self_sign()
660            .unwrap();
661        let csr = CsrBuilder::new()
662            .common_name("leaf")
663            .certificate_signing_request()
664            .unwrap();
665        let err = csr
666            .build_signed_certificate(
667                &ca,
668                CsrOptions::new().is_ca(true).pathlen(1, vec![]), // budget=1, m=1 → 1>=1
669            )
670            .err()
671            .expect("inflated pathLen must be rejected");
672        assert!(
673            err.to_string()
674                .contains("requested pathLen exceeds what the signer's chain permits"),
675            "got: {err}"
676        );
677    }
678
679    #[test]
680    fn csr_incomplete_chain_is_rejected() {
681        let ca = CertBuilder::new()
682            .common_name("root")
683            .is_ca(true)
684            .pathlen(2)
685            .build_and_self_sign()
686            .unwrap();
687        let inter = CertBuilder::new()
688            .common_name("inter")
689            .is_ca(true)
690            .pathlen(1)
691            .build_and_sign_with_chain(&ca, &[])
692            .unwrap();
693        let csr = CsrBuilder::new()
694            .common_name("leaf")
695            .certificate_signing_request()
696            .unwrap();
697        let err = csr
698            .build_signed_certificate(
699                &inter,
700                CsrOptions::new().is_ca(true).pathlen(0, vec![]), // root omitted
701            )
702            .err()
703            .expect("incomplete chain must be rejected");
704        assert!(
705            err.to_string().contains("Could not find self signed root"),
706            "got: {err}"
707        );
708    }
709
710    #[test]
711    fn csr_unlimited_signer_allows_ca_issuance() {
712        let ca = CertBuilder::new()
713            .common_name("root")
714            .is_ca(true) // no .pathlen() → None
715            .build_and_self_sign()
716            .unwrap();
717        let csr = CsrBuilder::new()
718            .common_name("leaf")
719            .certificate_signing_request()
720            .unwrap();
721        let cert = csr
722            .build_signed_certificate(
723                &ca,
724                CsrOptions::new().is_ca(true).pathlen(5, vec![]), // unlimited → any m ok
725            )
726            .unwrap();
727        assert_eq!(cert.x509.pathlen(), Some(5));
728    }
729    #[test]
730    fn build_signed_certificate_rejects_csr_with_bad_proof_of_possession() {
731        // A CSR whose self-signature does not match its public key (proof-of-
732        // possession failure) must be refused, not turned into a certificate.
733        let ca = CertBuilder::new()
734            .common_name("My Test Ca")
735            .is_ca(true)
736            .build_and_self_sign()
737            .unwrap();
738
739        let good = CsrBuilder::new()
740            .common_name("leaf")
741            .certificate_signing_request()
742            .unwrap();
743
744        // Flip the last byte of the DER — the tail of the signature BIT STRING —
745        // so the request no longer verifies against its own public key while
746        // staying structurally parseable.
747        let mut der = good.csr.to_der().unwrap();
748        let last = der.len() - 1;
749        der[last] ^= 0xFF;
750        let tampered = Csr {
751            csr: X509Req::from_der(&der).unwrap(),
752            pkey: None,
753        };
754
755        let err = tampered
756            .build_signed_certificate(&ca, CsrOptions::new())
757            .err()
758            .expect("CSR with broken proof-of-possession must be rejected");
759        assert!(
760            err.to_string().contains("proof-of-possession"),
761            "expected a proof-of-possession error, got: {err}"
762        );
763    }
764
765    /// The DNS names in a CSR's requested SubjectAltName, or `None` when the
766    /// request carries no SAN extension at all.
767    fn csr_san_dns(csr: &Csr) -> Option<Vec<String>> {
768        use x509_parser::certification_request::X509CertificationRequest;
769        use x509_parser::prelude::FromDer;
770
771        let der = csr.csr.to_der().unwrap();
772        let (_, parsed) = X509CertificationRequest::from_der(&der).unwrap();
773        parsed.requested_extensions()?.find_map(|ext| match ext {
774            ParsedExtension::SubjectAlternativeName(san) => Some(
775                san.general_names
776                    .iter()
777                    .filter_map(|gn| match gn {
778                        GeneralName::DNSName(n) => Some((*n).to_string()),
779                        _ => None,
780                    })
781                    .collect(),
782            ),
783            _ => None,
784        })
785    }
786
787    #[test]
788    fn csr_common_name_is_included_in_the_san() {
789        let csr = CsrBuilder::new()
790            .common_name("leaf.example.com")
791            .certificate_signing_request()
792            .unwrap();
793
794        assert_eq!(
795            csr_san_dns(&csr).expect("a CSR with a common name should request a SAN"),
796            ["leaf.example.com"]
797        );
798    }
799
800    #[test]
801    fn csr_common_name_listed_in_alternative_names_is_not_duplicated() {
802        let csr = CsrBuilder::new()
803            .common_name("example.com")
804            .alternative_names(vec!["example.com", "www.example.com"])
805            .certificate_signing_request()
806            .unwrap();
807
808        let mut dns = csr_san_dns(&csr).expect("CSR should request a SAN");
809        dns.sort();
810        assert_eq!(dns, ["example.com", "www.example.com"]);
811    }
812
813    #[test]
814    fn csr_without_any_names_emits_no_san_extension() {
815        // RFC 5280 §4.2.1.6 — a present SubjectAltName must hold at least one
816        // entry, so with nothing to put in it the extension must be absent
817        // rather than empty.
818        let csr = CsrBuilder::new()
819            .common_name("   ")
820            .certificate_signing_request()
821            .unwrap();
822
823        assert_eq!(csr_san_dns(&csr), None);
824    }
825
826    #[test]
827    fn ensure_that_supplied_private_key_is_used() {
828        let ca = CertBuilder::new()
829            .common_name("My Test Ca")
830            .is_ca(true)
831            .build_and_self_sign()
832            .unwrap();
833        let csr = CsrBuilder::new()
834            .common_name("leaf")
835            .private_key(ca.pkey.clone().unwrap())
836            .certificate_signing_request()
837            .unwrap();
838        let cert = csr
839            .build_signed_certificate(
840                &ca,
841                CsrOptions::new().is_ca(true), // unlimited → any m ok
842            )
843            .unwrap();
844        assert_eq!(
845            ca.x509.public_key().unwrap().public_key_to_der().unwrap(),
846            cert.x509.public_key().unwrap().public_key_to_der().unwrap()
847        )
848    }
849    #[test]
850    fn san_directory_name_should_give_an_error() {
851        let ca = CertBuilder::new()
852            .common_name("My Test Ca")
853            .is_ca(true)
854            .build_and_self_sign()
855            .unwrap();
856        let bad_csr = csr_with_directory_name_san();
857        let err = bad_csr
858            .build_signed_certificate(&ca, CsrOptions::new())
859            .unwrap_err();
860        assert!(
861            err.to_string().contains("directoryName"),
862            "expected error for san with directory name"
863        )
864    }
865
866    #[cfg(feature = "pqc")]
867    #[test]
868    fn do_not_allow_csr_with_pqc_key_and_encipherment_to_generate_certificate() {
869        let ca = CertBuilder::new()
870            .common_name("My Test Ca")
871            .is_ca(true)
872            .build_and_self_sign()
873            .unwrap();
874
875        let pkey = generate_pqc_key("ML-DSA-65").unwrap();
876        let mut builder = X509ReqBuilder::new().unwrap();
877        builder
878            .set_pubkey(&pkey)
879            .expect("failed to set public key in csr");
880        let mut exts = Stack::new().unwrap();
881        exts.push(
882            KeyUsage::new()
883                .critical()
884                .key_encipherment()
885                .build()
886                .unwrap(),
887        )
888        .unwrap();
889        builder.set_version(0).unwrap();
890        builder.add_extensions(&exts).unwrap();
891        let req = builder.build();
892
893        let _ = sign_x509_req_digestless(&req, &pkey);
894        let csr = Csr {
895            csr: req,
896            pkey: None,
897        };
898        let err = csr
899            .build_signed_certificate(&ca, CsrOptions::new())
900            .err()
901            .expect("a PQC signature key requesting keyEncipherment must be rejected");
902        assert!(
903            err.to_string().contains("keyEncipherment"),
904            "expected a keyEncipherment error, got: {err}"
905        );
906    }
907
908    #[test]
909    fn test_reading_csr_from_file() {
910        let csr_data = b"-----BEGIN CERTIFICATE REQUEST-----
911MIICzDCCAbQCAQAwXTEVMBMGA1UEAwwMZXhhbXBsZTIuY29tMQswCQYDVQQGEwJT
912RTESMBAGA1UECAwJU3RvY2tob2xtMRIwEAYDVQQHDAlTdG9ja2hvbG0xDzANBgNV
913BAoMBk15IG9yZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIeAXpCG
914hbIayESfdTzOO0DxIMsOAu4kUm0zF0W/+xDUHl6bGy3wlB9S9nzBG/qwqFZ27Om3
915o4zrZ8K8DBx0ERWNuhMmr0Nx8QpAWBEyxOc08Gn4c3XVBBkRZSn4AIqr9DGtcUqW
916tQZXvMGF6sRRljiEvOxO6zMzZKTGYwzIeQvH85cQ3uXsw0Kknsw/fcuywaAC8SS9
917aqs4jiEIgzdhxdH2OVXBNGj4cjVhK309JiWFHS9XJLNV/PKC+F1nkaANQwbW5A4F
9189vya4js9gk8f4SfF1u+qOJEvsDvAb+1xdjXPRzf77eGh3rC4KgGWQ6WrWfW8PItF
919BDg/jskq3bJXNL8CAwEAAaAqMCgGCSqGSIb3DQEJDjEbMBkwFwYDVR0RBBAwDoIM
920ZXhhbXBsZTIuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQAHeeSW8C6SMVhWiMvPn7iz
921FUHQedHRyPz6kTEfC01eNIbs0r4YghOAcm8PF67jncIXVrqgxo1uzq12qlV+0YYb
922jps31IbQNOz0eFLYvij15ielmOYeQZZ/2vqaGi3geVobLc6Ki5tadnA/NhjTN33j
923QcqDDic8riAOTbSQ6TH9KPTGJQOPk+taMpDGDHskIW0oME5iT2ewbhBHg6v/kSzy
924tss2kBY5O7vo2COtbNcwX5Xp9S2LH9kVUKr0GIjuQjwbv5xl+GNdDey09W9EDACU
925jcGV3++2wS4LN4h3CG4pWZ+LTXhm8ymhoWOapN95lfe3xLRAKFJwiLkGwS75++FW
926-----END CERTIFICATE REQUEST-----";
927        let mut csr_file = NamedTempFile::new().expect("Failed to create temp csr file");
928        csr_file.write_all(csr_data).expect("Failed to write csr");
929        let result = Csr::load_csr(csr_file.path());
930        assert!(result.is_ok(), "Failed to load csr: {:?}", result.err());
931    }
932
933    fn csr_with_directory_name_san() -> Csr {
934        let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap();
935        let pkey = PKey::from_ec_key(EcKey::generate(&group).unwrap()).unwrap();
936
937        let mut req = X509ReqBuilder::new().unwrap();
938        req.set_version(0).unwrap();
939
940        let mut subject = X509NameBuilder::new().unwrap();
941        subject.append_entry_by_text("CN", "leaf").unwrap();
942        req.set_subject_name(&subject.build()).unwrap();
943        req.set_pubkey(&pkey).unwrap();
944
945        let mut dir = X509NameBuilder::new().unwrap();
946        dir.append_entry_by_text("CN", "some directory name")
947            .unwrap();
948        let dir = dir.build();
949
950        let mut san = SubjectAlternativeName::new();
951        san.dir_name2(dir); // NOT dir_name — see below
952        let ext = san.build(&req.x509v3_context(None)).unwrap();
953
954        let mut exts = Stack::new().unwrap();
955        exts.push(ext).unwrap();
956        req.add_extensions(&exts).unwrap();
957
958        req.sign(&pkey, MessageDigest::sha256()).unwrap(); // required — see below
959
960        Csr {
961            csr: req.build(),
962            pkey: Some(pkey),
963        }
964    }
965}