Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/cert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,16 @@ impl<'a> Cert<'a> {
Self::from_input(cert_der, UnknownExtensionPolicy::default())
}

pub(crate) fn from_der_with_extension_policy(
cert_der: untrusted::Input<'a>,
ext_policy: UnknownExtensionPolicy<'_>,
) -> Result<Self, Error> {
Self::from_input(cert_der, ext_policy)
}

fn from_input(
cert_der: untrusted::Input<'a>,
ext_policy: UnknownExtensionPolicy,
ext_policy: UnknownExtensionPolicy<'_>,
) -> Result<Self, Error> {
let (tbs, signed_data) =
cert_der.read_all(Error::TrailingData(DerTypeId::Certificate), |cert_der| {
Expand Down Expand Up @@ -308,7 +315,7 @@ pub(crate) fn lenient_certificate_serial_number<'a>(
fn remember_cert_extension<'a>(
cert: &mut Cert<'a>,
extension: &Extension<'a>,
ext_policy: UnknownExtensionPolicy,
ext_policy: UnknownExtensionPolicy<'_>,
) -> Result<(), Error> {
// We don't do anything with certificate policies so we can safely ignore
// all policy-related stuff. We assume that the policy-related extensions
Expand Down
30 changes: 30 additions & 0 deletions src/end_entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ use pki_types::{CertificateDer, ServerName, SignatureVerificationAlgorithm};

use crate::error::Error;
use crate::subject_name::{verify_dns_names, verify_ip_address_names};
use crate::x509::{ExtensionId, UnknownExtensionPolicy};
use crate::{cert, sct, signed_data};

/// An end-entity certificate.
Expand Down Expand Up @@ -68,6 +69,35 @@ impl<'a> TryFrom<&'a CertificateDer<'a>> for EndEntityCert<'a> {
}
}

impl<'a> EndEntityCert<'a> {
/// Parse the ASN.1 DER-encoded X.509 encoding of the certificate, ignoring the
/// listed unsupported critical extensions.
///
/// By default, webpki rejects certificates containing unsupported critical extensions,
/// as required by RFC 5280. This constructor is an opt-in escape hatch for applications
/// that understand the listed unsupported extensions and want webpki to accept them when
/// they are marked critical.
/// The `ignored_critical_extensions` values are DER OBJECT IDENTIFIER value bytes, without
/// the OBJECT IDENTIFIER tag or length.
///
/// Supported extensions are still processed normally. Listing a supported extension here does
/// not disable validation of its value. This constructor only applies the policy when parsing
/// this end-entity certificate. Use
/// [`PathBuilder::with_ignored_critical_extensions`](crate::PathBuilder::with_ignored_critical_extensions)
/// to apply the same policy when parsing intermediate certificates during path building.
pub fn try_from_with_ignored_critical_extensions(
cert: &'a CertificateDer<'a>,
ignored_critical_extensions: &[ExtensionId<'_>],
) -> Result<Self, Error> {
Ok(Self {
inner: cert::Cert::from_der_with_extension_policy(
untrusted::Input::from(cert.as_ref()),
UnknownExtensionPolicy::AllowUnsupportedCritical(ignored_critical_extensions),
)?,
})
}
}

impl EndEntityCert<'_> {
/// Verifies that the certificate is valid for the given Subject Name.
pub fn verify_is_valid_for_subject_name(
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ pub use {
ExtendedKeyUsage, ExtendedKeyUsageValidator, IntermediateIterator, KeyPurposeId,
KeyPurposeIdIter, PathBuilder, RequiredEkuNotFoundContext, VerifiedPath,
},
x509::ExtensionId,
};

#[cfg(feature = "alloc")]
Expand Down
25 changes: 24 additions & 1 deletion src/verify_cert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ use crate::end_entity::EndEntityCert;
use crate::error::Error;
#[cfg(feature = "alloc")]
use crate::spki_for_anchor;
use crate::x509::{ExtensionId, UnknownExtensionPolicy};
use crate::{public_values_eq, subject_name};

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

impl<'a, 'p: 'a> PathBuilder<'a, 'p> {
Expand All @@ -65,6 +67,7 @@ impl<'a, 'p: 'a> PathBuilder<'a, 'p> {
intermediate_certs: &[],
revocation: None,
verify_path: None,
extension_policy: UnknownExtensionPolicy::default(),
}
}

Expand All @@ -84,6 +87,23 @@ impl<'a, 'p: 'a> PathBuilder<'a, 'p> {
self
}

/// Ignore the listed unsupported critical extensions while parsing intermediate
/// certificates for path building.
///
/// By default, path building rejects intermediate certificates containing unsupported
/// critical extensions. This is an opt-in escape hatch for applications that understand
/// the listed unsupported extensions and want webpki to accept them when they are marked
/// critical. The `ignored_critical_extensions` values identify DER OBJECT IDENTIFIER value
/// bytes through [`ExtensionId`]. Supported extensions are still processed normally.
pub fn with_ignored_critical_extensions(
mut self,
ignored_critical_extensions: &'a [ExtensionId<'a>],
) -> Self {
self.extension_policy =
UnknownExtensionPolicy::AllowUnsupportedCritical(ignored_critical_extensions);
self
}

/// Set a path verification function to use for path building.
///
/// `verify()` will only be called for potentially verified paths, that is, paths that
Expand Down Expand Up @@ -170,7 +190,10 @@ impl<'a, 'p: 'a> PathBuilder<'a, 'p> {
};

loop_while_non_fatal_error(err, self.intermediate_certs, |cert_der| {
let potential_issuer = Cert::from_der(untrusted::Input::from(cert_der))?;
let potential_issuer = Cert::from_der_with_extension_policy(
untrusted::Input::from(cert_der),
self.extension_policy,
)?;
if !public_values_eq(potential_issuer.subject, path.head().issuer) {
return Err(Error::UnknownIssuer.into());
}
Expand Down
76 changes: 71 additions & 5 deletions src/x509.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,49 @@
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

use core::fmt;

use crate::der::{self, CONSTRUCTED, CONTEXT_SPECIFIC, DerIterator, FromDer};
use crate::error::{DerTypeId, Error};
use crate::public_values_eq;
use crate::subject_name::GeneralName;
use crate::verify_cert::OidDecoder;

/// DER OBJECT IDENTIFIER value bytes identifying an X.509 extension.
#[derive(Clone, Copy)]
pub struct ExtensionId<'a> {
oid_value: untrusted::Input<'a>,
}

impl<'a> ExtensionId<'a> {
/// Construct a new [`ExtensionId`].
///
/// `oid` is the DER OBJECT IDENTIFIER value bytes, without the OBJECT IDENTIFIER tag or
/// length.
pub const fn new(oid: &'a [u8]) -> Self {
Self {
oid_value: untrusted::Input::from(oid),
}
}

fn matches(&self, oid: untrusted::Input<'_>) -> bool {
public_values_eq(self.oid_value, oid)
}
}

impl fmt::Debug for ExtensionId<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "ExtensionId(")?;
let decoder = OidDecoder::new(self.oid_value.as_slice_less_safe());
for (i, part) in decoder.enumerate() {
if i > 0 {
write!(f, ".")?;
}
write!(f, "{part}")?;
}
write!(f, ")")
}
}

pub(crate) struct Extension<'a> {
pub(crate) critical: bool,
Expand All @@ -23,9 +63,16 @@ pub(crate) struct Extension<'a> {
}

impl Extension<'_> {
pub(crate) fn unsupported(&self, policy: UnknownExtensionPolicy) -> Result<(), Error> {
match (policy, self.critical) {
(UnknownExtensionPolicy::Strict, true) => Err(Error::UnsupportedCriticalExtension),
pub(crate) fn unsupported(&self, policy: UnknownExtensionPolicy<'_>) -> Result<(), Error> {
match policy {
UnknownExtensionPolicy::Strict if self.critical => {
Err(Error::UnsupportedCriticalExtension)
}
UnknownExtensionPolicy::AllowUnsupportedCritical(ids)
if self.critical && !ids.iter().any(|id| id.matches(self.id)) =>
{
Err(Error::UnsupportedCriticalExtension)
}
_ => Ok(()),
}
}
Expand Down Expand Up @@ -63,7 +110,7 @@ pub(crate) fn set_extension_once<T>(

pub(crate) fn remember_extension(
extension: &Extension<'_>,
ext_policy: UnknownExtensionPolicy,
ext_policy: UnknownExtensionPolicy<'_>,
mut handler: impl FnMut(ExtensionOid) -> Result<(), Error>,
) -> Result<(), Error> {
match ExtensionOid::lookup(extension.id) {
Expand All @@ -73,10 +120,11 @@ pub(crate) fn remember_extension(
}

#[derive(Clone, Copy, Debug, Default)]
pub(crate) enum UnknownExtensionPolicy {
pub(crate) enum UnknownExtensionPolicy<'a> {
#[default]
Strict,
IgnoreCritical,
AllowUnsupportedCritical(&'a [ExtensionId<'a>]),
}

/// A certificate revocation list (CRL) distribution point name, describing a source of
Expand Down Expand Up @@ -151,3 +199,21 @@ const SCT_LIST_OID: [u8; 10] = [40 + 3, 6, 1, 4, 1, 214, 121, 2, 4, 2];
///
/// <https://www.rfc-editor.org/rfc/rfc5280#appendix-A.2>
const ID_CE: [u8; 2] = oid!(2, 5, 29);

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn unknown_extension_policy_debug() {
let ignored = [ExtensionId::new(&[43, 6, 1, 4, 1, 42, 1])];

assert_eq!(
format!(
"{:?}",
UnknownExtensionPolicy::AllowUnsupportedCritical(&ignored)
),
"AllowUnsupportedCritical([ExtensionId(1.3.6.1.4.1.42.1)])"
);
}
}
Loading