Skip to content

Commit bf3d39f

Browse files
committed
Allow selected unsupported critical extensions
Add ExtensionId and an opt-in EndEntityCert constructor that accepts a DER OID allowlist for unsupported critical extensions on the leaf certificate. Existing TryFrom parsing remains strict by default. Add PathBuilder::with_ignored_critical_extensions so callers can explicitly apply the same allowlist when path building parses intermediate certificates. PathBuilder remains strict for intermediates unless this builder option is used. Supported extensions are still parsed and validated normally; the allowlist only suppresses UnsupportedCriticalExtension for exact unsupported OID matches. Add integration tests for leaf and intermediate allowlisting, exact-match behavior, and supported-extension parsing.
1 parent aa4686f commit bf3d39f

6 files changed

Lines changed: 308 additions & 8 deletions

File tree

src/cert.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,16 @@ impl<'a> Cert<'a> {
5959
Self::from_input(cert_der, UnknownExtensionPolicy::default())
6060
}
6161

62+
pub(crate) fn from_der_with_extension_policy(
63+
cert_der: untrusted::Input<'a>,
64+
ext_policy: UnknownExtensionPolicy<'_>,
65+
) -> Result<Self, Error> {
66+
Self::from_input(cert_der, ext_policy)
67+
}
68+
6269
fn from_input(
6370
cert_der: untrusted::Input<'a>,
64-
ext_policy: UnknownExtensionPolicy,
71+
ext_policy: UnknownExtensionPolicy<'_>,
6572
) -> Result<Self, Error> {
6673
let (tbs, signed_data) =
6774
cert_der.read_all(Error::TrailingData(DerTypeId::Certificate), |cert_der| {
@@ -308,7 +315,7 @@ pub(crate) fn lenient_certificate_serial_number<'a>(
308315
fn remember_cert_extension<'a>(
309316
cert: &mut Cert<'a>,
310317
extension: &Extension<'a>,
311-
ext_policy: UnknownExtensionPolicy,
318+
ext_policy: UnknownExtensionPolicy<'_>,
312319
) -> Result<(), Error> {
313320
// We don't do anything with certificate policies so we can safely ignore
314321
// all policy-related stuff. We assume that the policy-related extensions

src/end_entity.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use pki_types::{CertificateDer, ServerName, SignatureVerificationAlgorithm};
1818

1919
use crate::error::Error;
2020
use crate::subject_name::{verify_dns_names, verify_ip_address_names};
21+
use crate::x509::{ExtensionId, UnknownExtensionPolicy};
2122
use crate::{cert, sct, signed_data};
2223

2324
/// An end-entity certificate.
@@ -68,6 +69,35 @@ impl<'a> TryFrom<&'a CertificateDer<'a>> for EndEntityCert<'a> {
6869
}
6970
}
7071

72+
impl<'a> EndEntityCert<'a> {
73+
/// Parse the ASN.1 DER-encoded X.509 encoding of the certificate, ignoring the
74+
/// listed unsupported critical extensions.
75+
///
76+
/// By default, webpki rejects certificates containing unsupported critical extensions,
77+
/// as required by RFC 5280. This constructor is an opt-in escape hatch for applications
78+
/// that understand the listed unsupported extensions and want webpki to accept them when
79+
/// they are marked critical.
80+
/// The `ignored_critical_extensions` values are DER OBJECT IDENTIFIER value bytes, without
81+
/// the OBJECT IDENTIFIER tag or length.
82+
///
83+
/// Supported extensions are still processed normally. Listing a supported extension here does
84+
/// not disable validation of its value. This constructor only applies the policy when parsing
85+
/// this end-entity certificate. Use
86+
/// [`PathBuilder::with_ignored_critical_extensions`](crate::PathBuilder::with_ignored_critical_extensions)
87+
/// to apply the same policy when parsing intermediate certificates during path building.
88+
pub fn try_from_with_ignored_critical_extensions(
89+
cert: &'a CertificateDer<'a>,
90+
ignored_critical_extensions: &[ExtensionId<'_>],
91+
) -> Result<Self, Error> {
92+
Ok(Self {
93+
inner: cert::Cert::from_der_with_extension_policy(
94+
untrusted::Input::from(cert.as_ref()),
95+
UnknownExtensionPolicy::AllowUnsupportedCritical(ignored_critical_extensions),
96+
)?,
97+
})
98+
}
99+
}
100+
71101
impl EndEntityCert<'_> {
72102
/// Verifies that the certificate is valid for the given Subject Name.
73103
pub fn verify_is_valid_for_subject_name(

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ pub use verify_cert::{
8888
ExtendedKeyUsage, ExtendedKeyUsageValidator, IntermediateIterator, KeyPurposeId,
8989
KeyPurposeIdIter, PathBuilder, RequiredEkuNotFoundContext, VerifiedPath,
9090
};
91+
pub use x509::ExtensionId;
9192

9293
fn public_values_eq(a: untrusted::Input<'_>, b: untrusted::Input<'_>) -> bool {
9394
a.as_slice_less_safe() == b.as_slice_less_safe()

src/verify_cert.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ use crate::end_entity::EndEntityCert;
2828
use crate::error::Error;
2929
#[cfg(feature = "alloc")]
3030
use crate::spki_for_anchor;
31+
use crate::x509::{ExtensionId, UnknownExtensionPolicy};
3132
use crate::{public_values_eq, subject_name};
3233

3334
/// Build a [`VerifiedPath`] for an end-entity certificate from the given trust anchors.
@@ -41,6 +42,7 @@ pub struct PathBuilder<'a, 'p> {
4142
pub(crate) revocation: Option<RevocationOptions<'a>>,
4243
#[expect(clippy::type_complexity)]
4344
pub(crate) verify_path: Option<&'a dyn Fn(&VerifiedPath<'_>) -> Result<(), Error>>,
45+
pub(crate) extension_policy: UnknownExtensionPolicy<'a>,
4446
}
4547

4648
impl<'a, 'p: 'a> PathBuilder<'a, 'p> {
@@ -65,6 +67,7 @@ impl<'a, 'p: 'a> PathBuilder<'a, 'p> {
6567
intermediate_certs: &[],
6668
revocation: None,
6769
verify_path: None,
70+
extension_policy: UnknownExtensionPolicy::default(),
6871
}
6972
}
7073

@@ -84,6 +87,23 @@ impl<'a, 'p: 'a> PathBuilder<'a, 'p> {
8487
self
8588
}
8689

90+
/// Ignore the listed unsupported critical extensions while parsing intermediate
91+
/// certificates for path building.
92+
///
93+
/// By default, path building rejects intermediate certificates containing unsupported
94+
/// critical extensions. This is an opt-in escape hatch for applications that understand
95+
/// the listed unsupported extensions and want webpki to accept them when they are marked
96+
/// critical. The `ignored_critical_extensions` values identify DER OBJECT IDENTIFIER value
97+
/// bytes through [`ExtensionId`]. Supported extensions are still processed normally.
98+
pub fn with_ignored_critical_extensions(
99+
mut self,
100+
ignored_critical_extensions: &'a [ExtensionId<'a>],
101+
) -> Self {
102+
self.extension_policy =
103+
UnknownExtensionPolicy::AllowUnsupportedCritical(ignored_critical_extensions);
104+
self
105+
}
106+
87107
/// Set a path verification function to use for path building.
88108
///
89109
/// `verify()` will only be called for potentially verified paths, that is, paths that
@@ -170,7 +190,10 @@ impl<'a, 'p: 'a> PathBuilder<'a, 'p> {
170190
};
171191

172192
loop_while_non_fatal_error(err, self.intermediate_certs, |cert_der| {
173-
let potential_issuer = Cert::from_der(untrusted::Input::from(cert_der))?;
193+
let potential_issuer = Cert::from_der_with_extension_policy(
194+
untrusted::Input::from(cert_der),
195+
self.extension_policy,
196+
)?;
174197
if !public_values_eq(potential_issuer.subject, path.head().issuer) {
175198
return Err(Error::UnknownIssuer.into());
176199
}

src/x509.rs

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,49 @@
1212
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1313
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1414

15+
use core::fmt;
16+
1517
use crate::der::{self, CONSTRUCTED, CONTEXT_SPECIFIC, DerIterator, FromDer};
1618
use crate::error::{DerTypeId, Error};
19+
use crate::public_values_eq;
1720
use crate::subject_name::GeneralName;
21+
use crate::verify_cert::OidDecoder;
22+
23+
/// DER OBJECT IDENTIFIER value bytes identifying an X.509 extension.
24+
#[derive(Clone, Copy)]
25+
pub struct ExtensionId<'a> {
26+
oid_value: untrusted::Input<'a>,
27+
}
28+
29+
impl<'a> ExtensionId<'a> {
30+
/// Construct a new [`ExtensionId`].
31+
///
32+
/// `oid` is the DER OBJECT IDENTIFIER value bytes, without the OBJECT IDENTIFIER tag or
33+
/// length.
34+
pub const fn new(oid: &'a [u8]) -> Self {
35+
Self {
36+
oid_value: untrusted::Input::from(oid),
37+
}
38+
}
39+
40+
fn matches(&self, oid: untrusted::Input<'_>) -> bool {
41+
public_values_eq(self.oid_value, oid)
42+
}
43+
}
44+
45+
impl fmt::Debug for ExtensionId<'_> {
46+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47+
write!(f, "ExtensionId(")?;
48+
let decoder = OidDecoder::new(self.oid_value.as_slice_less_safe());
49+
for (i, part) in decoder.enumerate() {
50+
if i > 0 {
51+
write!(f, ".")?;
52+
}
53+
write!(f, "{part}")?;
54+
}
55+
write!(f, ")")
56+
}
57+
}
1858

1959
pub(crate) struct Extension<'a> {
2060
pub(crate) critical: bool,
@@ -23,9 +63,16 @@ pub(crate) struct Extension<'a> {
2363
}
2464

2565
impl Extension<'_> {
26-
pub(crate) fn unsupported(&self, policy: UnknownExtensionPolicy) -> Result<(), Error> {
27-
match (policy, self.critical) {
28-
(UnknownExtensionPolicy::Strict, true) => Err(Error::UnsupportedCriticalExtension),
66+
pub(crate) fn unsupported(&self, policy: UnknownExtensionPolicy<'_>) -> Result<(), Error> {
67+
match policy {
68+
UnknownExtensionPolicy::Strict if self.critical => {
69+
Err(Error::UnsupportedCriticalExtension)
70+
}
71+
UnknownExtensionPolicy::AllowUnsupportedCritical(ids)
72+
if self.critical && !ids.iter().any(|id| id.matches(self.id)) =>
73+
{
74+
Err(Error::UnsupportedCriticalExtension)
75+
}
2976
_ => Ok(()),
3077
}
3178
}
@@ -63,7 +110,7 @@ pub(crate) fn set_extension_once<T>(
63110

64111
pub(crate) fn remember_extension(
65112
extension: &Extension<'_>,
66-
ext_policy: UnknownExtensionPolicy,
113+
ext_policy: UnknownExtensionPolicy<'_>,
67114
mut handler: impl FnMut(ExtensionOid) -> Result<(), Error>,
68115
) -> Result<(), Error> {
69116
match ExtensionOid::lookup(extension.id) {
@@ -73,10 +120,11 @@ pub(crate) fn remember_extension(
73120
}
74121

75122
#[derive(Clone, Copy, Debug, Default)]
76-
pub(crate) enum UnknownExtensionPolicy {
123+
pub(crate) enum UnknownExtensionPolicy<'a> {
77124
#[default]
78125
Strict,
79126
IgnoreCritical,
127+
AllowUnsupportedCritical(&'a [ExtensionId<'a>]),
80128
}
81129

82130
/// A certificate revocation list (CRL) distribution point name, describing a source of
@@ -151,3 +199,21 @@ const SCT_LIST_OID: [u8; 10] = [40 + 3, 6, 1, 4, 1, 214, 121, 2, 4, 2];
151199
///
152200
/// <https://www.rfc-editor.org/rfc/rfc5280#appendix-A.2>
153201
const ID_CE: [u8; 2] = oid!(2, 5, 29);
202+
203+
#[cfg(test)]
204+
mod tests {
205+
use super::*;
206+
207+
#[test]
208+
fn unknown_extension_policy_debug() {
209+
let ignored = [ExtensionId::new(&[43, 6, 1, 4, 1, 42, 1])];
210+
211+
assert_eq!(
212+
format!(
213+
"{:?}",
214+
UnknownExtensionPolicy::AllowUnsupportedCritical(&ignored)
215+
),
216+
"AllowUnsupportedCritical([ExtensionId(1.3.6.1.4.1.42.1)])"
217+
);
218+
}
219+
}

0 commit comments

Comments
 (0)