blob: 13f300ab3f21dff84b0d87cc407e75385ca2d7f8 [file] [log] [blame]
// Copyright 2023 Daniel McCarney.
//
// 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 crate::cert::lenient_certificate_serial_number;
use crate::der::Tag;
use crate::signed_data::{self, SignedData};
use crate::verify_cert::Budget;
use crate::x509::{remember_extension, set_extension_once, Extension};
use crate::{der, public_values_eq, Error, SignatureAlgorithm, Time};
#[cfg(feature = "alloc")]
use std::collections::HashMap;
use private::Sealed;
/// Operations over a RFC 5280[^1] profile Certificate Revocation List (CRL) required
/// for revocation checking. Implemented by [`OwnedCertRevocationList`] and
/// [`BorrowedCertRevocationList`].
///
/// [^1]: <https://www.rfc-editor.org/rfc/rfc5280#section-5>
pub trait CertRevocationList: Sealed {
/// Return the DER encoded issuer of the CRL.
fn issuer(&self) -> &[u8];
/// Try to find a revoked certificate in the CRL by DER encoded serial number. This
/// may yield an error if the CRL has malformed revoked certificates.
fn find_serial(&self, serial: &[u8]) -> Result<Option<BorrowedRevokedCert>, Error>;
/// Verify the CRL signature using the issuer's subject public key information (SPKI)
/// and a list of supported signature algorithms.
fn verify_signature(
&self,
supported_sig_algs: &[&SignatureAlgorithm],
issuer_spki: &[u8],
) -> Result<(), Error>;
}
/// Owned representation of a RFC 5280[^1] profile Certificate Revocation List (CRL).
///
/// [^1]: <https://www.rfc-editor.org/rfc/rfc5280#section-5>
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
#[allow(dead_code)] // we parse some fields we don't expose now, but may choose to expose in the future.
#[derive(Debug, Clone)]
pub struct OwnedCertRevocationList {
/// A map of the revoked certificates contained in then CRL, keyed by the DER encoding
/// of the revoked cert's serial number.
revoked_certs: HashMap<Vec<u8>, OwnedRevokedCert>,
issuer: Vec<u8>,
signed_data: signed_data::OwnedSignedData,
}
#[cfg(feature = "alloc")]
impl Sealed for OwnedCertRevocationList {}
#[cfg(feature = "alloc")]
impl CertRevocationList for OwnedCertRevocationList {
fn issuer(&self) -> &[u8] {
&self.issuer
}
fn find_serial(&self, serial: &[u8]) -> Result<Option<BorrowedRevokedCert>, Error> {
// note: this is infallible for the owned representation because we process all
// revoked certificates at the time of construction to build the `revoked_certs` map,
// returning any encountered errors at that time.
Ok(self
.revoked_certs
.get(serial)
.map(|owned_revoked_cert| owned_revoked_cert.borrow()))
}
fn verify_signature(
&self,
supported_sig_algs: &[&SignatureAlgorithm],
issuer_spki: &[u8],
) -> Result<(), Error> {
signed_data::verify_signed_data(
supported_sig_algs,
untrusted::Input::from(issuer_spki),
&self.signed_data.borrow(),
&mut Budget::default(),
)
}
}
/// Borrowed representation of a RFC 5280[^1] profile Certificate Revocation List (CRL).
///
/// [^1]: <https://www.rfc-editor.org/rfc/rfc5280#section-5>
#[derive(Debug)]
pub struct BorrowedCertRevocationList<'a> {
/// A `SignedData` structure that can be passed to `verify_signed_data`.
signed_data: SignedData<'a>,
/// Identifies the entity that has signed and issued this
/// CRL.
issuer: untrusted::Input<'a>,
/// List of certificates revoked by the issuer in this CRL.
revoked_certs: untrusted::Input<'a>,
}
impl<'a> BorrowedCertRevocationList<'a> {
/// Try to parse the given bytes as a RFC 5280[^1] profile Certificate Revocation List (CRL).
///
/// Webpki does not support:
/// * CRL versions other than version 2.
/// * CRLs missing the next update field.
/// * CRLs missing certificate revocation list extensions.
/// * Delta CRLs.
/// * CRLs larger than (2^32)-1 bytes in size.
///
/// [^1]: <https://www.rfc-editor.org/rfc/rfc5280#section-5>
pub fn from_der(crl_der: &'a [u8]) -> Result<Self, Error> {
// Try to parse the CRL.
let reader = untrusted::Input::from(crl_der);
let (tbs_cert_list, signed_data) = reader.read_all(Error::BadDer, |crl_der| {
der::nested_limited(
crl_der,
Tag::Sequence,
Error::BadDer,
|signed_der| SignedData::from_der(signed_der, der::MAX_DER_SIZE),
der::MAX_DER_SIZE,
)
})?;
let crl = tbs_cert_list.read_all(Error::BadDer, |tbs_cert_list| {
// RFC 5280 §5.1.2.1:
// This optional field describes the version of the encoded CRL. When
// extensions are used, as required by this profile, this field MUST be
// present and MUST specify version 2 (the integer value is 1).
// RFC 5280 §5.2:
// Conforming CRL issuers are REQUIRED to include the authority key
// identifier (Section 5.2.1) and the CRL number (Section 5.2.3)
// extensions in all CRLs issued.
// As a result of the above we parse this as a required section, not OPTIONAL.
// NOTE: Encoded value of version 2 is 1.
if der::small_nonnegative_integer(tbs_cert_list)? != 1 {
return Err(Error::UnsupportedCrlVersion);
}
// RFC 5280 §5.1.2.2:
// This field MUST contain the same algorithm identifier as the
// signatureAlgorithm field in the sequence CertificateList
let signature = der::expect_tag_and_get_value(tbs_cert_list, Tag::Sequence)?;
if !public_values_eq(signature, signed_data.algorithm) {
return Err(Error::SignatureAlgorithmMismatch);
}
// RFC 5280 §5.1.2.3:
// The issuer field MUST contain a non-empty X.500 distinguished name (DN).
let issuer = der::expect_tag_and_get_value(tbs_cert_list, Tag::Sequence)?;
// RFC 5280 §5.1.2.4:
// This field indicates the issue date of this CRL. thisUpdate may be
// encoded as UTCTime or GeneralizedTime.
// We do not presently enforce the correct choice of UTCTime or GeneralizedTime based on
// whether the date is post 2050.
der::time_choice(tbs_cert_list)?;
// While OPTIONAL in the ASN.1 module, RFC 5280 §5.1.2.5 says:
// Conforming CRL issuers MUST include the nextUpdate field in all CRLs.
// We do not presently enforce the correct choice of UTCTime or GeneralizedTime based on
// whether the date is post 2050.
der::time_choice(tbs_cert_list)?;
// RFC 5280 §5.1.2.6:
// When there are no revoked certificates, the revoked certificates list
// MUST be absent
// TODO(@cpu): Do we care to support empty CRLs if we don't support delta CRLs?
let revoked_certs = if tbs_cert_list.peek(Tag::Sequence.into()) {
der::expect_tag_and_get_value_limited(
tbs_cert_list,
Tag::Sequence,
der::MAX_DER_SIZE,
)?
} else {
untrusted::Input::from(&[])
};
let mut crl = BorrowedCertRevocationList {
signed_data,
issuer,
revoked_certs,
};
// RFC 5280 §5.1.2.7:
// This field may only appear if the version is 2 (Section 5.1.2.1). If
// present, this field is a sequence of one or more CRL extensions.
// RFC 5280 §5.2:
// Conforming CRL issuers are REQUIRED to include the authority key
// identifier (Section 5.2.1) and the CRL number (Section 5.2.3)
// extensions in all CRLs issued.
// As a result of the above we parse this as a required section, not OPTIONAL.
der::nested(
tbs_cert_list,
Tag::ContextSpecificConstructed0,
Error::MalformedExtensions,
|tagged| {
der::nested_of_mut(
tagged,
Tag::Sequence,
Tag::Sequence,
Error::BadDer,
|extension| {
// RFC 5280 §5.2:
// If a CRL contains a critical extension
// that the application cannot process, then the application MUST NOT
// use that CRL to determine the status of certificates. However,
// applications may ignore unrecognized non-critical extensions.
crl.remember_extension(&Extension::parse(extension)?)
},
)
},
)?;
Ok(crl)
})?;
Ok(crl)
}
/// Convert the CRL to an [`OwnedCertRevocationList`]. This may error if any of the revoked
/// certificates in the CRL are malformed or contain unsupported features.
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn to_owned(&self) -> Result<OwnedCertRevocationList, Error> {
// Parse and collect the CRL's revoked cert entries, ensuring there are no errors. With
// the full set in-hand, create a lookup map by serial number for fast revocation checking.
let revoked_certs = self
.into_iter()
.collect::<Result<Vec<_>, _>>()?
.iter()
.map(|revoked_cert| (revoked_cert.serial_number.to_vec(), revoked_cert.to_owned()))
.collect::<HashMap<_, _>>();
Ok(OwnedCertRevocationList {
signed_data: self.signed_data.to_owned(),
issuer: self.issuer.as_slice_less_safe().to_vec(),
revoked_certs,
})
}
fn remember_extension(&mut self, extension: &Extension<'a>) -> Result<(), Error> {
remember_extension(extension, |id| {
match id {
// id-ce-cRLNumber 2.5.29.20 - RFC 5280 §5.2.3
20 => {
// RFC 5280 §5.2.3:
// CRL verifiers MUST be able to handle CRLNumber values
// up to 20 octets. Conforming CRL issuers MUST NOT use CRLNumber
// values longer than 20 octets.
//
extension.value.read_all(Error::InvalidCrlNumber, |der| {
let crl_number = ring::io::der::positive_integer(der)
.map_err(|_| Error::InvalidCrlNumber)?
.big_endian_without_leading_zero();
if crl_number.len() <= 20 {
Ok(crl_number)
} else {
Err(Error::InvalidCrlNumber)
}
})?;
// We enforce the cRLNumber is sensible, but don't retain the value for use.
Ok(())
}
// id-ce-deltaCRLIndicator 2.5.29.27 - RFC 5280 §5.2.4
// We explicitly do not support delta CRLs.
27 => Err(Error::UnsupportedDeltaCrl),
// id-ce-issuingDistributionPoint 2.5.29.28 - RFC 5280 §5.2.4
// Although the extension is critical, conforming implementations are not
// required to support this extension. However, implementations that do not
// support this extension MUST either treat the status of any certificate not listed
// on this CRL as unknown or locate another CRL that does not contain any
// unrecognized critical extensions.
// TODO(@cpu): We may want to parse this enough to be able to error on indirectCRL
// bool == true, or to enforce validation based on onlyContainsUserCerts,
// onlyContainsCACerts, and onlySomeReasons. For now we use the carve-out where
// we'll treat it as understood without parsing and consider certificates not found
// in the list as unknown.
28 => Ok(()),
// id-ce-authorityKeyIdentifier 2.5.29.35 - RFC 5280 §5.2.1, §4.2.1.1
// We recognize the extension but don't retain its value for use.
35 => Ok(()),
// Unsupported extension
_ => extension.unsupported(),
}
})
}
}
impl Sealed for BorrowedCertRevocationList<'_> {}
impl CertRevocationList for BorrowedCertRevocationList<'_> {
fn issuer(&self) -> &[u8] {
self.issuer.as_slice_less_safe()
}
fn find_serial(&self, serial: &[u8]) -> Result<Option<BorrowedRevokedCert>, Error> {
for revoked_cert_result in self {
match revoked_cert_result {
Err(e) => return Err(e),
Ok(revoked_cert) => {
if revoked_cert.serial_number.eq(serial) {
return Ok(Some(revoked_cert));
}
}
}
}
Ok(None)
}
fn verify_signature(
&self,
supported_sig_algs: &[&SignatureAlgorithm],
issuer_spki: &[u8],
) -> Result<(), Error> {
signed_data::verify_signed_data(
supported_sig_algs,
untrusted::Input::from(issuer_spki),
&self.signed_data,
&mut Budget::default(),
)
}
}
impl<'a> IntoIterator for &'a BorrowedCertRevocationList<'a> {
type Item = Result<BorrowedRevokedCert<'a>, Error>;
type IntoIter = RevokedCerts<'a>;
fn into_iter(self) -> Self::IntoIter {
RevokedCerts {
reader: untrusted::Reader::new(self.revoked_certs),
}
}
}
#[derive(Debug)]
pub struct RevokedCerts<'a> {
reader: untrusted::Reader<'a>,
}
impl<'a> Iterator for RevokedCerts<'a> {
type Item = Result<BorrowedRevokedCert<'a>, Error>;
fn next(&mut self) -> Option<Self::Item> {
(!self.reader.at_end()).then(|| BorrowedRevokedCert::from_der(&mut self.reader))
}
}
/// Owned representation of a RFC 5280[^1] profile Certificate Revocation List (CRL) revoked
/// certificate entry.
///
/// Only available when the "alloc" feature is enabled.
///
/// [^1]: <https://www.rfc-editor.org/rfc/rfc5280#section-5>
#[cfg(feature = "alloc")]
#[derive(Clone, Debug)]
pub struct OwnedRevokedCert {
/// Serial number of the revoked certificate.
pub serial_number: Vec<u8>,
/// The date at which the CA processed the revocation.
pub revocation_date: Time,
/// Identifies the reason for the certificate revocation. When absent, the revocation reason
/// is assumed to be RevocationReason::Unspecified. For consistency with other extensions
/// and to ensure only one revocation reason extension may be present we maintain this field
/// as optional instead of defaulting to unspecified.
pub reason_code: Option<RevocationReason>,
/// Provides the date on which it is known or suspected that the private key was compromised or
/// that the certificate otherwise became invalid. This date may be earlier than the revocation
/// date which is the date at which the CA processed the revocation.
pub invalidity_date: Option<Time>,
}
#[cfg(feature = "alloc")]
impl OwnedRevokedCert {
/// Convert the owned representation of this revoked cert to a borrowed version.
pub fn borrow(&self) -> BorrowedRevokedCert {
BorrowedRevokedCert {
serial_number: &self.serial_number,
revocation_date: self.revocation_date,
reason_code: self.reason_code,
invalidity_date: self.invalidity_date,
}
}
}
/// Borrowed representation of a RFC 5280[^1] profile Certificate Revocation List (CRL) revoked
/// certificate entry.
///
/// [^1]: <https://www.rfc-editor.org/rfc/rfc5280#section-5>
#[derive(Debug)]
pub struct BorrowedRevokedCert<'a> {
/// Serial number of the revoked certificate.
pub serial_number: &'a [u8],
/// The date at which the CA processed the revocation.
pub revocation_date: Time,
/// Identifies the reason for the certificate revocation. When absent, the revocation reason
/// is assumed to be RevocationReason::Unspecified. For consistency with other extensions
/// and to ensure only one revocation reason extension may be present we maintain this field
/// as optional instead of defaulting to unspecified.
pub reason_code: Option<RevocationReason>,
/// Provides the date on which it is known or suspected that the private key was compromised or
/// that the certificate otherwise became invalid. This date may be earlier than the revocation
/// date which is the date at which the CA processed the revocation.
pub invalidity_date: Option<Time>,
}
impl<'a> BorrowedRevokedCert<'a> {
/// Construct an owned representation of the revoked certificate.
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn to_owned(&self) -> OwnedRevokedCert {
OwnedRevokedCert {
serial_number: self.serial_number.to_vec(),
revocation_date: self.revocation_date,
reason_code: self.reason_code,
invalidity_date: self.invalidity_date,
}
}
fn from_der(der: &mut untrusted::Reader<'a>) -> Result<Self, Error> {
der::nested(der, Tag::Sequence, Error::BadDer, |der| {
// RFC 5280 §4.1.2.2:
// Certificate users MUST be able to handle serialNumber values up to 20 octets.
// Conforming CAs MUST NOT use serialNumber values longer than 20 octets.
//
// Note: Non-conforming CAs may issue certificates with serial numbers
// that are negative or zero. Certificate users SHOULD be prepared to
// gracefully handle such certificates.
// Like the handling in cert.rs we choose to be lenient here, not enforcing the length
// of a CRL revoked certificate's serial number is less than 20 octets in encoded form.
let serial_number = lenient_certificate_serial_number(der)
.map_err(|_| Error::InvalidSerialNumber)?
.as_slice_less_safe();
let revocation_date = der::time_choice(der)?;
let mut revoked_cert = BorrowedRevokedCert {
serial_number,
revocation_date,
reason_code: None,
invalidity_date: None,
};
// RFC 5280 §5.3:
// Support for the CRL entry extensions defined in this specification is
// optional for conforming CRL issuers and applications. However, CRL
// issuers SHOULD include reason codes (Section 5.3.1) and invalidity
// dates (Section 5.3.2) whenever this information is available.
if der.at_end() {
return Ok(revoked_cert);
}
// It would be convenient to use der::nested_of_mut here to unpack a SEQUENCE of one or
// more SEQUENCEs, however CAs have been mis-encoding the absence of extensions as an
// empty SEQUENCE so we must be tolerant of that.
let ext_seq = der::expect_tag_and_get_value(der, Tag::Sequence)?;
if ext_seq.is_empty() {
return Ok(revoked_cert);
}
let mut reader = untrusted::Reader::new(ext_seq);
loop {
der::nested(&mut reader, Tag::Sequence, Error::BadDer, |ext_der| {
// RFC 5280 §5.3:
// If a CRL contains a critical CRL entry extension that the application cannot
// process, then the application MUST NOT use that CRL to determine the
// status of any certificates. However, applications may ignore
// unrecognized non-critical CRL entry extensions.
revoked_cert.remember_extension(&Extension::parse(ext_der)?)
})?;
if reader.at_end() {
break;
}
}
Ok(revoked_cert)
})
}
fn remember_extension(&mut self, extension: &Extension<'a>) -> Result<(), Error> {
remember_extension(extension, |id| {
match id {
// id-ce-cRLReasons 2.5.29.21 - RFC 5280 §5.3.1.
21 => set_extension_once(&mut self.reason_code, || {
RevocationReason::from_der(extension.value)
}),
// id-ce-invalidityDate 2.5.29.24 - RFC 5280 §5.3.2.
24 => set_extension_once(&mut self.invalidity_date, || {
extension.value.read_all(Error::BadDer, der::time_choice)
}),
// id-ce-certificateIssuer 2.5.29.29 - RFC 5280 §5.3.3.
// This CRL entry extension identifies the certificate issuer associated
// with an entry in an indirect CRL, that is, a CRL that has the
// indirectCRL indicator set in its issuing distribution point
// extension.
// We choose not to support indirect CRLs and so turn this into a more specific
// error rather than simply letting it fail as an unsupported critical extension.
29 => Err(Error::UnsupportedIndirectCrl),
// Unsupported extension
_ => extension.unsupported(),
}
})
}
}
/// Identifies the reason a certificate was revoked.
/// See RFC 5280 §5.3.1[^1]
///
/// [^1] <https://www.rfc-editor.org/rfc/rfc5280#section-5.3.1>
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[allow(missing_docs)] // Not much to add above the code name.
pub enum RevocationReason {
/// Unspecified should not be used, and is instead assumed by the absence of a RevocationReason
/// extension.
Unspecified = 0,
KeyCompromise = 1,
CaCompromise = 2,
AffiliationChanged = 3,
Superseded = 4,
CessationOfOperation = 5,
CertificateHold = 6,
// 7 is not used.
/// RemoveFromCrl only appears in delta CRLs that are unsupported.
RemoveFromCrl = 8,
PrivilegeWithdrawn = 9,
AaCompromise = 10,
}
impl RevocationReason {
// RFC 5280 §5.3.1.
fn from_der(value: untrusted::Input<'_>) -> Result<Self, Error> {
value.read_all(Error::BadDer, |enumerated_reason| {
let value = der::expect_tag(enumerated_reason, Tag::Enum)?;
Self::try_from(value.value().read_all(Error::BadDer, |reason| {
reason.read_byte().map_err(|_| Error::BadDer)
})?)
})
}
}
impl TryFrom<u8> for RevocationReason {
type Error = Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
// See https://www.rfc-editor.org/rfc/rfc5280#section-5.3.1
match value {
0 => Ok(RevocationReason::Unspecified),
1 => Ok(RevocationReason::KeyCompromise),
2 => Ok(RevocationReason::CaCompromise),
3 => Ok(RevocationReason::AffiliationChanged),
4 => Ok(RevocationReason::Superseded),
5 => Ok(RevocationReason::CessationOfOperation),
6 => Ok(RevocationReason::CertificateHold),
// 7 is not used.
8 => Ok(RevocationReason::RemoveFromCrl),
9 => Ok(RevocationReason::PrivilegeWithdrawn),
10 => Ok(RevocationReason::AaCompromise),
_ => Err(Error::UnsupportedRevocationReason),
}
}
}
mod private {
pub trait Sealed {}
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use crate::{Error, RevocationReason};
#[test]
fn revocation_reasons() {
// Test that we can convert the allowed u8 revocation reason code values into the expected
// revocation reason variant.
let testcases: Vec<(u8, RevocationReason)> = vec![
(0, RevocationReason::Unspecified),
(1, RevocationReason::KeyCompromise),
(2, RevocationReason::CaCompromise),
(3, RevocationReason::AffiliationChanged),
(4, RevocationReason::Superseded),
(5, RevocationReason::CessationOfOperation),
(6, RevocationReason::CertificateHold),
// Note: 7 is unused.
(8, RevocationReason::RemoveFromCrl),
(9, RevocationReason::PrivilegeWithdrawn),
(10, RevocationReason::AaCompromise),
];
for tc in testcases.iter() {
let (id, expected) = tc;
let actual = <u8 as TryInto<RevocationReason>>::try_into(*id)
.expect("unexpected reason code conversion error");
assert_eq!(actual, *expected);
#[cfg(feature = "alloc")]
{
// revocation reasons should be Debug.
println!("{:?}", actual);
}
}
// Unsupported/unknown revocation reason codes should produce an error.
let res = <u8 as TryInto<RevocationReason>>::try_into(7);
assert!(matches!(res, Err(Error::UnsupportedRevocationReason)));
}
#[test]
#[cfg(feature = "alloc")]
// redundant clone, clone_on_copy allowed to verify derived traits.
#[allow(clippy::redundant_clone, clippy::clone_on_copy)]
fn test_derived_traits() {
let crl = crate::crl::BorrowedCertRevocationList::from_der(include_bytes!(
"../tests/crls/crl.valid.der"
))
.unwrap();
println!("{:?}", crl); // BorrowedCertRevocationList should be debug.
let owned_crl = crl.to_owned().unwrap();
println!("{:?}", owned_crl); // OwnedCertRevocationList should be debug.
let _ = owned_crl.clone(); // OwnedCertRevocationList should be clone.
let mut revoked_certs = crl.into_iter();
println!("{:?}", revoked_certs); // RevokedCert should be debug.
let revoked_cert = revoked_certs.next().unwrap().unwrap();
println!("{:?}", revoked_cert); // BorrowedRevokedCert should be debug.
let owned_revoked_cert = revoked_cert.to_owned();
println!("{:?}", owned_revoked_cert); // OwnedRevokedCert should be debug.
let _ = owned_revoked_cert.clone(); // OwnedRevokedCert should be clone.
}
}