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