Skip to content

Commit 86f4cb2

Browse files
committed
verify_cert: use enum for build chain error
The `loop_while_non_fatal_error` helper can return one of three things: * success, when a validated chain to a trust anchor was built. * a fatal error, e.g. when a `Budget` has been exceeded and no further path building should occur because we've exhausted a budget. * a non-fatal error, when a candidate chain results in an error condition, but other paths could be considered if the options are not exhausted. This commit attempts to express this in the type system, centralizing a check for what is/isn't a fatal error and ensuring that downstream callers to `loop_while_non_fatal_error` handle the fatal case appropriately.
1 parent 50a2930 commit 86f4cb2

File tree

2 files changed

+58
-33
lines changed

2 files changed

+58
-33
lines changed

src/error.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1414

1515
use core::fmt;
16+
use core::ops::ControlFlow;
1617

1718
/// An error that occurs during certificate validation or name validation.
1819
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -129,6 +130,17 @@ impl Error {
129130
}
130131
}
131132

133+
impl From<Error> for ControlFlow<Error, Error> {
134+
fn from(value: Error) -> Self {
135+
match value {
136+
// If an error is fatal, we've exhausted the potential for continued search.
137+
err if err.is_fatal() => Self::Break(err),
138+
// Otherwise we've rejected one candidate chain, but may continue to search for others.
139+
err => Self::Continue(err),
140+
}
141+
}
142+
}
143+
132144
impl fmt::Display for Error {
133145
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
134146
write!(f, "{:?}", self)

src/verify_cert.rs

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1414

1515
use core::default::Default;
16+
use core::ops::ControlFlow;
1617

1718
use crate::{
1819
cert::{self, Cert, EndEntityOrCa},
@@ -38,6 +39,10 @@ pub(crate) fn build_chain(
3839
0,
3940
&mut Budget::default(),
4041
)
42+
.map_err(|e| match e {
43+
ControlFlow::Break(err) => err,
44+
ControlFlow::Continue(err) => err,
45+
})
4146
}
4247

4348
#[allow(clippy::too_many_arguments)]
@@ -50,7 +55,7 @@ fn build_chain_inner(
5055
time: time::Time,
5156
sub_ca_count: usize,
5257
budget: &mut Budget,
53-
) -> Result<(), Error> {
58+
) -> Result<(), ControlFlow<Error, Error>> {
5459
let used_as_ca = used_as_ca(&cert.ee_or_ca);
5560

5661
check_issuer_independent_properties(
@@ -68,7 +73,7 @@ fn build_chain_inner(
6873
const MAX_SUB_CA_COUNT: usize = 6;
6974

7075
if sub_ca_count >= MAX_SUB_CA_COUNT {
71-
return Err(Error::MaximumPathDepthExceeded);
76+
return Err(Error::MaximumPathDepthExceeded.into());
7277
}
7378
}
7479
UsedAsCa::No => {
@@ -91,7 +96,7 @@ fn build_chain_inner(
9196
let result = loop_while_non_fatal_error(trust_anchors, |trust_anchor: &TrustAnchor| {
9297
let trust_anchor_subject = untrusted::Input::from(trust_anchor.subject);
9398
if cert.issuer != trust_anchor_subject {
94-
return Err(Error::UnknownIssuer);
99+
return Err(Error::UnknownIssuer.into());
95100
}
96101

97102
// TODO: check_distrust(trust_anchor_subject, trust_anchor_spki)?;
@@ -113,17 +118,17 @@ fn build_chain_inner(
113118
match result {
114119
Ok(()) => return Ok(()),
115120
// Fatal errors should halt further path building.
116-
res @ Err(err) if err.is_fatal() => return res,
121+
res @ Err(ControlFlow::Break(_)) => return res,
117122
// Non-fatal errors should allow path building to continue.
118-
_ => {}
123+
Err(ControlFlow::Continue(_)) => {}
119124
};
120125

121126
loop_while_non_fatal_error(intermediate_certs, |cert_der| {
122127
let potential_issuer =
123128
cert::parse_cert(untrusted::Input::from(cert_der), EndEntityOrCa::Ca(cert))?;
124129

125130
if potential_issuer.subject != cert.issuer {
126-
return Err(Error::UnknownIssuer);
131+
return Err(Error::UnknownIssuer.into());
127132
}
128133

129134
// Prevent loops; see RFC 4158 section 5.2.
@@ -132,7 +137,7 @@ fn build_chain_inner(
132137
if potential_issuer.spki.value() == prev.spki.value()
133138
&& potential_issuer.subject == prev.subject
134139
{
135-
return Err(Error::UnknownIssuer);
140+
return Err(Error::UnknownIssuer.into());
136141
}
137142
match &prev.ee_or_ca {
138143
EndEntityOrCa::EndEntity => {
@@ -168,7 +173,7 @@ fn check_signed_chain(
168173
cert_chain: &Cert,
169174
trust_anchor_key: untrusted::Input,
170175
budget: &mut Budget,
171-
) -> Result<(), Error> {
176+
) -> Result<(), ControlFlow<Error, Error>> {
172177
let mut spki_value = trust_anchor_key;
173178
let mut cert = cert_chain;
174179
loop {
@@ -195,7 +200,7 @@ fn check_signed_chain_name_constraints(
195200
trust_anchor: &TrustAnchor,
196201
subject_common_name_contents: subject_name::SubjectCommonNameContents,
197202
budget: &mut Budget,
198-
) -> Result<(), Error> {
203+
) -> Result<(), ControlFlow<Error, Error>> {
199204
let mut cert = cert_chain;
200205
let mut name_constraints = trust_anchor
201206
.name_constraints
@@ -455,8 +460,8 @@ fn check_eku(
455460

456461
fn loop_while_non_fatal_error<V>(
457462
values: V,
458-
mut f: impl FnMut(V::Item) -> Result<(), Error>,
459-
) -> Result<(), Error>
463+
mut f: impl FnMut(V::Item) -> Result<(), ControlFlow<Error, Error>>,
464+
) -> Result<(), ControlFlow<Error, Error>>
460465
where
461466
V: IntoIterator,
462467
{
@@ -465,12 +470,12 @@ where
465470
match f(v) {
466471
Ok(()) => return Ok(()),
467472
// Fatal errors should halt further looping.
468-
res @ Err(err) if err.is_fatal() => return res,
473+
res @ Err(ControlFlow::Break(_)) => return res,
469474
// Non-fatal errors should allow looping to continue.
470-
_ => {}
475+
Err(ControlFlow::Continue(_)) => {}
471476
}
472477
}
473-
Err(Error::UnknownIssuer)
478+
Err(Error::UnknownIssuer.into())
474479
}
475480

476481
#[cfg(test)]
@@ -489,7 +494,7 @@ mod tests {
489494
intermediate_count: usize,
490495
trust_anchor_is_actual_issuer: TrustAnchorIsActualIssuer,
491496
budget: Option<Budget>,
492-
) -> Error {
497+
) -> ControlFlow<Error, Error> {
493498
let ca_cert = make_issuer("Bogus Subject", None);
494499
let ca_cert_der = ca_cert.serialize_der().unwrap();
495500

@@ -518,16 +523,16 @@ mod tests {
518523
#[test]
519524
#[cfg(feature = "alloc")]
520525
fn test_too_many_signatures() {
521-
assert_eq!(
526+
assert!(matches!(
522527
build_degenerate_chain(5, TrustAnchorIsActualIssuer::Yes, None),
523-
Error::MaximumSignatureChecksExceeded
524-
);
528+
ControlFlow::Break(Error::MaximumSignatureChecksExceeded)
529+
));
525530
}
526531

527532
#[test]
528533
#[cfg(feature = "alloc")]
529534
fn test_too_many_path_calls() {
530-
assert_eq!(
535+
assert!(matches!(
531536
build_degenerate_chain(
532537
10,
533538
TrustAnchorIsActualIssuer::No,
@@ -539,12 +544,12 @@ mod tests {
539544
..Budget::default()
540545
})
541546
),
542-
Error::MaximumPathBuildCallsExceeded
543-
);
547+
ControlFlow::Break(Error::MaximumPathBuildCallsExceeded)
548+
));
544549
}
545550

546551
#[cfg(feature = "alloc")]
547-
fn build_linear_chain(chain_length: usize) -> Result<(), Error> {
552+
fn build_linear_chain(chain_length: usize) -> Result<(), ControlFlow<Error, Error>> {
548553
let ca_cert = make_issuer(format!("Bogus Subject {chain_length}"), None);
549554
let ca_cert_der = ca_cert.serialize_der().unwrap();
550555

@@ -568,20 +573,23 @@ mod tests {
568573
#[test]
569574
#[cfg(feature = "alloc")]
570575
fn longest_allowed_path() {
571-
assert_eq!(build_linear_chain(1), Ok(()));
572-
assert_eq!(build_linear_chain(2), Ok(()));
573-
assert_eq!(build_linear_chain(3), Ok(()));
574-
assert_eq!(build_linear_chain(4), Ok(()));
575-
assert_eq!(build_linear_chain(5), Ok(()));
576-
assert_eq!(build_linear_chain(6), Ok(()));
576+
assert!(build_linear_chain(1).is_ok());
577+
assert!(build_linear_chain(2).is_ok());
578+
assert!(build_linear_chain(3).is_ok());
579+
assert!(build_linear_chain(4).is_ok());
580+
assert!(build_linear_chain(5).is_ok());
581+
assert!(build_linear_chain(6).is_ok());
577582
}
578583

579584
#[test]
580585
#[cfg(feature = "alloc")]
581586
fn path_too_long() {
582587
// Note: webpki 0.101.x and earlier surface all non-fatal errors as UnknownIssuer,
583588
// eating the more specific MaximumPathDepthExceeded error.
584-
assert_eq!(build_linear_chain(7), Err(Error::UnknownIssuer));
589+
assert!(matches!(
590+
build_linear_chain(7),
591+
Err(ControlFlow::Continue(Error::UnknownIssuer))
592+
));
585593
}
586594

587595
#[test]
@@ -631,13 +639,13 @@ mod tests {
631639
// Validation should succeed with the name constraint comparison budget allocated above.
632640
// This shows that we're not consuming budget on unused intermediates: we didn't budget
633641
// enough comparisons for that to pass the overall chain building.
634-
verify_chain(
642+
assert!(verify_chain(
635643
&ca_cert_der,
636644
&intermediates_der,
637645
&ee_cert,
638646
Some(passing_budget),
639647
)
640-
.unwrap();
648+
.is_ok());
641649

642650
let failing_budget = Budget {
643651
// See passing_budget: 2 comparisons is not sufficient.
@@ -654,7 +662,12 @@ mod tests {
654662
Some(failing_budget),
655663
);
656664

657-
assert_eq!(result, Err(Error::MaximumNameConstraintComparisonsExceeded));
665+
assert!(matches!(
666+
result,
667+
Err(ControlFlow::Break(
668+
Error::MaximumNameConstraintComparisonsExceeded
669+
))
670+
));
658671
}
659672

660673
#[cfg(feature = "alloc")]
@@ -663,7 +676,7 @@ mod tests {
663676
intermediates_der: &[Vec<u8>],
664677
ee_cert_der: &[u8],
665678
budget: Option<Budget>,
666-
) -> Result<(), Error> {
679+
) -> Result<(), ControlFlow<Error, Error>> {
667680
use crate::ECDSA_P256_SHA256;
668681
use crate::{EndEntityCert, Time};
669682

0 commit comments

Comments
 (0)