webpki/crl/
mod.rs

1// Copyright 2023 Daniel McCarney.
2//
3// Permission to use, copy, modify, and/or distribute this software for any
4// purpose with or without fee is hereby granted, provided that the above
5// copyright notice and this permission notice appear in all copies.
6//
7// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
8// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
10// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
15use pki_types::{SignatureVerificationAlgorithm, UnixTime};
16
17use crate::error::Error;
18use crate::verify_cert::{Budget, PathNode, Role};
19use crate::{der, public_values_eq};
20
21use core::fmt::Debug;
22
23mod types;
24pub use types::{
25    BorrowedCertRevocationList, BorrowedRevokedCert, CertRevocationList, RevocationReason,
26};
27#[cfg(feature = "alloc")]
28pub use types::{OwnedCertRevocationList, OwnedRevokedCert};
29
30/// Builds a RevocationOptions instance to control how revocation checking is performed.
31#[derive(Debug, Copy, Clone)]
32pub struct RevocationOptionsBuilder<'a> {
33    crls: &'a [&'a CertRevocationList<'a>],
34
35    depth: RevocationCheckDepth,
36
37    status_policy: UnknownStatusPolicy,
38
39    expiration_policy: ExpirationPolicy,
40}
41
42impl<'a> RevocationOptionsBuilder<'a> {
43    /// Create a builder that will perform revocation checking using the provided certificate
44    /// revocation lists (CRLs). At least one CRL must be provided.
45    ///
46    /// Use [RevocationOptionsBuilder::build] to create a [RevocationOptions] instance.
47    ///
48    /// By default revocation checking will be performed on both the end-entity (leaf) certificate
49    /// and intermediate certificates. This can be customized using the
50    /// [RevocationOptionsBuilder::with_depth] method.
51    ///
52    /// By default revocation checking will fail if the revocation status of a certificate cannot
53    /// be determined. This can be customized using the
54    /// [RevocationOptionsBuilder::with_status_policy] method.
55    ///
56    /// By default revocation checking will *not* fail if the verification time is beyond the time
57    /// in the CRL nextUpdate field. This can be customized using the
58    /// [RevocationOptionsBuilder::with_expiration_policy] method.
59    pub fn new(crls: &'a [&'a CertRevocationList<'a>]) -> Result<Self, CrlsRequired> {
60        if crls.is_empty() {
61            return Err(CrlsRequired(()));
62        }
63
64        Ok(Self {
65            crls,
66            depth: RevocationCheckDepth::Chain,
67            status_policy: UnknownStatusPolicy::Deny,
68            expiration_policy: ExpirationPolicy::Ignore,
69        })
70    }
71
72    /// Customize the depth at which revocation checking will be performed, controlling
73    /// whether only the end-entity (leaf) certificate in the chain to a trust anchor will
74    /// have its revocation status checked, or whether the intermediate certificates will as well.
75    pub fn with_depth(mut self, depth: RevocationCheckDepth) -> Self {
76        self.depth = depth;
77        self
78    }
79
80    /// Customize whether unknown revocation status is an error, or permitted.
81    pub fn with_status_policy(mut self, policy: UnknownStatusPolicy) -> Self {
82        self.status_policy = policy;
83        self
84    }
85
86    /// Customize whether the CRL nextUpdate field (i.e. expiration) is enforced.
87    pub fn with_expiration_policy(mut self, policy: ExpirationPolicy) -> Self {
88        self.expiration_policy = policy;
89        self
90    }
91
92    /// Construct a [RevocationOptions] instance based on the builder's configuration.
93    pub fn build(self) -> RevocationOptions<'a> {
94        RevocationOptions {
95            crls: self.crls,
96            depth: self.depth,
97            status_policy: self.status_policy,
98            expiration_policy: self.expiration_policy,
99        }
100    }
101}
102
103/// Describes how revocation checking is performed, if at all. Can be constructed with a
104/// [RevocationOptionsBuilder] instance.
105#[derive(Debug, Copy, Clone)]
106pub struct RevocationOptions<'a> {
107    pub(crate) crls: &'a [&'a CertRevocationList<'a>],
108    pub(crate) depth: RevocationCheckDepth,
109    pub(crate) status_policy: UnknownStatusPolicy,
110    pub(crate) expiration_policy: ExpirationPolicy,
111}
112
113impl RevocationOptions<'_> {
114    #[allow(clippy::too_many_arguments)]
115    pub(crate) fn check(
116        &self,
117        path: &PathNode<'_>,
118        issuer_subject: untrusted::Input<'_>,
119        issuer_spki: untrusted::Input<'_>,
120        issuer_ku: Option<untrusted::Input<'_>>,
121        supported_sig_algs: &[&dyn SignatureVerificationAlgorithm],
122        budget: &mut Budget,
123        time: UnixTime,
124    ) -> Result<Option<CertNotRevoked>, Error> {
125        assert!(public_values_eq(path.cert.issuer, issuer_subject));
126
127        // If the policy only specifies checking EndEntity revocation state and we're looking at an
128        // issuer certificate, return early without considering the certificate's revocation state.
129        if let (RevocationCheckDepth::EndEntity, Role::Issuer) = (self.depth, path.role()) {
130            return Ok(None);
131        }
132
133        let crl = self
134            .crls
135            .iter()
136            .find(|candidate_crl| candidate_crl.authoritative(path));
137
138        use UnknownStatusPolicy::*;
139        let crl = match (crl, self.status_policy) {
140            (Some(crl), _) => crl,
141            // If the policy allows unknown, return Ok(None) to indicate that the certificate
142            // was not confirmed as CertNotRevoked, but that this isn't an error condition.
143            (None, Allow) => return Ok(None),
144            // Otherwise, this is an error condition based on the provided policy.
145            (None, _) => return Err(Error::UnknownRevocationStatus),
146        };
147
148        // Verify the CRL signature with the issuer SPKI.
149        // TODO(XXX): consider whether we can refactor so this happens once up-front, instead
150        //            of per-lookup.
151        //            https://github.com/rustls/webpki/issues/81
152        crl.verify_signature(supported_sig_algs, issuer_spki, budget)
153            .map_err(crl_signature_err)?;
154
155        if self.expiration_policy == ExpirationPolicy::Enforce {
156            crl.check_expiration(time)?;
157        }
158
159        // Verify that if the issuer has a KeyUsage bitstring it asserts cRLSign.
160        KeyUsageMode::CrlSign.check(issuer_ku)?;
161
162        // Try to find the cert serial in the verified CRL contents.
163        let cert_serial = path.cert.serial.as_slice_less_safe();
164        match crl.find_serial(cert_serial)? {
165            None => Ok(Some(CertNotRevoked::assertion())),
166            Some(_) => Err(Error::CertRevoked),
167        }
168    }
169}
170
171// https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.3
172#[repr(u8)]
173#[derive(Clone, Copy)]
174enum KeyUsageMode {
175    // DigitalSignature = 0,
176    // ContentCommitment = 1,
177    // KeyEncipherment = 2,
178    // DataEncipherment = 3,
179    // KeyAgreement = 4,
180    // CertSign = 5,
181    CrlSign = 6,
182    // EncipherOnly = 7,
183    // DecipherOnly = 8,
184}
185
186impl KeyUsageMode {
187    // https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.3
188    fn check(self, input: Option<untrusted::Input<'_>>) -> Result<(), Error> {
189        let bit_string = match input {
190            Some(input) => {
191                der::expect_tag(&mut untrusted::Reader::new(input), der::Tag::BitString)?
192            }
193            // While RFC 5280 requires KeyUsage be present, historically the absence of a KeyUsage
194            // has been treated as "Any Usage". We follow that convention here and assume the absence
195            // of KeyUsage implies the required_ku_bit_if_present we're checking for.
196            None => return Ok(()),
197        };
198
199        let flags = der::bit_string_flags(bit_string)?;
200        #[allow(clippy::as_conversions)] // u8 always fits in usize.
201        match flags.bit_set(self as usize) {
202            true => Ok(()),
203            false => Err(Error::IssuerNotCrlSigner),
204        }
205    }
206}
207
208// When verifying CRL signed data we want to disambiguate the context of possible errors by mapping
209// them to CRL specific variants that a consumer can use to tell the issue was with the CRL's
210// signature, not a certificate.
211fn crl_signature_err(err: Error) -> Error {
212    match err {
213        Error::UnsupportedSignatureAlgorithm => Error::UnsupportedCrlSignatureAlgorithm,
214        Error::UnsupportedSignatureAlgorithmForPublicKey => {
215            Error::UnsupportedCrlSignatureAlgorithmForPublicKey
216        }
217        Error::InvalidSignatureForPublicKey => Error::InvalidCrlSignatureForPublicKey,
218        _ => err,
219    }
220}
221
222/// Describes how much of a certificate chain is checked for revocation status.
223#[derive(Debug, Copy, Clone, PartialEq, Eq)]
224pub enum RevocationCheckDepth {
225    /// Only check the end entity (leaf) certificate's revocation status.
226    EndEntity,
227    /// Check the revocation status of the end entity (leaf) and all intermediates.
228    Chain,
229}
230
231/// Describes how to handle the case where a certificate's revocation status is unknown.
232#[derive(Debug, Copy, Clone, PartialEq, Eq)]
233pub enum UnknownStatusPolicy {
234    /// Treat unknown revocation status permissively, acting as if the certificate were
235    /// not revoked.
236    Allow,
237    /// Treat unknown revocation status as an error condition, yielding
238    /// [Error::UnknownRevocationStatus].
239    Deny,
240}
241
242/// Describes how to handle the nextUpdate field of the CRL (i.e. expiration).
243#[derive(Debug, Copy, Clone, PartialEq, Eq)]
244pub enum ExpirationPolicy {
245    /// Enforce the verification time is before the time in the nextUpdate field.
246    /// Treats an expired CRL as an error condition yielding [Error::CrlExpired].
247    Enforce,
248    /// Ignore the CRL nextUpdate field.
249    Ignore,
250}
251
252// Zero-sized marker type representing positive assertion that revocation status was checked
253// for a certificate and the result was that the certificate is not revoked.
254pub(crate) struct CertNotRevoked(());
255
256impl CertNotRevoked {
257    // Construct a CertNotRevoked marker.
258    fn assertion() -> Self {
259        Self(())
260    }
261}
262
263#[derive(Debug, Copy, Clone)]
264/// An opaque error indicating the caller must provide at least one CRL when building a
265/// [RevocationOptions] instance.
266pub struct CrlsRequired(pub(crate) ());
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271
272    #[test]
273    // redundant clone, clone_on_copy allowed to verify derived traits.
274    #[allow(clippy::redundant_clone, clippy::clone_on_copy)]
275    fn test_revocation_opts_builder() {
276        // Trying to build a RevocationOptionsBuilder w/o CRLs should err.
277        let result = RevocationOptionsBuilder::new(&[]);
278        assert!(matches!(result, Err(CrlsRequired(_))));
279
280        // The CrlsRequired error should be debug and clone when alloc is enabled.
281        #[cfg(feature = "alloc")]
282        {
283            let err = result.unwrap_err();
284            std::println!("{:?}", err.clone());
285        }
286
287        // It should be possible to build a revocation options builder with defaults.
288        let crl = include_bytes!("../../tests/crls/crl.valid.der");
289        let crl = BorrowedCertRevocationList::from_der(&crl[..])
290            .unwrap()
291            .into();
292        let crls = [&crl];
293        let builder = RevocationOptionsBuilder::new(&crls).unwrap();
294        #[cfg(feature = "alloc")]
295        {
296            // The builder should be debug, and clone when alloc is enabled
297            std::println!("{:?}", builder);
298            _ = builder.clone();
299        }
300        let opts = builder.build();
301        assert_eq!(opts.depth, RevocationCheckDepth::Chain);
302        assert_eq!(opts.status_policy, UnknownStatusPolicy::Deny);
303        assert_eq!(opts.expiration_policy, ExpirationPolicy::Ignore);
304        assert_eq!(opts.crls.len(), 1);
305
306        // It should be possible to build a revocation options builder with custom depth.
307        let opts = RevocationOptionsBuilder::new(&crls)
308            .unwrap()
309            .with_depth(RevocationCheckDepth::EndEntity)
310            .build();
311        assert_eq!(opts.depth, RevocationCheckDepth::EndEntity);
312        assert_eq!(opts.status_policy, UnknownStatusPolicy::Deny);
313        assert_eq!(opts.expiration_policy, ExpirationPolicy::Ignore);
314        assert_eq!(opts.crls.len(), 1);
315
316        // It should be possible to build a revocation options builder that allows unknown
317        // revocation status.
318        let opts = RevocationOptionsBuilder::new(&crls)
319            .unwrap()
320            .with_status_policy(UnknownStatusPolicy::Allow)
321            .build();
322        assert_eq!(opts.depth, RevocationCheckDepth::Chain);
323        assert_eq!(opts.status_policy, UnknownStatusPolicy::Allow);
324        assert_eq!(opts.expiration_policy, ExpirationPolicy::Ignore);
325        assert_eq!(opts.crls.len(), 1);
326
327        // It should be possible to specify both depth and unknown status policy together.
328        let opts = RevocationOptionsBuilder::new(&crls)
329            .unwrap()
330            .with_status_policy(UnknownStatusPolicy::Allow)
331            .with_depth(RevocationCheckDepth::EndEntity)
332            .build();
333        assert_eq!(opts.depth, RevocationCheckDepth::EndEntity);
334        assert_eq!(opts.status_policy, UnknownStatusPolicy::Allow);
335        assert_eq!(opts.expiration_policy, ExpirationPolicy::Ignore);
336        assert_eq!(opts.crls.len(), 1);
337
338        // The same should be true for explicitly forbidding unknown status.
339        let opts = RevocationOptionsBuilder::new(&crls)
340            .unwrap()
341            .with_status_policy(UnknownStatusPolicy::Deny)
342            .with_depth(RevocationCheckDepth::EndEntity)
343            .build();
344        assert_eq!(opts.depth, RevocationCheckDepth::EndEntity);
345        assert_eq!(opts.status_policy, UnknownStatusPolicy::Deny);
346        assert_eq!(opts.expiration_policy, ExpirationPolicy::Ignore);
347        assert_eq!(opts.crls.len(), 1);
348
349        // It should be possible to build a revocation options builder that allows unknown
350        // revocation status.
351        let opts = RevocationOptionsBuilder::new(&crls)
352            .unwrap()
353            .with_expiration_policy(ExpirationPolicy::Enforce)
354            .build();
355        assert_eq!(opts.depth, RevocationCheckDepth::Chain);
356        assert_eq!(opts.status_policy, UnknownStatusPolicy::Deny);
357        assert_eq!(opts.expiration_policy, ExpirationPolicy::Enforce);
358        assert_eq!(opts.crls.len(), 1);
359
360        // Built revocation options should be debug and clone when alloc is enabled.
361        #[cfg(feature = "alloc")]
362        {
363            std::println!("{:?}", opts.clone());
364        }
365    }
366}