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;
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
40impl<'a> RevocationOptionsBuilder<'a> {
41 /// Create a builder that will perform revocation checking using the provided certificate
42 /// revocation lists (CRLs). At least one CRL must be provided.
43 ///
44 /// Use [RevocationOptionsBuilder::build] to create a [RevocationOptions] instance.
45 ///
46 /// By default revocation checking will be performed on both the end-entity (leaf) certificate
47 /// and intermediate certificates. This can be customized using the
48 /// [RevocationOptionsBuilder::with_depth] method.
49 ///
50 /// By default revocation checking will fail if the revocation status of a certificate cannot
51 /// be determined. This can be customized using the
52 /// [RevocationOptionsBuilder::with_status_policy] method.
53 pub fn new(crls: &'a [&'a CertRevocationList<'a>]) -> Result<Self, CrlsRequired> {
54 if crls.is_empty() {
55 return Err(CrlsRequired(()));
56 }
57
58 Ok(Self {
59 crls,
60 depth: RevocationCheckDepth::Chain,
61 status_policy: UnknownStatusPolicy::Deny,
62 })
63 }
64
65 /// Customize the depth at which revocation checking will be performed, controlling
66 /// whether only the end-entity (leaf) certificate in the chain to a trust anchor will
67 /// have its revocation status checked, or whether the intermediate certificates will as well.
68 pub fn with_depth(mut self, depth: RevocationCheckDepth) -> Self {
69 self.depth = depth;
70 self
71 }
72
73 /// Customize whether unknown revocation status is an error, or permitted.
74 pub fn with_status_policy(mut self, policy: UnknownStatusPolicy) -> Self {
75 self.status_policy = policy;
76 self
77 }
78
79 /// Construct a [RevocationOptions] instance based on the builder's configuration.
80 pub fn build(self) -> RevocationOptions<'a> {
81 RevocationOptions {
82 crls: self.crls,
83 depth: self.depth,
84 status_policy: self.status_policy,
85 }
86 }
87}
88
89/// Describes how revocation checking is performed, if at all. Can be constructed with a
90/// [RevocationOptionsBuilder] instance.
91#[derive(Debug, Copy, Clone)]
92pub struct RevocationOptions<'a> {
93 pub(crate) crls: &'a [&'a CertRevocationList<'a>],
94 pub(crate) depth: RevocationCheckDepth,
95 pub(crate) status_policy: UnknownStatusPolicy,
96}
97
98impl<'a> RevocationOptions<'a> {
99 pub(crate) fn check(
100 &self,
101 path: &PathNode<'_>,
102 issuer_subject: untrusted::Input,
103 issuer_spki: untrusted::Input,
104 issuer_ku: Option<untrusted::Input>,
105 supported_sig_algs: &[&dyn SignatureVerificationAlgorithm],
106 budget: &mut Budget,
107 ) -> Result<Option<CertNotRevoked>, Error> {
108 assert!(public_values_eq(path.cert.issuer, issuer_subject));
109
110 // If the policy only specifies checking EndEntity revocation state and we're looking at an
111 // issuer certificate, return early without considering the certificate's revocation state.
112 if let (RevocationCheckDepth::EndEntity, Role::Issuer) = (self.depth, path.role()) {
113 return Ok(None);
114 }
115
116 let crl = self
117 .crls
118 .iter()
119 .find(|candidate_crl| candidate_crl.authoritative(path));
120
121 use UnknownStatusPolicy::*;
122 let crl = match (crl, self.status_policy) {
123 (Some(crl), _) => crl,
124 // If the policy allows unknown, return Ok(None) to indicate that the certificate
125 // was not confirmed as CertNotRevoked, but that this isn't an error condition.
126 (None, Allow) => return Ok(None),
127 // Otherwise, this is an error condition based on the provided policy.
128 (None, _) => return Err(Error::UnknownRevocationStatus),
129 };
130
131 // Verify the CRL signature with the issuer SPKI.
132 // TODO(XXX): consider whether we can refactor so this happens once up-front, instead
133 // of per-lookup.
134 // https://github.com/rustls/webpki/issues/81
135 crl.verify_signature(supported_sig_algs, issuer_spki, budget)
136 .map_err(crl_signature_err)?;
137
138 // Verify that if the issuer has a KeyUsage bitstring it asserts cRLSign.
139 KeyUsageMode::CrlSign.check(issuer_ku)?;
140
141 // Try to find the cert serial in the verified CRL contents.
142 let cert_serial = path.cert.serial.as_slice_less_safe();
143 match crl.find_serial(cert_serial)? {
144 None => Ok(Some(CertNotRevoked::assertion())),
145 Some(_) => Err(Error::CertRevoked),
146 }
147 }
148}
149
150// https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.3
151#[repr(u8)]
152#[derive(Clone, Copy)]
153enum KeyUsageMode {
154 // DigitalSignature = 0,
155 // ContentCommitment = 1,
156 // KeyEncipherment = 2,
157 // DataEncipherment = 3,
158 // KeyAgreement = 4,
159 // CertSign = 5,
160 CrlSign = 6,
161 // EncipherOnly = 7,
162 // DecipherOnly = 8,
163}
164
165impl KeyUsageMode {
166 // https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.3
167 fn check(self, input: Option<untrusted::Input>) -> Result<(), Error> {
168 let bit_string: Input<'_> = match input {
169 Some(input: Input<'_>) => {
170 der::expect_tag(&mut untrusted::Reader::new(input), der::Tag::BitString)?
171 }
172 // While RFC 5280 requires KeyUsage be present, historically the absence of a KeyUsage
173 // has been treated as "Any Usage". We follow that convention here and assume the absence
174 // of KeyUsage implies the required_ku_bit_if_present we're checking for.
175 None => return Ok(()),
176 };
177
178 let flags: BitStringFlags<'_> = der::bit_string_flags(input:bit_string)?;
179 #[allow(clippy::as_conversions)] // u8 always fits in usize.
180 match flags.bit_set(self as usize) {
181 true => Ok(()),
182 false => Err(Error::IssuerNotCrlSigner),
183 }
184 }
185}
186
187// When verifying CRL signed data we want to disambiguate the context of possible errors by mapping
188// them to CRL specific variants that a consumer can use to tell the issue was with the CRL's
189// signature, not a certificate.
190fn crl_signature_err(err: Error) -> Error {
191 match err {
192 Error::UnsupportedSignatureAlgorithm => Error::UnsupportedCrlSignatureAlgorithm,
193 Error::UnsupportedSignatureAlgorithmForPublicKey => {
194 Error::UnsupportedCrlSignatureAlgorithmForPublicKey
195 }
196 Error::InvalidSignatureForPublicKey => Error::InvalidCrlSignatureForPublicKey,
197 _ => err,
198 }
199}
200
201/// Describes how much of a certificate chain is checked for revocation status.
202#[derive(Debug, Copy, Clone, PartialEq, Eq)]
203pub enum RevocationCheckDepth {
204 /// Only check the end entity (leaf) certificate's revocation status.
205 EndEntity,
206 /// Check the revocation status of the end entity (leaf) and all intermediates.
207 Chain,
208}
209
210/// Describes how to handle the case where a certificate's revocation status is unknown.
211#[derive(Debug, Copy, Clone, PartialEq, Eq)]
212pub enum UnknownStatusPolicy {
213 /// Treat unknown revocation status permissively, acting as if the certificate were
214 /// not revoked.
215 Allow,
216 /// Treat unknown revocation status as an error condition, yielding
217 /// [Error::UnknownRevocationStatus].
218 Deny,
219}
220
221// Zero-sized marker type representing positive assertion that revocation status was checked
222// for a certificate and the result was that the certificate is not revoked.
223pub(crate) struct CertNotRevoked(());
224
225impl CertNotRevoked {
226 // Construct a CertNotRevoked marker.
227 fn assertion() -> Self {
228 Self(())
229 }
230}
231
232#[derive(Debug, Copy, Clone)]
233/// An opaque error indicating the caller must provide at least one CRL when building a
234/// [RevocationOptions] instance.
235pub struct CrlsRequired(pub(crate) ());
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 // redundant clone, clone_on_copy allowed to verify derived traits.
243 #[allow(clippy::redundant_clone, clippy::clone_on_copy)]
244 fn test_revocation_opts_builder() {
245 // Trying to build a RevocationOptionsBuilder w/o CRLs should err.
246 let result = RevocationOptionsBuilder::new(&[]);
247 assert!(matches!(result, Err(CrlsRequired(_))));
248
249 // The CrlsRequired error should be debug and clone when alloc is enabled.
250 #[cfg(feature = "alloc")]
251 {
252 let err = result.unwrap_err();
253 println!("{:?}", err.clone());
254 }
255
256 // It should be possible to build a revocation options builder with defaults.
257 let crl = include_bytes!("../../tests/crls/crl.valid.der");
258 let crl: CertRevocationList = BorrowedCertRevocationList::from_der(&crl[..])
259 .unwrap()
260 .into();
261 let crls = [&crl];
262 let builder = RevocationOptionsBuilder::new(&crls).unwrap();
263 #[cfg(feature = "alloc")]
264 {
265 // The builder should be debug, and clone when alloc is enabled
266 println!("{:?}", builder);
267 _ = builder.clone();
268 }
269 let opts = builder.build();
270 assert_eq!(opts.depth, RevocationCheckDepth::Chain);
271 assert_eq!(opts.status_policy, UnknownStatusPolicy::Deny);
272 assert_eq!(opts.crls.len(), 1);
273
274 // It should be possible to build a revocation options builder with custom depth.
275 let opts = RevocationOptionsBuilder::new(&crls)
276 .unwrap()
277 .with_depth(RevocationCheckDepth::EndEntity)
278 .build();
279 assert_eq!(opts.depth, RevocationCheckDepth::EndEntity);
280 assert_eq!(opts.status_policy, UnknownStatusPolicy::Deny);
281 assert_eq!(opts.crls.len(), 1);
282
283 // It should be possible to build a revocation options builder that allows unknown
284 // revocation status.
285 let opts = RevocationOptionsBuilder::new(&crls)
286 .unwrap()
287 .with_status_policy(UnknownStatusPolicy::Allow)
288 .build();
289 assert_eq!(opts.depth, RevocationCheckDepth::Chain);
290 assert_eq!(opts.status_policy, UnknownStatusPolicy::Allow);
291 assert_eq!(opts.crls.len(), 1);
292
293 // It should be possible to specify both depth and unknown status policy together.
294 let opts = RevocationOptionsBuilder::new(&crls)
295 .unwrap()
296 .with_status_policy(UnknownStatusPolicy::Allow)
297 .with_depth(RevocationCheckDepth::EndEntity)
298 .build();
299 assert_eq!(opts.depth, RevocationCheckDepth::EndEntity);
300 assert_eq!(opts.status_policy, UnknownStatusPolicy::Allow);
301 assert_eq!(opts.crls.len(), 1);
302
303 // The same should be true for explicitly forbidding unknown status.
304 let opts = RevocationOptionsBuilder::new(&crls)
305 .unwrap()
306 .with_status_policy(UnknownStatusPolicy::Deny)
307 .with_depth(RevocationCheckDepth::EndEntity)
308 .build();
309 assert_eq!(opts.depth, RevocationCheckDepth::EndEntity);
310 assert_eq!(opts.status_policy, UnknownStatusPolicy::Deny);
311 assert_eq!(opts.crls.len(), 1);
312
313 // Built revocation options should be debug and clone when alloc is enabled.
314 #[cfg(feature = "alloc")]
315 {
316 println!("{:?}", opts.clone());
317 }
318 }
319}
320