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