mirror of https://github.com/briansmith/webpki
345 lines
12 KiB
Rust
345 lines
12 KiB
Rust
// Copyright 2015 Brian Smith.
|
|
//
|
|
// Permission to use, copy, modify, and/or distribute this software for any
|
|
// purpose with or without fee is hereby granted, provided that the above
|
|
// copyright notice and this permission notice appear in all copies.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES
|
|
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR
|
|
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
use super::{
|
|
dns_name::{self, DnsNameRef},
|
|
ip_address,
|
|
};
|
|
use crate::{
|
|
cert::{Cert, EndEntityOrCa},
|
|
der, equal, Error,
|
|
};
|
|
|
|
pub fn verify_cert_dns_name(
|
|
cert: &crate::EndEntityCert,
|
|
dns_name: DnsNameRef,
|
|
) -> Result<(), Error> {
|
|
let cert = cert.inner();
|
|
let dns_name = untrusted::Input::from(dns_name.as_ref());
|
|
iterate_names(
|
|
cert.subject,
|
|
cert.subject_alt_name,
|
|
Err(Error::CertNotValidForName),
|
|
&|name| {
|
|
match name {
|
|
GeneralName::DnsName(presented_id) => {
|
|
match dns_name::presented_id_matches_reference_id(presented_id, dns_name) {
|
|
Some(true) => {
|
|
return NameIteration::Stop(Ok(()));
|
|
}
|
|
Some(false) => (),
|
|
None => {
|
|
return NameIteration::Stop(Err(Error::BadDer));
|
|
}
|
|
}
|
|
}
|
|
_ => (),
|
|
}
|
|
NameIteration::KeepGoing
|
|
},
|
|
)
|
|
}
|
|
|
|
// https://tools.ietf.org/html/rfc5280#section-4.2.1.10
|
|
pub fn check_name_constraints(
|
|
input: Option<&mut untrusted::Reader>,
|
|
subordinate_certs: &Cert,
|
|
) -> Result<(), Error> {
|
|
let input = match input {
|
|
Some(input) => input,
|
|
None => {
|
|
return Ok(());
|
|
}
|
|
};
|
|
|
|
fn parse_subtrees<'b>(
|
|
inner: &mut untrusted::Reader<'b>,
|
|
subtrees_tag: der::Tag,
|
|
) -> Result<Option<untrusted::Input<'b>>, Error> {
|
|
if !inner.peek(subtrees_tag.into()) {
|
|
return Ok(None);
|
|
}
|
|
der::expect_tag_and_get_value(inner, subtrees_tag).map(Some)
|
|
}
|
|
|
|
let permitted_subtrees = parse_subtrees(input, der::Tag::ContextSpecificConstructed0)?;
|
|
let excluded_subtrees = parse_subtrees(input, der::Tag::ContextSpecificConstructed1)?;
|
|
|
|
let mut child = subordinate_certs;
|
|
loop {
|
|
iterate_names(child.subject, child.subject_alt_name, Ok(()), &|name| {
|
|
check_presented_id_conforms_to_constraints(name, permitted_subtrees, excluded_subtrees)
|
|
})?;
|
|
|
|
child = match child.ee_or_ca {
|
|
EndEntityOrCa::Ca(child_cert) => child_cert,
|
|
EndEntityOrCa::EndEntity => {
|
|
break;
|
|
}
|
|
};
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn check_presented_id_conforms_to_constraints(
|
|
name: GeneralName,
|
|
permitted_subtrees: Option<untrusted::Input>,
|
|
excluded_subtrees: Option<untrusted::Input>,
|
|
) -> NameIteration {
|
|
match check_presented_id_conforms_to_constraints_in_subtree(
|
|
name,
|
|
Subtrees::PermittedSubtrees,
|
|
permitted_subtrees,
|
|
) {
|
|
stop @ NameIteration::Stop(..) => {
|
|
return stop;
|
|
}
|
|
NameIteration::KeepGoing => (),
|
|
};
|
|
|
|
check_presented_id_conforms_to_constraints_in_subtree(
|
|
name,
|
|
Subtrees::ExcludedSubtrees,
|
|
excluded_subtrees,
|
|
)
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum Subtrees {
|
|
PermittedSubtrees,
|
|
ExcludedSubtrees,
|
|
}
|
|
|
|
fn check_presented_id_conforms_to_constraints_in_subtree(
|
|
name: GeneralName,
|
|
subtrees: Subtrees,
|
|
constraints: Option<untrusted::Input>,
|
|
) -> NameIteration {
|
|
let mut constraints = match constraints {
|
|
Some(constraints) => untrusted::Reader::new(constraints),
|
|
None => {
|
|
return NameIteration::KeepGoing;
|
|
}
|
|
};
|
|
|
|
let mut has_permitted_subtrees_match = false;
|
|
let mut has_permitted_subtrees_mismatch = false;
|
|
|
|
loop {
|
|
// http://tools.ietf.org/html/rfc5280#section-4.2.1.10: "Within this
|
|
// profile, the minimum and maximum fields are not used with any name
|
|
// forms, thus, the minimum MUST be zero, and maximum MUST be absent."
|
|
//
|
|
// Since the default value isn't allowed to be encoded according to the
|
|
// DER encoding rules for DEFAULT, this is equivalent to saying that
|
|
// neither minimum or maximum must be encoded.
|
|
fn general_subtree<'b>(
|
|
input: &mut untrusted::Reader<'b>,
|
|
) -> Result<GeneralName<'b>, Error> {
|
|
let general_subtree = der::expect_tag_and_get_value(input, der::Tag::Sequence)?;
|
|
general_subtree.read_all(Error::BadDer, general_name)
|
|
}
|
|
|
|
let base = match general_subtree(&mut constraints) {
|
|
Ok(base) => base,
|
|
Err(err) => {
|
|
return NameIteration::Stop(Err(err));
|
|
}
|
|
};
|
|
|
|
let matches = match (name, base) {
|
|
(GeneralName::DnsName(name), GeneralName::DnsName(base)) => {
|
|
dns_name::presented_id_matches_constraint(name, base).ok_or(Error::BadDer)
|
|
}
|
|
|
|
(GeneralName::DirectoryName(name), GeneralName::DnsName(base)) => {
|
|
common_name(name).map(|cn| cn == base)
|
|
}
|
|
|
|
(GeneralName::DirectoryName(name), GeneralName::DirectoryName(base)) => Ok(
|
|
presented_directory_name_matches_constraint(name, base, subtrees),
|
|
),
|
|
|
|
(GeneralName::IpAddress(name), GeneralName::IpAddress(base)) => {
|
|
ip_address::presented_id_matches_constraint(name, base)
|
|
}
|
|
|
|
// RFC 4280 says "If a name constraints extension that is marked as
|
|
// critical imposes constraints on a particular name form, and an
|
|
// instance of that name form appears in the subject field or
|
|
// subjectAltName extension of a subsequent certificate, then the
|
|
// application MUST either process the constraint or reject the
|
|
// certificate." Later, the CABForum agreed to support non-critical
|
|
// constraints, so it is important to reject the cert without
|
|
// considering whether the name constraint it critical.
|
|
(GeneralName::Unsupported(name_tag), GeneralName::Unsupported(base_tag))
|
|
if name_tag == base_tag =>
|
|
{
|
|
Err(Error::NameConstraintViolation)
|
|
}
|
|
|
|
_ => Ok(false),
|
|
};
|
|
|
|
match (subtrees, matches) {
|
|
(Subtrees::PermittedSubtrees, Ok(true)) => {
|
|
has_permitted_subtrees_match = true;
|
|
}
|
|
|
|
(Subtrees::PermittedSubtrees, Ok(false)) => {
|
|
has_permitted_subtrees_mismatch = true;
|
|
}
|
|
|
|
(Subtrees::ExcludedSubtrees, Ok(true)) => {
|
|
return NameIteration::Stop(Err(Error::NameConstraintViolation));
|
|
}
|
|
|
|
(Subtrees::ExcludedSubtrees, Ok(false)) => (),
|
|
|
|
(_, Err(err)) => {
|
|
return NameIteration::Stop(Err(err));
|
|
}
|
|
}
|
|
|
|
if constraints.at_end() {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if has_permitted_subtrees_mismatch && !has_permitted_subtrees_match {
|
|
// If there was any entry of the given type in permittedSubtrees, then
|
|
// it required that at least one of them must match. Since none of them
|
|
// did, we have a failure.
|
|
NameIteration::Stop(Err(Error::NameConstraintViolation))
|
|
} else {
|
|
NameIteration::KeepGoing
|
|
}
|
|
}
|
|
|
|
// TODO: document this.
|
|
fn presented_directory_name_matches_constraint(
|
|
name: untrusted::Input,
|
|
constraint: untrusted::Input,
|
|
subtrees: Subtrees,
|
|
) -> bool {
|
|
match subtrees {
|
|
Subtrees::PermittedSubtrees => equal(name, constraint),
|
|
Subtrees::ExcludedSubtrees => true,
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
enum NameIteration {
|
|
KeepGoing,
|
|
Stop(Result<(), Error>),
|
|
}
|
|
|
|
fn iterate_names(
|
|
subject: untrusted::Input,
|
|
subject_alt_name: Option<untrusted::Input>,
|
|
result_if_never_stopped_early: Result<(), Error>,
|
|
f: &dyn Fn(GeneralName) -> NameIteration,
|
|
) -> Result<(), Error> {
|
|
match subject_alt_name {
|
|
Some(subject_alt_name) => {
|
|
let mut subject_alt_name = untrusted::Reader::new(subject_alt_name);
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1143085: An empty
|
|
// subjectAltName is not legal, but some certificates have an empty
|
|
// subjectAltName. Since we don't support CN-IDs, the certificate
|
|
// will be rejected either way, but checking `at_end` before
|
|
// attempting to parse the first entry allows us to return a better
|
|
// error code.
|
|
while !subject_alt_name.at_end() {
|
|
let name = general_name(&mut subject_alt_name)?;
|
|
match f(name) {
|
|
NameIteration::Stop(result) => {
|
|
return result;
|
|
}
|
|
NameIteration::KeepGoing => (),
|
|
}
|
|
}
|
|
}
|
|
None => (),
|
|
}
|
|
|
|
match f(GeneralName::DirectoryName(subject)) {
|
|
NameIteration::Stop(result) => result,
|
|
NameIteration::KeepGoing => result_if_never_stopped_early,
|
|
}
|
|
}
|
|
|
|
// It is *not* valid to derive `Eq`, `PartialEq, etc. for this type. In
|
|
// particular, for the types of `GeneralName`s that we don't understand, we
|
|
// don't even store the value. Also, the meaning of a `GeneralName` in a name
|
|
// constraint is different than the meaning of the identically-represented
|
|
// `GeneralName` in other contexts.
|
|
#[derive(Clone, Copy)]
|
|
enum GeneralName<'a> {
|
|
DnsName(untrusted::Input<'a>),
|
|
DirectoryName(untrusted::Input<'a>),
|
|
IpAddress(untrusted::Input<'a>),
|
|
|
|
// The value is the `tag & ~(der::CONTEXT_SPECIFIC | der::CONSTRUCTED)` so
|
|
// that the name constraint checking matches tags regardless of whether
|
|
// those bits are set.
|
|
Unsupported(u8),
|
|
}
|
|
|
|
fn general_name<'a>(input: &mut untrusted::Reader<'a>) -> Result<GeneralName<'a>, Error> {
|
|
use ring::io::der::{CONSTRUCTED, CONTEXT_SPECIFIC};
|
|
#[allow(clippy::identity_op)]
|
|
const OTHER_NAME_TAG: u8 = CONTEXT_SPECIFIC | CONSTRUCTED | 0;
|
|
const RFC822_NAME_TAG: u8 = CONTEXT_SPECIFIC | 1;
|
|
const DNS_NAME_TAG: u8 = CONTEXT_SPECIFIC | 2;
|
|
const X400_ADDRESS_TAG: u8 = CONTEXT_SPECIFIC | CONSTRUCTED | 3;
|
|
const DIRECTORY_NAME_TAG: u8 = CONTEXT_SPECIFIC | CONSTRUCTED | 4;
|
|
const EDI_PARTY_NAME_TAG: u8 = CONTEXT_SPECIFIC | CONSTRUCTED | 5;
|
|
const UNIFORM_RESOURCE_IDENTIFIER_TAG: u8 = CONTEXT_SPECIFIC | 6;
|
|
const IP_ADDRESS_TAG: u8 = CONTEXT_SPECIFIC | 7;
|
|
const REGISTERED_ID_TAG: u8 = CONTEXT_SPECIFIC | 8;
|
|
|
|
let (tag, value) = der::read_tag_and_get_value(input)?;
|
|
let name = match tag {
|
|
DNS_NAME_TAG => GeneralName::DnsName(value),
|
|
DIRECTORY_NAME_TAG => GeneralName::DirectoryName(value),
|
|
IP_ADDRESS_TAG => GeneralName::IpAddress(value),
|
|
|
|
OTHER_NAME_TAG
|
|
| RFC822_NAME_TAG
|
|
| X400_ADDRESS_TAG
|
|
| EDI_PARTY_NAME_TAG
|
|
| UNIFORM_RESOURCE_IDENTIFIER_TAG
|
|
| REGISTERED_ID_TAG => GeneralName::Unsupported(tag & !(CONTEXT_SPECIFIC | CONSTRUCTED)),
|
|
|
|
_ => return Err(Error::BadDer),
|
|
};
|
|
Ok(name)
|
|
}
|
|
|
|
static COMMON_NAME: untrusted::Input = untrusted::Input::from(&[85, 4, 3]);
|
|
|
|
fn common_name(input: untrusted::Input) -> Result<untrusted::Input, Error> {
|
|
let inner = &mut untrusted::Reader::new(input);
|
|
der::nested(inner, der::Tag::Set, Error::BadDer, |tagged| {
|
|
der::nested(tagged, der::Tag::Sequence, Error::BadDer, |tagged| {
|
|
let value = der::expect_tag_and_get_value(tagged, der::Tag::OID)?;
|
|
if value != COMMON_NAME {
|
|
return Err(Error::BadDer);
|
|
}
|
|
der::expect_tag_and_get_value(tagged, der::Tag::UTF8String)
|
|
})
|
|
})
|
|
}
|