rcgen/crl.rs
1#[cfg(feature = "pem")]
2use pem::Pem;
3use pki_types::CertificateRevocationListDer;
4use time::OffsetDateTime;
5use yasna::DERWriter;
6use yasna::Tag;
7
8#[cfg(feature = "pem")]
9use crate::ENCODE_CONFIG;
10use crate::{
11 oid, write_distinguished_name, write_dt_utc_or_generalized,
12 write_x509_authority_key_identifier, write_x509_extension, Certificate, Error, Issuer,
13 KeyIdMethod, KeyPair, KeyUsagePurpose, SerialNumber,
14};
15
16/// A certificate revocation list (CRL)
17///
18/// ## Example
19///
20/// ```
21/// extern crate rcgen;
22/// use rcgen::*;
23///
24/// #[cfg(not(feature = "crypto"))]
25/// struct MyKeyPair { public_key: Vec<u8> }
26/// #[cfg(not(feature = "crypto"))]
27/// impl RemoteKeyPair for MyKeyPair {
28/// fn public_key(&self) -> &[u8] { &self.public_key }
29/// fn sign(&self, _: &[u8]) -> Result<Vec<u8>, rcgen::Error> { Ok(vec![]) }
30/// fn algorithm(&self) -> &'static SignatureAlgorithm { &PKCS_ED25519 }
31/// }
32/// # fn main () {
33/// // Generate a CRL issuer.
34/// let mut issuer_params = CertificateParams::new(vec!["crl.issuer.example.com".to_string()]).unwrap();
35/// issuer_params.serial_number = Some(SerialNumber::from(9999));
36/// issuer_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
37/// issuer_params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::DigitalSignature, KeyUsagePurpose::CrlSign];
38/// #[cfg(feature = "crypto")]
39/// let key_pair = KeyPair::generate().unwrap();
40/// #[cfg(not(feature = "crypto"))]
41/// let remote_key_pair = MyKeyPair { public_key: vec![] };
42/// #[cfg(not(feature = "crypto"))]
43/// let key_pair = KeyPair::from_remote(Box::new(remote_key_pair)).unwrap();
44/// let issuer = issuer_params.self_signed(&key_pair).unwrap();
45/// // Describe a revoked certificate.
46/// let revoked_cert = RevokedCertParams{
47/// serial_number: SerialNumber::from(9999),
48/// revocation_time: date_time_ymd(2024, 06, 17),
49/// reason_code: Some(RevocationReason::KeyCompromise),
50/// invalidity_date: None,
51/// };
52/// // Create a CRL signed by the issuer, revoking revoked_cert.
53/// let crl = CertificateRevocationListParams{
54/// this_update: date_time_ymd(2023, 06, 17),
55/// next_update: date_time_ymd(2024, 06, 17),
56/// crl_number: SerialNumber::from(1234),
57/// issuing_distribution_point: None,
58/// revoked_certs: vec![revoked_cert],
59/// #[cfg(feature = "crypto")]
60/// key_identifier_method: KeyIdMethod::Sha256,
61/// #[cfg(not(feature = "crypto"))]
62/// key_identifier_method: KeyIdMethod::PreSpecified(vec![]),
63/// }.signed_by(&issuer, &key_pair).unwrap();
64///# }
65pub struct CertificateRevocationList {
66 params: CertificateRevocationListParams,
67 der: CertificateRevocationListDer<'static>,
68}
69
70impl CertificateRevocationList {
71 /// Returns the certificate revocation list (CRL) parameters.
72 pub fn params(&self) -> &CertificateRevocationListParams {
73 &self.params
74 }
75
76 /// Get the CRL in PEM encoded format.
77 #[cfg(feature = "pem")]
78 pub fn pem(&self) -> Result<String, Error> {
79 let p = Pem::new("X509 CRL", &*self.der);
80 Ok(pem::encode_config(&p, ENCODE_CONFIG))
81 }
82
83 /// Get the CRL in DER encoded format.
84 ///
85 /// [`CertificateRevocationListDer`] implements `Deref<Target = [u8]>` and `AsRef<[u8]>`,
86 /// so you can easily extract the DER bytes from the return value.
87 pub fn der(&self) -> &CertificateRevocationListDer<'static> {
88 &self.der
89 }
90}
91
92impl From<CertificateRevocationList> for CertificateRevocationListDer<'static> {
93 fn from(crl: CertificateRevocationList) -> Self {
94 crl.der
95 }
96}
97
98/// A certificate revocation list (CRL) distribution point, to be included in a certificate's
99/// [distribution points extension](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.13) or
100/// a CRL's [issuing distribution point extension](https://datatracker.ietf.org/doc/html/rfc5280#section-5.2.5)
101#[derive(Debug, PartialEq, Eq, Clone)]
102pub struct CrlDistributionPoint {
103 /// One or more URI distribution point names, indicating a place the current CRL can
104 /// be retrieved. When present, SHOULD include at least one LDAP or HTTP URI.
105 pub uris: Vec<String>,
106}
107
108impl CrlDistributionPoint {
109 pub(crate) fn write_der(&self, writer: DERWriter) {
110 // DistributionPoint SEQUENCE
111 writer.write_sequence(|writer| {
112 write_distribution_point_name_uris(writer.next(), &self.uris);
113 });
114 }
115}
116
117fn write_distribution_point_name_uris<'a>(
118 writer: DERWriter,
119 uris: impl IntoIterator<Item = &'a String>,
120) {
121 // distributionPoint DistributionPointName
122 writer.write_tagged_implicit(Tag::context(0), |writer| {
123 writer.write_sequence(|writer| {
124 // fullName GeneralNames
125 writer
126 .next()
127 .write_tagged_implicit(Tag::context(0), |writer| {
128 // GeneralNames
129 writer.write_sequence(|writer| {
130 for uri in uris.into_iter() {
131 // uniformResourceIdentifier [6] IA5String,
132 writer
133 .next()
134 .write_tagged_implicit(Tag::context(6), |writer| {
135 writer.write_ia5_string(uri)
136 });
137 }
138 })
139 });
140 });
141 });
142}
143
144/// Identifies the reason a certificate was revoked.
145/// See [RFC 5280 §5.3.1][1]
146///
147/// [1]: <https://www.rfc-editor.org/rfc/rfc5280#section-5.3.1>
148#[derive(Debug, Clone, Copy, Eq, PartialEq)]
149#[allow(missing_docs)] // Not much to add above the code name.
150pub enum RevocationReason {
151 Unspecified = 0,
152 KeyCompromise = 1,
153 CaCompromise = 2,
154 AffiliationChanged = 3,
155 Superseded = 4,
156 CessationOfOperation = 5,
157 CertificateHold = 6,
158 // 7 is not defined.
159 RemoveFromCrl = 8,
160 PrivilegeWithdrawn = 9,
161 AaCompromise = 10,
162}
163
164/// Parameters used for certificate revocation list (CRL) generation
165pub struct CertificateRevocationListParams {
166 /// Issue date of the CRL.
167 pub this_update: OffsetDateTime,
168 /// The date by which the next CRL will be issued.
169 pub next_update: OffsetDateTime,
170 /// A monotonically increasing sequence number for a given CRL scope and issuer.
171 pub crl_number: SerialNumber,
172 /// An optional CRL extension identifying the CRL distribution point and scope for a
173 /// particular CRL as described in RFC 5280 Section 5.2.5[^1].
174 ///
175 /// [^1]: <https://datatracker.ietf.org/doc/html/rfc5280#section-5.2.5>
176 pub issuing_distribution_point: Option<CrlIssuingDistributionPoint>,
177 /// A list of zero or more parameters describing revoked certificates included in the CRL.
178 pub revoked_certs: Vec<RevokedCertParams>,
179 /// Method to generate key identifiers from public keys
180 ///
181 /// Defaults to SHA-256.
182 pub key_identifier_method: KeyIdMethod,
183}
184
185impl CertificateRevocationListParams {
186 /// Serializes the certificate revocation list (CRL).
187 ///
188 /// Including a signature from the issuing certificate authority's key.
189 pub fn signed_by(
190 self,
191 issuer: &Certificate,
192 issuer_key: &KeyPair,
193 ) -> Result<CertificateRevocationList, Error> {
194 if self.next_update.le(&self.this_update) {
195 return Err(Error::InvalidCrlNextUpdate);
196 }
197
198 let issuer = Issuer {
199 distinguished_name: &issuer.params.distinguished_name,
200 key_identifier_method: &issuer.params.key_identifier_method,
201 key_usages: &issuer.params.key_usages,
202 key_pair: issuer_key,
203 };
204
205 if !issuer.key_usages.is_empty() && !issuer.key_usages.contains(&KeyUsagePurpose::CrlSign) {
206 return Err(Error::IssuerNotCrlSigner);
207 }
208
209 Ok(CertificateRevocationList {
210 der: self.serialize_der(issuer)?.into(),
211 params: self,
212 })
213 }
214
215 fn serialize_der(&self, issuer: Issuer) -> Result<Vec<u8>, Error> {
216 issuer.key_pair.sign_der(|writer| {
217 // Write CRL version.
218 // RFC 5280 §5.1.2.1:
219 // This optional field describes the version of the encoded CRL. When
220 // extensions are used, as required by this profile, this field MUST be
221 // present and MUST specify version 2 (the integer value is 1).
222 // RFC 5280 §5.2:
223 // Conforming CRL issuers are REQUIRED to include the authority key
224 // identifier (Section 5.2.1) and the CRL number (Section 5.2.3)
225 // extensions in all CRLs issued.
226 writer.next().write_u8(1);
227
228 // Write algorithm identifier.
229 // RFC 5280 §5.1.2.2:
230 // This field MUST contain the same algorithm identifier as the
231 // signatureAlgorithm field in the sequence CertificateList
232 issuer.key_pair.alg.write_alg_ident(writer.next());
233
234 // Write issuer.
235 // RFC 5280 §5.1.2.3:
236 // The issuer field MUST contain a non-empty X.500 distinguished name (DN).
237 write_distinguished_name(writer.next(), &issuer.distinguished_name);
238
239 // Write thisUpdate date.
240 // RFC 5280 §5.1.2.4:
241 // This field indicates the issue date of this CRL. thisUpdate may be
242 // encoded as UTCTime or GeneralizedTime.
243 write_dt_utc_or_generalized(writer.next(), self.this_update);
244
245 // Write nextUpdate date.
246 // While OPTIONAL in the ASN.1 module, RFC 5280 §5.1.2.5 says:
247 // Conforming CRL issuers MUST include the nextUpdate field in all CRLs.
248 write_dt_utc_or_generalized(writer.next(), self.next_update);
249
250 // Write revokedCertificates.
251 // RFC 5280 §5.1.2.6:
252 // When there are no revoked certificates, the revoked certificates list
253 // MUST be absent
254 if !self.revoked_certs.is_empty() {
255 writer.next().write_sequence(|writer| {
256 for revoked_cert in &self.revoked_certs {
257 revoked_cert.write_der(writer.next());
258 }
259 });
260 }
261
262 // Write crlExtensions.
263 // RFC 5280 §5.1.2.7:
264 // This field may only appear if the version is 2 (Section 5.1.2.1). If
265 // present, this field is a sequence of one or more CRL extensions.
266 // RFC 5280 §5.2:
267 // Conforming CRL issuers are REQUIRED to include the authority key
268 // identifier (Section 5.2.1) and the CRL number (Section 5.2.3)
269 // extensions in all CRLs issued.
270 writer.next().write_tagged(Tag::context(0), |writer| {
271 writer.write_sequence(|writer| {
272 // Write authority key identifier.
273 write_x509_authority_key_identifier(
274 writer.next(),
275 self.key_identifier_method
276 .derive(issuer.key_pair.public_key_der()),
277 );
278
279 // Write CRL number.
280 write_x509_extension(writer.next(), oid::CRL_NUMBER, false, |writer| {
281 writer.write_bigint_bytes(self.crl_number.as_ref(), true);
282 });
283
284 // Write issuing distribution point (if present).
285 if let Some(issuing_distribution_point) = &self.issuing_distribution_point {
286 write_x509_extension(
287 writer.next(),
288 oid::CRL_ISSUING_DISTRIBUTION_POINT,
289 true,
290 |writer| {
291 issuing_distribution_point.write_der(writer);
292 },
293 );
294 }
295 });
296 });
297
298 Ok(())
299 })
300 }
301}
302
303/// A certificate revocation list (CRL) issuing distribution point, to be included in a CRL's
304/// [issuing distribution point extension](https://datatracker.ietf.org/doc/html/rfc5280#section-5.2.5).
305pub struct CrlIssuingDistributionPoint {
306 /// The CRL's distribution point, containing a sequence of URIs the CRL can be retrieved from.
307 pub distribution_point: CrlDistributionPoint,
308 /// An optional description of the CRL's scope. If omitted, the CRL may contain
309 /// both user certs and CA certs.
310 pub scope: Option<CrlScope>,
311}
312
313impl CrlIssuingDistributionPoint {
314 fn write_der(&self, writer: DERWriter) {
315 // IssuingDistributionPoint SEQUENCE
316 writer.write_sequence(|writer| {
317 // distributionPoint [0] DistributionPointName OPTIONAL
318 write_distribution_point_name_uris(writer.next(), &self.distribution_point.uris);
319
320 // -- at most one of onlyContainsUserCerts, onlyContainsCACerts,
321 // -- and onlyContainsAttributeCerts may be set to TRUE.
322 if let Some(scope) = self.scope {
323 let tag = match scope {
324 // onlyContainsUserCerts [1] BOOLEAN DEFAULT FALSE,
325 CrlScope::UserCertsOnly => Tag::context(1),
326 // onlyContainsCACerts [2] BOOLEAN DEFAULT FALSE,
327 CrlScope::CaCertsOnly => Tag::context(2),
328 };
329 writer.next().write_tagged_implicit(tag, |writer| {
330 writer.write_bool(true);
331 });
332 }
333 });
334 }
335}
336
337/// Describes the scope of a CRL for an issuing distribution point extension.
338#[derive(Debug, Clone, Copy, Eq, PartialEq)]
339pub enum CrlScope {
340 /// The CRL contains only end-entity user certificates.
341 UserCertsOnly,
342 /// The CRL contains only CA certificates.
343 CaCertsOnly,
344}
345
346/// Parameters used for describing a revoked certificate included in a [`CertificateRevocationList`].
347pub struct RevokedCertParams {
348 /// Serial number identifying the revoked certificate.
349 pub serial_number: SerialNumber,
350 /// The date at which the CA processed the revocation.
351 pub revocation_time: OffsetDateTime,
352 /// An optional reason code identifying why the certificate was revoked.
353 pub reason_code: Option<RevocationReason>,
354 /// An optional field describing the date on which it was known or suspected that the
355 /// private key was compromised or the certificate otherwise became invalid. This date
356 /// may be earlier than the [`RevokedCertParams::revocation_time`].
357 pub invalidity_date: Option<OffsetDateTime>,
358}
359
360impl RevokedCertParams {
361 fn write_der(&self, writer: DERWriter) {
362 writer.write_sequence(|writer| {
363 // Write serial number.
364 // RFC 5280 §4.1.2.2:
365 // Certificate users MUST be able to handle serialNumber values up to 20 octets.
366 // Conforming CAs MUST NOT use serialNumber values longer than 20 octets.
367 //
368 // Note: Non-conforming CAs may issue certificates with serial numbers
369 // that are negative or zero. Certificate users SHOULD be prepared to
370 // gracefully handle such certificates.
371 writer
372 .next()
373 .write_bigint_bytes(self.serial_number.as_ref(), true);
374
375 // Write revocation date.
376 write_dt_utc_or_generalized(writer.next(), self.revocation_time);
377
378 // Write extensions if applicable.
379 // RFC 5280 §5.3:
380 // Support for the CRL entry extensions defined in this specification is
381 // optional for conforming CRL issuers and applications. However, CRL
382 // issuers SHOULD include reason codes (Section 5.3.1) and invalidity
383 // dates (Section 5.3.2) whenever this information is available.
384 let has_reason_code =
385 matches!(self.reason_code, Some(reason) if reason != RevocationReason::Unspecified);
386 let has_invalidity_date = self.invalidity_date.is_some();
387 if has_reason_code || has_invalidity_date {
388 writer.next().write_sequence(|writer| {
389 // Write reason code if present.
390 if let Some(reason_code) = self.reason_code {
391 write_x509_extension(writer.next(), oid::CRL_REASONS, false, |writer| {
392 writer.write_enum(reason_code as i64);
393 });
394 }
395
396 // Write invalidity date if present.
397 if let Some(invalidity_date) = self.invalidity_date {
398 write_x509_extension(
399 writer.next(),
400 oid::CRL_INVALIDITY_DATE,
401 false,
402 |writer| {
403 write_dt_utc_or_generalized(writer, invalidity_date);
404 },
405 )
406 }
407 });
408 }
409 })
410 }
411}