Skip to content

Commit

Permalink
Add PKCS#10 attributes to CSR serializer
Browse files Browse the repository at this point in the history
  • Loading branch information
lvkv committed Nov 13, 2024
1 parent c588982 commit 9ea4db2
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 5 deletions.
49 changes: 46 additions & 3 deletions rcgen/src/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,25 @@ impl CertificateParams {
pub fn serialize_request(
&self,
subject_key: &KeyPair,
) -> Result<CertificateSigningRequest, Error> {
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][1].
///
/// 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.
///
/// [1]: <https://datatracker.ietf.org/doc/html/rfc2986#section-4>
pub fn serialize_request_with_attributes(
&self,
subject_key: &KeyPair,
attrs: Vec<Attribute>,
) -> Result<CertificateSigningRequest, Error> {
// No .. pattern, we use this to ensure every field is used
#[deny(unused)]
Expand Down Expand Up @@ -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()
Expand All @@ -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);
});
}
});
});

Expand Down Expand Up @@ -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]: <https://datatracker.ietf.org/doc/html/rfc5280#appendix-A.1>
/// [2]: <https://datatracker.ietf.org/doc/html/rfc2986#section-4>
#[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][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<u8>,
}

/// 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)]
Expand Down
4 changes: 2 additions & 2 deletions rcgen/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
57 changes: 57 additions & 0 deletions rcgen/tests/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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][1], a simple
/// ATTRIBUTE that contains a single UTF8String.
///
/// [1]: <https://datatracker.ietf.org/doc/html/rfc2985>
#[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;
Expand Down

0 comments on commit 9ea4db2

Please sign in to comment.