From 9d66031ab1845fd6d1f508a1410f55b400b8f5e4 Mon Sep 17 00:00:00 2001 From: Ravi Khadiwala Date: Thu, 12 Oct 2023 16:08:52 -0700 Subject: [PATCH] boring: Support validating certificates against CRLs Adds CRL types and validation of certificates against CRLs with a `X509Store/X509StoreContext`. Also enables customizing verification flags on the `X509Store`, required to enable CRL checking. --- boring/src/stack.rs | 10 + boring/src/x509/crl.rs | 506 ++++++++++++++++++++++++++++++++++++ boring/src/x509/mod.rs | 28 ++ boring/src/x509/store.rs | 20 ++ boring/src/x509/tests.rs | 306 +++++++++++++++++++++- boring/src/x509/verify.rs | 84 +++++- boring/test/bad_sig.pem | 11 + boring/test/crl.pem | 13 + boring/test/empty_crl.pem | 11 + boring/test/invalid_crl.pem | 11 + 10 files changed, 998 insertions(+), 2 deletions(-) create mode 100644 boring/src/x509/crl.rs create mode 100644 boring/test/bad_sig.pem create mode 100644 boring/test/crl.pem create mode 100644 boring/test/empty_crl.pem create mode 100644 boring/test/invalid_crl.pem diff --git a/boring/src/stack.rs b/boring/src/stack.rs index 67b952739..fc07d12f4 100644 --- a/boring/src/stack.rs +++ b/boring/src/stack.rs @@ -253,6 +253,16 @@ impl StackRef { } } +impl fmt::Debug for StackRef +where + T: Stackable, + T::Ref: fmt::Debug, +{ + fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { + fmt.debug_list().entries(self).finish() + } +} + impl Index for StackRef { type Output = T::Ref; diff --git a/boring/src/x509/crl.rs b/boring/src/x509/crl.rs new file mode 100644 index 000000000..181343cc8 --- /dev/null +++ b/boring/src/x509/crl.rs @@ -0,0 +1,506 @@ +//! Certificate revocation lists describe certificates that have been revoked +//! by their issuer and should no longer be trusted. +//! +//! An `X509CRL` can be provided along with an issuing `X509` to verify that +//! issued certificates have not been revoked. +//! +//! # Example +//! +//! ```rust +//! use boring::hash::MessageDigest; +//! use boring::pkey::{PKey, Private}; +//! use boring::x509::crl::{X509CRLBuilder, X509Revoked}; +//! use boring::x509::extension::BasicConstraints; +//! use boring::x509::verify::X509VerifyFlags; +//! use boring::x509::X509Extension; +//! use boring::x509::X509; +//! use boring::x509::store::{X509Store, X509StoreBuilder}; +//! use boring::asn1::Asn1Time; +//! use boring::bn::BigNum; +//! use boring::error::ErrorStack; +//! +//! fn crl_checking_store(issuer: X509, pkey: PKey) -> Result { +//! let mut builder = X509CRLBuilder::new()?; +//! builder.set_issuer_name(issuer.subject_name())?; +//! builder.add_revoked(X509Revoked::from_parts( +//! &*BigNum::from_u32(1)?.to_asn1_integer()?, +//! &*Asn1Time::days_from_now(0)? +//! )?)?; +//! builder.set_last_update(&*Asn1Time::days_from_now(0)?)?; +//! builder.set_next_update(&*Asn1Time::days_from_now(30)?)?; +//! builder.sign(&pkey, MessageDigest::sha256())?; +//! +//! let mut store_builder = X509StoreBuilder::new()?; +//! store_builder.add_cert(issuer)?; +//! store_builder.add_crl(builder.build())?; +//! store_builder +//! .param_mut() +//! .set_flags(X509VerifyFlags::CRL_CHECK | X509VerifyFlags::CRL_CHECK_ALL)?; +//! Ok(store_builder.build()) +//! } +//! ``` + +use crate::asn1::{Asn1BitStringRef, Asn1IntegerRef, Asn1TimeRef}; +use crate::foreign_types::ForeignType; +use crate::foreign_types::ForeignTypeRef; +use crate::hash::{DigestBytes, MessageDigest}; +use crate::pkey::{HasPrivate, HasPublic, PKeyRef}; +use crate::stack::{StackRef, Stackable}; +use crate::x509::X509ExtensionRef; +use crate::x509::{X509AlgorithmRef, X509Extension, X509NameRef}; +use crate::{cvt, cvt_n, cvt_p, ErrorStack}; +use std::convert::TryInto; +use std::fmt::Formatter; +use std::{fmt, mem, ptr}; + +foreign_type_and_impl_send_sync! { + type CType = ffi::X509_REVOKED; + fn drop = ffi::X509_REVOKED_free; + + /// An `X509_REVOKED` containing information about a revoked certificate + pub struct X509Revoked; +} + +impl Stackable for X509Revoked { + type StackType = ffi::stack_st_X509_REVOKED; +} + +impl X509Revoked { + /// Create an `X509Revoked` + /// + /// This corresponds to [`X509_REVOKED_new`] followed by calls to + /// [`X509_REVOKED_set_serialNumber`] and [`X509_REVOKED_set_revocationDate`] + /// with the provided parameters + /// + /// [`X509_REVOKED_new`]: https://www.openssl.org/docs/man1.1.1/man3/X509_REVOKED_new.html + /// [`X509_REVOKED_set_serialNumber`]: https://www.openssl.org/docs/man1.1.1/man3/X509_REVOKED_set_serialNumber.html + /// [`X509_REVOKED_set_revocationDate`]: https://www.openssl.org/docs/man1.1.1/man3/X509_REVOKED_set_revocationDate.html + pub fn from_parts( + serial_number: &Asn1IntegerRef, + revocation_date: &Asn1TimeRef, + ) -> Result { + unsafe { + ffi::init(); + let revoked = cvt_p(ffi::X509_REVOKED_new())?; + cvt(ffi::X509_REVOKED_set_serialNumber( + revoked, + serial_number.as_ptr(), + ))?; + cvt(ffi::X509_REVOKED_set_revocationDate( + revoked, + revocation_date.as_ptr(), + ))?; + Ok(X509Revoked::from_ptr(revoked)) + } + } +} + +impl X509RevokedRef { + /// Returns the serial number of the revoked certificate + /// + /// This corresponds to [`X509_REVOKED_get0_serialNumber`]. + /// + /// [`X509_REVOKED_get0_serialNumber`]: https://www.openssl.org/docs/man1.1.1/man3/X509_REVOKED_get0_serialNumber.html + pub fn serial_number(&self) -> &Asn1IntegerRef { + unsafe { + let r = ffi::X509_REVOKED_get0_serialNumber(self.as_ptr()); + assert!(!r.is_null()); + Asn1IntegerRef::from_ptr(r as *mut _) + } + } + + /// Returns certificate's revocation date + /// + /// This corresponds to [`X509_REVOKED_get0_revocationDate`]. + /// + /// [`X509_REVOKED_get0_revocationDate`]: https://www.openssl.org/docs/man1.1.1/man3/X509_REVOKED_get0_revocationDate + pub fn revocation_date(&self) -> &Asn1TimeRef { + unsafe { + let date = ffi::X509_REVOKED_get0_revocationDate(self.as_ptr()); + assert!(!date.is_null()); + Asn1TimeRef::from_ptr(date as *mut _) + } + } +} + +impl fmt::Debug for X509RevokedRef { + fn fmt(&self, fmt: &mut Formatter) -> fmt::Result { + let sn = self.serial_number().to_bn().and_then(|bn| bn.to_hex_str()); + let sn = sn.as_ref().map(|x| &***x).unwrap_or(""); + + fmt.debug_struct("X509Revoked") + .field("serial_number", &sn) + .field("revocation_date", self.revocation_date()) + .finish() + } +} + +foreign_type_and_impl_send_sync! { + type CType = ffi::X509_CRL; + fn drop = ffi::X509_CRL_free; + + /// An `X509CRL` certificate revocation list + pub struct X509CRL; +} + +impl Stackable for X509CRL { + type StackType = ffi::stack_st_X509_CRL; +} + +impl X509CRL { + from_pem! { + /// Deserializes a PEM-encoded X509CRL structure. + /// + /// The input should have a header of `-----BEGIN X509 CRL-----` + /// + /// This corresponds to [`PEM_read_bio_X509_CRL`]. + /// + /// [`PEM_read_bio_X509_CRL`]: https://www.openssl.org/docs/man1.1.1/man3/PEM_read_bio_X509_CRL + from_pem, + X509CRL, + ffi::PEM_read_bio_X509_CRL + } + + from_der! { + /// Deserializes a DER-encoded X509 structure. + /// + /// This corresponds to [`d2i_X509_CRL`]. + /// + /// [`d2i_X509_CRL`]: https://www.openssl.org/docs/man1.1.1/man3/d2i_X509_CRL.html + from_der, + X509CRL, + ffi::d2i_X509_CRL, + ::libc::c_long + } +} + +impl X509CRLRef { + /// Returns the CRL's last update time + /// + /// This corresponds to [`X509_CRL_get0_lastUpdate`] + /// + /// [`X509_CRL_get0_lastUpdate`]: https://www.openssl.org/docs/man1.1.1/man3/X509_CRL_get0_lastUpdate + pub fn last_update(&self) -> Option<&Asn1TimeRef> { + unsafe { + let date = ffi::X509_CRL_get0_lastUpdate(self.as_ptr()); + if date.is_null() { + None + } else { + Some(Asn1TimeRef::from_ptr(date as *mut _)) + } + } + } + + /// Returns the CRL's next update time + /// + /// This corresponds to [`X509_CRL_get0_nextUpdate`] + /// + /// [`X509_CRL_get0_nextUpdate`]: https://www.openssl.org/docs/man1.1.1/man3/X509_CRL_get0_nextUpdate + pub fn next_update(&self) -> Option<&Asn1TimeRef> { + unsafe { + let date = ffi::X509_CRL_get0_nextUpdate(self.as_ptr()); + if date.is_null() { + None + } else { + Some(Asn1TimeRef::from_ptr(date as *mut _)) + } + } + } + + /// Returns the CRL's issuer name + /// + /// This corresponds to [`X509_CRL_get_issuer`] + /// + /// [`X509_CRL_get_issuer`]: https://www.openssl.org/docs/man1.1.1/man3/X509_CRL_get_issuer + pub fn issuer(&self) -> &X509NameRef { + unsafe { + let name = ffi::X509_CRL_get_issuer(self.as_ptr()); + + assert!(!name.is_null()); + X509NameRef::from_ptr(name) + } + } + + /// Returns the CRL's extensions + /// + /// This corresponds to [`X509_CRL_get0_extensions`] + /// + /// [`X509_CRL_get0_extensions`]: https://www.openssl.org/docs/man1.1.1/man3/X509_CRL_get0_extensions + pub fn extensions(&self) -> Option<&StackRef> { + unsafe { + let extensions = ffi::X509_CRL_get0_extensions(self.as_ptr()); + if extensions.is_null() { + None + } else { + Some(StackRef::from_ptr(extensions as *mut _)) + } + } + } + + /// Returns the revoked certificates in this CRL + /// + /// This corresponds to [`X509_CRL_get_REVOKED`] + /// + /// [`X509_CRL_get_REVOKED`]: https://www.openssl.org/docs/man1.1.1/man3/X509_CRL_get_REVOKED + pub fn revoked(&self) -> Option<&StackRef> { + unsafe { + let revoked = ffi::X509_CRL_get_REVOKED(self.as_ptr()); + if revoked.is_null() { + None + } else { + Some(StackRef::from_ptr(revoked)) + } + } + } + + /// Returns the CRL's signature and signature algorithm + /// + /// This corresponds to [`X509_CRL_get0_signature`] + /// + /// [`X509_CRL_get0_signature`]: https://www.openssl.org/docs/man1.1.1/man3/X509_CRL_get0_signature + pub fn signature(&self) -> (&Asn1BitStringRef, &X509AlgorithmRef) { + unsafe { + let mut signature = ptr::null(); + let mut algor = ptr::null(); + ffi::X509_CRL_get0_signature(self.as_ptr(), &mut signature, &mut algor); + assert!(!algor.is_null()); + assert!(!signature.is_null()); + ( + Asn1BitStringRef::from_ptr(signature as *mut _), + X509AlgorithmRef::from_ptr(algor as *mut _), + ) + } + } + + /// Returns a digest of the DER representation of the CRL + /// + /// This corresponds to [`X509_CRL_digest`] + /// + /// [`X509_CRL_digest`]: https://www.openssl.org/docs/man1.1.1/man3/X509_CRL_digest + pub fn digest(&self, hash_type: MessageDigest) -> Result { + unsafe { + let mut digest = DigestBytes { + buf: [0; ffi::EVP_MAX_MD_SIZE as usize], + len: ffi::EVP_MAX_MD_SIZE as usize, + }; + let mut len = ffi::EVP_MAX_MD_SIZE.try_into().unwrap(); + cvt(ffi::X509_CRL_digest( + self.as_ptr(), + hash_type.as_ptr(), + digest.buf.as_mut_ptr() as *mut _, + &mut len, + ))?; + digest.len = len as usize; + + Ok(digest) + } + } + + /// Check if the CRL is signed using the given public key. + /// + /// Only the signature is checked: no other checks (such as certificate chain validity) + /// are performed. + /// + /// Returns `true` if verification succeeds. + /// + /// This corresponds to [`X509_CRL_verify"]. + /// + /// [`X509_CRL_verify`]: https://www.openssl.org/docs/man1.1.1/man3/X509_CRL_verify + pub fn verify(&self, key: &PKeyRef) -> Result + where + T: HasPublic, + { + unsafe { cvt_n(ffi::X509_CRL_verify(self.as_ptr(), key.as_ptr())).map(|n| n != 0) } + } + + to_pem! { + /// Serializes the CRL into a PEM-encoded X509 CRL structure. + /// + /// The output will have a header of `-----BEGIN X509 CRL-----` + /// + /// This corresponds to [`PEM_write_bio_X509_CRL`] + /// + /// [`PEM_write_bio_X509_CRL`]: https://www.openssl.org/docs/man1.1.1/man3/PEM_write_bio_X509_CRL + to_pem, + ffi::PEM_write_bio_X509_CRL + } + + to_der! { + /// Serializes the CRL into a DER-encoded X509 CRL structure + /// + /// This corresponds to `i2d_X509_CRL` + /// + /// [`i2d_X509_CRL`]: https://www.openssl.org/docs/man1.1.1/man3/i2d_X509_CRL + to_der, + ffi::i2d_X509_CRL + } +} +impl ToOwned for X509CRLRef { + type Owned = X509CRL; + + fn to_owned(&self) -> X509CRL { + unsafe { + ffi::X509_CRL_up_ref(self.as_ptr()); + X509CRL::from_ptr(self.as_ptr()) + } + } +} + +impl Clone for X509CRL { + fn clone(&self) -> X509CRL { + X509CRLRef::to_owned(self) + } +} + +impl fmt::Debug for X509CRLRef { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + let mut debug_struct = formatter.debug_struct("X509CRL"); + debug_struct.field("issuer", self.issuer()); + debug_struct.field("signature_algorithm", &self.signature().1.object()); + + if let Some(next_update) = self.next_update() { + debug_struct.field("next_update", next_update); + } + if let Some(last_update) = self.last_update() { + debug_struct.field("last_update", last_update); + } + if let Some(revoked) = self.revoked() { + debug_struct.field("revoked", &revoked); + } + debug_struct.finish() + } +} + +impl fmt::Debug for X509CRL { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + let x: &X509CRLRef = self; + x.fmt(formatter) + } +} + +/// A builder used to construct an `X509CRL` +pub struct X509CRLBuilder(X509CRL); + +impl X509CRLBuilder { + /// Creates a new builder. + pub fn new() -> Result { + unsafe { + ffi::init(); + cvt_p(ffi::X509_CRL_new()).map(|p| X509CRLBuilder(X509CRL::from_ptr(p))) + } + } + + /// Append an `X509Extension` to the certificate revocation list + /// + /// This corresponds to [`X509_CRL_add_ext`] + /// + /// [`X509_CRL_add_ext`]: https://www.openssl.org/docs/man1.1.1/man3/X509_CRL_add_ext + pub fn append_extension(&mut self, extension: &X509ExtensionRef) -> Result<(), ErrorStack> { + unsafe { + // -1 indicates append to end + cvt(ffi::X509_CRL_add_ext( + self.0.as_ptr(), + extension.as_ptr(), + -1, + ))?; + Ok(()) + } + } + + /// Signs the certificate revocation list with a private key. + /// + /// This corresponds to [`X509_CRL_sign`] + /// + /// [`X509_CRL_sign`]: https://www.openssl.org/docs/man1.1.1/man3/X509_CRL_sign + pub fn sign(&mut self, key: &PKeyRef, hash: MessageDigest) -> Result<(), ErrorStack> + where + T: HasPrivate, + { + unsafe { + cvt(ffi::X509_CRL_sign( + self.0.as_ptr(), + key.as_ptr(), + hash.as_ptr(), + )) + .map(|_| ()) + } + } + + /// Add a revoked certificate to the certificate revocation list + /// + /// This corresponds to [`X509_CRL_add0_revoked`] + /// + /// [`X509_CRL_add0_revoked`]: https://www.openssl.org/docs/man1.1.1/man3/X509_CRL_add0_revoked + pub fn add_revoked(&mut self, revoked: X509Revoked) -> Result<(), ErrorStack> { + unsafe { + cvt(ffi::X509_CRL_add0_revoked( + self.0.as_ptr(), + revoked.as_ptr(), + ))?; + mem::forget(revoked); + Ok(()) + } + } + + /// Sets the issuer name of the certificate revocation list. + /// + /// This corresponds to [`X509_CRL_set_issuer_name`] + /// + /// [`X509_CRL_set_issuer_name`]: https://www.openssl.org/docs/man1.1.1/man3/X509_CRL_set_issuer_name + pub fn set_issuer_name(&mut self, issuer_name: &X509NameRef) -> Result<(), ErrorStack> { + unsafe { + cvt(ffi::X509_CRL_set_issuer_name( + self.0.as_ptr(), + issuer_name.as_ptr(), + )) + .map(|_| ()) + } + } + + /// Sets the version of the certificate revocation list. + /// + /// Note that the version is zero-indexed; that is, a certificate corresponding to version 3 of + /// the X.509 standard should pass `2` to this method. + /// + /// This corresponds to [`X509_CRL_set_version`] + /// + /// [`X509_CRL_set_version`]: https://www.openssl.org/docs/man1.1.1/man3/X509_CRL_set_version + pub fn set_version(&mut self, version: i32) -> Result<(), ErrorStack> { + unsafe { cvt(ffi::X509_CRL_set_version(self.0.as_ptr(), version.into())).map(|_| ()) } + } + + /// Sets the last update time on the certificate revocation list. + /// + /// This corresponds to [`X509_CRL_set1_lastUpdate`] + /// + /// [`X509_CRL_set1_lastUpdate`]: https://www.openssl.org/docs/man1.1.1/man3/X509_CRL_set1_lastUpdate + pub fn set_last_update(&mut self, last_update: &Asn1TimeRef) -> Result<(), ErrorStack> { + unsafe { + cvt(ffi::X509_CRL_set1_lastUpdate( + self.0.as_ptr(), + last_update.as_ptr(), + )) + .map(|_| ()) + } + } + + /// Sets the next update time on the certificate revocation list. + /// + /// This corresponds to [`X509_CRL_set1_nextUpdate`] + /// + /// [`X509_CRL_set1_nextUpdate`]: https://www.openssl.org/docs/man1.1.1/man3/X509_CRL_set1_nextUpdate + pub fn set_next_update(&mut self, next_update: &Asn1TimeRef) -> Result<(), ErrorStack> { + unsafe { + cvt(ffi::X509_CRL_set1_nextUpdate( + self.0.as_ptr(), + next_update.as_ptr(), + )) + .map(|_| ()) + } + } + + /// Consumes the builder, returning the certificate revocation list + pub fn build(self) -> X509CRL { + self.0 + } +} diff --git a/boring/src/x509/mod.rs b/boring/src/x509/mod.rs index 30b5e725b..3a383e282 100644 --- a/boring/src/x509/mod.rs +++ b/boring/src/x509/mod.rs @@ -36,8 +36,10 @@ use crate::pkey::{HasPrivate, HasPublic, PKey, PKeyRef, Public}; use crate::ssl::SslRef; use crate::stack::{Stack, StackRef, Stackable}; use crate::string::OpensslString; +use crate::x509::crl::X509CRL; use crate::{cvt, cvt_n, cvt_p}; +pub mod crl; pub mod extension; pub mod store; pub mod verify; @@ -161,6 +163,32 @@ impl X509StoreContextRef { unsafe { cvt_n(ffi::X509_verify_cert(self.as_ptr())).map(|n| n != 0) } } + /// Verifies the stored certificate with additional untrusted CRLs + /// + /// Returns `true` if verification succeeds. The `error` method will return the specific + /// validation error if the certificate was not valid. + /// + /// This will only work inside of a call to `init`. + /// + /// This corresponds to [`X509_STORE_CTX_set0_crls`] followed by [`X509_verify_cert`]. + /// + /// [`X509_STORE_CTX_set0_crls`]: https://www.openssl.org/docs/man1.0.2/crypto/X509_STORE_CTX_set0_crls.html + /// [`X509_verify_cert`]: https://www.openssl.org/docs/man1.0.2/crypto/X509_verify_cert.html + pub fn verify_cert_with_crls( + &mut self, + untrusted_crls: Stack, + ) -> Result { + unsafe { + ffi::X509_STORE_CTX_set0_crls(self.as_ptr(), untrusted_crls.as_ptr()); + let res = cvt_n(ffi::X509_verify_cert(self.as_ptr())).map(|n| n != 0); + // set0_crls does not take ownership of the stack, so we'll drop and free + // untrusted_crls after this method. null out the crls in ctx to make sure + // no one has a reference to it. + ffi::X509_STORE_CTX_set0_crls(self.as_ptr(), ptr::null_mut()); + res + } + } + /// Set the verify result of the context. /// /// This corresponds to [`X509_STORE_CTX_set_error`]. diff --git a/boring/src/x509/store.rs b/boring/src/x509/store.rs index 7033450a3..abdf79486 100644 --- a/boring/src/x509/store.rs +++ b/boring/src/x509/store.rs @@ -46,6 +46,8 @@ use std::mem; use crate::error::ErrorStack; use crate::stack::StackRef; +use crate::x509::crl::X509CRL; +use crate::x509::verify::X509VerifyParamRef; use crate::x509::{X509Object, X509}; use crate::{cvt, cvt_p}; @@ -84,6 +86,24 @@ impl X509StoreBuilderRef { unsafe { cvt(ffi::X509_STORE_add_cert(self.as_ptr(), cert.as_ptr())).map(|_| ()) } } + /// Adds a CRL to the certificate store. + /// + /// This corresponds to [`X509_STORE_add_crl`]. + /// + /// [`X509_STORE_add_crl`]: https://www.openssl.org/docs/man1.1.1/man3/X509_STORE_add_crl + pub fn add_crl(&mut self, crl: X509CRL) -> Result<(), ErrorStack> { + unsafe { cvt(ffi::X509_STORE_add_crl(self.as_ptr(), crl.as_ptr())).map(|_| ()) } + } + + /// Returns a mutable reference to the X509 verification configuration. + /// + /// This corresponds to [`X509_STORE_get0_param`]. + /// + /// [`X509_STORE_get0_param`]: https://www.openssl.org/docs/man1.1.1/man3/X509_STORE_get0_param + pub fn param_mut(&mut self) -> &mut X509VerifyParamRef { + unsafe { X509VerifyParamRef::from_ptr_mut(ffi::X509_STORE_get0_param(self.as_ptr())) } + } + /// Load certificates from their default locations. /// /// These locations are read from the `SSL_CERT_FILE` and `SSL_CERT_DIR` diff --git a/boring/src/x509/tests.rs b/boring/src/x509/tests.rs index e656873d3..69f82e279 100644 --- a/boring/src/x509/tests.rs +++ b/boring/src/x509/tests.rs @@ -6,12 +6,14 @@ use crate::hash::MessageDigest; use crate::nid::Nid; use crate::pkey::{PKey, Private}; use crate::rsa::Rsa; -use crate::stack::Stack; +use crate::stack::{Stack, Stackable}; +use crate::x509::crl::{X509CRLBuilder, X509Revoked, X509CRL}; use crate::x509::extension::{ AuthorityKeyIdentifier, BasicConstraints, ExtendedKeyUsage, KeyUsage, SubjectAlternativeName, SubjectKeyIdentifier, }; use crate::x509::store::X509StoreBuilder; +use crate::x509::verify::X509VerifyFlags; use crate::x509::{X509Extension, X509Name, X509Req, X509StoreContext, X509}; fn pkey() -> PKey { @@ -19,6 +21,15 @@ fn pkey() -> PKey { PKey::from_rsa(rsa).unwrap() } +fn stack_of(item: T) -> Stack +where + T: Stackable, +{ + let mut stack = Stack::new().expect("unable to initialize stack"); + stack.push(item).expect("failed to add to stack"); + stack +} + #[test] fn test_cert_loading() { let cert = include_bytes!("../../test/cert.pem"); @@ -250,6 +261,48 @@ fn x509_builder() { assert_eq!(serial, x509.serial_number().to_bn().unwrap()); } +#[test] +fn x509_crl_builder() { + let mut builder = X509CRLBuilder::new().unwrap(); + + let mut name = X509Name::builder().unwrap(); + name.append_entry_by_nid(Nid::COMMONNAME, "foobar.com") + .unwrap(); + let name = name.build(); + builder.set_issuer_name(&name).unwrap(); + + let mut serial = BigNum::new().unwrap(); + serial.rand(128, MsbOption::MAYBE_ZERO, false).unwrap(); + let serial_asn = serial.to_asn1_integer().unwrap(); + let revoked = + X509Revoked::from_parts(&serial_asn, &Asn1Time::days_from_now(0).unwrap()).unwrap(); + builder.add_revoked(revoked).unwrap(); + + builder + .set_last_update(&Asn1Time::days_from_now(0).unwrap()) + .unwrap(); + builder + .set_next_update(&Asn1Time::days_from_now(30).unwrap()) + .unwrap(); + + let pkey = pkey(); + builder.sign(&pkey, MessageDigest::sha256()).unwrap(); + + let crl = builder.build(); + + let cn = crl.issuer().entries_by_nid(Nid::COMMONNAME).next().unwrap(); + assert_eq!(cn.data().as_slice(), b"foobar.com"); + let revoked_sn = crl + .revoked() + .unwrap() + .iter() + .next() + .unwrap() + .serial_number(); + assert_eq!(serial, revoked_sn.to_bn().unwrap()); + assert!(crl.verify(&pkey).unwrap()); +} + #[test] fn x509_extension_new() { assert!(X509Extension::new(None, None, "crlDistributionPoints", "section").is_err()); @@ -434,6 +487,257 @@ fn test_verify_fails() { .unwrap()); } +#[test] +fn test_verify_revoked() { + let cert = include_bytes!("../../test/cert.pem"); + let cert = X509::from_pem(cert).unwrap(); + let ca = include_bytes!("../../test/root-ca.pem"); + let ca = X509::from_pem(ca).unwrap(); + let crl = include_bytes!("../../test/crl.pem"); + let crl = X509CRL::from_pem(crl).unwrap(); + let chain = Stack::new().unwrap(); + + let mut store_bldr = X509StoreBuilder::new().unwrap(); + store_bldr.add_cert(ca).unwrap(); + store_bldr.add_crl(crl).unwrap(); + store_bldr + .param_mut() + .set_flags(X509VerifyFlags::CRL_CHECK | X509VerifyFlags::CRL_CHECK_ALL) + .unwrap(); + let store = store_bldr.build(); + + let mut context = X509StoreContext::new().unwrap(); + assert!(!context + .init(&store, &cert, &chain, |c| c.verify_cert()) + .unwrap()); +} + +#[test] +fn test_crl_signature() { + let ca = include_bytes!("../../test/root-ca.pem"); + let ca = X509::from_pem(ca).unwrap(); + + let crl = include_bytes!("../../test/bad_sig.pem"); + let crl = X509CRL::from_pem(crl).unwrap(); + assert!(!crl.verify(&ca.public_key().unwrap()).unwrap()); + + let crl = include_bytes!("../../test/crl.pem"); + let crl = X509CRL::from_pem(crl).unwrap(); + assert!(crl.verify(&ca.public_key().unwrap()).unwrap()); +} + +#[test] +fn test_untrusted_valid_crl() { + let cert = include_bytes!("../../test/cert.pem"); + let cert = X509::from_pem(cert).unwrap(); + let ca = include_bytes!("../../test/root-ca.pem"); + let ca = X509::from_pem(ca).unwrap(); + let chain = Stack::new().unwrap(); + + let mut store_bldr = X509StoreBuilder::new().unwrap(); + store_bldr.add_cert(ca).unwrap(); + store_bldr + .param_mut() + .set_flags(X509VerifyFlags::CRL_CHECK | X509VerifyFlags::CRL_CHECK_ALL) + .unwrap(); + store_bldr.param_mut().set_time(1656633600); // 2022-07-01, everything is valid + let store = store_bldr.build(); + + // cert is not revoked + let crl = include_bytes!("../../test/empty_crl.pem"); + let crl = X509CRL::from_pem(crl).unwrap(); + let mut context = X509StoreContext::new().unwrap(); + assert!(context + .init(&store, &cert, &chain, |c| c + .verify_cert_with_crls(stack_of(crl))) + .unwrap()); + + // cert is revoked + let crl = include_bytes!("../../test/crl.pem"); + let crl = X509CRL::from_pem(crl).unwrap(); + let mut context = X509StoreContext::new().unwrap(); + assert!(!context + .init(&store, &cert, &chain, |c| c + .verify_cert_with_crls(stack_of(crl))) + .unwrap()); + assert_eq!( + context.verify_result().unwrap_err().as_raw(), + ffi::X509_V_ERR_CERT_REVOKED + ); +} + +#[test] +fn test_untrusted_invalid_crl() { + let cert = include_bytes!("../../test/cert.pem"); + let cert = X509::from_pem(cert).unwrap(); + let ca = include_bytes!("../../test/root-ca.pem"); + let ca = X509::from_pem(ca).unwrap(); + let chain = Stack::new().unwrap(); + + let mut store_bldr = X509StoreBuilder::new().unwrap(); + store_bldr.add_cert(ca).unwrap(); + store_bldr + .param_mut() + .set_flags(X509VerifyFlags::CRL_CHECK | X509VerifyFlags::CRL_CHECK_ALL) + .unwrap(); + store_bldr.param_mut().set_time(1656633600); // 2022-07-01, everything is valid + let store = store_bldr.build(); + + // this CRL was issued by a different CA (not in the trusted store) + let crl = include_bytes!("../../test/invalid_crl.pem"); + let crl = X509CRL::from_pem(crl).unwrap(); + let mut context = X509StoreContext::new().unwrap(); + assert!(!context + .init(&store, &cert, &chain, |c| c + .verify_cert_with_crls(stack_of(crl))) + .unwrap()); + assert_eq!( + context.verify_result().unwrap_err().as_raw(), + ffi::X509_V_ERR_UNABLE_TO_GET_CRL + ); + + // this CRL has an invalid signature + let crl = include_bytes!("../../test/bad_sig.pem"); + let crl = X509CRL::from_pem(crl).unwrap(); + let mut context = X509StoreContext::new().unwrap(); + assert!(!context + .init(&store, &cert, &chain, |c| c + .verify_cert_with_crls(stack_of(crl))) + .unwrap()); + assert_eq!( + context.verify_result().unwrap_err().as_raw(), + ffi::X509_V_ERR_CRL_SIGNATURE_FAILURE + ); +} + +#[test] +fn test_revoked_serial_numbers() { + let cert = include_bytes!("../../test/cert.pem"); + let cert = X509::from_pem(cert).unwrap(); + let cert_sn = cert.serial_number().to_bn().unwrap(); + + let crl = include_bytes!("../../test/crl.pem"); + let crl = X509CRL::from_pem(crl).unwrap(); + + assert_eq!( + crl.revoked() + .unwrap() + .iter() + .filter(|revoked| revoked.serial_number().to_bn().unwrap() == cert_sn) + .count(), + 1 + ); +} + +#[test] +fn test_serialize_crl() { + let crl = include_bytes!("../../test/crl.pem"); + let crl = X509CRL::from_pem(crl).unwrap(); + let digest = hex::encode(crl.digest(MessageDigest::sha1()).unwrap()); + + let serialized = crl.to_pem().unwrap(); + let crl_deserialized = X509CRL::from_pem(&serialized).unwrap(); + let new_digest = crl_deserialized.digest(MessageDigest::sha1()).unwrap(); + assert_eq!(digest, hex::encode(new_digest)); +} + +#[test] +fn test_debug_crl() { + let crl = include_bytes!("../../test/crl.pem"); + let crl = X509CRL::from_pem(crl).unwrap(); + let debugged = format!("{:#?}", crl); + assert!(debugged.contains(r#"countryName = "AU""#)); + assert!(debugged.contains(r#"stateOrProvinceName = "Some-State""#)); + assert!(debugged.contains(r#"organizationName = "Internet Widgits Pty Ltd""#)); + assert!(debugged.contains(r#"last_update: Jun 21 20:22:02 2022 GMT"#)); + assert!(debugged.contains(r#"next_update: Jun 18 20:22:02 2032 GMT"#)); + assert!(debugged.contains(r#"revocation_date: Jun 21 20:21:55 2022 GMT"#)); + assert!(debugged.contains(r#"serial_number: "8771f7bdee982fa5""#)); +} + +#[test] +fn test_custom_time_valid() { + let cert = include_bytes!("../../test/cert.pem"); + let cert = X509::from_pem(cert).unwrap(); + let ca = include_bytes!("../../test/root-ca.pem"); + let ca = X509::from_pem(ca).unwrap(); + let chain = Stack::new().unwrap(); + + let mut store_bldr = X509StoreBuilder::new().unwrap(); + store_bldr.add_cert(ca).unwrap(); + store_bldr.param_mut().set_time(1656633600); // 2022-07-01, everything is valid + let store = store_bldr.build(); + + let mut context = X509StoreContext::new().unwrap(); + assert!(context + .init(&store, &cert, &chain, |c| c.verify_cert()) + .unwrap()); +} + +#[test] +fn test_custom_time_expired() { + let cert = include_bytes!("../../test/cert.pem"); + let cert = X509::from_pem(cert).unwrap(); + let ca = include_bytes!("../../test/root-ca.pem"); + let ca = X509::from_pem(ca).unwrap(); + let chain = Stack::new().unwrap(); + + let mut store_bldr = X509StoreBuilder::new().unwrap(); + store_bldr.add_cert(ca).unwrap(); + store_bldr.param_mut().set_time(1786838400); // 2026-08-16, after the root and leaf expiration + let store = store_bldr.build(); + + let mut context = X509StoreContext::new().unwrap(); + assert!(!context + .init(&store, &cert, &chain, |c| c.verify_cert()) + .unwrap()); +} + +#[test] +fn test_custom_time_too_soon() { + let cert = include_bytes!("../../test/cert.pem"); + let cert = X509::from_pem(cert).unwrap(); + let ca = include_bytes!("../../test/root-ca.pem"); + let ca = X509::from_pem(ca).unwrap(); + let chain = Stack::new().unwrap(); + + let mut store_bldr = X509StoreBuilder::new().unwrap(); + store_bldr.add_cert(ca).unwrap(); + store_bldr.param_mut().set_time(1262304000); // 2010-01-01, before the root and leaf issuance + let store = store_bldr.build(); + + let mut context = X509StoreContext::new().unwrap(); + assert!(!context + .init(&store, &cert, &chain, |c| c.verify_cert()) + .unwrap()); +} + +#[test] +fn test_custom_time_crl() { + let cert = include_bytes!("../../test/cert.pem"); + let cert = X509::from_pem(cert).unwrap(); + let ca = include_bytes!("../../test/root-ca.pem"); + let ca = X509::from_pem(ca).unwrap(); + let crl = include_bytes!("../../test/crl.pem"); + let crl = X509CRL::from_pem(crl).unwrap(); + let chain = Stack::new().unwrap(); + + let mut store_bldr = X509StoreBuilder::new().unwrap(); + store_bldr.add_cert(ca).unwrap(); + store_bldr + .param_mut() + .set_flags(X509VerifyFlags::CRL_CHECK | X509VerifyFlags::CRL_CHECK_ALL) + .unwrap(); + store_bldr.param_mut().set_time(1640995200); // 2022-01-01, before the CRL's issue date + let store = store_bldr.build(); + + let mut context = X509StoreContext::new().unwrap(); + assert!(!context + .init(&store, &cert, &chain, |c| c + .verify_cert_with_crls(stack_of(crl))) + .unwrap()); +} + #[test] fn test_save_subject_der() { let cert = include_bytes!("../../test/cert.pem"); diff --git a/boring/src/x509/verify.rs b/boring/src/x509/verify.rs index 8bc17a587..2241b9d9d 100644 --- a/boring/src/x509/verify.rs +++ b/boring/src/x509/verify.rs @@ -1,6 +1,6 @@ use crate::ffi; use foreign_types::ForeignTypeRef; -use libc::c_uint; +use libc::{c_uint, time_t}; use std::net::IpAddr; use crate::cvt; @@ -22,6 +22,30 @@ bitflags! { } } +bitflags! { + /// Flags used to configure verification of an `X509` certificate + pub struct X509VerifyFlags: c_uint { + const CB_ISSUER_CHECK = ffi::X509_V_FLAG_CB_ISSUER_CHECK as _; + const USE_CHECK_TIME = ffi::X509_V_FLAG_USE_CHECK_TIME as _; + const CRL_CHECK = ffi::X509_V_FLAG_CRL_CHECK as _; + const CRL_CHECK_ALL = ffi::X509_V_FLAG_CRL_CHECK_ALL as _; + const IGNORE_CRITICAL = ffi::X509_V_FLAG_IGNORE_CRITICAL as _; + const X509_STRICT = ffi::X509_V_FLAG_X509_STRICT as _; + const ALLOW_PROXY_CERTS = ffi::X509_V_FLAG_ALLOW_PROXY_CERTS as _; + const POLICY_CHECK = ffi::X509_V_FLAG_POLICY_CHECK as _; + const EXPLICIT_POLICY = ffi::X509_V_FLAG_EXPLICIT_POLICY as _; + const INHIBIT_ANY = ffi::X509_V_FLAG_INHIBIT_ANY as _; + const INHIBIT_MAP = ffi::X509_V_FLAG_INHIBIT_MAP as _; + const NOTIFY_POLICY = ffi::X509_V_FLAG_NOTIFY_POLICY as _; + const EXTENDED_CRL_SUPPORT = ffi::X509_V_FLAG_EXTENDED_CRL_SUPPORT as _; + const FLAG_USE_DELTAS = ffi::X509_V_FLAG_USE_DELTAS as _; + const CHECK_SS_SIGNATURE = ffi::X509_V_FLAG_CHECK_SS_SIGNATURE as _; + const TRUSTED_FIRST = ffi::X509_V_FLAG_TRUSTED_FIRST as _; + const PARTIAL_CHAIN = ffi::X509_V_FLAG_PARTIAL_CHAIN as _; + const NO_ALT_CHAINS = ffi::X509_V_FLAG_NO_ALT_CHAINS as _; + } +} + foreign_type_and_impl_send_sync! { type CType = ffi::X509_VERIFY_PARAM; fn drop = ffi::X509_VERIFY_PARAM_free; @@ -84,4 +108,62 @@ impl X509VerifyParamRef { .map(|_| ()) } } + + /// Sets the time to check certificates and CRLs against. + /// + /// If unset, uses the current time. + /// + /// This corresponds to [`X509_VERIFY_PARAM_set_time`]. Note that BoringSSL does not support + /// the OpenSSL `NO_CHECK_TIME` option. + /// + /// [`X509_VERIFY_PARAM_set_time`]: https://www.openssl.org/docs/man1.1.1/man3/X509_VERIFY_PARAM_set_time.html + /// + /// # Example + /// + /// ``` + /// use boring::x509::store::X509StoreBuilder; + /// use std::convert::TryInto; + /// use std::time::SystemTime; + /// + /// let mut store_builder = X509StoreBuilder::new().expect("can create a new builder"); + /// let duration_since_epoch = SystemTime::UNIX_EPOCH.elapsed().expect("it's after 1970"); + /// let seconds_since_epoch: libc::time_t = duration_since_epoch + /// .as_secs() + /// .try_into() + /// .expect("time_t is large enough to represent this time"); + /// store_builder.param_mut().set_time(seconds_since_epoch); + /// ``` + pub fn set_time(&mut self, unix_time: time_t) { + unsafe { ffi::X509_VERIFY_PARAM_set_time(self.as_ptr(), unix_time) } + } + + /// Set the verify flags by OR-ing them with `flags` + /// + /// This corresponds to [`X509_VERIFY_PARAM_set_flags`]. + /// + /// [`X509_VERIFY_PARAM_set_flags`]: https://www.openssl.org/docs/man1.1.1/man3/X509_VERIFY_PARAM_set_flags.html + pub fn set_flags(&mut self, flags: X509VerifyFlags) -> Result<(), ErrorStack> { + unsafe { + cvt(ffi::X509_VERIFY_PARAM_set_flags( + self.as_ptr(), + flags.bits().into(), + )) + .map(|_| ()) + } + } + + /// Clears the verify flags in `flags` + /// + /// This corresponds to [`X509_VERIFY_PARAM_clear_flags`] + /// + /// [`X509_VERIFY_PARAM_clear_flags`]: https://www.openssl.org/docs/man1.1.1/man3/X509_VERIFY_PARAM_clear_flags.html + pub fn clear_flags(&mut self, flags: X509VerifyFlags) -> Result<(), ErrorStack> { + unsafe { + cvt(ffi::X509_VERIFY_PARAM_clear_flags( + self.as_ptr(), + flags.bits().into(), + )) + .map(|_| ()) + } + } } diff --git a/boring/test/bad_sig.pem b/boring/test/bad_sig.pem new file mode 100644 index 000000000..59e4860bc --- /dev/null +++ b/boring/test/bad_sig.pem @@ -0,0 +1,11 @@ +-----BEGIN X509 CRL----- +MIIBqTCBkjANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwK +U29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkFw0y +MjA2MTYyMjQ0NDFaFw0yMzA2MTYyMjQ0NDFaMBwwGgIJAIdx973umC+lFw0yMjA2 +MTYyMjIxMDVaMA0GCSqGSIb3DQEBBQUAA4IBAQApqFdwm46jxkJK8J5kprGm6cp8 +b7XMKB1epvhJGIkXHjp7O+2rxYGIExcNlM7jPcwqnUE0E50qGrqSMEupmtaBH03a +fmmDKyhLema7KD64UaERLqWjaW2DPeX9VX6vL4ECc6zTVLxfmYzxVt6A9hhqCm3b +fu8klWczGTa79r/WhTbA7uVf5+OI98da5tlxw+DlAQfqd34L2qq5aFg2dcTGqIdz +3pxP6UlTyj0ZPK3tUtTpIURVO2/MX3j5V+QjWz81UeCv0gQcmOiIVSRUGwi9c6JY +jDqBIvDY6df0riz5is1SS+D94sp1iovBlluwpq4kB8xyDuwt7vblkzleS2YU +-----END X509 CRL----- diff --git a/boring/test/crl.pem b/boring/test/crl.pem new file mode 100644 index 000000000..0f971375a --- /dev/null +++ b/boring/test/crl.pem @@ -0,0 +1,13 @@ +-----BEGIN X509 CRL----- +MIIB3jCBxwIBATANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJBVTETMBEGA1UE +CAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk +Fw0yMjA2MjEyMDIyMDJaFw0zMjA2MTgyMDIyMDJaMBwwGgIJAIdx973umC+lFw0y +MjA2MjEyMDIxNTVaoDAwLjAfBgNVHSMEGDAWgBRs06UDqw1fLMmNipyIp4h3uDf9 +mjALBgNVHRQEBAICEAEwDQYJKoZIhvcNAQELBQADggEBABgsWr/sVZqXm1AzgGCJ +JBMJW1oUY18aqroxo4kAIoI4QveLmHxi1Wm2I4dqdc6pM09SJhU5v5CfqpJ2BDc0 +JBfEk8KKi5O/OYyLcUKa4dEpAlYPgeDyLc6zF8rGLtJoDIYuk4JUeuuByoXt0Sh+ +7vx6UzuI7EH+mr4ZjnyAkD3f9jZy+mDcTm/+0REuh4iZ1AotE2YuQWQgxc1Y8TlD +eK+ks1zBKI23s0hPBxJQunmz2k3Uu9Yf+Sg0KxCiDgJZWFiGSw/6DtnT0oYAFGaD +mCyQWtwmS6zGBg+p76wNXkwyJMVvSDgrXSZ55bmImNmA38yKqOLOpB5i+FAS3r4V +ApQ= +-----END X509 CRL----- diff --git a/boring/test/empty_crl.pem b/boring/test/empty_crl.pem new file mode 100644 index 000000000..5fbb3755c --- /dev/null +++ b/boring/test/empty_crl.pem @@ -0,0 +1,11 @@ +-----BEGIN X509 CRL----- +MIIBijB0MA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApT +b21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQXDTIy +MDYxNjIyNDU1NVoXDTIzMDYxNjIyNDU1NVowDQYJKoZIhvcNAQEFBQADggEBAEtx +nBr3aI0qlegMVJsJn3GfkMzaPVTSTHuw76Dzdl9eGDj0hXzAzZW5k4WBHvaInzNT +NKkeISoQJHLH981R9sQU2zA8sTESLTJGyCFu05Y6XhdmqX4ywmzVRjL6p/aoHNdZ +H1mxgK16wG+Sv0pd+9qNJgC/cNFmNbWzbiEAi5kID4IUxSmId/FZsXsms1EjqDH4 +DFIwLIQO/kR5zwE5fZ5EjqUBdAxoSVHfD+OPKl4x2t8CHMmao+ih2FOfd70+NLBD +2oxaJMjZL/SIf8vYxjpjimMR+7yJ5J5P1j/RBfG3LwwUDP0RtWLIvRQo/dZUyXTg +LuC1vNuUoObe12z/NQ4= +-----END X509 CRL----- diff --git a/boring/test/invalid_crl.pem b/boring/test/invalid_crl.pem new file mode 100644 index 000000000..7ca89c7e8 --- /dev/null +++ b/boring/test/invalid_crl.pem @@ -0,0 +1,11 @@ +-----BEGIN X509 CRL----- +MIIBmTCBggIBATANBgkqhkiG9w0BAQsFADAhMQ0wCwYDVQQKDARGQUtFMRAwDgYD +VQQDDAdGQUtFIENBFw0yMjA2MjcyMTQ3NTBaFw0zMjA2MjQyMTQ3NTBaMBwwGgIJ +AIdx973umC+lFw0yMjA2MjcyMTQ2MjhaoA8wDTALBgNVHRQEBAICEAMwDQYJKoZI +hvcNAQELBQADggEBAMXZRqTG28rnJSUPnVaqkmePSH15iz5Q/e4MdrM0cipXGuzX +z5C8Oh0D2uT3ddawBxTosnbjuzlT7Tanbp3xCRBm9spRPxFbGaFWysBlG1aLTDka +e9t9YeErg7wpwU6Qar0dzkLL5IkW3NArgbe8gP9PkYQxz/B0ESdHIPYJP1YBMNG6 +tLgEhg74Xs9UhOBInNsQB8qMGsEeOnzfiuvfspU/yvKHHzvjAcjeIrONLJaZcu2Y +Dsfm5gOXGkHEm5/qJZ/IILoY0GSsSBekCAZda5+v3nvyjfRaBPyhx3Zv+rXh7u5z +77bIzPZP60rslomacgEr4p/Y52E4GmKGV+X2+Hc= +-----END X509 CRL-----