Skip to content

Commit

Permalink
CSR serializer: add PKCS #10 attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
lvkv committed Oct 29, 2024
1 parent 2faf2e1 commit c16c5dc
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 46 deletions.
104 changes: 77 additions & 27 deletions rcgen/src/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ pub struct CertificateParams {
/// [^1]: <https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.13>
pub crl_distribution_points: Vec<CrlDistributionPoint>,
pub custom_extensions: Vec<CustomExtension>,
/// An optional set of custom PKCS #10 certificate signing request (CSR) attributes as
/// defined in [RFC 2986][1].
///
/// [1]: <https://datatracker.ietf.org/doc/html/rfc2986#section-4>
pub custom_csr_attributes: Vec<Attribute>,
/// 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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(())
})?;
Expand Down Expand Up @@ -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]: <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
142 changes: 125 additions & 17 deletions rcgen/tests/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]: <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 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;
Expand Down Expand Up @@ -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(&params);
}

#[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,
Expand All @@ -395,12 +452,63 @@ mod test_csr {
// KeyUsagePurpose::EncipherOnly,
KeyUsagePurpose::DecipherOnly,
];
generate_and_test_parsed_csr(&params);
}

#[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(&params);
}

#[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(&params);
}

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
);
}
}

0 comments on commit c16c5dc

Please sign in to comment.