Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PKCS#10 attributes to CSR serializer #296

Merged
merged 9 commits into from
Nov 13, 2024
122 changes: 91 additions & 31 deletions rcgen/src/certificate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ impl From<Certificate> for CertificateDer<'static> {
/// Parameters used for certificate generation
#[allow(missing_docs)]
#[non_exhaustive]
#[derive(Clone)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct CertificateParams {
pub not_before: OffsetDateTime,
pub not_after: OffsetDateTime,
Expand Down Expand Up @@ -427,6 +427,33 @@ impl CertificateParams {
Ok(result)
}

/// Write a CSR extension request attribute as defined in [RFC 2985].
///
/// [RFC 2985]: <https://datatracker.ietf.org/doc/html/rfc2985>
fn write_extension_request_attribute(&self, writer: DERWriter) {
writer.write_sequence(|writer| {
writer.next().write_oid(&ObjectIdentifier::from_slice(
oid::PKCS_9_AT_EXTENSION_REQUEST,
));
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 &self.custom_extensions {
write_x509_extension(writer.next(), &ext.oid, ext.critical, |writer| {
writer.write_der(ext.content())
});
}
});
});
});
}

/// Write a certificate's KeyUsage as defined in RFC 5280.
fn write_key_usage(&self, writer: DERWriter) {
// RFC 5280 defines 9 key usages, which we detail in our key usage enum
Expand Down Expand Up @@ -461,6 +488,10 @@ impl CertificateParams {
}

fn write_subject_alt_names(&self, writer: DERWriter) {
if self.subject_alt_names.is_empty() {
djc marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -502,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].
///
/// 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]: <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 @@ -542,45 +592,36 @@ impl CertificateParams {
return Err(Error::UnsupportedInCsr);
}

// 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();

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().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| {
cpu marked this conversation as resolved.
Show resolved Hide resolved
// RFC 2986 specifies that attributes are a SET OF Attribute
writer.write_set_of(|writer| {
if write_extension_request {
self.write_extension_request_attribute(writer.next());
djc marked this conversation as resolved.
Show resolved Hide resolved
}

for Attribute { oid, values } in attrs {
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()),
);
}
writer.next().write_oid(&ObjectIdentifier::from_slice(&oid));
writer.next().write_der(&values);
});
});
}
});
}
});
});

Ok(())
})?;
Expand Down Expand Up @@ -829,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]: <https://datatracker.ietf.org/doc/html/rfc5280#appendix-A.1>
/// [RFC 2986]: <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] 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<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
144 changes: 129 additions & 15 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], a simple
/// ATTRIBUTE that contains a single UTF8String.
///
/// [RFC 2985]: <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 Expand Up @@ -359,28 +416,59 @@ mod test_parse_other_name_alt_name {
}

#[cfg(feature = "x509-parser")]
mod test_csr {
use rcgen::{CertificateParams, CertificateSigningRequestParams, KeyPair, KeyUsagePurpose};
mod test_csr_extension_request {
use rcgen::{CertificateParams, ExtendedKeyUsagePurpose, KeyPair, KeyUsagePurpose};
use x509_parser::prelude::{FromDer, ParsedExtension, X509CertificationRequest};

#[test]
fn test_csr_roundtrip() {
fn dont_write_sans_extension_if_no_sans_are_present() {
let mut params = CertificateParams::default();
params.key_usages.push(KeyUsagePurpose::DigitalSignature);
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 csr = params.serialize_request(&key_pair).unwrap();
let (_, parsed_csr) = X509CertificationRequest::from_der(csr.der()).unwrap();
assert!(!parsed_csr
.requested_extensions()
.unwrap()
.any(|ext| matches!(ext, ParsedExtension::SubjectAlternativeName(_))));
}

#[test]
fn test_nontrivial_csr_roundtrip() {
fn write_extension_request_if_ekus_are_present() {
let mut params = CertificateParams::default();
params
.extended_key_usages
.push(ExtendedKeyUsagePurpose::ClientAuth);
let key_pair = KeyPair::generate().unwrap();
let csr = params.serialize_request(&key_pair).unwrap();
let (_, parsed_csr) = X509CertificationRequest::from_der(csr.der()).unwrap();
let requested_extensions = parsed_csr
.requested_extensions()
.unwrap()
.collect::<Vec<_>>();
assert!(matches!(
requested_extensions.first().unwrap(),
ParsedExtension::ExtendedKeyUsage(_)
));
}
}

#[cfg(feature = "x509-parser")]
mod test_csr {
use rcgen::{
CertificateParams, CertificateSigningRequestParams, ExtendedKeyUsagePurpose, KeyPair,
KeyUsagePurpose,
};

#[test]
fn test_csr_roundtrip() {
// We should be able to serialize a CSR, and then parse the CSR.
let params = CertificateParams::default();
generate_and_test_parsed_csr(&params);
}

#[test]
fn test_csr_with_key_usages_roundtrip() {
let mut params = CertificateParams::default();
params.key_usages = vec![
KeyUsagePurpose::DigitalSignature,
Expand All @@ -395,12 +483,38 @@ 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) {
djc marked this conversation as resolved.
Show resolved Hide resolved
// 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_eq!(key_pair.algorithm(), csrp.public_key.algorithm());
// Ensure key usages match.
assert_eq!(csrp.params.key_usages, params.key_usages);
// Assert that our parsed parameters match our initial parameters
assert_eq!(*params, csrp.params);
}
}