From 331b8fd64dd9f868db48ae4d3c75542fb886985a 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 | 49 +++++++++++++++++++++++++++++++--- rcgen/src/lib.rs | 4 +-- rcgen/tests/generic.rs | 57 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 5 deletions(-) diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index caf6d710..0c409048 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -533,6 +533,25 @@ impl CertificateParams { pub fn serialize_request( &self, subject_key: &KeyPair, + ) -> Result { + self.serialize_request_with_attributes(subject_key, Vec::new()) + } + + /// Generate and serialize a certificate signing request (CSR) with custom PKCS #10 attributes. + /// as defined in [RFC 2986]. + /// + /// 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_with_attributes()` will not produce the exact + /// same output. + /// + /// [RFC 2986]: + 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 +601,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 +613,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); + }); + } }); }); @@ -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] and constrained +/// by [RFC 2986]. +/// +/// [RFC 5280]: +/// [RFC 2986]: +#[derive(Debug, PartialEq, Eq, Hash, Clone)] +pub struct Attribute { + /// `AttributeType` of the `Attribute`, defined as an `OBJECT IDENTIFIER`. + pub oid: &'static [u64], + /// DER-encoded values of the `Attribute`, defined by [RFC 2986] as: + /// + /// ```text + /// SET SIZE(1..MAX) OF ATTRIBUTE.&Type({IOSet}{@type}) + /// ``` + /// + /// [RFC 2986]: 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 3c336888..73324a42 100644 --- a/rcgen/tests/generic.rs +++ b/rcgen/tests/generic.rs @@ -135,6 +135,63 @@ mod test_x509_custom_ext { } } +#[cfg(feature = "x509-parser")] +mod test_csr_custom_attributes { + use rcgen::{Attribute, CertificateParams, KeyPair}; + use x509_parser::{ + der_parser::Oid, + prelude::{FromDer, X509CertificationRequest}, + }; + + /// Test serializing a CSR with custom attributes. + /// This test case uses `challengePassword` from [RFC 2985], a simple + /// ATTRIBUTE that contains a single UTF8String. + /// + /// [RFC 2985]: + #[test] + fn test_csr_custom_attributes() { + // OID for challengePassword + const CHALLENGE_PWD_OID: &[u64] = &[1, 2, 840, 113549, 1, 9, 7]; + + // 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, + 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;