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 9, 2024
1 parent 24a5518 commit e0fefb8
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 7 deletions.
53 changes: 48 additions & 5 deletions rcgen/src/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,17 +522,21 @@ 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,
/// 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(
///
/// [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 +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()
Expand All @@ -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);
});
}
});
});

Expand All @@ -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<CertificateSigningRequest, Error> {
self.serialize_request_with_attributes(subject_key, Vec::new())
}

pub(crate) fn serialize_der_with_signer<K: PublicKeyData>(
&self,
pub_key: &K,
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: Vec<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
61 changes: 61 additions & 0 deletions rcgen/tests/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]: <https://datatracker.ietf.org/doc/html/rfc2985>
#[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::<Vec<u64>>();

// 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;
Expand Down

0 comments on commit e0fefb8

Please sign in to comment.