Skip to content

Commit

Permalink
Introduce more restricted public key usage API
Browse files Browse the repository at this point in the history
  • Loading branch information
djc authored and ctz committed Jul 26, 2023
1 parent 3814c03 commit ee95916
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 88 deletions.
10 changes: 5 additions & 5 deletions src/end_entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
#[cfg(feature = "alloc")]
use crate::subject_name::GeneralDnsNameRef;
use crate::{
cert, signed_data, subject_name, verify_cert, CertRevocationList, Error, ExtendedKeyUsage,
cert, signed_data, subject_name, verify_cert, CertRevocationList, Error, KeyUsage,
NonTlsTrustAnchors, SignatureAlgorithm, SubjectNameRef, Time, TlsClientTrustAnchors,
TlsServerTrustAnchors, TrustAnchor,
};
Expand Down Expand Up @@ -81,7 +81,7 @@ impl<'a> EndEntityCert<'a> {
trust_anchors: &[TrustAnchor],
intermediate_certs: &[&[u8]],
time: Time,
eku: ExtendedKeyUsage,
eku: KeyUsage,
crls: &[&dyn CertRevocationList],
) -> Result<(), Error> {
verify_cert::build_chain(
Expand Down Expand Up @@ -113,7 +113,7 @@ impl<'a> EndEntityCert<'a> {
&NonTlsTrustAnchors(trust_anchors): &NonTlsTrustAnchors,
intermediate_certs: &[&[u8]],
time: Time,
eku: ExtendedKeyUsage,
eku: KeyUsage,
crls: &[&dyn CertRevocationList],
) -> Result<(), Error> {
self.verify_is_valid_cert(
Expand Down Expand Up @@ -148,7 +148,7 @@ impl<'a> EndEntityCert<'a> {
trust_anchors,
intermediate_certs,
time,
ExtendedKeyUsage::RequiredIfPresent(verify_cert::EKU_SERVER_AUTH),
KeyUsage::server_auth(),
&[],
)
}
Expand Down Expand Up @@ -177,7 +177,7 @@ impl<'a> EndEntityCert<'a> {
trust_anchors,
intermediate_certs,
time,
ExtendedKeyUsage::RequiredIfPresent(verify_cert::EKU_CLIENT_AUTH),
KeyUsage::client_auth(),
crls,
)
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ pub use {
},
time::Time,
trust_anchor::{NonTlsTrustAnchors, TlsClientTrustAnchors, TlsServerTrustAnchors, TrustAnchor},
verify_cert::{ExtendedKeyUsage, KeyPurposeId},
verify_cert::KeyUsage,
};

#[cfg(feature = "alloc")]
Expand Down
113 changes: 67 additions & 46 deletions src/verify_cert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::{
};

pub(crate) struct ChainOptions<'a> {
pub(crate) eku: ExtendedKeyUsage,
pub(crate) eku: KeyUsage,
pub(crate) supported_sig_algs: &'a [&'a SignatureAlgorithm],
pub(crate) trust_anchors: &'a [TrustAnchor<'a>],
pub(crate) intermediate_certs: &'a [&'a [u8]],
Expand All @@ -38,7 +38,7 @@ fn build_chain_inner(
) -> Result<(), Error> {
let used_as_ca = used_as_ca(&cert.ee_or_ca);

check_issuer_independent_properties(cert, time, used_as_ca, sub_ca_count, opts.eku)?;
check_issuer_independent_properties(cert, time, used_as_ca, sub_ca_count, opts.eku.inner)?;

// TODO: HPKP checks.

Expand All @@ -59,7 +59,10 @@ fn build_chain_inner(
// for the purpose of name constraints checking, only end-entity server certificates
// could plausibly have a DNS name as a subject commonName that could contribute to
// path validity
let subject_common_name_contents = if opts.eku.key_purpose_id_equals(EKU_SERVER_AUTH.oid_value)
let subject_common_name_contents = if opts
.eku
.inner
.key_purpose_id_equals(EKU_SERVER_AUTH.oid_value)
&& used_as_ca == UsedAsCa::No
{
subject_name::SubjectCommonNameContents::DnsName
Expand Down Expand Up @@ -334,9 +337,49 @@ fn check_basic_constraints(
}
}

/// The expected key usage of a certificate.
///
/// This type represents the expected key usage of an end entity certificate. Although for most
/// kinds of certificates the extended key usage extension is optional (and so certificates
/// not carrying a particular value in the EKU extension are acceptable). If the extension
/// is present, the certificate MUST only be used for one of the purposes indicated.
///
/// <https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.12>
#[derive(Clone, Copy)]
pub struct KeyUsage {
inner: ExtendedKeyUsage,
}

impl KeyUsage {
/// Construct a new [`KeyUsage`] as appropriate for server certificate authentication.
///
/// As specified in <https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.12>, this does not require the certificate to specify the eKU extension.
pub const fn server_auth() -> Self {
Self {
inner: ExtendedKeyUsage::RequiredIfPresent(EKU_SERVER_AUTH),
}
}

/// Construct a new [`KeyUsage`] as appropriate for client certificate authentication.
///
/// As specified in <>, this does not require the certificate to specify the eKU extension.
pub const fn client_auth() -> Self {
Self {
inner: ExtendedKeyUsage::RequiredIfPresent(EKU_CLIENT_AUTH),
}
}

/// Construct a new [`KeyUsage`] requiring a certificate to support the specified OID.
pub const fn required(oid: &'static [u8]) -> Self {
Self {
inner: ExtendedKeyUsage::Required(KeyPurposeId::new(oid)),
}
}
}

/// Extended Key Usage (EKU) of a certificate.
#[derive(Clone, Copy)]
pub enum ExtendedKeyUsage {
enum ExtendedKeyUsage {
/// The certificate must contain the specified [`KeyPurposeId`] as EKU.
Required(KeyPurposeId),

Expand All @@ -347,40 +390,25 @@ pub enum ExtendedKeyUsage {
impl ExtendedKeyUsage {
// https://tools.ietf.org/html/rfc5280#section-4.2.1.12
fn check(&self, input: Option<&mut untrusted::Reader>) -> Result<(), Error> {
match input {
Some(input) => {
loop {
let value = der::expect_tag_and_get_value(input, der::Tag::OID)?;
if self.key_purpose_id_equals(value) {
input.skip_to_end();
break;
}
if input.at_end() {
return Err(Error::RequiredEkuNotFound);
}
}
Ok(())
let input = match (input, self) {
(Some(input), _) => input,
(None, Self::RequiredIfPresent(_)) => return Ok(()),
(None, Self::Required(_)) => return Err(Error::RequiredEkuNotFound),
};

loop {
let value = der::expect_tag_and_get_value(input, der::Tag::OID)?;
if self.key_purpose_id_equals(value) {
input.skip_to_end();
break;
}
None => {
if matches!(self, Self::Required(_)) {
return Err(Error::RequiredEkuNotFound);
}
// http://tools.ietf.org/html/rfc6960#section-4.2.2.2:
// "OCSP signing delegation SHALL be designated by the inclusion of
// id-kp-OCSPSigning in an extended key usage certificate extension
// included in the OCSP response signer's certificate."
//
// A missing EKU extension generally means "any EKU", but it is
// important that id-kp-OCSPSigning is explicit so that a normal
// end-entity certificate isn't able to sign trusted OCSP responses
// for itself or for other certificates issued by its issuing CA.
if self.key_purpose_id_equals(EKU_OCSP_SIGNING.oid_value) {
return Err(Error::RequiredEkuNotFound);
}

Ok(())
if input.at_end() {
return Err(Error::RequiredEkuNotFound);
}
}

Ok(())
}

fn key_purpose_id_equals(&self, value: untrusted::Input<'_>) -> bool {
Expand All @@ -395,15 +423,15 @@ impl ExtendedKeyUsage {

/// An OID value indicating an Extended Key Usage (EKU) key purpose.
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct KeyPurposeId {
struct KeyPurposeId {
oid_value: untrusted::Input<'static>,
}

impl KeyPurposeId {
/// Construct a new [`KeyPurposeId`].
///
/// `oid` is the OBJECT IDENTIFIER in bytes.
pub const fn new(oid: &'static [u8]) -> Self {
const fn new(oid: &'static [u8]) -> Self {
Self {
oid_value: untrusted::Input::from(oid),
}
Expand All @@ -415,18 +443,11 @@ impl KeyPurposeId {

// id-kp-serverAuth OBJECT IDENTIFIER ::= { id-kp 1 }
#[allow(clippy::identity_op)] // TODO: Make this clearer
pub(crate) static EKU_SERVER_AUTH: KeyPurposeId =
KeyPurposeId::new(&[(40 * 1) + 3, 6, 1, 5, 5, 7, 3, 1]);
const EKU_SERVER_AUTH: KeyPurposeId = KeyPurposeId::new(&[(40 * 1) + 3, 6, 1, 5, 5, 7, 3, 1]);

// id-kp-clientAuth OBJECT IDENTIFIER ::= { id-kp 2 }
#[allow(clippy::identity_op)] // TODO: Make this clearer
pub(crate) static EKU_CLIENT_AUTH: KeyPurposeId =
KeyPurposeId::new(&[(40 * 1) + 3, 6, 1, 5, 5, 7, 3, 2]);

// id-kp-OCSPSigning OBJECT IDENTIFIER ::= { id-kp 9 }
#[allow(clippy::identity_op)] // TODO: Make this clearer
pub(crate) static EKU_OCSP_SIGNING: KeyPurposeId =
KeyPurposeId::new(&[(40 * 1) + 3, 6, 1, 5, 5, 7, 3, 9]);
const EKU_CLIENT_AUTH: KeyPurposeId = KeyPurposeId::new(&[(40 * 1) + 3, 6, 1, 5, 5, 7, 3, 2]);

// https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.3
#[repr(u8)]
Expand Down Expand Up @@ -483,7 +504,7 @@ where

#[cfg(test)]
mod tests {
use crate::{verify_cert::EKU_SERVER_AUTH, ExtendedKeyUsage};
use super::*;

#[test]
fn eku_key_purpose_id() {
Expand Down
47 changes: 11 additions & 36 deletions tests/custom_ekus.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#![cfg(feature = "alloc")]

use webpki::ExtendedKeyUsage::{Required, RequiredIfPresent};
use webpki::KeyUsage;

fn check_cert(
ee: &[u8],
ca: &[u8],
eku: webpki::ExtendedKeyUsage,
eku: KeyUsage,
time: webpki::Time,
result: Result<(), webpki::Error>,
) {
Expand All @@ -24,56 +24,31 @@ fn check_cert(
);
}

#[allow(clippy::identity_op)]
static EKU_CLIENT_AUTH: webpki::KeyPurposeId =
webpki::KeyPurposeId::new(&[(40 * 1) + 3, 6, 1, 5, 5, 7, 3, 2]);

#[allow(clippy::identity_op)]
static EKU_SERVER_AUTH: webpki::KeyPurposeId =
webpki::KeyPurposeId::new(&[(40 * 1) + 3, 6, 1, 5, 5, 7, 3, 1]);

#[allow(clippy::identity_op)]
static EKU_MDOC_ISSUER_AUTH: webpki::KeyPurposeId =
webpki::KeyPurposeId::new(&[(40 * 1) + 0, 129, 140, 93, 5, 1, 2]);

#[test]
pub fn verify_custom_eku_mdoc() {
let err = Err(webpki::Error::RequiredEkuNotFound);
let time = webpki::Time::from_seconds_since_unix_epoch(1609459200); // Jan 1 01:00:00 CET 2021

let ee = include_bytes!("misc/mdoc_eku.ee.der");
let ca = include_bytes!("misc/mdoc_eku.ca.der");
check_cert(ee, ca, Required(EKU_MDOC_ISSUER_AUTH), time, Ok(()));
check_cert(ee, ca, Required(EKU_SERVER_AUTH), time, err);
check_cert(
ee,
ca,
RequiredIfPresent(EKU_MDOC_ISSUER_AUTH),
time,
Ok(()),
);
check_cert(ee, ca, RequiredIfPresent(EKU_SERVER_AUTH), time, err);

let eku_mdoc = KeyUsage::required(&[(40 * 1) + 0, 129, 140, 93, 5, 1, 2]);
check_cert(ee, ca, eku_mdoc, time, Ok(()));
check_cert(ee, ca, KeyUsage::server_auth(), time, err);
check_cert(ee, ca, eku_mdoc, time, Ok(()));
check_cert(ee, ca, KeyUsage::server_auth(), time, err);
}

#[test]
pub fn verify_custom_eku_client() {
let err = Err(webpki::Error::RequiredEkuNotFound);
let time = webpki::Time::from_seconds_since_unix_epoch(0x1fed_f00d);

let ee = include_bytes!("client_auth/cert_with_no_eku_accepted_for_client_auth.ee.der");
let ca = include_bytes!("client_auth/cert_with_no_eku_accepted_for_client_auth.ca.der");
check_cert(ee, ca, Required(EKU_CLIENT_AUTH), time, err);
check_cert(ee, ca, RequiredIfPresent(EKU_CLIENT_AUTH), time, Ok(()));
check_cert(ee, ca, KeyUsage::client_auth(), time, Ok(()));

let ee = include_bytes!("client_auth/cert_with_both_ekus_accepted_for_client_auth.ee.der");
let ca = include_bytes!("client_auth/cert_with_both_ekus_accepted_for_client_auth.ca.der");
check_cert(ee, ca, Required(EKU_CLIENT_AUTH), time, Ok(()));
check_cert(ee, ca, Required(EKU_SERVER_AUTH), time, Ok(()));
check_cert(ee, ca, RequiredIfPresent(EKU_CLIENT_AUTH), time, Ok(()));
check_cert(ee, ca, RequiredIfPresent(EKU_SERVER_AUTH), time, Ok(()));
}

#[test]
fn key_purpose_id() {
webpki::KeyPurposeId::new(&[1, 2, 3]);
check_cert(ee, ca, KeyUsage::client_auth(), time, Ok(()));
check_cert(ee, ca, KeyUsage::server_auth(), time, Ok(()));
}

0 comments on commit ee95916

Please sign in to comment.