Skip to content

Commit 9e5f117

Browse files
committed
lib: add cert CRL distribution points ext. support.
This branch extends rcgen to allow generating certificates that contain an RFC 5280 certificate revocation list (CRL) distribution points extension. This is a useful mechanism for helping ensure CRL coverage when performing revocation checks, and is newly supported by rustls/webpki. See this upstream webpki issue[0] and RFC 5280 §4.2.1.13[1] for more background. Using the new `crl_distribution_points` field of the `CertificateParams` struct it's possible to encode one or more distribution points specifying URI general names where up-to-date CRL information for the certificate can be found. Similar to existing rcgen CRL generation, the support for this extension is not extensive, but instead tailored towards usage in the web PKI with a RFC 5280 profile. Notably this means: * There's no support for specifying the 'reasons' flag - RFC 5280 "RECOMMENDS against segmenting CRLs by reason code". * There's no support for specifying a 'cRLIssuer' in the DP - this is specific to indirect CRLs, and neither rcgen's CRL generation code or webpki's parsing/validation support these. * There's no support for specifying a 'nameRelativeToCrlIssuer' in the DP name instead of a sequence of general names for similar reasons as above: 5280 says: "Conforming CAs SHOULD NOT use nameRelativeToCRLIssuer to specify distribution point names." * There's no support for specifying general names of type other than URI within a DP name's full name. Other name types either don't make sense in the context of this extension, or are rarely useful in practice (e.g. directory name). Test coverage is mixed based on the support of the relevant third party libraries. OpenSSL and openssl-rs parse this extension well, and so the `openssl.rs` test coverage is the most extensive. The `x509-parser` crate can pull out the extension, but doesn't decompose the value (I may attempt to land code for this upstream in the future, stay tuned). Webpki recognizes this extension for use during revocation checking, but doesn't expose it externally so a simple parse test is added. Botan's rust bindings do not recognize the extension or offer a way to pull out arbitrary extensions, so no test coverage is added there. [0] rustls/webpki#121 [1] https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.13
1 parent 83e548a commit 9e5f117

File tree

5 files changed

+137
-6
lines changed

5 files changed

+137
-6
lines changed

src/lib.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,13 @@ const OID_AUTHORITY_KEY_IDENTIFIER :&[u64] = &[2, 5, 29, 35];
140140
const OID_EXT_KEY_USAGE :&[u64] = &[2, 5, 29, 37];
141141

142142
// id-ce-nameConstraints in
143-
/// https://tools.ietf.org/html/rfc5280#section-4.2.1.10
143+
// https://tools.ietf.org/html/rfc5280#section-4.2.1.10
144144
const OID_NAME_CONSTRAINTS :&[u64] = &[2, 5, 29, 30];
145145

146+
// id-ce-cRLDistributionPoints in
147+
// https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.13
148+
const OID_CRL_DISTRIBUTION_POINTS :&[u64] = &[2, 5, 29, 31];
149+
146150
// id-pe-acmeIdentifier in
147151
// https://www.iana.org/assignments/smi-numbers/smi-numbers.xhtml#smi-numbers-1.3.6.1.5.5.7.1
148152
const OID_PE_ACME :&[u64] = &[1, 3, 6, 1, 5, 5, 7, 1, 31];
@@ -733,6 +737,12 @@ pub struct CertificateParams {
733737
pub key_usages :Vec<KeyUsagePurpose>,
734738
pub extended_key_usages :Vec<ExtendedKeyUsagePurpose>,
735739
pub name_constraints :Option<NameConstraints>,
740+
/// An optional list of certificate revocation list (CRL) distribution points as described
741+
/// in RFC 5280 Section 4.2.1.13[^1]. Each distribution point contains one or more URIs where
742+
/// an up-to-date CRL with scope including this certificate can be retrieved.
743+
///
744+
/// [^1]: <https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.13>
745+
pub crl_distribution_points :Vec<CrlDistributionPoint>,
736746
pub custom_extensions :Vec<CustomExtension>,
737747
/// The certificate's key pair, a new random key pair will be generated if this is `None`
738748
pub key_pair :Option<KeyPair>,
@@ -762,6 +772,7 @@ impl Default for CertificateParams {
762772
key_usages : Vec::new(),
763773
extended_key_usages : Vec::new(),
764774
name_constraints : None,
775+
crl_distribution_points : Vec::new(),
765776
custom_extensions : Vec::new(),
766777
key_pair : None,
767778
use_authority_key_identifier_extension : false,
@@ -1020,6 +1031,7 @@ impl CertificateParams {
10201031
key_usages,
10211032
extended_key_usages,
10221033
name_constraints,
1034+
crl_distribution_points,
10231035
custom_extensions,
10241036
key_pair,
10251037
use_authority_key_identifier_extension,
@@ -1037,6 +1049,7 @@ impl CertificateParams {
10371049
|| !key_usages.is_empty()
10381050
|| !extended_key_usages.is_empty()
10391051
|| name_constraints.is_some()
1052+
|| !crl_distribution_points.is_empty()
10401053
|| *use_authority_key_identifier_extension
10411054
{
10421055
return Err(RcgenError::UnsupportedInCsr);
@@ -1230,6 +1243,15 @@ impl CertificateParams {
12301243
});
12311244
}
12321245
}
1246+
if !self.crl_distribution_points.is_empty() {
1247+
write_x509_extension(writer.next(), OID_CRL_DISTRIBUTION_POINTS, false, |writer| {
1248+
writer.write_sequence(|writer| {
1249+
for distribution_point in &self.crl_distribution_points {
1250+
write_crl_distribution_point(writer.next(), distribution_point);
1251+
}
1252+
})
1253+
});
1254+
}
12331255
match self.is_ca {
12341256
IsCa::Ca(ref constraint) => {
12351257
// Write subject_key_identifier
@@ -1381,6 +1403,15 @@ impl NameConstraints {
13811403
}
13821404
}
13831405

1406+
/// A certificate revocation list (CRL) distribution point, to be included in a certificate's
1407+
/// [distribution points extension](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.13).
1408+
#[derive(Debug, PartialEq, Eq, Clone)]
1409+
pub struct CrlDistributionPoint {
1410+
/// One or more URI distribution point names, indicating a place the current CRL can
1411+
/// be retrieved. When present, SHOULD include at least one LDAP or HTTP URI.
1412+
pub uris :Vec<String>,
1413+
}
1414+
13841415
/// One of the purposes contained in the [key usage](https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3) extension
13851416
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
13861417
pub enum KeyUsagePurpose {
@@ -1797,6 +1828,29 @@ fn write_general_subtrees(writer :DERWriter, tag :u64, general_subtrees :&[Gener
17971828
});
17981829
}
17991830

1831+
fn write_crl_distribution_point(writer :DERWriter, dp :&CrlDistributionPoint) {
1832+
// DistributionPoint SEQUENCE
1833+
writer.write_sequence(|writer| {
1834+
// distributionPoint DistributionPointName
1835+
writer.next().write_tagged_implicit(Tag::context(0), |writer| {
1836+
writer.write_sequence(|writer| {
1837+
// fullName GeneralNames
1838+
writer.next().write_tagged_implicit(Tag::context(0), | writer| {
1839+
// GeneralNames
1840+
writer.write_sequence(|writer| {
1841+
for uri in dp.uris.iter() {
1842+
// uniformResourceIdentifier [6] IA5String,
1843+
writer.next().write_tagged_implicit(Tag::context(6), |writer| {
1844+
writer.write_ia5_string(uri)
1845+
});
1846+
}
1847+
})
1848+
});
1849+
});
1850+
});
1851+
});
1852+
}
1853+
18001854
impl Certificate {
18011855
/// Generates a new certificate from the given parameters.
18021856
///

tests/generic.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,27 @@ mod test_x509_parser_crl {
123123
// We should be able to verify the CRL signature with the issuer.
124124
assert!(x509_crl.verify_signature(&x509_issuer.public_key()).is_ok());
125125
}
126-
}
126+
}
127+
128+
#[cfg(feature = "x509-parser")]
129+
mod test_parse_crl_dps {
130+
use crate::util;
131+
132+
#[test]
133+
fn parse_crl_dps() {
134+
// Generate and parse a certificate that includes two CRL distribution points.
135+
let der = util::cert_with_crl_dps();
136+
let (_, parsed_cert) = x509_parser::parse_x509_certificate(&der).unwrap();
137+
138+
// We should find a CRL DP extension was parsed.
139+
let crl_dps = parsed_cert.get_extension_unique(&x509_parser::oid_registry::OID_X509_EXT_CRL_DISTRIBUTION_POINTS)
140+
.expect("malformed CRL distribution points extension")
141+
.expect("missing CRL distribution points extension");
142+
143+
// The extension should not be critical.
144+
assert!(!crl_dps.critical);
145+
146+
// TODO(XXX): x509-parser does not parse this extension yet, limiting what we can test from
147+
// here. See `openssl.rs` for more thorough coverage.
148+
}
149+
}

tests/openssl.rs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
use rcgen::{Certificate, NameConstraints, GeneralSubtree, IsCa,
2-
BasicConstraints, CertificateParams, DnType, DnValue};
1+
use rcgen::{Certificate, NameConstraints, GeneralSubtree, IsCa, BasicConstraints, CertificateParams, DnType, DnValue};
32
use openssl::pkey::PKey;
43
use openssl::x509::{CrlStatus, X509, X509Crl, X509Req, X509StoreContext};
54
use openssl::x509::store::{X509StoreBuilder, X509Store};
@@ -426,4 +425,35 @@ fn test_openssl_crl_parse() {
426425
// We should be able to verify the CRL signature with the issuer's public key.
427426
let issuer_pkey = openssl_issuer.public_key().unwrap();
428427
assert!(openssl_crl.verify(&issuer_pkey).expect("failed to verify CRL signature"));
429-
}
428+
}
429+
430+
#[test]
431+
fn test_openssl_crl_dps_parse() {
432+
// Generate and parse a certificate that includes two CRL distribution points.
433+
let der = util::cert_with_crl_dps();
434+
let cert = X509::from_der(&der).expect("failed to parse cert DER");
435+
436+
// We should find the CRL DPs extension.
437+
let dps = cert.crl_distribution_points().expect("missing crl distribution points extension");
438+
assert!(!dps.is_empty());
439+
440+
// We should find two distribution points, each with a distribution point name containing
441+
// a full name sequence of general names.
442+
let general_names = dps.iter().flat_map(|dp|
443+
dp.distpoint()
444+
.expect("distribution point missing distribution point name")
445+
.fullname()
446+
.expect("distribution point name missing general names")
447+
.iter()
448+
)
449+
.collect::<Vec<_>>();
450+
451+
// Each general name should be a URI name.
452+
let uris = general_names.iter().map(|general_name|
453+
general_name.uri().expect("general name is not a directory name")
454+
)
455+
.collect::<Vec<_>>();
456+
457+
// We should find the expected URIs.
458+
assert_eq!(uris, &["http://example.com/crl.der", "http://crls.example.com/1234", "ldap://example.com/crl.der"]);
459+
}

tests/util.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use time::{Duration, OffsetDateTime};
2-
use rcgen::{BasicConstraints, Certificate, CertificateParams, CertificateRevocationList};
2+
use rcgen::{BasicConstraints, Certificate, CertificateParams, CertificateRevocationList, CrlDistributionPoint};
33
use rcgen::{CertificateRevocationListParams, DnType, IsCa, KeyIdMethod};
44
use rcgen::{KeyUsagePurpose, PKCS_ECDSA_P256_SHA256, RevocationReason, RevokedCertParams, SerialNumber};
55

@@ -99,3 +99,19 @@ pub fn test_crl() -> (CertificateRevocationList, Certificate) {
9999

100100
(crl, issuer)
101101
}
102+
103+
#[allow(unused)] // Used by openssl + x509-parser features.
104+
pub fn cert_with_crl_dps() -> Vec<u8> {
105+
let mut params = default_params();
106+
params.crl_distribution_points = vec![
107+
CrlDistributionPoint{
108+
uris: vec!["http://example.com/crl.der".to_string(), "http://crls.example.com/1234".to_string()],
109+
},
110+
CrlDistributionPoint{
111+
uris: vec!["ldap://example.com/crl.der".to_string()],
112+
}
113+
];
114+
115+
let cert = Certificate::from_params(params).unwrap();
116+
cert.serialize_der().unwrap()
117+
}

tests/webpki.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,3 +526,11 @@ fn test_webpki_crl_revoke() {
526526
);
527527
assert!(matches!(result, Err(webpki::Error::CertRevoked)));
528528
}
529+
530+
#[test]
531+
fn test_webpki_cert_crl_dps() {
532+
let der = util::cert_with_crl_dps();
533+
webpki::EndEntityCert::try_from(der.as_ref()).expect("failed to parse cert with CRL DPs ext");
534+
// Webpki doesn't expose the parsed CRL distribution extension, so we can't interrogate that
535+
// it matches the expected form. See `openssl.rs` for more extensive coverage.
536+
}

0 commit comments

Comments
 (0)