diff --git a/verifier/src/x509/certs.rs b/verifier/src/x509/certs.rs index 942ef99..4c5174b 100644 --- a/verifier/src/x509/certs.rs +++ b/verifier/src/x509/certs.rs @@ -10,15 +10,17 @@ use core::time::Duration; use p256::ecdsa::signature::Verifier; use p256::ecdsa::{Signature, VerifyingKey}; use x509_cert::der::{Decode, Encode}; +use x509_cert::name::Name; +use x509_cert::serial_number::SerialNumber; use x509_cert::Certificate as X509Certificate; /// A certificate whose signature has not been verified. -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct UnverifiedCertificate { // In order to verify the signature, we need to access the original DER // bytes der_bytes: Vec, - pub(crate) certificate: X509Certificate, + certificate: X509Certificate, // The signature and key are persisted here since they are fallible // operations and it's more ergonomic to fail fast than fail later for a // bad key or signature @@ -26,19 +28,6 @@ pub struct UnverifiedCertificate { key: VerifyingKey, } -/// A certificate whose signature has been verified. -#[derive(Debug, PartialEq, Eq)] -pub struct VerifiedCertificate { - _certificate: X509Certificate, - key: VerifyingKey, -} - -impl VerifiedCertificate { - pub(crate) fn public_key(&self) -> VerifyingKey { - self.key - } -} - impl UnverifiedCertificate { pub fn verify_self_signed(&self, unix_time: Duration) -> Result { self.verify(&self.key, unix_time) @@ -47,8 +36,8 @@ impl UnverifiedCertificate { /// Verify the certificate signature and time are valid. /// /// # Arguments - /// - `key` - The public key to verify the certificate signature with - /// - `unix_time` - The duration since + /// * `key` - The public key to verify the certificate signature with + /// * `unix_time` - The duration since /// [`UNIX_EPOCH`](https://doc.rust-lang.org/std/time/constant.UNIX_EPOCH.html). /// This is expected to be generated by the caller using: /// ```ignore @@ -60,7 +49,7 @@ impl UnverifiedCertificate { self.verify_signature(key)?; Ok(VerifiedCertificate { - _certificate: self.certificate.clone(), + certificate: self.certificate.clone(), key: self.key, }) } @@ -136,6 +125,46 @@ impl TryFrom<&[u8]> for UnverifiedCertificate { } } +/// A certificate whose signature has been verified. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct VerifiedCertificate { + certificate: X509Certificate, + key: VerifyingKey, +} + +impl VerifiedCertificate { + /// Try to convert and verify a self signed PEM-encoded certificate + /// + /// # Arguments + /// * `pem` - The PEM-encoded certificate + /// * `unix_time` - The duration since + /// [`UNIX_EPOCH`](https://doc.rust-lang.org/std/time/constant.UNIX_EPOCH.html). + /// This is expected to be generated by the caller using: + /// ```ignore + /// SystemTime::now().duration_since(UNIX_EPOCH) + /// ``` + /// or equivalent + pub fn try_from_self_signed_pem(pem: &str, unix_time: Duration) -> Result { + let unverified_cert = UnverifiedCertificate::try_from(pem)?; + unverified_cert.verify_self_signed(unix_time) + } + + /// Get the public signing key of the certificate + pub fn public_key(&self) -> VerifyingKey { + self.key + } + + /// Get the subject name of the certificate + pub fn subject_name(&self) -> &Name { + &self.certificate.tbs_certificate.subject + } + + /// Get the serial number of the certificate + pub fn serial_number(&self) -> &SerialNumber { + &self.certificate.tbs_certificate.serial_number + } +} + #[cfg(test)] mod test { use super::*; @@ -450,4 +479,55 @@ mod test { Err(Error::CertificateExpired) ); } + + #[test] + fn verified_from_self_signed_pem() { + let pem = ROOT_CA; + let root_cert = + UnverifiedCertificate::try_from(pem).expect("Failed to decode certificate from PEM"); + let unix_time = root_cert + .certificate + .tbs_certificate + .validity + .not_after + .to_unix_duration(); + assert!(VerifiedCertificate::try_from_self_signed_pem(pem, unix_time).is_ok()); + } + + #[test] + fn verified_from_self_signed_pem_fails_for_wrong_time() { + let pem = ROOT_CA; + let root_cert = + UnverifiedCertificate::try_from(pem).expect("Failed to decode certificate from PEM"); + let mut unix_time = root_cert + .certificate + .tbs_certificate + .validity + .not_after + .to_unix_duration(); + unix_time += Duration::from_nanos(1); + assert_eq!( + VerifiedCertificate::try_from_self_signed_pem(pem, unix_time), + Err(Error::CertificateExpired) + ); + } + + #[test] + fn verified_from_self_signed_pem_fails_for_decoding_error() { + let pem = ROOT_CA; + let root_cert = + UnverifiedCertificate::try_from(pem).expect("Failed to decode certificate from PEM"); + let unix_time = root_cert + .certificate + .tbs_certificate + .validity + .not_after + .to_unix_duration(); + + let bad_pem = pem.replace("-----END CERTIFICATE-----", ""); + assert!(matches!( + VerifiedCertificate::try_from_self_signed_pem(bad_pem.as_str(), unix_time), + Err(Error::PemDecoding(_)) + )); + } } diff --git a/verifier/src/x509/chain.rs b/verifier/src/x509/chain.rs index 7b955a6..af7c04f 100644 --- a/verifier/src/x509/chain.rs +++ b/verifier/src/x509/chain.rs @@ -5,8 +5,9 @@ extern crate alloc; use super::certs::UnverifiedCertificate; -use super::Result; +use super::{Error, Result}; use crate::x509::certs::VerifiedCertificate; +use crate::x509::crl::UnverifiedCrl; use alloc::vec::Vec; use core::time::Duration; use p256::ecdsa::VerifyingKey; @@ -29,19 +30,72 @@ impl CertificateChain { /// Returning the signing key from the leaf certificate /// - /// The chain will be verified against the `trust_root` and the `unix_time`. + /// The chain will be verified against the `trust_anchor`, `unix_time`, and + /// `crls`. + /// + /// # Arguments + /// * `trust_anchor` - The trust anchor of the chain. This is often a + /// previously known certificate. + /// * `unix_time` - The duration since + /// [`UNIX_EPOCH`](https://doc.rust-lang.org/std/time/constant.UNIX_EPOCH.html). + /// This is expected to be generated by the caller using: + /// ```ignore + /// SystemTime::now().duration_since(UNIX_EPOCH) + /// ``` + /// or equivalent + /// * `crls` - A list of certificate revocation lists that will be used to + /// reject revoked certificates. pub fn signing_key( &self, - trust_root: VerifiedCertificate, + trust_anchor: &VerifiedCertificate, unix_time: Duration, + crls: &[UnverifiedCrl], ) -> Result { - let mut key = trust_root.public_key(); + let mut signing_cert = trust_anchor.clone(); for cert in &self.certificates { + let key = signing_cert.public_key(); let verified_cert = cert.verify(&key, unix_time)?; - key = verified_cert.public_key(); + verify_certificate_not_revoked(&verified_cert, &signing_cert, crls, unix_time)?; + signing_cert = verified_cert; + } + Ok(signing_cert.public_key()) + } +} + +/// Convert a series PEM-encoded certificates into a [`CertificateChain`]. +/// +/// The PEMs must be in order from the trust anchor to the leaf certificate. +impl TryFrom<&[&str]> for CertificateChain { + type Error = Error; + + fn try_from(pems: &[&str]) -> ::core::result::Result { + let certificates = pems + .iter() + .map(|pem| UnverifiedCertificate::try_from(*pem)) + .collect::>() + .into_iter() + .collect::>>()?; + + Ok(Self { certificates }) + } +} + +fn verify_certificate_not_revoked( + cert: &VerifiedCertificate, + trust_anchor: &VerifiedCertificate, + crls: &[UnverifiedCrl], + unix_time: Duration, +) -> Result<()> { + for crl in crls { + if crl.issuer() == trust_anchor.subject_name() { + let verified_crl = crl.verify(&trust_anchor.public_key(), unix_time)?; + if verified_crl.is_cert_revoked(cert.serial_number()) { + return Err(Error::CertificateRevoked); + } } - Ok(key) } + + Ok(()) } #[cfg(test)] @@ -49,6 +103,7 @@ mod test { use super::super::Error; use super::*; + use x509_cert::crl::CertificateList; use x509_cert::der::Decode; use x509_cert::Certificate as X509Certificate; use yare::parameterized; @@ -56,6 +111,8 @@ mod test { const LEAF_CERT: &str = include_str!("../../data/tests/leaf_cert.pem"); const PROCESSOR_CA: &str = include_str!("../../data/tests/processor_ca.pem"); const ROOT_CA: &str = include_str!("../../data/tests/root_ca.pem"); + const ROOT_CRL: &str = include_str!("../../data/tests/root_crl.pem"); + const PROCESSOR_CRL: &str = include_str!("../../data/tests/processor_crl.pem"); fn key_and_start_time(cert: &str) -> (VerifyingKey, Duration) { let (_, der_bytes) = pem_rfc7468::decode_vec(cert.as_bytes()).expect("Failed decoding PEM"); @@ -81,94 +138,125 @@ mod test { only_root = { &[ROOT_CA] }, )] fn signing_key_from_certificate_chain(pem_chain: &[&str]) { - let certs = pem_chain - .iter() - .map(|pem| UnverifiedCertificate::try_from(*pem).expect("Failed decoding pem")) - .collect::>(); + let chain = CertificateChain::try_from(pem_chain).expect("Failed decoding pems"); let end = pem_chain .last() .expect("Should be at least one certificate"); let (expected_key, unix_time) = key_and_start_time(end); - let chain = CertificateChain::new(certs); - let unverified_root = - UnverifiedCertificate::try_from(ROOT_CA).expect("Failed decoding pem"); - let root = unverified_root - .verify_self_signed(unix_time) - .expect("Failed verifying root certificate"); + let root = VerifiedCertificate::try_from_self_signed_pem(ROOT_CA, unix_time) + .expect("Failed verifying root"); let signing_key = chain - .signing_key(root, unix_time) + .signing_key(&root, unix_time, &[]) .expect("Failed getting signing key"); assert_eq!(signing_key, expected_key); } #[test] fn signing_key_fails_when_outside_valid_time() { - let pem_chain = [ROOT_CA, PROCESSOR_CA, LEAF_CERT]; - let certs = pem_chain - .iter() - .map(|pem| UnverifiedCertificate::try_from(*pem).expect("Failed decoding pem")) - .collect::>(); + let chain = CertificateChain::try_from([ROOT_CA, PROCESSOR_CA, LEAF_CERT].as_slice()) + .expect("Failed decoding pems"); let (_, mut unix_time) = key_and_start_time(LEAF_CERT); unix_time -= Duration::from_nanos(1); - let chain = CertificateChain::new(certs); - let unverified_root = - UnverifiedCertificate::try_from(ROOT_CA).expect("Failed decoding pem"); - let root = unverified_root - .verify_self_signed(unix_time) - .expect("Failed verifying root certificate"); + let root = VerifiedCertificate::try_from_self_signed_pem(ROOT_CA, unix_time) + .expect("Failed verifying root"); + assert_eq!( - chain.signing_key(root, unix_time), + chain.signing_key(&root, unix_time, &[]), Err(Error::CertificateNotYetValid) ); } #[test] fn cert_chain_out_of_order_fails() { - let pem_chain = [ROOT_CA, LEAF_CERT, PROCESSOR_CA]; - let certs = pem_chain - .iter() - .map(|pem| UnverifiedCertificate::try_from(*pem).expect("Failed decoding pem")) - .collect::>(); - + let chain = CertificateChain::try_from([ROOT_CA, LEAF_CERT, PROCESSOR_CA].as_slice()) + .expect("Failed decoding pems"); let (_, unix_time) = key_and_start_time(LEAF_CERT); + let root = VerifiedCertificate::try_from_self_signed_pem(ROOT_CA, unix_time) + .expect("Failed verifying root"); - let chain = CertificateChain::new(certs); - let unverified_root = - UnverifiedCertificate::try_from(ROOT_CA).expect("Failed decoding pem"); - let root = unverified_root - .verify_self_signed(unix_time) - .expect("Failed verifying root certificate"); assert_eq!( - chain.signing_key(root, unix_time), + chain.signing_key(&root, unix_time, &[]), Err(Error::SignatureVerification) ); } #[test] fn cert_chain_missing_intermediate_ca_fails() { - let pem_chain = [ROOT_CA, LEAF_CERT]; - let certs = pem_chain + let chain = CertificateChain::try_from([ROOT_CA, LEAF_CERT].as_slice()) + .expect("Failed decoding pems"); + let (_, unix_time) = key_and_start_time(LEAF_CERT); + let root = VerifiedCertificate::try_from_self_signed_pem(ROOT_CA, unix_time) + .expect("Failed verifying root"); + + assert_eq!( + chain.signing_key(&root, unix_time, &[]), + Err(Error::SignatureVerification) + ); + } + + #[test] + fn certifiate_chain_with_crls() { + let chain = CertificateChain::try_from([ROOT_CA, PROCESSOR_CA, LEAF_CERT].as_slice()) + .expect("Failed decoding pems"); + + let crls = [ROOT_CRL, PROCESSOR_CRL] .iter() - .map(|pem| UnverifiedCertificate::try_from(*pem).expect("Failed decoding pem")) + .map(|crl| UnverifiedCrl::try_from(*crl).expect("Failed decoding CRL")) .collect::>(); - let (_, unix_time) = key_and_start_time(LEAF_CERT); + let (expected_key, _) = key_and_start_time(LEAF_CERT); + + // CRLs are often only for a month, while certs are for years, so use CRL's time + let (_, der_bytes) = + pem_rfc7468::decode_vec(PROCESSOR_CRL.as_bytes()).expect("Failed decoding PEM"); + let crl = CertificateList::from_der(der_bytes.as_slice()).expect("Falied decoding DER"); + let mut unix_time = crl + .tbs_cert_list + .next_update + .expect("CRL should have next update") + .to_unix_duration(); + unix_time -= Duration::from_nanos(1); + + let root = VerifiedCertificate::try_from_self_signed_pem(ROOT_CA, unix_time) + .expect("Failed verifying root"); + + let signing_key = chain + .signing_key(&root, unix_time, crls.as_slice()) + .expect("Failed getting signing key"); + assert_eq!(signing_key, expected_key); + } + + #[test] + fn crl_after_valid_date_fails() { + let chain = CertificateChain::try_from([ROOT_CA, PROCESSOR_CA, LEAF_CERT].as_slice()) + .expect("Failed decoding pems"); + + let crls = [ROOT_CRL, PROCESSOR_CRL] + .iter() + .map(|crl| UnverifiedCrl::try_from(*crl).expect("Failed decoding CRL")) + .collect::>(); + + let (_, der_bytes) = + pem_rfc7468::decode_vec(PROCESSOR_CRL.as_bytes()).expect("Failed decoding PEM"); + let crl = CertificateList::from_der(der_bytes.as_slice()).expect("Falied decoding DER"); + let unix_time = crl + .tbs_cert_list + .next_update + .expect("CRL should have next update") + .to_unix_duration(); + + let root = VerifiedCertificate::try_from_self_signed_pem(ROOT_CA, unix_time) + .expect("Failed verifying root"); - let chain = CertificateChain::new(certs); - let unverified_root = - UnverifiedCertificate::try_from(ROOT_CA).expect("Failed decoding pem"); - let root = unverified_root - .verify_self_signed(unix_time) - .expect("Failed verifying root certificate"); assert_eq!( - chain.signing_key(root, unix_time), - Err(Error::SignatureVerification) + chain.signing_key(&root, unix_time, crls.as_slice()), + Err(Error::CrlExpired) ); } } diff --git a/verifier/src/x509/crl.rs b/verifier/src/x509/crl.rs index 1410397..368720f 100644 --- a/verifier/src/x509/crl.rs +++ b/verifier/src/x509/crl.rs @@ -12,6 +12,8 @@ use p256::ecdsa::signature::Verifier; use p256::ecdsa::{Signature, VerifyingKey}; use x509_cert::crl::CertificateList; use x509_cert::der::{Decode, Encode}; +use x509_cert::name::Name; +use x509_cert::serial_number::SerialNumber; use x509_cert::time::Time; /// A certificate revocation list (CRL). @@ -32,12 +34,6 @@ pub struct UnverifiedCrl { next_update: Time, } -/// A certificate whose signature has been verified. -#[derive(Debug, PartialEq, Eq)] -pub struct VerifiedCrl { - _crl: CertificateList, -} - impl UnverifiedCrl { /// Verify the CRL signature is valid. /// @@ -50,11 +46,18 @@ impl UnverifiedCrl { /// SystemTime::now().duration_since(UNIX_EPOCH) /// ``` /// or equivalent - pub fn verify(self, key: &VerifyingKey, unix_time: Duration) -> Result { + pub fn verify(&self, key: &VerifyingKey, unix_time: Duration) -> Result { self.verify_signature(key)?; self.verify_time(unix_time)?; - Ok(VerifiedCrl { _crl: self.crl }) + Ok(VerifiedCrl { + crl: self.crl.clone(), + }) + } + + /// Get the issuer of the CRL + pub fn issuer(&self) -> &Name { + &self.crl.tbs_cert_list.issuer } fn verify_signature(&self, key: &VerifyingKey) -> Result<()> { @@ -118,6 +121,25 @@ impl TryFrom<&[u8]> for UnverifiedCrl { } } +/// A certificate whose signature has been verified. +#[derive(Debug, PartialEq, Eq)] +pub struct VerifiedCrl { + crl: CertificateList, +} + +impl VerifiedCrl { + /// Is a certificate (serial number) revoked? + pub fn is_cert_revoked(&self, serial_number: &SerialNumber) -> bool { + let revoked_certs = match &self.crl.tbs_cert_list.revoked_certificates { + None => return false, + Some(revoked_certs) => revoked_certs, + }; + revoked_certs + .iter() + .any(|r| r.serial_number == *serial_number) + } +} + #[cfg(test)] mod test { use super::super::certs::UnverifiedCertificate; diff --git a/verifier/src/x509/error.rs b/verifier/src/x509/error.rs index 331923c..e7e42bb 100644 --- a/verifier/src/x509/error.rs +++ b/verifier/src/x509/error.rs @@ -13,6 +13,8 @@ pub enum Error { PemDecoding(pem_rfc7468::Error), /// The certificate is not yet valid CertificateNotYetValid, + /// The certificate has been revoked + CertificateRevoked, /// An error occurred decoding the key from a certificate KeyDecoding, /// The certificate revocation list has expired