From e0fefb8abdb446b49130d7abdbef908f1b230c1c Mon Sep 17 00:00:00 2001 From: Lukas Velikov Date: Sat, 9 Nov 2024 16:02:48 -0500 Subject: [PATCH] Add PKCS#10 attributes to CSR serializer --- rcgen/src/certificate.rs | 53 ++++++++++++++++++++++++++++++---- rcgen/src/lib.rs | 4 +-- rcgen/tests/generic.rs | 61 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 7 deletions(-) diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index e091d091..9aea5d2b 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -522,7 +522,8 @@ impl CertificateParams { }); } - /// Generate and serialize a certificate signing request (CSR). + /// Generate and serialize a certificate signing request (CSR) with custom PKCS #10 attributes. + /// as defined in [RFC 2986][1]. /// /// The constructed CSR will contain attributes based on the certificate parameters, /// and include the subject public key information from `subject_key`. Additionally, @@ -530,9 +531,12 @@ impl CertificateParams { /// /// Note that subsequent invocations of `serialize_request()` will not produce the exact /// same output. - pub fn serialize_request( + /// + /// [1]: + pub fn serialize_request_with_attributes( &self, subject_key: &KeyPair, + attrs: Vec, ) -> Result { // No .. pattern, we use this to ensure every field is used #[deny(unused)] @@ -582,11 +586,9 @@ impl CertificateParams { let der = subject_key.sign_der(|writer| { // Write version writer.next().write_u8(0); - // Write subject name write_distinguished_name(writer.next(), distinguished_name); - // Write subjectPublicKeyInfo serialize_public_key_der(subject_key, writer.next()); - // Write extensions + // According to the spec in RFC 2986, even if attributes are empty we need the empty attribute tag writer .next() @@ -596,6 +598,13 @@ impl CertificateParams { if write_extension_request { self.write_extension_request_attribute(writer.next()); } + + for Attribute { oid, values } in attrs { + writer.next().write_sequence(|writer| { + writer.next().write_oid(&ObjectIdentifier::from_slice(&oid)); + writer.next().write_der(&values); + }); + } }); }); @@ -607,6 +616,21 @@ impl CertificateParams { }) } + /// Generate and serialize a certificate signing request (CSR). + /// + /// The constructed CSR will contain attributes based on the certificate parameters, + /// and include the subject public key information from `subject_key`. Additionally, + /// the CSR will be self-signed using the subject key. + /// + /// Note that subsequent invocations of `serialize_request()` will not produce the exact + /// same output. + pub fn serialize_request( + &self, + subject_key: &KeyPair, + ) -> Result { + self.serialize_request_with_attributes(subject_key, Vec::new()) + } + pub(crate) fn serialize_der_with_signer( &self, pub_key: &K, @@ -846,6 +870,25 @@ fn write_general_subtrees(writer: DERWriter, tag: u64, general_subtrees: &[Gener }); } +/// A PKCS #10 CSR attribute, as defined in [RFC 5280][1] and constrained +/// by [RFC 2986][2]. +/// +/// [1]: +/// [2]: +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct Attribute { + /// `AttributeType` of the `Attribute`, defined as an `OBJECT IDENTIFIER`. + pub oid: Vec, + /// DER-encoded values of the `Attribute`, defined by [RFC 2986][1] as: + /// + /// ```text + /// SET SIZE(1..MAX) OF ATTRIBUTE.&Type({IOSet}{@type}) + /// ``` + /// + /// [1]: https://datatracker.ietf.org/doc/html/rfc2986#section-4 + pub values: Vec, +} + /// A custom extension of a certificate, as specified in /// [RFC 5280](https://tools.ietf.org/html/rfc5280#section-4.2) #[derive(Debug, PartialEq, Eq, Hash, Clone)] diff --git a/rcgen/src/lib.rs b/rcgen/src/lib.rs index 6aea3f2d..4f8fb639 100644 --- a/rcgen/src/lib.rs +++ b/rcgen/src/lib.rs @@ -49,8 +49,8 @@ use yasna::DERWriter; use yasna::Tag; pub use certificate::{ - date_time_ymd, BasicConstraints, Certificate, CertificateParams, CidrSubnet, CustomExtension, - DnType, ExtendedKeyUsagePurpose, GeneralSubtree, IsCa, NameConstraints, + date_time_ymd, Attribute, BasicConstraints, Certificate, CertificateParams, CidrSubnet, + CustomExtension, DnType, ExtendedKeyUsagePurpose, GeneralSubtree, IsCa, NameConstraints, }; pub use crl::{ CertificateRevocationList, CertificateRevocationListParams, CrlDistributionPoint, diff --git a/rcgen/tests/generic.rs b/rcgen/tests/generic.rs index 248a4bd8..8bbe9061 100644 --- a/rcgen/tests/generic.rs +++ b/rcgen/tests/generic.rs @@ -135,6 +135,67 @@ mod test_x509_custom_ext { } } +#[cfg(feature = "x509-parser")] +mod test_csr_custom_attributes { + use rcgen::{Attribute, CertificateParams, KeyPair}; + use x509_parser::{ + der_parser::{asn1_rs, Oid}, + prelude::{FromDer, X509CertificationRequest}, + }; + + /// Test serializing a CSR with custom attributes. + /// This test case uses `challengePassword` from [RFC 2985][1], a simple + /// ATTRIBUTE that contains a single UTF8String. + /// + /// [1]: + #[test] + fn test_csr_custom_attributes() { + // OID for challengePassword + let challenge_pwd_oid = asn1_rs::Oid::from(&[1, 2, 840, 113549, 1, 9, 7]) + .unwrap() + .iter() + .unwrap() + .collect::>(); + + // Attribute values for challengePassword + let challenge_pwd_values = yasna::try_construct_der::<_, ()>(|writer| { + // Reminder: CSR attribute values are contained in a SET + writer.write_set(|writer| { + // Challenge passwords only have one value, a UTF8String + writer + .next() + .write_utf8_string("nobody uses challenge passwords anymore"); + Ok(()) + }) + }) + .unwrap(); + + // Challenge password attribute + let challenge_password_attribute = Attribute { + oid: challenge_pwd_oid.clone(), + values: challenge_pwd_values.clone(), + }; + + // Serialize a DER-encoded CSR + let params = CertificateParams::default(); + let key_pair = KeyPair::generate().unwrap(); + let csr = params + .serialize_request_with_attributes(&key_pair, vec![challenge_password_attribute]) + .unwrap(); + + // Parse the CSR + let (_, x509_csr) = X509CertificationRequest::from_der(csr.der()).unwrap(); + let parsed_attribute_value = x509_csr + .certification_request_info + .attributes_map() + .unwrap() + .get(&Oid::from(&challenge_pwd_oid).unwrap()) + .unwrap() + .value; + assert_eq!(parsed_attribute_value, challenge_pwd_values); + } +} + #[cfg(feature = "x509-parser")] mod test_x509_parser_crl { use crate::util;