From c6d907b3fa69dce5d9b10f33c0a8c01306235b3e Mon Sep 17 00:00:00 2001 From: Lukas Velikov Date: Tue, 29 Oct 2024 18:29:02 -0400 Subject: [PATCH] CSR serializer: add PKCS #10 attributes --- rcgen/src/certificate.rs | 104 ++++++++++++++++++++-------- rcgen/src/lib.rs | 4 +- rcgen/tests/generic.rs | 142 ++++++++++++++++++++++++++++++++++----- 3 files changed, 204 insertions(+), 46 deletions(-) diff --git a/rcgen/src/certificate.rs b/rcgen/src/certificate.rs index 68ec0f67..0328969f 100644 --- a/rcgen/src/certificate.rs +++ b/rcgen/src/certificate.rs @@ -81,6 +81,11 @@ pub struct CertificateParams { /// [^1]: pub crl_distribution_points: Vec, pub custom_extensions: Vec, + /// An optional set of custom PKCS #10 certificate signing request (CSR) attributes as + /// defined in [RFC 2986][1]. + /// + /// [1]: + pub custom_csr_attributes: Vec, /// If `true`, the 'Authority Key Identifier' extension will be added to the generated cert pub use_authority_key_identifier_extension: bool, /// Method to generate key identifiers from public keys @@ -108,6 +113,7 @@ impl Default for CertificateParams { name_constraints: None, crl_distribution_points: Vec::new(), custom_extensions: Vec::new(), + custom_csr_attributes: Vec::new(), use_authority_key_identifier_extension: false, #[cfg(feature = "crypto")] key_identifier_method: KeyIdMethod::Sha256, @@ -461,6 +467,10 @@ impl CertificateParams { } fn write_subject_alt_names(&self, writer: DERWriter) { + if self.subject_alt_names.is_empty() { + return; + } + write_x509_extension(writer, oid::SUBJECT_ALT_NAME, false, |writer| { writer.write_sequence(|writer| { for san in self.subject_alt_names.iter() { @@ -517,6 +527,7 @@ impl CertificateParams { name_constraints, crl_distribution_points, custom_extensions, + custom_csr_attributes: custom_attributes, use_authority_key_identifier_extension, key_identifier_method, } = self; @@ -550,37 +561,57 @@ impl CertificateParams { // Write subjectPublicKeyInfo serialize_public_key_der(subject_key, writer.next()); // Write extensions + + // Whether or not to write an extension request attribute + let write_extension_request = !key_usages.is_empty() + || !subject_alt_names.is_empty() + || !extended_key_usages.is_empty() + || !custom_extensions.is_empty(); + // According to the spec in RFC 2986, even if attributes are empty we need the empty attribute tag - writer.next().write_tagged(Tag::context(0), |writer| { - if !key_usages.is_empty() - || !subject_alt_names.is_empty() - || !custom_extensions.is_empty() - { - writer.write_sequence(|writer| { - let oid = ObjectIdentifier::from_slice(oid::PKCS_9_AT_EXTENSION_REQUEST); - writer.next().write_oid(&oid); - writer.next().write_set(|writer| { + writer + .next() + .write_tagged_implicit(Tag::context(0), |writer| { + // RFC 2986 specifies that attributes are a SET OF Attribute + writer.write_set_of(|writer| { + // Write the extension request CSR attribute + if write_extension_request { writer.next().write_sequence(|writer| { - // Write key_usage - self.write_key_usage(writer.next()); - // Write subject_alt_names - self.write_subject_alt_names(writer.next()); - self.write_extended_key_usage(writer.next()); - - // Write custom extensions - for ext in custom_extensions { - write_x509_extension( - writer.next(), - &ext.oid, - ext.critical, - |writer| writer.write_der(ext.content()), - ); - } + let oid = + ObjectIdentifier::from_slice(oid::PKCS_9_AT_EXTENSION_REQUEST); + writer.next().write_oid(&oid); + writer.next().write_set(|writer| { + writer.next().write_sequence(|writer| { + // Write key_usage + self.write_key_usage(writer.next()); + // Write subject_alt_names + self.write_subject_alt_names(writer.next()); + self.write_extended_key_usage(writer.next()); + + // Write custom extensions + for ext in custom_extensions { + write_x509_extension( + writer.next(), + &ext.oid, + ext.critical, + |writer| writer.write_der(ext.content()), + ); + } + }); + }); }); - }); + } + + // Write any custom CSR attributes + for Attribute { oid, values } in custom_attributes { + writer.next().write_sequence(|writer| { + let oid = ObjectIdentifier::from_slice(oid); + writer.next().write_oid(&oid); + writer.next().write_der(&values); + }); + } }); - } - }); + }); Ok(()) })?; @@ -829,6 +860,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 d9eef933..351f66aa 100644 --- a/rcgen/tests/generic.rs +++ b/rcgen/tests/generic.rs @@ -135,6 +135,70 @@ 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 mut params = CertificateParams::default(); + params + .custom_csr_attributes + .push(challenge_password_attribute); + // params.key_usages.push(KeyUsagePurpose::DigitalSignature); + let key_pair = KeyPair::generate().unwrap(); + let csr = params.serialize_request(&key_pair).unwrap(); + eprintln!("{}", csr.pem().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; @@ -360,27 +424,20 @@ mod test_parse_other_name_alt_name { #[cfg(feature = "x509-parser")] mod test_csr { - use rcgen::{CertificateParams, CertificateSigningRequestParams, KeyPair, KeyUsagePurpose}; + use rcgen::{ + CertificateParams, CertificateSigningRequestParams, ExtendedKeyUsagePurpose, KeyPair, + KeyUsagePurpose, + }; #[test] fn test_csr_roundtrip() { - let key_pair = KeyPair::generate().unwrap(); - // We should be able to serialize a CSR, and then parse the CSR. - let csr = CertificateParams::default() - .serialize_request(&key_pair) - .unwrap(); - let csrp = CertificateSigningRequestParams::from_der(csr.der()).unwrap(); - - // Ensure algorithms match. - assert_eq!(key_pair.algorithm(), csrp.public_key.algorithm()); + let params = CertificateParams::default(); + generate_and_test_parsed_csr(¶ms); } #[test] - fn test_nontrivial_csr_roundtrip() { - let key_pair = KeyPair::generate().unwrap(); - - // We should be able to serialize a CSR, and then parse the CSR. + fn test_csr_with_key_usages_roundtrip() { let mut params = CertificateParams::default(); params.key_usages = vec![ KeyUsagePurpose::DigitalSignature, @@ -395,12 +452,63 @@ mod test_csr { // KeyUsagePurpose::EncipherOnly, KeyUsagePurpose::DecipherOnly, ]; + generate_and_test_parsed_csr(¶ms); + } + + #[test] + fn test_csr_with_extended_key_usages_roundtrip() { + let mut params = CertificateParams::default(); + params.extended_key_usages = vec![ + ExtendedKeyUsagePurpose::ServerAuth, + ExtendedKeyUsagePurpose::ClientAuth, + ]; + generate_and_test_parsed_csr(¶ms); + } + + #[test] + fn test_csr_with_key_usgaes_and_extended_key_usages_roundtrip() { + let mut params = CertificateParams::default(); + params.key_usages = vec![KeyUsagePurpose::DigitalSignature]; + params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth]; + generate_and_test_parsed_csr(¶ms); + } + + fn generate_and_test_parsed_csr(params: &CertificateParams) { + // Generate a key pair for the CSR + let key_pair = KeyPair::generate().unwrap(); + // Serialize the CSR into DER from the given parameters let csr = params.serialize_request(&key_pair).unwrap(); + // Parse the CSR we just serialized let csrp = CertificateSigningRequestParams::from_der(csr.der()).unwrap(); - // Ensure algorithms match. + // Assert that our parsed parameters match our initial parameters + // Certificate parameters are #[non_exhaustive] and don't implement equality assert_eq!(key_pair.algorithm(), csrp.public_key.algorithm()); - // Ensure key usages match. - assert_eq!(csrp.params.key_usages, params.key_usages); + assert_eq!(params.not_before, csrp.params.not_before); + assert_eq!(params.not_after, csrp.params.not_after); + assert_eq!(params.serial_number, csrp.params.serial_number); + assert_eq!(params.subject_alt_names, csrp.params.subject_alt_names); + assert_eq!(params.distinguished_name, csrp.params.distinguished_name); + assert_eq!(params.is_ca, csrp.params.is_ca); + assert_eq!(params.key_usages, csrp.params.key_usages); + assert_eq!(params.extended_key_usages, csrp.params.extended_key_usages); + assert_eq!(params.name_constraints, csrp.params.name_constraints); + assert_eq!( + params.crl_distribution_points, + csrp.params.crl_distribution_points + ); + assert_eq!(params.custom_extensions, csrp.params.custom_extensions); + assert_eq!( + params.custom_csr_attributes, + csrp.params.custom_csr_attributes + ); + assert_eq!( + params.use_authority_key_identifier_extension, + csrp.params.use_authority_key_identifier_extension, + ); + assert_eq!( + params.key_identifier_method, + csrp.params.key_identifier_method + ); } }