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 | |
15 | use pki_types::SignatureVerificationAlgorithm; |
16 | |
17 | use crate::error::Error; |
18 | use crate::verify_cert::{Budget, PathNode, Role}; |
19 | use crate::{der, public_values_eq}; |
20 | |
21 | use core::fmt::Debug; |
22 | |
23 | mod types; |
24 | pub use types::{ |
25 | BorrowedCertRevocationList, BorrowedRevokedCert, CertRevocationList, RevocationReason, |
26 | }; |
27 | #[cfg (feature = "alloc" )] |
28 | pub use types::{OwnedCertRevocationList, OwnedRevokedCert}; |
29 | |
30 | /// Builds a RevocationOptions instance to control how revocation checking is performed. |
31 | #[derive (Debug, Copy, Clone)] |
32 | pub struct RevocationOptionsBuilder<'a> { |
33 | crls: &'a [&'a CertRevocationList<'a>], |
34 | |
35 | depth: RevocationCheckDepth, |
36 | |
37 | status_policy: UnknownStatusPolicy, |
38 | } |
39 | |
40 | impl<'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)] |
92 | pub struct RevocationOptions<'a> { |
93 | pub(crate) crls: &'a [&'a CertRevocationList<'a>], |
94 | pub(crate) depth: RevocationCheckDepth, |
95 | pub(crate) status_policy: UnknownStatusPolicy, |
96 | } |
97 | |
98 | impl<'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)] |
153 | enum 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 | |
165 | impl 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. |
190 | fn 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)] |
203 | pub 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)] |
212 | pub 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. |
223 | pub(crate) struct CertNotRevoked(()); |
224 | |
225 | impl 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. |
235 | pub struct CrlsRequired(pub(crate) ()); |
236 | |
237 | #[cfg (test)] |
238 | mod 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 | |