Skip to content

Commit 331b8fd

Browse files
lvkvcpu
authored andcommitted
Add PKCS#10 attributes to CSR serializer
1 parent 349ffe4 commit 331b8fd

File tree

3 files changed

+105
-5
lines changed

3 files changed

+105
-5
lines changed

rcgen/src/certificate.rs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,25 @@ impl CertificateParams {
533533
pub fn serialize_request(
534534
&self,
535535
subject_key: &KeyPair,
536+
) -> Result<CertificateSigningRequest, Error> {
537+
self.serialize_request_with_attributes(subject_key, Vec::new())
538+
}
539+
540+
/// Generate and serialize a certificate signing request (CSR) with custom PKCS #10 attributes.
541+
/// as defined in [RFC 2986].
542+
///
543+
/// The constructed CSR will contain attributes based on the certificate parameters,
544+
/// and include the subject public key information from `subject_key`. Additionally,
545+
/// the CSR will be self-signed using the subject key.
546+
///
547+
/// Note that subsequent invocations of `serialize_request_with_attributes()` will not produce the exact
548+
/// same output.
549+
///
550+
/// [RFC 2986]: <https://datatracker.ietf.org/doc/html/rfc2986#section-4>
551+
pub fn serialize_request_with_attributes(
552+
&self,
553+
subject_key: &KeyPair,
554+
attrs: Vec<Attribute>,
536555
) -> Result<CertificateSigningRequest, Error> {
537556
// No .. pattern, we use this to ensure every field is used
538557
#[deny(unused)]
@@ -582,11 +601,9 @@ impl CertificateParams {
582601
let der = subject_key.sign_der(|writer| {
583602
// Write version
584603
writer.next().write_u8(0);
585-
// Write subject name
586604
write_distinguished_name(writer.next(), distinguished_name);
587-
// Write subjectPublicKeyInfo
588605
serialize_public_key_der(subject_key, writer.next());
589-
// Write extensions
606+
590607
// According to the spec in RFC 2986, even if attributes are empty we need the empty attribute tag
591608
writer
592609
.next()
@@ -596,6 +613,13 @@ impl CertificateParams {
596613
if write_extension_request {
597614
self.write_extension_request_attribute(writer.next());
598615
}
616+
617+
for Attribute { oid, values } in attrs {
618+
writer.next().write_sequence(|writer| {
619+
writer.next().write_oid(&ObjectIdentifier::from_slice(&oid));
620+
writer.next().write_der(&values);
621+
});
622+
}
599623
});
600624
});
601625

@@ -846,6 +870,25 @@ fn write_general_subtrees(writer: DERWriter, tag: u64, general_subtrees: &[Gener
846870
});
847871
}
848872

873+
/// A PKCS #10 CSR attribute, as defined in [RFC 5280] and constrained
874+
/// by [RFC 2986].
875+
///
876+
/// [RFC 5280]: <https://datatracker.ietf.org/doc/html/rfc5280#appendix-A.1>
877+
/// [RFC 2986]: <https://datatracker.ietf.org/doc/html/rfc2986#section-4>
878+
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
879+
pub struct Attribute {
880+
/// `AttributeType` of the `Attribute`, defined as an `OBJECT IDENTIFIER`.
881+
pub oid: &'static [u64],
882+
/// DER-encoded values of the `Attribute`, defined by [RFC 2986] as:
883+
///
884+
/// ```text
885+
/// SET SIZE(1..MAX) OF ATTRIBUTE.&Type({IOSet}{@type})
886+
/// ```
887+
///
888+
/// [RFC 2986]: https://datatracker.ietf.org/doc/html/rfc2986#section-4
889+
pub values: Vec<u8>,
890+
}
891+
849892
/// A custom extension of a certificate, as specified in
850893
/// [RFC 5280](https://tools.ietf.org/html/rfc5280#section-4.2)
851894
#[derive(Debug, PartialEq, Eq, Hash, Clone)]

rcgen/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ use yasna::DERWriter;
4949
use yasna::Tag;
5050

5151
pub use certificate::{
52-
date_time_ymd, BasicConstraints, Certificate, CertificateParams, CidrSubnet, CustomExtension,
53-
DnType, ExtendedKeyUsagePurpose, GeneralSubtree, IsCa, NameConstraints,
52+
date_time_ymd, Attribute, BasicConstraints, Certificate, CertificateParams, CidrSubnet,
53+
CustomExtension, DnType, ExtendedKeyUsagePurpose, GeneralSubtree, IsCa, NameConstraints,
5454
};
5555
pub use crl::{
5656
CertificateRevocationList, CertificateRevocationListParams, CrlDistributionPoint,

rcgen/tests/generic.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,63 @@ mod test_x509_custom_ext {
135135
}
136136
}
137137

138+
#[cfg(feature = "x509-parser")]
139+
mod test_csr_custom_attributes {
140+
use rcgen::{Attribute, CertificateParams, KeyPair};
141+
use x509_parser::{
142+
der_parser::Oid,
143+
prelude::{FromDer, X509CertificationRequest},
144+
};
145+
146+
/// Test serializing a CSR with custom attributes.
147+
/// This test case uses `challengePassword` from [RFC 2985], a simple
148+
/// ATTRIBUTE that contains a single UTF8String.
149+
///
150+
/// [RFC 2985]: <https://datatracker.ietf.org/doc/html/rfc2985>
151+
#[test]
152+
fn test_csr_custom_attributes() {
153+
// OID for challengePassword
154+
const CHALLENGE_PWD_OID: &[u64] = &[1, 2, 840, 113549, 1, 9, 7];
155+
156+
// Attribute values for challengePassword
157+
let challenge_pwd_values = yasna::try_construct_der::<_, ()>(|writer| {
158+
// Reminder: CSR attribute values are contained in a SET
159+
writer.write_set(|writer| {
160+
// Challenge passwords only have one value, a UTF8String
161+
writer
162+
.next()
163+
.write_utf8_string("nobody uses challenge passwords anymore");
164+
Ok(())
165+
})
166+
})
167+
.unwrap();
168+
169+
// Challenge password attribute
170+
let challenge_password_attribute = Attribute {
171+
oid: CHALLENGE_PWD_OID,
172+
values: challenge_pwd_values.clone(),
173+
};
174+
175+
// Serialize a DER-encoded CSR
176+
let params = CertificateParams::default();
177+
let key_pair = KeyPair::generate().unwrap();
178+
let csr = params
179+
.serialize_request_with_attributes(&key_pair, vec![challenge_password_attribute])
180+
.unwrap();
181+
182+
// Parse the CSR
183+
let (_, x509_csr) = X509CertificationRequest::from_der(csr.der()).unwrap();
184+
let parsed_attribute_value = x509_csr
185+
.certification_request_info
186+
.attributes_map()
187+
.unwrap()
188+
.get(&Oid::from(CHALLENGE_PWD_OID).unwrap())
189+
.unwrap()
190+
.value;
191+
assert_eq!(parsed_attribute_value, challenge_pwd_values);
192+
}
193+
}
194+
138195
#[cfg(feature = "x509-parser")]
139196
mod test_x509_parser_crl {
140197
use crate::util;

0 commit comments

Comments
 (0)