From 4e7fc9f0f11991114ac197b2d374c82ac7db5253 Mon Sep 17 00:00:00 2001 From: Arjen van Veen Date: Fri, 24 Nov 2023 10:52:23 +0100 Subject: [PATCH 01/12] mdoc auth and iaca certificate validation --- Cargo.toml | 9 +- src/definitions/helpers/non_empty_vec.rs | 2 +- src/definitions/namespaces/latin1.rs | 12 +- src/issuance/x5chain.rs | 107 +++- src/presentation/device.rs | 28 +- src/presentation/mdoc_auth.rs | 124 ++++ src/presentation/mod.rs | 2 + src/presentation/reader.rs | 369 +++++++++++- src/presentation/trust_anchor.rs | 546 ++++++++++++++++++ .../presentation/isomdl_iaca_intermediate.pem | 17 + test/presentation/isomdl_iaca_leaf_signer.pem | 17 + test/presentation/isomdl_iaca_root_cert.pem | 15 + test/presentation/isomdl_iaca_signer.pem | 17 + .../isomdl_incorrect_iaca_signer.pem | 17 + 14 files changed, 1236 insertions(+), 46 deletions(-) create mode 100644 src/presentation/mdoc_auth.rs create mode 100644 src/presentation/trust_anchor.rs create mode 100644 test/presentation/isomdl_iaca_intermediate.pem create mode 100644 test/presentation/isomdl_iaca_leaf_signer.pem create mode 100644 test/presentation/isomdl_iaca_root_cert.pem create mode 100644 test/presentation/isomdl_iaca_signer.pem create mode 100644 test/presentation/isomdl_incorrect_iaca_signer.pem diff --git a/Cargo.toml b/Cargo.toml index 0c1a4587..8a2c1fb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ homepage = "https://github.com/spruceid/isomdl" repository = "https://github.com/spruceid/isomdl" license = "Apache-2.0" exclude = ["test/"] - + [dependencies] anyhow = "1.0" ecdsa = { version = "0.16.0", features = ["serde"] } @@ -35,12 +35,15 @@ async-signature = "0.3.0" #tracing = "0.1" base64 = "0.13" pem-rfc7468 = "0.7.0" -x509-cert = { version = "0.1.1", features = ["pem"] } - +x509-cert = {version = "0.2.3", features = ["std"]} +const-oid = "0.9.2" ssi-jwk = { version = "0.1" } isomdl-macros = { version = "0.1.0", path = "macros" } clap = { version = "4", features = ["derive"] } clap-stdin = "0.2.1" +der = { version = "0.7", features = ["std", "derive", "alloc"] } +hex = "0.4.3" +asn1-rs = { version = "0.5.2", features = ["bits"]} [dependencies.cose-rs] git = "https://github.com/spruceid/cose-rs" diff --git a/src/definitions/helpers/non_empty_vec.rs b/src/definitions/helpers/non_empty_vec.rs index 154f7d40..21123f50 100644 --- a/src/definitions/helpers/non_empty_vec.rs +++ b/src/definitions/helpers/non_empty_vec.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use std::ops::Deref; -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, Hash)] #[serde(try_from = "Vec", into = "Vec")] pub struct NonEmptyVec(Vec); diff --git a/src/definitions/namespaces/latin1.rs b/src/definitions/namespaces/latin1.rs index 07067d6e..fbe8d8c5 100644 --- a/src/definitions/namespaces/latin1.rs +++ b/src/definitions/namespaces/latin1.rs @@ -95,12 +95,12 @@ mod test { #[test] fn upper_latin() { let upper_latin_chars = vec![ - ' ', '¡', '¢', '£', '¤', '¥', '¦', '§', '¨', '©', 'ª', '«', '¬', '­', '®', '¯', '°', - '±', '²', '³', '´', 'µ', '¶', '·', '¸', '¹', 'º', '»', '¼', '½', '¾', '¿', 'À', 'Á', - 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ð', 'Ñ', 'Ò', - 'Ó', 'Ô', 'Õ', 'Ö', '×', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'Þ', 'ß', 'à', 'á', 'â', 'ã', - 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ð', 'ñ', 'ò', 'ó', 'ô', - 'õ', 'ö', '÷', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ', 'ÿ', + ' ', '¡', '¢', '£', '¤', '¥', '¦', '§', '¨', '©', 'ª', '«', '¬', '\u{AD}', '®', '¯', + '°', '±', '²', '³', '´', 'µ', '¶', '·', '¸', '¹', 'º', '»', '¼', '½', '¾', '¿', 'À', + 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ð', 'Ñ', + 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', '×', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'Þ', 'ß', 'à', 'á', 'â', + 'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ð', 'ñ', 'ò', 'ó', + 'ô', 'õ', 'ö', '÷', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ', 'ÿ', ]; assert!(upper_latin_chars.iter().all(is_upper_latin)); } diff --git a/src/issuance/x5chain.rs b/src/issuance/x5chain.rs index 898ba765..90cf782e 100644 --- a/src/issuance/x5chain.rs +++ b/src/issuance/x5chain.rs @@ -1,17 +1,50 @@ -use crate::definitions::helpers::NonEmptyVec; +use crate::presentation::reader::find_anchor; + +use crate::presentation::reader::Error; +use crate::presentation::trust_anchor::validate_with_trust_anchor; +use crate::presentation::trust_anchor::TrustAnchorRegistry; +use crate::{definitions::helpers::NonEmptyVec, presentation::trust_anchor::check_validity_period}; use anyhow::{anyhow, Result}; + +use const_oid::AssociatedOid; + +use elliptic_curve::{ + sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint}, + AffinePoint, CurveArithmetic, FieldBytesSize, PublicKey, +}; +use p256::NistP256; +use serde::{Deserialize, Serialize}; use serde_cbor::Value as CborValue; +use signature::Verifier; use std::{fs::File, io::Read}; +use x509_cert::der::Encode; use x509_cert::{ certificate::Certificate, - der::{Decode, Encode}, + der::{referenced::OwnedToRef, Decode}, }; pub const X5CHAIN_HEADER_LABEL: i128 = 33; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] pub struct X509 { - bytes: Vec, + pub bytes: Vec, +} + +impl X509 { + pub fn public_key(&self) -> Result, Error> + where + C: AssociatedOid + CurveArithmetic, + AffinePoint: FromEncodedPoint + ToEncodedPoint, + FieldBytesSize: ModulusSize, + { + let cert = x509_cert::Certificate::from_der(&self.bytes)?; + cert.tbs_certificate + .subject_public_key_info + .owned_to_ref() + .try_into() + .map_err(|e| format!("could not parse public key from pkcs8 spki: {e}")) + .map_err(|_e| Error::MdocAuth("could not parse public key from pkcs8 spki".to_string())) + } } #[derive(Debug, Clone)] @@ -41,6 +74,63 @@ impl X5Chain { ), } } + + pub fn validate( + &self, + trust_anchor_registry: Option, + ) -> Result, Error> { + let x5chain = self.0.as_ref(); + let mut results: Vec> = x5chain + .windows(2) + .map(|chain_link| { + let target = &chain_link[0]; + let issuer = &chain_link[1]; + check_signature(target, issuer) + }) + .collect(); + + for x509 in x5chain { + let cert = x509_cert::Certificate::from_der(&x509.bytes)?; + results.push(check_validity_period(&cert)) + } + + let mut errors: Vec = vec![]; + + //validate the last certificate in the chain against trust anchor + let last_in_chain = x5chain.to_vec().pop(); + if let Some(x509) = last_in_chain { + let inner = x509_cert::Certificate::from_der(&x509.bytes)?; + if let Some(trust_anchor) = find_anchor(inner, trust_anchor_registry)? { + errors.append(&mut validate_with_trust_anchor(x509, trust_anchor)?); + } else { + errors.push(Error::MdocAuth( + "No matching trust anchor found".to_string(), + )); + }; + } else { + errors.push(Error::MdocAuth("Empty certificate chain".to_string())) + } + + let mut sig_errors = results + .into_iter() + .filter(|result| result.is_err()) + .collect::>>() + .into_iter() + .map(|e| e.expect_err("something went wrong")) + .collect::>(); + + errors.append(&mut sig_errors); + Ok(errors) + } +} + +pub fn check_signature(target: &X509, issuer: &X509) -> Result<(), Error> { + let parent_public_key = ecdsa::VerifyingKey::from(issuer.public_key()?); + let child_cert = x509_cert::Certificate::from_der(&target.bytes)?; + let sig: ecdsa::Signature = + ecdsa::Signature::from_der(child_cert.signature.raw_bytes())?; + let bytes = child_cert.tbs_certificate.to_der()?; + Ok(parent_public_key.verify(&bytes, &sig)?) } #[derive(Default, Debug, Clone)] @@ -57,7 +147,8 @@ impl Builder { .map_err(|e| anyhow!("unable to parse certificate from der: {}", e))?; let x509 = X509 { bytes: cert - .to_vec() + .encode_to_vec(&mut vec![])? + .to_der() .map_err(|e| anyhow!("unable to convert certificate to bytes: {}", e))?, }; self.certs.push(x509); @@ -68,7 +159,8 @@ impl Builder { .map_err(|e| anyhow!("unable to parse certificate from der encoding: {}", e))?; let x509 = X509 { bytes: cert - .to_vec() + .encode_to_vec(&mut vec![])? + .to_der() .map_err(|e| anyhow!("unable to convert certificate to bytes: {}", e))?, }; self.certs.push(x509); @@ -179,4 +271,7 @@ pub mod test { // Algorithm::ES512 //)); } + + #[test] + pub fn validate_x5chain() {} } diff --git a/src/presentation/device.rs b/src/presentation/device.rs index 82711a2b..95e82855 100644 --- a/src/presentation/device.rs +++ b/src/presentation/device.rs @@ -92,20 +92,20 @@ pub struct Document { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PreparedDeviceResponse { - prepared_documents: Vec, - signed_documents: Vec, - document_errors: Option, - status: Status, + pub prepared_documents: Vec, + pub signed_documents: Vec, + pub document_errors: Option, + pub status: Status, } #[derive(Debug, Clone, Serialize, Deserialize)] -struct PreparedDocument { - id: Uuid, - doc_type: String, - issuer_signed: IssuerSigned, - device_namespaces: DeviceNamespacesBytes, - prepared_cose_sign1: PreparedCoseSign1, - errors: Option, +pub struct PreparedDocument { + pub id: Uuid, + pub doc_type: String, + pub issuer_signed: IssuerSigned, + pub device_namespaces: DeviceNamespacesBytes, + pub prepared_cose_sign1: PreparedCoseSign1, + pub errors: Option, } type Namespaces = NonEmptyMap>; @@ -631,7 +631,7 @@ impl From for Document { } /// Filter permitted items to only permit the items that were requested. -fn filter_permitted(request: &RequestedItems, permitted: PermittedItems) -> PermittedItems { +pub fn filter_permitted(request: &RequestedItems, permitted: PermittedItems) -> PermittedItems { permitted .into_iter() .filter_map(|(doc_type, namespaces)| { @@ -832,9 +832,9 @@ mod test { let issuer_item1 = Tag24::new(issuer_signed_item1).unwrap(); let issuer_item2 = Tag24::new(issuer_signed_item2).unwrap(); let issuer_item3 = Tag24::new(issuer_signed_item3).unwrap(); - let mut issuer_items = NonEmptyMap::new(element_identifier1, issuer_item1.clone()); + let mut issuer_items = NonEmptyMap::new(element_identifier1, issuer_item1); issuer_items.insert(element_identifier2, issuer_item2.clone()); - issuer_items.insert(element_identifier3, issuer_item3.clone()); + issuer_items.insert(element_identifier3, issuer_item3); let result = nearest_age_attestation(requested_element_identifier, issuer_items) .expect("failed to process age attestation request"); diff --git a/src/presentation/mdoc_auth.rs b/src/presentation/mdoc_auth.rs new file mode 100644 index 00000000..ee38d7f2 --- /dev/null +++ b/src/presentation/mdoc_auth.rs @@ -0,0 +1,124 @@ +use crate::definitions::device_response::Document; +use crate::definitions::issuer_signed; +use crate::definitions::DeviceAuth; +use crate::definitions::Mso; +use crate::definitions::{ + device_signed::DeviceAuthentication, helpers::Tag24, SessionTranscript180135, +}; +use crate::presentation::reader::Error; +use crate::presentation::reader::Error as ReaderError; +use anyhow::Result; +use elliptic_curve::generic_array::GenericArray; +use issuer_signed::IssuerSigned; +use p256::ecdsa::Signature; +use p256::ecdsa::VerifyingKey; +use p256::pkcs8::DecodePublicKey; +use serde_cbor::Value as CborValue; +use ssi_jwk::Params; +use ssi_jwk::JWK as SsiJwk; +use x509_cert::der::Decode; + +pub fn issuer_authentication(x5chain: CborValue, issuer_signed: IssuerSigned) -> Result<(), Error> { + let signer_key = get_signer_key(&x5chain)?; + let issuer_auth = issuer_signed.issuer_auth; + let verification_result: cose_rs::sign1::VerificationResult = + issuer_auth.verify::(&signer_key, None, None); + if !verification_result.success() { + Err(ReaderError::ParsingError)? + } else { + Ok(()) + } +} + +pub fn device_authentication( + mso: Tag24, + document: Document, + session_transcript: SessionTranscript180135, +) -> Result<(), Error> { + let device_key = mso.into_inner().device_key_info.device_key; + let jwk = SsiJwk::try_from(device_key)?; + match jwk.params { + Params::EC(p) => { + let x_coordinate = p.x_coordinate.clone(); + let y_coordinate = p.y_coordinate.clone(); + let (Some(x), Some(y)) = (x_coordinate, y_coordinate) else { + return Err(ReaderError::MdocAuth( + "device key jwk is missing coordinates".to_string(), + )); + }; + let encoded_point = p256::EncodedPoint::from_affine_coordinates( + GenericArray::from_slice(x.0.as_slice()), + GenericArray::from_slice(y.0.as_slice()), + false, + ); + let verifying_key = VerifyingKey::from_encoded_point(&encoded_point)?; + let namespaces_bytes = document.device_signed.namespaces; + let device_auth: DeviceAuth = document.device_signed.device_auth; + + //TODO: fix for attended use case: + match device_auth { + DeviceAuth::Signature { device_signature } => { + let detached_payload = Tag24::new(DeviceAuthentication::new( + session_transcript, + document.doc_type, + namespaces_bytes, + )) + .map_err(|_| ReaderError::CborDecodingError)?; + let external_aad = None; + let cbor_payload = serde_cbor::to_vec(&detached_payload)?; + let result = device_signature.verify::( + &verifying_key, + Some(cbor_payload), + external_aad, + ); + if !result.success() { + Err(ReaderError::ParsingError)? + } else { + Ok(()) + } + } + DeviceAuth::Mac { .. } => { + Err(ReaderError::Unsupported) + // send not yet supported error + } + } + } + _ => Err(Error::MdocAuth("Unsupported device_key type".to_string())), + } +} + +fn get_signer_key(x5chain: &CborValue) -> Result { + let signer = match x5chain { + CborValue::Text(t) => { + let x509 = x509_cert::Certificate::from_der(t.as_bytes())?; + + x509.tbs_certificate + .subject_public_key_info + .subject_public_key + } + CborValue::Array(a) => match a.first() { + Some(CborValue::Text(t)) => { + let x509 = x509_cert::Certificate::from_der(t.as_bytes())?; + + x509.tbs_certificate + .subject_public_key_info + .subject_public_key + } + _ => return Err(ReaderError::CborDecodingError)?, + }, + CborValue::Bytes(b) => { + let x509 = x509_cert::Certificate::from_der(b)?; + + x509.tbs_certificate + .subject_public_key_info + .subject_public_key + } + _ => { + return Err(ReaderError::MdocAuth(format!( + "Unexpected type for x5chain header: {:?} ", + x5chain + ))) + } + }; + Ok(VerifyingKey::from_public_key_der(signer.raw_bytes())?) +} diff --git a/src/presentation/mod.rs b/src/presentation/mod.rs index e38a2dd9..0ee4040f 100644 --- a/src/presentation/mod.rs +++ b/src/presentation/mod.rs @@ -1,5 +1,7 @@ pub mod device; +pub mod mdoc_auth; pub mod reader; +pub mod trust_anchor; use anyhow::Result; use base64::{decode, encode}; diff --git a/src/presentation/reader.rs b/src/presentation/reader.rs index e9fb2aa0..90076e75 100644 --- a/src/presentation/reader.rs +++ b/src/presentation/reader.rs @@ -1,12 +1,25 @@ -use crate::definitions::{ - device_engagement::DeviceRetrievalMethod, - device_request::{self, DeviceRequest, DocRequest, ItemsRequest}, - helpers::{NonEmptyVec, Tag24}, - session::{ - self, create_p256_ephemeral_keys, derive_session_key, get_shared_secret, Handover, - SessionEstablishment, +use super::{ + mdoc_auth::device_authentication, mdoc_auth::issuer_authentication, + trust_anchor::ValidationRuleSet, +}; +use crate::definitions::device_key::cose_key::Error as CoseError; +use crate::definitions::Mso; +use crate::issuance::x5chain::X509; +use crate::presentation::reader::Error as ReaderError; +use crate::presentation::trust_anchor::TrustAnchorRegistry; +use crate::{ + definitions::{ + device_engagement::DeviceRetrievalMethod, + device_request::{self, DeviceRequest, DocRequest, ItemsRequest}, + helpers::{non_empty_vec, NonEmptyVec, Tag24}, + session::{ + self, create_p256_ephemeral_keys, derive_session_key, get_shared_secret, Handover, + SessionEstablishment, + }, }, - DeviceEngagement, DeviceResponse, SessionData, SessionTranscript180135, + definitions::{DeviceEngagement, DeviceResponse, SessionData, SessionTranscript180135}, + issuance::X5Chain, + presentation::trust_anchor::TrustAnchor, }; use anyhow::{anyhow, Result}; use serde::{Deserialize, Serialize}; @@ -14,8 +27,12 @@ use serde_cbor::Value as CborValue; use serde_json::json; use serde_json::Value; use std::collections::BTreeMap; +use std::collections::HashSet; +use std::hash::Hash; use uuid::Uuid; +use x509_cert::{certificate::CertificateInner, der::Decode}; + #[derive(Serialize, Deserialize)] pub struct SessionManager { session_transcript: SessionTranscript180135, @@ -23,6 +40,24 @@ pub struct SessionManager { device_message_counter: u32, sk_reader: [u8; 32], reader_message_counter: u32, + validation_ruleset: Option, + trust_anchor_registry: Option, +} + +pub struct ValidatedResponse { + pub response: BTreeMap, + pub issuer_authentication: Status, + pub device_authentication: Status, + pub errors: ValidationErrors, +} + +pub struct ValidationErrors(pub BTreeMap>); + +#[derive(Serialize, Deserialize)] +pub enum Status { + Unchecked, + Invalid, + Valid, } #[derive(Debug, thiserror::Error)] @@ -49,6 +84,10 @@ pub enum Error { ParsingError, #[error("Request for data is invalid.")] InvalidRequest, + #[error("Failed mdoc authentication: {0}")] + MdocAuth(String), + #[error("Currently unsupported format")] + Unsupported, } impl From for Error { @@ -63,10 +102,48 @@ impl From for Error { } } +impl From for Error { + fn from(value: x509_cert::der::Error) -> Self { + Error::MdocAuth(value.to_string()) + } +} + +impl From for Error { + fn from(value: p256::ecdsa::Error) -> Self { + Error::MdocAuth(value.to_string()) + } +} + +impl From for Error { + fn from(value: x509_cert::spki::Error) -> Self { + Error::MdocAuth(value.to_string()) + } +} + +impl From for Error { + fn from(value: CoseError) -> Self { + Error::MdocAuth(value.to_string()) + } +} + +impl From for Error { + fn from(value: non_empty_vec::Error) -> Self { + Error::MdocAuth(value.to_string()) + } +} + +impl From for Error { + fn from(value: asn1_rs::Error) -> Self { + Error::MdocAuth(value.to_string()) + } +} + impl SessionManager { pub fn establish_session( qr_code: String, namespaces: device_request::Namespaces, + trust_anchor_registry: Option, + validation_ruleset: Option, ) -> Result<(Self, Vec, [u8; 16])> { let device_engagement_bytes = Tag24::::from_qr_code_uri(&qr_code).map_err(Error::InvalidQrCode)?; @@ -108,6 +185,8 @@ impl SessionManager { device_message_counter: 0, sk_reader, reader_message_counter: 0, + validation_ruleset, + trust_anchor_registry, }; let request = session_manager.build_request(namespaces)?; @@ -179,7 +258,8 @@ impl SessionManager { pub fn handle_response( &mut self, response: &[u8], - ) -> Result>, Error> { + session_transcript: SessionTranscript180135, + ) -> Result { let session_data: SessionData = serde_cbor::from_slice(response)?; let encrypted_response = match session_data.data { None => return Err(Error::HolderError), @@ -191,12 +271,36 @@ impl SessionManager { &mut self.device_message_counter, ) .map_err(|_e| Error::DecryptionError)?; - let response: DeviceResponse = serde_cbor::from_slice(&decrypted_response)?; let mut core_namespace = BTreeMap::::new(); let mut aamva_namespace = BTreeMap::::new(); - let mut parsed_response = BTreeMap::>::new(); - let mut namespaces = response + let device_response: DeviceResponse = serde_cbor::from_slice(&decrypted_response)?; + + let document = device_response + .documents + .clone() + .ok_or(ReaderError::DeviceTransmissionError)? + .into_inner() + .into_iter() + .find(|doc| doc.doc_type == "org.iso.18013.5.1.mDL") + .ok_or(ReaderError::DocumentTypeError)?; + + let issuer_signed = document.issuer_signed.clone(); + + let mso_bytes = issuer_signed + .issuer_auth + .payload() + .expect("expected a COSE_Sign1 with attached payload, found detached payload"); + let mso: Tag24 = + serde_cbor::from_slice(mso_bytes).expect("unable to parse payload as Mso"); + + let header = issuer_signed.issuer_auth.unprotected(); + let Some(x5chain) = header.get_i(33) else { + return Err(ReaderError::MdocAuth("Missing x5chain header".to_string())); + }; + + let mut parsed_response = BTreeMap::::new(); + let mut namespaces = device_response .documents .ok_or(Error::DeviceTransmissionError)? .into_inner() @@ -221,7 +325,10 @@ impl SessionManager { } }); - parsed_response.insert("org.iso.18013.5.1".to_string(), core_namespace); + parsed_response.insert( + "org.iso.18013.5.1".to_string(), + serde_json::to_value(core_namespace)?, + ); if let Some(aamva_response) = namespaces.remove("org.iso.18013.5.1.aamva") { aamva_response @@ -235,10 +342,141 @@ impl SessionManager { } }); - parsed_response.insert("org.iso.18013.5.1.aamva".to_string(), aamva_namespace); + parsed_response.insert( + "org.iso.18013.5.1.aamva".to_string(), + serde_json::to_value(aamva_namespace)?, + ); + } + + let mut validated_response = ValidatedResponse { + response: parsed_response, + issuer_authentication: Status::Unchecked, + device_authentication: Status::Unchecked, + errors: ValidationErrors(BTreeMap::new()), + }; + + let certificate_errors = + validate_x5chain(x5chain.to_owned(), self.trust_anchor_registry.clone()); + + match certificate_errors { + Ok(r) => { + validated_response + .errors + .0 + .insert("certificate_errors".to_string(), r); + let valid_issuer_authentication = + issuer_authentication(x5chain.clone(), issuer_signed); + match valid_issuer_authentication { + Ok(_r) => { + validated_response.issuer_authentication = Status::Valid; + } + Err(e) => { + validated_response.issuer_authentication = Status::Invalid; + validated_response + .errors + .0 + .insert("issuer_authentication_errors".to_string(), vec![e]); + } + } + } + Err(_e) => validated_response.issuer_authentication = Status::Invalid, } - Ok(parsed_response) + let valid_device_authentication = device_authentication(mso, document, session_transcript); + match valid_device_authentication { + Ok(_r) => { + validated_response.device_authentication = Status::Valid; + } + Err(e) => { + validated_response.device_authentication = Status::Invalid; + validated_response + .errors + .0 + .insert("device_authentication_errors".to_string(), vec![e]); + } + } + + Ok(validated_response) + } +} + +pub fn find_anchor( + leaf_certificate: CertificateInner, + trust_anchor_registry: Option, +) -> Result, Error> { + let leaf_issuer = leaf_certificate.tbs_certificate.issuer; + + let Some(root_certificates) = trust_anchor_registry else { + return Ok(None); + }; + let Some(trust_anchor) = root_certificates + .certificates + .into_iter() + .find(|trust_anchor| match trust_anchor { + TrustAnchor::Iaca(certificate) => { + match x509_cert::Certificate::from_der(&certificate.bytes) { + Ok(root_cert) => root_cert.tbs_certificate.subject == leaf_issuer, + Err(_) => false, + } + } + TrustAnchor::Custom(certificate, _ruleset) => { + match x509_cert::Certificate::from_der(&certificate.bytes) { + Ok(root_cert) => root_cert.tbs_certificate.subject == leaf_issuer, + Err(_) => false, + } + } + TrustAnchor::Aamva(certificate) => { + match x509_cert::Certificate::from_der(&certificate.bytes) { + Ok(root_cert) => root_cert.tbs_certificate.subject == leaf_issuer, + Err(_) => false, + } + } + }) + else { + return Err(Error::MdocAuth( + "The certificate issuer does not match any known trusted issuer".to_string(), + )); + }; + Ok(Some(trust_anchor)) +} + +// In 18013-5 the TrustAnchorRegistry is also referred to as the Verified Issuer Certificate Authority List (VICAL) +pub fn validate_x5chain( + x5chain: CborValue, + trust_anchor_registry: Option, +) -> Result, Error> { + match x5chain { + CborValue::Bytes(bytes) => { + let chain: Vec = vec![X509 { + bytes: serde_cbor::from_slice(&bytes)?, + }]; + let x5chain = X5Chain::from(NonEmptyVec::try_from(chain)?); + x5chain.validate(trust_anchor_registry) + } + CborValue::Array(x509s) => { + let mut chain = vec![]; + for x509 in x509s { + match x509 { + CborValue::Bytes(bytes) => { + chain.push(X509{bytes: serde_cbor::from_slice(&bytes)?}) + + }, + _ => return Err(Error::MdocAuth(format!("Expecting x509 certificate in the x5chain to be a cbor encoded bytestring, but received: {:?}", x509))) + } + } + + if !has_unique_elements(chain.clone()) { + return Err(Error::MdocAuth( + "x5chain header contains at least one duplicate certificate".to_string(), + )); + } + + let x5chain = X5Chain::from(NonEmptyVec::try_from(chain)?); + x5chain.validate(trust_anchor_registry) + } + _ => { + Err(Error::MdocAuth(format!("Expecting x509 certificate in the x5chain to be a cbor encoded bytestring, but received: {:?}", x5chain))) + } } } @@ -297,10 +535,37 @@ fn _validate_request(namespaces: device_request::Namespaces) -> Result(iter: T) -> bool +where + T: IntoIterator, + T::Item: Eq + Hash, +{ + let mut uniq = HashSet::new(); + iter.into_iter().all(move |x| uniq.insert(x)) +} #[cfg(test)] -mod test { +pub mod test { use super::*; + use crate::presentation::reader::validate_x5chain; + use crate::{ + issuance::x5chain::X509, + presentation::trust_anchor::{TrustAnchor, TrustAnchorRegistry}, + }; + use anyhow::anyhow; + + static IACA_ROOT: &[u8] = include_bytes!("../../test/presentation/isomdl_iaca_root_cert.pem"); + //TODO fix this cert to contain issuer alternative name + static IACA_INTERMEDIATE: &[u8] = + include_bytes!("../../test/presentation/isomdl_iaca_intermediate.pem"); + // signed by the intermediate certificate + //TODO fix this cert to contain issuer alternative name + static IACA_LEAF_SIGNER: &[u8] = + include_bytes!("../../test/presentation/isomdl_iaca_leaf_signer.pem"); + // signed directly by the root certificate + static IACA_SIGNER: &[u8] = include_bytes!("../../test/presentation/isomdl_iaca_signer.pem"); + static INCORRECT_IACA_SIGNER: &[u8] = + include_bytes!("../../test/presentation/isomdl_incorrect_iaca_signer.pem"); #[test] fn nested_response_values() { @@ -326,4 +591,76 @@ mod test { ); assert_eq!(json, expected) } + #[test] + fn validate_x509_with_trust_anchor() { + let root_bytes = pem_rfc7468::decode_vec(IACA_ROOT) + .map_err(|e| anyhow!("unable to parse pem: {}", e)) + .unwrap() + .1; + let trust_anchor = TrustAnchor::Iaca(X509 { bytes: root_bytes }); + let trust_anchor_registry = TrustAnchorRegistry { + certificates: vec![trust_anchor], + }; + let bytes = pem_rfc7468::decode_vec(IACA_SIGNER) + .map_err(|e| anyhow!("unable to parse pem: {}", e)) + .unwrap() + .1; + let x5chain: serde_cbor::Value = + serde_cbor::Value::Bytes(serde_cbor::to_vec(&bytes).unwrap()); + + let result = validate_x5chain(x5chain, Some(trust_anchor_registry)); + println!("result: {:?}", result) + } + + #[test] + fn validate_incorrect_x509_with_trust_anchor() { + let root_bytes = pem_rfc7468::decode_vec(IACA_ROOT) + .map_err(|e| anyhow!("unable to parse pem: {}", e)) + .unwrap() + .1; + let trust_anchor = TrustAnchor::Iaca(X509 { bytes: root_bytes }); + let trust_anchor_registry = TrustAnchorRegistry { + certificates: vec![trust_anchor], + }; + let bytes = pem_rfc7468::decode_vec(INCORRECT_IACA_SIGNER) + .map_err(|e| anyhow!("unable to parse pem: {}", e)) + .unwrap() + .1; + let x5chain: serde_cbor::Value = + serde_cbor::Value::Bytes(serde_cbor::to_vec(&bytes).unwrap()); + + let result = validate_x5chain(x5chain, Some(trust_anchor_registry)); + println!("result: {:?}", result) + } + + #[test] + fn validate_x5chain_with_trust_anchor() { + let root_bytes = pem_rfc7468::decode_vec(IACA_ROOT) + .map_err(|e| anyhow!("unable to parse pem: {}", e)) + .unwrap() + .1; + let trust_anchor = TrustAnchor::Iaca(X509 { bytes: root_bytes }); + let trust_anchor_registry = TrustAnchorRegistry { + certificates: vec![trust_anchor], + }; + + let intermediate_bytes = pem_rfc7468::decode_vec(IACA_INTERMEDIATE) + .map_err(|e| anyhow!("unable to parse pem: {}", e)) + .unwrap() + .1; + + let leaf_signer_bytes = pem_rfc7468::decode_vec(IACA_LEAF_SIGNER) + .map_err(|e| anyhow!("unable to parse pem: {}", e)) + .unwrap() + .1; + + let intermediate_b = + serde_cbor::Value::Bytes(serde_cbor::to_vec(&intermediate_bytes).unwrap()); + let leaf_signer_b = + serde_cbor::Value::Bytes(serde_cbor::to_vec(&leaf_signer_bytes).unwrap()); + + let x5chain = serde_cbor::Value::Array(vec![leaf_signer_b, intermediate_b]); + let result = validate_x5chain(x5chain, Some(trust_anchor_registry)); + println!("result: {:?}", result) + } } diff --git a/src/presentation/trust_anchor.rs b/src/presentation/trust_anchor.rs new file mode 100644 index 00000000..408c4328 --- /dev/null +++ b/src/presentation/trust_anchor.rs @@ -0,0 +1,546 @@ +use crate::issuance::x5chain::{check_signature, X509}; +use crate::presentation::reader::Error; +use asn1_rs::{Any, BitString, FromDer, Oid, SequenceOf}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use time::OffsetDateTime; +use x509_cert::attr::AttributeTypeAndValue; +use x509_cert::certificate::CertificateInner; +use x509_cert::ext::Extension; +use x509_cert::{der::Decode, Certificate}; +// -- DISTINGUISHED NAMES AND OID OVERVIEW -- // +// CN commonName 2.5.4.3 +// SN surname 2.5.4.4 +// SERIALNUMBER serialNumber 2.5.4.5 +// C countryName 2.5.4.6 +// L localityName 2.5.4.7 +// ST or S stateOrProvinceName 2.5.4.8 +// STREET streetAddress 2.5.4.9 +// O organizationName 2.5.4.10 +// OU organizationalUnit 2.5.4.11 +// T or TITLE title 2.5.4.12 +// G or GN givenName 2.5.4.42 +// initials initials 2.5.4.43 +// generationQualifier generation qualifier 2.5.4.44 +// dnQualifier distinguished name qualifier 2.5.4.46 +// pseudonym pseudonym 2.5.4.65 + +// -- IACA/AAMVA DISALLOWED EXTENSION OIDs -- // +// Policy Mappings 2.5.29.33 +// NameConstraints 2.5.29.30 +// PolicyConstraints 2.5.29.36 +// InhibitAnyPolicy 2.5.29.54 +// FreshestCRL 2.5.29.46 + +// -- IACA X509 Extension OIDs -- // +const OID_KEY_USAGE: &str = "2.5.29.15"; +const OID_ISSUER_ALTERNATIVE_NAME: &str = "2.5.29.18"; +const OID_BASIC_CONSTRAINTS: &str = "2.5.29.19"; +const OID_CRL_DISTRIBUTION_POINTS: &str = "2.5.29.31"; +const OID_EXTENDED_KEY_USAGE: &str = "2.5.29.37"; + +// -- 18013-5 IACA SPECIFIC ROOT EXTENSION VALUE CHECKS -- // +// Key Usage: 5, 6 (keyCertSign, crlSign) +// Basic Constraints: Pathlen:0 +// CRL Distribution Points must have tag 0 +// Issuer Alternative Name must be of type rfc822Name or a URI (tag 1 and tag 6) + +// -- 18013-5 IACA SPECIFIC LEAF EXTENSION VALUE CHECKS -- // +// Extended Key Usage: 1.0.18013.5.1.2 +// Key Usage: 0 (digitalSignature) +// CRL Distribution Points must have tag 0 +// Issuer Alternative Name must be of type rfc822Name or a URI (tag 1 and tag 6) +// + +const EXTENDED_KEY_USAGE: &str = "1.0.18013.5.1.2"; +const ROOT_KEY_USAGE: [usize; 2] = [5, 6]; +const LEAF_KEY_USAGE: [usize; 1] = [0]; +const BASIC_CONSTRAINTS: [u32; 1] = [0]; +const CRL_DISTRIBUTION_POINT: [u32; 1] = [0]; +const ISSUER_ALTERNATIVE_NAME: [u32; 2] = [1, 6]; + +#[derive(Serialize, Deserialize, Clone)] +pub enum TrustAnchor { + Iaca(X509), + Aamva(X509), + Custom(X509, ValidationRuleSet), +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct ValidationRuleSet { + pub distinguished_names: Vec, + #[serde(rename = "type")] + pub typ: RuleSetType, +} + +#[derive(Serialize, Deserialize, Clone)] +pub enum RuleSetType { + IACA, + AAMVA, + Custom, + ReaderAuth, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct TrustAnchorRegistry { + pub certificates: Vec, +} + +fn iaca_disallowed_x509_extensions() -> Vec { + vec![ + "2.5.29.30".to_string(), + "2.5.29.33".to_string(), + "2.5.29.36".to_string(), + "2.5.29.46".to_string(), + "2.5.29.54".to_string(), + ] +} + +pub fn iaca_root_extension_rules() -> BTreeMap> { + BTreeMap::from([ + ("2.5.29.15".to_string(), vec![5, 6]), + ("2.5.29.19".to_string(), vec![0]), + ("2.5.29.18".to_string(), vec![1, 6]), + ("2.5.29.37".to_string(), vec![0]), + ]) +} + +pub fn validate_with_trust_anchor( + leaf_x509: X509, + trust_anchor: TrustAnchor, +) -> Result, Error> { + let leaf_certificate = x509_cert::Certificate::from_der(&leaf_x509.bytes)?; + let mut results: Vec = vec![]; + match trust_anchor { + //TODO: AAMVA TrustAnchor rules + TrustAnchor::Iaca(certificate) => { + // 18013-5 specifies checks that shall be performed for IACA certificates + let rule_set = ValidationRuleSet { + distinguished_names: vec!["2.5.4.6".to_string(), "2.5.4.8".to_string()], + typ: RuleSetType::IACA, + }; + + let root_certificate = x509_cert::Certificate::from_der(&certificate.bytes)?; + results.append(&mut apply_ruleset( + leaf_certificate, + root_certificate.clone(), + rule_set, + )?); + check_validity_period(&root_certificate)?; + check_signature(&leaf_x509, &certificate)?; + Ok(results) + } + TrustAnchor::Aamva(certificate) => { + //The Aamva ruleset follows the IACA ruleset, but makes the ST value mandatory + let rule_set = ValidationRuleSet { + distinguished_names: vec!["2.5.4.6".to_string(), "2.5.4.8".to_string()], + typ: RuleSetType::IACA, + }; + let root_certificate = x509_cert::Certificate::from_der(&certificate.bytes)?; + results.append(&mut apply_ruleset( + leaf_certificate, + root_certificate.clone(), + rule_set, + )?); + check_validity_period(&root_certificate)?; + check_signature(&leaf_x509, &certificate)?; + Ok(results) + } + TrustAnchor::Custom(certificate, _ruleset) => { + let _root_certificate = x509_cert::Certificate::from_der(&certificate.bytes)?; + Ok(results) + } + } +} + +pub fn check_validity_period(certificate: &Certificate) -> Result<(), Error> { + let validity = certificate.tbs_certificate.validity; + if validity.not_after.to_unix_duration().as_secs() + < OffsetDateTime::now_utc().unix_timestamp() as u64 + { + return Err(Error::MdocAuth(format!( + "Expired certificate with subject: {:?}", + certificate.tbs_certificate.subject + ))); + }; + if validity.not_before.to_unix_duration().as_secs() + > OffsetDateTime::now_utc().unix_timestamp() as u64 + { + return Err(Error::MdocAuth(format!( + "Not yet valid certificate with subject: {:?}", + certificate.tbs_certificate.subject + ))); + }; + + Ok(()) +} + +fn apply_ruleset( + leaf_certificate: CertificateInner, + root_certificate: CertificateInner, + rule_set: ValidationRuleSet, +) -> Result, Error> { + let root_distinguished_names: Vec = root_certificate + .tbs_certificate + .subject + .0 + .into_iter() + .map(|rdn| { + rdn.0 + .into_vec() + .into_iter() + .filter(|atv| { + rule_set + .distinguished_names + .iter() + .any(|oid| oid == &atv.oid.to_string()) + }) + .collect::>() + }) + .collect::>>() + .into_iter() + .flatten() + .collect(); + + let leaf_distinguished_names: Vec = leaf_certificate + .tbs_certificate + .issuer + .0 + .into_iter() + .map(|r| { + r.0.into_vec() + .into_iter() + .filter(|atv| { + rule_set + .distinguished_names + .iter() + .any(|oid| oid == &atv.oid.to_string()) + }) + .collect::>() + }) + .collect::>>() + .into_iter() + .flatten() + .collect(); + + // fix this + if root_distinguished_names.len() != rule_set.distinguished_names.len() { + return Err(Error::MdocAuth("The congifured validation ruleset requires a distinguished name that is not found in the submitted root certificate".to_string())); + } + + let Some(root_extensions) = root_certificate.tbs_certificate.extensions else { + return Err(Error::MdocAuth( + "The root certificate is expected to have extensions, but none were found".to_string(), + )); + }; + + let Some(leaf_extensions) = leaf_certificate.tbs_certificate.extensions else { + return Err(Error::MdocAuth( + "The signer certificate is expected to have extensions, but none were found" + .to_string(), + )); + }; + + match rule_set.typ { + RuleSetType::IACA => { + let mut root_extension_errors = validate_iaca_root_extensions(root_extensions)?; + let mut signer_extension_errors = validate_iaca_signer_extensions(leaf_extensions)?; + root_extension_errors.append(&mut signer_extension_errors); + for dn in leaf_distinguished_names { + let disallowed = iaca_disallowed_x509_extensions(); + if let Some(disallowed_extension) = + disallowed.iter().find(|oid| dn.oid.to_string() == **oid) + { + return Err(Error::MdocAuth(format!("The extension with oid: {:?} is not allowed in the IACA certificate profile", disallowed_extension))); + } + + //Under the IACA ruleset, the values for S or ST should be the same in subject and issuer if they are present in both + if dn.oid.to_string() == *"2.5.4.8" { + let state_or_province = + root_distinguished_names.iter().find(|r| r.oid == dn.oid); + if let Some(st_or_s) = state_or_province { + if dn != *st_or_s { + return Err(Error::MdocAuth(format!("Mismatch between supplied certificate issuer attribute: {:?} and the trust anchor registry.", dn))); + } + } + } else { + let Some(_root_dn) = root_distinguished_names.iter().find(|r| r == &&dn) else { + return Err(Error::MdocAuth(format!("Mismatch between supplied certificate issuer attribute: {:?} and the trust anchor registry.", dn.value))); + }; + } + } + Ok(root_extension_errors) + } + RuleSetType::AAMVA => { + let mut root_extension_errors = validate_iaca_root_extensions(root_extensions)?; + let mut signer_extension_errors = validate_iaca_signer_extensions(leaf_extensions)?; + root_extension_errors.append(&mut signer_extension_errors); + for dn in leaf_distinguished_names { + let disallowed = iaca_disallowed_x509_extensions(); + if let Some(disallowed_extension) = + disallowed.iter().find(|oid| dn.oid.to_string() == **oid) + { + return Err(Error::MdocAuth(format!("The extension with oid: {:?} is not allowed in the IACA certificate profile", disallowed_extension))); + } + + let Some(_root_dn) = root_distinguished_names.iter().find(|r| r == &&dn) else { + return Err(Error::MdocAuth(format!("Mismatch between supplied certificate issuer attribute: {:?} and the trust anchor registry.", dn.value))); + }; + } + Ok(root_extension_errors) + } + RuleSetType::Custom => { + //TODO + Err(Error::MdocAuth("Unimplemented ruleset".to_string())) + } + RuleSetType::ReaderAuth => { + //TODO + Err(Error::MdocAuth("Unimplemented ruleset".to_string())) + } + } +} + +pub fn validate_iaca_root_extensions(root_extensions: Vec) -> Result, Error> { + let disallowed = iaca_disallowed_x509_extensions(); + let mut errors: Vec = vec![]; + for extension in root_extensions.clone() { + if let Some(disallowed_extension) = disallowed + .iter() + .find(|oid| extension.extn_id.to_string() == **oid) + { + errors.push(Error::MdocAuth(format!( + "The extension with oid: {:?} is not allowed in the IACA certificate profile", + disallowed_extension + ))); + } + } + + let root_crit_extensions: Vec<&Extension> = + root_extensions.iter().filter(|ext| ext.critical).collect(); + + // Key Usage 2.5.29.15 + let Some(key_usage) = root_crit_extensions + .iter() + .find(|ext| ext.extn_id.to_string() == *OID_KEY_USAGE) + else { + return Err(Error::MdocAuth( + "The root certificate is expected to have its key usage limited to keyCertSign and crlSign, but no restrictions were specified".to_string(), + )); + }; + + let decoded_key_usage_value: (_, BitString) = + FromDer::from_der(key_usage.extn_value.as_bytes()) + .map_err(|e| Error::MdocAuth(e.to_string()))?; + let Some(bitslice) = decoded_key_usage_value.1.as_bitslice() else { + return Err(Error::MdocAuth( + "Error decoding extension value as a bitslice".to_string(), + )); + }; + + if bitslice.iter_ones().collect::>().as_slice() != ROOT_KEY_USAGE { + errors.push(Error::MdocAuth( + "the root certificate key usage extension is invalid".to_string(), + )); + } + + // Basic Constraints 2.5.29.19 + let Some(basic_constraints) = root_crit_extensions + .iter() + .find(|ext| ext.extn_id.to_string() == *OID_BASIC_CONSTRAINTS) + else { + return Err(Error::MdocAuth( + "The root certificate is expected to have critical basic constraints specificied, but the extensions was not found".to_string() + )); + }; + + let decoded_basic_constraints: (_, SequenceOf) = + FromDer::from_der(basic_constraints.extn_value.as_bytes()) + .map_err(|e| Error::MdocAuth(e.to_string()))?; + let mut iter = decoded_basic_constraints.1.iter(); + let Some(ca) = iter.next() else { + return Err(Error::MdocAuth( + "The root certificate is expected to contain CA=true in the Basic Constraints, but found an empty sequence".to_string() + )); + }; + + if !ca.as_boolean()?.bool() { + errors.push(Error::MdocAuth(format!("The root certificate is expected to contain Basic Constraints CA=true, but found: {:?}", ca))); + } + let Some(path_len) = iter.next() else { + return Err(Error::MdocAuth("The root certificate is expected to contain pathLen:0 in the Basic Constraints, but it was not found".to_string())); + }; + + if [path_len.as_integer()?.as_u32()?] != BASIC_CONSTRAINTS { + errors.push(Error::MdocAuth(format!("The root certificate is expected to contain Basic Constraints pathLen=0, but found: {:?}", path_len))); + } + + //CRL Distribution Points 2.5.29.31 + let Some(crl_distribution_point) = root_extensions + .iter() + .find(|ext| ext.extn_id.to_string() == *OID_CRL_DISTRIBUTION_POINTS) + else { + return Err(Error::MdocAuth("The root certificate is expected to have a crl distribution point specificied, but the extensions was not found".to_string())); + }; + + let crl_dp: (_, SequenceOf) = + FromDer::from_der(crl_distribution_point.extn_value.as_bytes()) + .map_err(|e| Error::MdocAuth(e.to_string()))?; + let Some(distribution_points) = crl_dp.1.iter().next() else { + return Err(Error::MdocAuth( + "The root certificate is expected to have a crl distribution point specificied, but the extension value was not found" + .to_string(), + )); + }; + let dp: (_, Any) = + FromDer::from_der(distribution_points.data).map_err(|e| Error::MdocAuth(e.to_string()))?; + if dp.1.tag().0 != 0 { + errors.push(Error::MdocAuth( + "reason and crlIssuer fields shall not be used in the crl distribution point" + .to_string(), + )); + } + + // Issuer Alternative Name 2.5.29.18 + let Some(issuer_alternative_name) = root_extensions + .iter() + .find(|ext| ext.extn_id.to_string() == *OID_ISSUER_ALTERNATIVE_NAME) + else { + return Err(Error::MdocAuth( + "The root certificate is expected to have issuer alternative name specificied, but the extensions was not found".to_string() + )); + }; + + let ian: (_, SequenceOf) = + FromDer::from_der(issuer_alternative_name.extn_value.as_bytes()) + .map_err(|e| Error::MdocAuth(e.to_string()))?; + for item in ian.1.iter() { + if item.tag().0 != 1 && item.tag().0 != 6 { + errors.push(Error::MdocAuth(format!( + "issuer alternative name is expected to be an rfc822name or a URI, but found: {:?}", + item + ))); + } + } + + Ok(errors) +} + +pub fn validate_iaca_signer_extensions( + leaf_extensions: Vec, +) -> Result, Error> { + let disallowed = iaca_disallowed_x509_extensions(); + let mut errors: Vec = vec![]; + for extension in leaf_extensions.clone() { + if let Some(disallowed_extension) = disallowed + .iter() + .find(|oid| extension.extn_id.to_string() == **oid) + { + errors.push(Error::MdocAuth(format!( + "The extension with oid: {:?} is not allowed in the IACA certificate profile", + disallowed_extension + ))); + } + } + + let leaf_crit_extensions: Vec<&Extension> = + leaf_extensions.iter().filter(|ext| ext.critical).collect(); + // Key Usage 2.5.29.15 + let Some(key_usage) = leaf_crit_extensions + .iter() + .find(|ext| ext.extn_id.to_string() == *OID_KEY_USAGE) + else { + return Err(Error::MdocAuth( + "Missing critical KeyUsage extension in the signer certificate".to_string(), + )); + }; + + let decoded_key_usage_value: (_, BitString) = + FromDer::from_der(key_usage.extn_value.as_bytes()) + .map_err(|e| Error::MdocAuth(e.to_string()))?; + let Some(bitslice) = decoded_key_usage_value.1.as_bitslice() else { + return Err(Error::MdocAuth( + "Error decoding extension value as a bitslice".to_string(), + )); + }; + + let leaf_key_usage_bit: Vec = bitslice.iter_ones().collect(); + if leaf_key_usage_bit.as_slice() != LEAF_KEY_USAGE { + errors.push(Error::MdocAuth( + "the signer certificate key usage extension is invalid".to_string(), + )); + } + + // Extended Key Usage 2.5.29.37 + let Some(extended_key_usage) = leaf_crit_extensions + .iter() + .find(|ext| ext.extn_id.to_string() == *OID_EXTENDED_KEY_USAGE) + else { + return Err(Error::MdocAuth( + "Missing critical ExtendedKeyUsage extension in the signer certificate".to_string(), + )); + }; + + let ext_ku: (_, SequenceOf) = FromDer::from_der(extended_key_usage.extn_value.as_bytes()) + .map_err(|e| Error::MdocAuth(e.to_string()))?; + let Some(eku) = ext_ku.1.iter().next() else { + return Err(Error::MdocAuth( + "missing critical ExtendedKeyUsage value".to_string(), + )); + }; + + if eku.to_id_string() != *EXTENDED_KEY_USAGE { + errors.push(Error::MdocAuth( + "Invalid value for Extended Key Usage in signer certificate to sign mDLs".to_string(), + )); + } + + //CRL Distribution Points 2.5.29.31 + let Some(crl_distribution_point) = leaf_extensions + .iter() + .find(|ext| ext.extn_id.to_string() == *OID_CRL_DISTRIBUTION_POINTS) + else { + return Err(Error::MdocAuth( + "The leaf certificate is expected to have a crl distribution point specificied, but the extensions was not found".to_string(), + )); + }; + + let crl_dp: (_, SequenceOf) = + FromDer::from_der(crl_distribution_point.extn_value.as_bytes()) + .map_err(|e| Error::MdocAuth(e.to_string()))?; + let Some(distribution_points) = crl_dp.1.iter().next() else { + return Err(Error::MdocAuth( + "The leaf certificate is expected to have a crl distribution point specificied, but the extensions was not found".to_string(), + )); + }; + + let dp: (_, Any) = + FromDer::from_der(distribution_points.data).map_err(|e| Error::MdocAuth(e.to_string()))?; + if !CRL_DISTRIBUTION_POINT.contains(&dp.1.tag().0) { + errors.push(Error::MdocAuth( + "reason and crlIssuer fields shall not be used in the crl distribution point" + .to_string(), + )); + } + + // Issuer Alternative Name 2.5.29.18 + let Some(issuer_alternative_name) = leaf_extensions + .iter() + .find(|ext| ext.extn_id.to_string() == *OID_ISSUER_ALTERNATIVE_NAME) + else { + return Err(Error::MdocAuth("The leaf certificate is expected to have issuer alternative name specificied, but the extensions was not found".to_string())); + }; + + let ian: (_, SequenceOf) = + FromDer::from_der(issuer_alternative_name.extn_value.as_bytes()) + .map_err(|e| Error::MdocAuth(e.to_string()))?; + for item in ian.1.iter() { + if !ISSUER_ALTERNATIVE_NAME.contains(&item.tag().0) { + errors.push(Error::MdocAuth(format!( + "issuer alternative name is expected to be an rfc822name or a URI, but found: {:?}", + item + ))); + } + } + + Ok(errors) +} diff --git a/test/presentation/isomdl_iaca_intermediate.pem b/test/presentation/isomdl_iaca_intermediate.pem new file mode 100644 index 00000000..2614d056 --- /dev/null +++ b/test/presentation/isomdl_iaca_intermediate.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICszCCAlmgAwIBAgIUDtWU4pfEqD+snpSJWqZdVCOmm1YwCgYIKoZIzj0EAwIw +YDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMRkwFwYDVQQKDBBJU09tREwgVGVz +dCBSb290MSkwJwYDVQQDDCBJU08xODAxMy01IFRlc3QgQ2VydGlmaWNhdGUgUm9v +dDAeFw0yMzEwMjcxMTU1MzVaFw0yNDEwMjYxMTU1MzVaMHAxCzAJBgNVBAYTAlVT +MQswCQYDVQQIDAJOWTEhMB8GA1UECgwYVGVzdCBJc3N1ZXIgSW50ZXJtZWRpYXRl +MTEwLwYDVQQDDChJU08xODAxMy01IFRlc3QgQ2VydGlmaWNhdGUgSW50ZXJtZWRp +YXRlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbPSWUqsMjJORnuuuY+bEnC0C +8snYSVoLjal5GDHI9EnFsQ61zEyzjXG5sTP9C8ZKtKDDwlC33oDuCyivTz4seKOB +4DCB3TAdBgNVHQ4EFgQUc6xThx1W03ausUIu7SVVJ9HzyK0wHwYDVR0jBBgwFoAU +NUaDsc9OHULyjxe+lYQX/ozL9xAwLwYJYIZIAYb4QgENBCIWIFNwcnVjZUlEIFRl +c3QgU2lnbmVyIENlcnRpZmljYXRlMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUBAf8E +CzAJBgcogYxdBQECMBIGA1UdEwEB/wQIMAYBAf8CAQAwLwYDVR0fBCgwJjAkoCKg +IIYeaHR0cHM6Ly9leGFtcGxlLmNvbS9JU09tREwuY3JsMAoGCCqGSM49BAMCA0gA +MEUCIQDxJWFzncbLUZHLC6I/oI7Pe9pxkzwbtnhM9Y0d0aUzxQIgFw4nCHkKcQ60 +R4Vyx3ylTXgs1RZfC/J/5IEGfA/x4pk= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/presentation/isomdl_iaca_leaf_signer.pem b/test/presentation/isomdl_iaca_leaf_signer.pem new file mode 100644 index 00000000..e804e69c --- /dev/null +++ b/test/presentation/isomdl_iaca_leaf_signer.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICwDCCAmagAwIBAgIUc/HqsvizevQlo9eQDVjNsnWgyAkwCgYIKoZIzj0EAwIw +cDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMSEwHwYDVQQKDBhUZXN0IElzc3Vl +ciBJbnRlcm1lZGlhdGUxMTAvBgNVBAMMKElTTzE4MDEzLTUgVGVzdCBDZXJ0aWZp +Y2F0ZSBJbnRlcm1lZGlhdGUwHhcNMjMxMDI3MTE1NzM1WhcNMjQxMDI2MTE1NzM1 +WjBrMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxIjAgBgNVBAoMGUlTT21ETCBU +ZXN0IElzc3VlciBTaWduZXIxKzApBgNVBAMMIklTTzE4MDEzLTUgVGVzdCBDZXJ0 +aWZpY2F0ZSBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQgaK6KmQ1m +WKt+Vo6ixfHxsmX9YlGAuUPkOvQ/uHrxgsZLC6FheRwtU3v+5GGkHD70FJNmz7DJ +UiR6G8TWMYZGo4HiMIHfMB0GA1UdDgQWBBQEpN0hSF6BFZJCDvZwASaa6ewoXzAf +BgNVHSMEGDAWgBRzrFOHHVbTdq6xQi7tJVUn0fPIrTAxBglghkgBhvhCAQ0EJBYi +SVNPMTgwMTMtNSBUZXN0IFNpZ25lciBDZXJ0aWZpY2F0ZTAOBgNVHQ8BAf8EBAMC +B4AwFQYDVR0lAQH/BAswCQYHKIGMXQUBAjASBgNVHRMBAf8ECDAGAQH/AgEAMC8G +A1UdHwQoMCYwJKAioCCGHmh0dHBzOi8vZXhhbXBsZS5jb20vSVNPbURMLmNybDAK +BggqhkjOPQQDAgNIADBFAiEA7eSLtsp2y/p9diZGAVzgc0evUbIi+HYAd70U5PUQ +2hoCIAa9cHJ5wB0Rz4VZqTAf1jNlUaWNo1p+tmYRbtAprJ0Q +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/presentation/isomdl_iaca_root_cert.pem b/test/presentation/isomdl_iaca_root_cert.pem new file mode 100644 index 00000000..497091cd --- /dev/null +++ b/test/presentation/isomdl_iaca_root_cert.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICWTCCAf+gAwIBAgIUOx5RMyn2LSq23/Ua8xK9VqYDWXowCgYIKoZIzj0EAwIw +YDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMRkwFwYDVQQKDBBJU09tREwgVGVz +dCBSb290MSkwJwYDVQQDDCBJU08xODAxMy01IFRlc3QgQ2VydGlmaWNhdGUgUm9v +dDAeFw0yMzEwMjcxMTMxMTFaFw0yNDEwMjYxMTMxMTFaMGAxCzAJBgNVBAYTAlVT +MQswCQYDVQQIDAJOWTEZMBcGA1UECgwQSVNPbURMIFRlc3QgUm9vdDEpMCcGA1UE +AwwgSVNPMTgwMTMtNSBUZXN0IENlcnRpZmljYXRlIFJvb3QwWTATBgcqhkjOPQIB +BggqhkjOPQMBBwNCAATsKrbaIJnKjZpyVuNq75Nv9oOMkq2ufxKclcOof1ab6ivr +mnKqA9BcIpA8sFM9DUz3KIRo7iRprmcWVBYuMBeTo4GWMIGTMB0GA1UdDgQWBBQ1 +RoOxz04dQvKPF76VhBf+jMv3EDASBgNVHRMBAf8ECDAGAQH/AgEAMC8GA1UdHwQo +MCYwJKAioCCGHmh0dHBzOi8vZXhhbXBsZS5jb20vSVNPbURMLmNybDAOBgNVHQ8B +Af8EBAMCAQYwHQYDVR0SBBYwFIESZXhhbXBsZUBpc29tZGwuY29tMAoGCCqGSM49 +BAMCA0gAMEUCIQCzFJlt0Wl03lBgOOuO184budY3dyqVb/xxdTKACl8hkAIgCCrb +qgXqMD7z4K9XyaGxUq7QhdMBKiE3rNS193xqGs8= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/test/presentation/isomdl_iaca_signer.pem b/test/presentation/isomdl_iaca_signer.pem new file mode 100644 index 00000000..38a4335d --- /dev/null +++ b/test/presentation/isomdl_iaca_signer.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICujCCAmGgAwIBAgIUDtWU4pfEqD+snpSJWqZdVCOmm1cwCgYIKoZIzj0EAwIw +YDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMRkwFwYDVQQKDBBJU09tREwgVGVz +dCBSb290MSkwJwYDVQQDDCBJU08xODAxMy01IFRlc3QgQ2VydGlmaWNhdGUgUm9v +dDAeFw0yMzExMTQxMDAxMDVaFw0yNDExMTMxMDAxMDVaMGsxCzAJBgNVBAYTAlVT +MQswCQYDVQQIDAJOWTEiMCAGA1UECgwZSVNPbURMIFRlc3QgSXNzdWVyIFNpZ25l +cjErMCkGA1UEAwwiSVNPMTgwMTMtNSBUZXN0IENlcnRpZmljYXRlIFNpZ25lcjBZ +MBMGByqGSM49AgEGCCqGSM49AwEHA0IABCBoroqZDWZYq35WjqLF8fGyZf1iUYC5 +Q+Q69D+4evGCxksLoWF5HC1Te/7kYaQcPvQUk2bPsMlSJHobxNYxhkajge0wgeow +HQYDVR0OBBYEFASk3SFIXoEVkkIO9nABJprp7ChfMB8GA1UdIwQYMBaAFDVGg7HP +Th1C8o8XvpWEF/6My/cQMDEGCWCGSAGG+EIBDQQkFiJJU08xODAxMy01IFRlc3Qg +U2lnbmVyIENlcnRpZmljYXRlMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUBAf8ECzAJ +BgcogYxdBQECMB0GA1UdEgQWMBSBEmV4YW1wbGVAaXNvbWRsLmNvbTAvBgNVHR8E +KDAmMCSgIqAghh5odHRwczovL2V4YW1wbGUuY29tL0lTT21ETC5jcmwwCgYIKoZI +zj0EAwIDRwAwRAIgZXkJDQQUaMXNUFKpZ9o9VQzOJ6xxVDhb6XHDlurucIECIF/U +P1I94DTlJ1/SJYdqdbc3QUG1LsjPld1IpZPYiowo +-----END CERTIFICATE----- diff --git a/test/presentation/isomdl_incorrect_iaca_signer.pem b/test/presentation/isomdl_incorrect_iaca_signer.pem new file mode 100644 index 00000000..0abbe0a1 --- /dev/null +++ b/test/presentation/isomdl_incorrect_iaca_signer.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICsTCCAlagAwIBAgIUDtWU4pfEqD+snpSJWqZdVCOmm1UwCgYIKoZIzj0EAwIw +YDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMRkwFwYDVQQKDBBJU09tREwgVGVz +dCBSb290MSkwJwYDVQQDDCBJU08xODAxMy01IFRlc3QgQ2VydGlmaWNhdGUgUm9v +dDAeFw0yMzEwMjcxMTUzMjRaFw0yNDEwMjYxMTUzMjRaMGsxCzAJBgNVBAYTAlVT +MQswCQYDVQQIDAJOWTEiMCAGA1UECgwZSVNPbURMIFRlc3QgSXNzdWVyIFNpZ25l +cjErMCkGA1UEAwwiSVNPMTgwMTMtNSBUZXN0IENlcnRpZmljYXRlIFNpZ25lcjBZ +MBMGByqGSM49AgEGCCqGSM49AwEHA0IABCBoroqZDWZYq35WjqLF8fGyZf1iUYC5 +Q+Q69D+4evGCxksLoWF5HC1Te/7kYaQcPvQUk2bPsMlSJHobxNYxhkajgeIwgd8w +HQYDVR0OBBYEFASk3SFIXoEVkkIO9nABJprp7ChfMB8GA1UdIwQYMBaAFDVGg7HP +Th1C8o8XvpWEF/6My/cQMDEGCWCGSAGG+EIBDQQkFiJJU08xODAxMy01IFRlc3Qg +U2lnbmVyIENlcnRpZmljYXRlMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUBAf8ECzAJ +BgcogYxdBQECMBIGA1UdEwEB/wQIMAYBAf8CAQAwLwYDVR0fBCgwJjAkoCKgIIYe +aHR0cHM6Ly9leGFtcGxlLmNvbS9JU09tREwuY3JsMAoGCCqGSM49BAMCA0kAMEYC +IQD7jaJK++LzevufF2oLiE1d9GCoij7ibBqDaGSI1SkQSQIhAMBs/ErQQkFADiep +5lN3kt8xhZrrFxxqHTTisJq5v+qW +-----END CERTIFICATE----- \ No newline at end of file From b1ec197bf82e9ad59553c87ebb9ee6b02379d4d3 Mon Sep 17 00:00:00 2001 From: Arjen van Veen Date: Tue, 28 Nov 2023 17:01:20 +0100 Subject: [PATCH 02/12] parent 4e7fc9f0f11991114ac197b2d374c82ac7db5253 author Arjen van Veen 1701187280 +0100 committer Arjen van Veen 1717744754 +0200 refactor for readability WIP clean up comments and duplicates clean up, add some comments cargo fmt clippy fix fmt assert on tests address pr comments refactor handle_response to return a validated_response, submit parsing and decryption errors under errors support creating a trust_anchor_registry from pem strings Fix x5chain encoding. X5Chain decoding fixes and version checking Improve reader validation code. - Also add a CLI tool for validating issuer certificates. Fix public key parsing Feat/reader auth cn (#79) * rebase onto feat/mdoc-auth * rebase and use mdoc-auth functions * wip experiment with cert building * small clean up * Fix inconsistency. (#78) * validated request improvements --------- Co-authored-by: Jacob remove duplicate code clippy fix --- Cargo.toml | 14 +- src/bin/utils.rs | 84 +++ src/bin/x509/mod.rs | 41 ++ src/definitions/device_engagement.rs | 2 +- src/definitions/device_engagement/error.rs | 2 +- src/definitions/mod.rs | 4 + src/definitions/validated_request.rs | 18 + src/definitions/validated_response.rs | 23 + src/definitions/x509/error.rs | 63 +++ src/definitions/x509/extensions.rs | 381 ++++++++++++++ src/definitions/x509/mod.rs | 6 + src/definitions/x509/trust_anchor.rs | 379 ++++++++++++++ src/definitions/x509/x5chain.rs | 304 +++++++++++ src/issuance/mdoc.rs | 2 +- src/issuance/mod.rs | 2 - src/issuance/x5chain.rs | 277 ---------- src/main.rs | 48 -- src/presentation/device.rs | 193 +++++-- src/presentation/mdoc_auth.rs | 66 +-- src/presentation/mod.rs | 1 - src/presentation/reader.rs | 563 ++++++++++----------- src/presentation/trust_anchor.rs | 546 -------------------- src/x509/mod.rs | 0 test/presentation/reader_auth.pem | 16 + test/presentation/reader_key.pem | 5 + 25 files changed, 1784 insertions(+), 1256 deletions(-) create mode 100644 src/bin/utils.rs create mode 100644 src/bin/x509/mod.rs create mode 100644 src/definitions/validated_request.rs create mode 100644 src/definitions/validated_response.rs create mode 100644 src/definitions/x509/error.rs create mode 100644 src/definitions/x509/extensions.rs create mode 100644 src/definitions/x509/mod.rs create mode 100644 src/definitions/x509/trust_anchor.rs create mode 100644 src/definitions/x509/x5chain.rs delete mode 100644 src/issuance/x5chain.rs delete mode 100644 src/main.rs delete mode 100644 src/presentation/trust_anchor.rs create mode 100644 src/x509/mod.rs create mode 100644 test/presentation/reader_auth.pem create mode 100644 test/presentation/reader_key.pem diff --git a/Cargo.toml b/Cargo.toml index 8a2c1fb2..d8409e58 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,10 +8,14 @@ homepage = "https://github.com/spruceid/isomdl" repository = "https://github.com/spruceid/isomdl" license = "Apache-2.0" exclude = ["test/"] - + +[[bin]] +name = "isomdl-utils" +path = "src/bin/utils.rs" + [dependencies] anyhow = "1.0" -ecdsa = { version = "0.16.0", features = ["serde"] } +ecdsa = { version = "0.16.0", features = ["serde","verifying"] } p256 = { version = "0.13.0", features = ["serde", "ecdh"] } p384 = { version = "0.13.0", features = ["serde", "ecdh"] } rand = { version = "0.8.5", features = ["getrandom"] } @@ -35,15 +39,15 @@ async-signature = "0.3.0" #tracing = "0.1" base64 = "0.13" pem-rfc7468 = "0.7.0" -x509-cert = {version = "0.2.3", features = ["std"]} -const-oid = "0.9.2" +x509-cert = { version = "0.2.4", features = ["pem", "builder"] } ssi-jwk = { version = "0.1" } isomdl-macros = { version = "0.1.0", path = "macros" } clap = { version = "4", features = ["derive"] } clap-stdin = "0.2.1" +const-oid = "0.9.2" der = { version = "0.7", features = ["std", "derive", "alloc"] } hex = "0.4.3" -asn1-rs = { version = "0.5.2", features = ["bits"]} +asn1-rs = { version = "0.5.2", features = ["bits"] } [dependencies.cose-rs] git = "https://github.com/spruceid/cose-rs" diff --git a/src/bin/utils.rs b/src/bin/utils.rs new file mode 100644 index 00000000..72c712cf --- /dev/null +++ b/src/bin/utils.rs @@ -0,0 +1,84 @@ +use std::{collections::BTreeMap, fs::File, io::Read, path::PathBuf}; + +use anyhow::{Context, Error, Ok}; +use clap::Parser; +use clap_stdin::MaybeStdin; +use isomdl::presentation::{device::Document, Stringify}; + +mod x509; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + #[command(subcommand)] + action: Action, +} + +#[derive(Debug, clap::Subcommand)] +enum Action { + /// Print the namespaces and element identifiers used in an mDL. + GetNamespaces { + /// Base64 encoded mDL in the format used in the issuance module of this crate. + mdl: MaybeStdin, + }, + /// Validate a document signer cert against a possible root certificate. + ValidateCerts { + /// Validation rule set. + rules: RuleSet, + /// Path to PEM-encoded document signer cert. + ds: PathBuf, + /// Path to PEM-encoded IACA root cert. + root: PathBuf, + }, +} + +#[derive(Debug, Clone, Copy, clap::ValueEnum)] +enum RuleSet { + Iaca, + Aamva, + NamesOnly, +} + +fn main() -> Result<(), Error> { + match Args::parse().action { + Action::GetNamespaces { mdl } => print_namespaces(mdl.to_string()), + Action::ValidateCerts { rules, ds, root } => validate_certs(rules, ds, root), + } +} + +fn print_namespaces(mdl: String) -> Result<(), Error> { + let claims = Document::parse(mdl) + .context("could not parse mdl")? + .namespaces + .into_inner() + .into_iter() + .map(|(ns, inner)| (ns, inner.into_inner().into_keys().collect())) + .collect::>>(); + println!("{}", serde_json::to_string_pretty(&claims)?); + Ok(()) +} + +fn validate_certs(rules: RuleSet, ds: PathBuf, root: PathBuf) -> Result<(), Error> { + let mut ds_bytes = vec![]; + File::open(ds)?.read_to_end(&mut ds_bytes)?; + let mut root_bytes = vec![]; + File::open(root)?.read_to_end(&mut root_bytes)?; + let validation_errors = x509::validate(rules, &ds_bytes, &root_bytes)?; + if validation_errors.is_empty() { + println!("Validated!"); + } else { + println!( + "Validation errors:\n{}", + serde_json::to_string_pretty(&validation_errors)? + ) + } + Ok(()) +} + +#[cfg(test)] +mod test { + #[test] + fn print_namespaces() { + super::print_namespaces(include_str!("../../test/stringified-mdl.txt").to_string()).unwrap() + } +} diff --git a/src/bin/x509/mod.rs b/src/bin/x509/mod.rs new file mode 100644 index 00000000..827d21c5 --- /dev/null +++ b/src/bin/x509/mod.rs @@ -0,0 +1,41 @@ +use anyhow::anyhow; +use isomdl::definitions::x509::{ + error::Error as X509Error, + trust_anchor::{RuleSetType, TrustAnchor, TrustAnchorRegistry, ValidationRuleSet}, + x5chain::X509, + X5Chain, +}; + +use crate::RuleSet; + +pub fn validate( + rules: RuleSet, + signer: &[u8], + root: &[u8], +) -> Result, anyhow::Error> { + let root_bytes = pem_rfc7468::decode_vec(root) + .map_err(|e| anyhow!("unable to parse pem: {}", e))? + .1; + + let ruleset = ValidationRuleSet { + distinguished_names: vec!["2.5.4.6".to_string(), "2.5.4.8".to_string()], + typ: match rules { + RuleSet::Iaca => RuleSetType::IACA, + RuleSet::Aamva => RuleSetType::AAMVA, + RuleSet::NamesOnly => RuleSetType::NamesOnly, + }, + }; + + let trust_anchor = TrustAnchor::Custom(X509 { bytes: root_bytes }, ruleset); + let trust_anchor_registry = TrustAnchorRegistry { + certificates: vec![trust_anchor], + }; + let bytes = pem_rfc7468::decode_vec(signer) + .map_err(|e| anyhow!("unable to parse pem: {}", e))? + .1; + let x5chain_cbor: serde_cbor::Value = serde_cbor::Value::Bytes(bytes); + + let x5chain = X5Chain::from_cbor(x5chain_cbor)?; + + Ok(x5chain.validate(Some(trust_anchor_registry))) +} diff --git a/src/definitions/device_engagement.rs b/src/definitions/device_engagement.rs index 5f7bf7c8..5302ae93 100644 --- a/src/definitions/device_engagement.rs +++ b/src/definitions/device_engagement.rs @@ -122,7 +122,7 @@ impl TryFrom for DeviceEngagement { if let CborValue::Map(mut map) = v { let device_engagement_version = map.remove(&CborValue::Integer(0)); if let Some(CborValue::Text(v)) = device_engagement_version { - if v != "1.0" { + if !v.starts_with("1.") { return Err(Error::UnsupportedVersion); } } else { diff --git a/src/definitions/device_engagement/error.rs b/src/definitions/device_engagement/error.rs index 593cb6dc..76d6ed5e 100644 --- a/src/definitions/device_engagement/error.rs +++ b/src/definitions/device_engagement/error.rs @@ -5,7 +5,7 @@ use serde_cbor::Error as SerdeCborError; /// Errors that can occur when deserialising a DeviceEngagement. #[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)] pub enum Error { - #[error("Expected isomdl version 1.0")] + #[error("Expected isomdl major version 1")] UnsupportedVersion, #[error("Unsupported device retrieval method")] UnsupportedDRM, diff --git a/src/definitions/mod.rs b/src/definitions/mod.rs index f146dccf..0d1f20ca 100644 --- a/src/definitions/mod.rs +++ b/src/definitions/mod.rs @@ -9,7 +9,10 @@ pub mod mso; pub mod namespaces; pub mod session; pub mod traits; +pub mod validated_request; +pub mod validated_response; pub mod validity_info; +pub mod x509; pub use device_engagement::{ BleOptions, DeviceEngagement, DeviceRetrievalMethod, NfcOptions, Security, WifiOptions, @@ -22,4 +25,5 @@ pub use device_signed::{DeviceAuth, DeviceSigned}; pub use issuer_signed::{IssuerSigned, IssuerSignedItem}; pub use mso::{DigestAlgorithm, DigestId, DigestIds, Mso}; pub use session::{SessionData, SessionEstablishment, SessionTranscript180135}; +pub use validated_response::{Status, ValidatedResponse, ValidationErrors}; pub use validity_info::ValidityInfo; diff --git a/src/definitions/validated_request.rs b/src/definitions/validated_request.rs new file mode 100644 index 00000000..311662e8 --- /dev/null +++ b/src/definitions/validated_request.rs @@ -0,0 +1,18 @@ +use crate::{definitions::ValidationErrors, presentation::device::RequestedItems}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Default)] +pub struct ValidatedRequest { + pub items_requests: RequestedItems, + pub common_name: Option, + pub reader_authentication: Status, + pub errors: ValidationErrors, +} + +#[derive(Serialize, Deserialize, Default)] +pub enum Status { + #[default] + Unchecked, + Invalid, + Valid, +} diff --git a/src/definitions/validated_response.rs b/src/definitions/validated_response.rs new file mode 100644 index 00000000..ef090e4d --- /dev/null +++ b/src/definitions/validated_response.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::BTreeMap; + +#[derive(Serialize, Deserialize, Default)] +pub struct ValidatedResponse { + pub response: BTreeMap, + pub decryption: Status, + pub parsing: Status, + pub issuer_authentication: Status, + pub device_authentication: Status, + pub errors: ValidationErrors, +} + +pub type ValidationErrors = BTreeMap; + +#[derive(Serialize, Deserialize, Default)] +pub enum Status { + #[default] + Unchecked, + Invalid, + Valid, +} diff --git a/src/definitions/x509/error.rs b/src/definitions/x509/error.rs new file mode 100644 index 00000000..803b4141 --- /dev/null +++ b/src/definitions/x509/error.rs @@ -0,0 +1,63 @@ +use crate::definitions::device_key::cose_key::Error as CoseError; +use crate::definitions::helpers::non_empty_vec; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, thiserror::Error)] +pub enum Error { + #[error("Error occurred while validating x509 certificate: {0}")] + ValidationError(String), + #[error("Error occurred while decoding a x509 certificate: {0}")] + DecodingError(String), + #[error("Error decoding cbor")] + CborDecodingError, + #[error("Error decoding json")] + JsonError, +} + +impl From for Error { + fn from(_: serde_cbor::Error) -> Self { + Error::CborDecodingError + } +} + +impl From for Error { + fn from(_: serde_json::Error) -> Self { + Error::JsonError + } +} + +impl From for Error { + fn from(value: x509_cert::der::Error) -> Self { + Error::ValidationError(value.to_string()) + } +} + +impl From for Error { + fn from(value: p256::ecdsa::Error) -> Self { + Error::ValidationError(value.to_string()) + } +} + +impl From for Error { + fn from(value: x509_cert::spki::Error) -> Self { + Error::ValidationError(value.to_string()) + } +} + +impl From for Error { + fn from(value: CoseError) -> Self { + Error::ValidationError(value.to_string()) + } +} + +impl From for Error { + fn from(value: non_empty_vec::Error) -> Self { + Error::ValidationError(value.to_string()) + } +} + +impl From for Error { + fn from(value: asn1_rs::Error) -> Self { + Error::ValidationError(value.to_string()) + } +} diff --git a/src/definitions/x509/extensions.rs b/src/definitions/x509/extensions.rs new file mode 100644 index 00000000..472aa1f7 --- /dev/null +++ b/src/definitions/x509/extensions.rs @@ -0,0 +1,381 @@ +use crate::definitions::x509::error::Error; +use der::Decode; +use x509_cert::ext::pkix::name::DistributionPointName; +use x509_cert::ext::pkix::name::GeneralName; +use x509_cert::ext::pkix::{ + BasicConstraints, CrlDistributionPoints, ExtendedKeyUsage, IssuerAltName, KeyUsage, KeyUsages, +}; +use x509_cert::ext::Extension; + +// -- IACA X509 Extension OIDs -- // +const OID_KEY_USAGE: &str = "2.5.29.15"; +const OID_ISSUER_ALTERNATIVE_NAME: &str = "2.5.29.18"; +const OID_BASIC_CONSTRAINTS: &str = "2.5.29.19"; +const OID_CRL_DISTRIBUTION_POINTS: &str = "2.5.29.31"; +const OID_EXTENDED_KEY_USAGE: &str = "2.5.29.37"; + +// -- 18013-5 IACA SPECIFIC ROOT EXTENSION VALUE CHECKS -- // +// Key Usage: 5, 6 (keyCertSign, crlSign) +// Basic Constraints: Pathlen:0 +// CRL Distribution Points must have tag 0 +// Issuer Alternative Name must be of type rfc822Name or a URI (tag 1 and tag 6) + +// -- 18013-5 IACA SPECIFIC LEAF EXTENSION VALUE CHECKS -- // +// Extended Key Usage: 1.0.18013.5.1.2 +// Key Usage: 0 (digitalSignature) +// CRL Distribution Points must have tag 0 +// Issuer Alternative Name must be of type rfc822Name or a URI (tag 1 and tag 6) + +/* All the checks in this file relate to requirements for IACA x509 certificates as +detailed in Annex B of ISO18013-5. Specifically, the requirements for values in +root and signer certificates are given in tables B.2 and B.4 */ +pub fn validate_iaca_root_extensions(root_extensions: Vec) -> Vec { + //A specific subset of x509 extensions is not allowed in IACA certificates. + //We enter an error for every present disallowed x509 extension + let disallowed = iaca_disallowed_x509_extensions(); + let mut x509_errors: Vec = vec![]; + + for extension in root_extensions.clone() { + if let Some(disallowed_extension) = disallowed + .iter() + .find(|oid| extension.extn_id.to_string() == **oid) + { + x509_errors.push(Error::ValidationError(format!( + "The extension with oid: {:?} is not allowed in the IACA certificate profile", + disallowed_extension + ))); + } + } + + let root_crit_extensions: Vec<&Extension> = + root_extensions.iter().filter(|ext| ext.critical).collect(); + + //TODO: check for any critical extensions beyond what is expected + + // Key Usage 2.5.29.15 + if let Some(key_usage) = root_crit_extensions + .iter() + .find(|ext| ext.extn_id.to_string() == *OID_KEY_USAGE) + { + x509_errors.append(&mut validate_root_key_usage( + key_usage.extn_value.as_bytes().to_vec(), + )); + } else { + x509_errors.push(Error::ValidationError( + "The root certificate is expected to have its key usage limited to keyCertSign and crlSign, but no restrictions were specified".to_string(), + )); + }; + + // Basic Constraints 2.5.29.19 + if let Some(basic_constraints) = root_crit_extensions + .iter() + .find(|ext| ext.extn_id.to_string() == *OID_BASIC_CONSTRAINTS) + { + x509_errors.append(&mut validate_basic_constraints( + basic_constraints.extn_value.as_bytes().to_vec(), + )); + } else { + x509_errors.push(Error::ValidationError( + "The root certificate is expected to have critical basic constraints specificied, but the extensions was not found".to_string() + )); + }; + + //CRL Distribution Points 2.5.29.31 + if let Some(crl_distribution_point) = root_extensions + .iter() + .find(|ext| ext.extn_id.to_string() == *OID_CRL_DISTRIBUTION_POINTS) + { + x509_errors.append(&mut validate_crl_distribution_point( + crl_distribution_point.extn_value.as_bytes().to_vec(), + )); + } else { + x509_errors.push(Error::ValidationError("The root certificate is expected to have a crl distribution point specificied, but the extensions was not found".to_string())); + }; + + // Issuer Alternative Name 2.5.29.18 + if let Some(issuer_alternative_name) = root_extensions + .iter() + .find(|ext| ext.extn_id.to_string() == *OID_ISSUER_ALTERNATIVE_NAME) + { + x509_errors.append(&mut validate_issuer_alternative_name( + issuer_alternative_name.extn_value.as_bytes().to_vec(), + )); + } else { + x509_errors.push(Error::ValidationError( + "The root certificate is expected to have issuer alternative name specificied, but the extensions was not found".to_string() + )); + }; + + x509_errors +} + +pub fn validate_iaca_signer_extensions( + leaf_extensions: Vec, + value_extended_key_usage: &str, +) -> Vec { + let disallowed = iaca_disallowed_x509_extensions(); + let mut x509_errors: Vec = vec![]; + let mut errors: Vec = vec![]; + for extension in leaf_extensions.clone() { + if let Some(disallowed_extension) = disallowed + .iter() + .find(|oid| extension.extn_id.to_string() == **oid) + { + errors.push(Error::ValidationError(format!( + "The extension with oid: {:?} is not allowed in the IACA certificate profile", + disallowed_extension + ))); + } + } + + let leaf_crit_extensions: Vec<&Extension> = + leaf_extensions.iter().filter(|ext| ext.critical).collect(); + + // Key Usage 2.5.29.15 + if let Some(key_usage) = leaf_crit_extensions + .iter() + .find(|ext| ext.extn_id.to_string() == *OID_KEY_USAGE) + { + x509_errors.append(&mut validate_signer_key_usage( + key_usage.extn_value.as_bytes().to_vec(), + )); + } else { + x509_errors.push(Error::ValidationError( + "Missing critical KeyUsage extension in the signer certificate".to_string(), + )); + } + + // Extended Key Usage 2.5.29.37 + if let Some(extended_key_usage) = leaf_crit_extensions + .iter() + .find(|ext| ext.extn_id.to_string() == OID_EXTENDED_KEY_USAGE) + { + x509_errors.append(&mut validate_extended_key_usage( + extended_key_usage.extn_value.as_bytes().to_vec(), + value_extended_key_usage, + )); + } else { + x509_errors.push(Error::ValidationError( + "Missing critical ExtendedKeyUsage extension in the signer certificate".to_string(), + )); + }; + + //CRL Distribution Points 2.5.29.31 + if let Some(crl_distribution_point) = leaf_extensions + .iter() + .find(|ext| ext.extn_id.to_string() == *OID_CRL_DISTRIBUTION_POINTS) + { + x509_errors.append(&mut validate_crl_distribution_point( + crl_distribution_point.extn_value.as_bytes().to_vec(), + )); + } else { + x509_errors.push(Error::ValidationError( + "The leaf certificate is expected to have a crl distribution point specificied, but the extensions was not found".to_string(), + )); + }; + + // Issuer Alternative Name 2.5.29.18 + if let Some(issuer_alternative_name) = leaf_extensions + .iter() + .find(|ext| ext.extn_id.to_string() == *OID_ISSUER_ALTERNATIVE_NAME) + { + x509_errors.append(&mut validate_issuer_alternative_name( + issuer_alternative_name.extn_value.as_bytes().to_vec(), + )); + } else { + x509_errors.push(Error::ValidationError("The leaf certificate is expected to have issuer alternative name specificied, but the extensions was not found".to_string())); + }; + + x509_errors +} + +/* A signer certificate should have digital signatures set for it's key usage, +but not other key usages are allowed */ +pub fn validate_signer_key_usage(bytes: Vec) -> Vec { + let mut errors: Vec = vec![]; + let key_usage = KeyUsage::from_der(&bytes); + + match key_usage { + Ok(ku) => { + if !ku.digital_signature() { + errors.push(Error::ValidationError( + "Signer key usage should be set to digital signature".to_string(), + )) + } + if ku + .0 + .into_iter() + .any(|flag| flag != KeyUsages::DigitalSignature) + { + errors.push(Error::ValidationError( + "Key usage is set beyond scope of IACA signer certificates".to_string(), + )) + } + } + Err(e) => { + errors.push(e.into()); + } + }; + errors +} + +/* A root certificate should have KeyCertSign and CRLSign set for key usage, +but no other key usages are allowed */ +pub fn validate_root_key_usage(bytes: Vec) -> Vec { + let mut errors = vec![]; + let key_usage = KeyUsage::from_der(&bytes); + match key_usage { + Ok(ku) => { + if !ku.crl_sign() { + errors.push(Error::ValidationError( + "CrlSign should be set on the root certificate key usage".to_string(), + )) + }; + if !ku.key_cert_sign() { + errors.push(Error::ValidationError( + "KeyCertSign should be set on the root certificate key usage".to_string(), + )) + }; + + if ku + .0 + .into_iter() + .any(|flag| flag != KeyUsages::CRLSign && flag != KeyUsages::KeyCertSign) + { + errors.push(Error::ValidationError(format!("The key usage of the root certificate goes beyond the scope of IACA root certificates {:?}", ku))) + }; + errors + } + Err(e) => { + vec![Error::DecodingError(e.to_string())] + } + } +} + +/* Extended key usage in the signer certificate should be set to this OID meant specifically for mDL signing. +Note that this value will be different for other types of mdocs */ + +pub fn validate_extended_key_usage(bytes: Vec, value_extended_key_usage: &str) -> Vec { + let extended_key_usage = ExtendedKeyUsage::from_der(&bytes); + match extended_key_usage { + Ok(eku) => { + if !eku + .0 + .into_iter() + .any(|oid| oid.to_string() == value_extended_key_usage) + { + return vec![Error::ValidationError( + "Invalid extended key usage, expected: 1.0.18013.5.1.2".to_string(), + )]; + }; + vec![] + } + Err(e) => { + vec![Error::DecodingError(e.to_string())] + } + } +} + +/* The CRL DistributionPoint shall not contain values for crl_issuer and reasons. +Every Distribution Point must be of a type URI or RFC822Name */ +pub fn validate_crl_distribution_point(bytes: Vec) -> Vec { + let mut errors: Vec = vec![]; + let crl_distribution_point = CrlDistributionPoints::from_der(&bytes); + match crl_distribution_point { + Ok(crl_dp) => { + for point in crl_dp.0.into_iter() { + if point.crl_issuer.is_some() || point.reasons.is_some() { + errors.push(Error::ValidationError(format!("crl_issuer and reasons may not be set on CrlDistributionPoints, but is set for: {:?}", point))) + } + + if !point + .distribution_point + .clone() + .is_some_and(|dpn| match dpn { + DistributionPointName::FullName(names) => { + let type_errors: Vec = check_general_name_types(names); + type_errors.is_empty() + } + DistributionPointName::NameRelativeToCRLIssuer(_) => false, + }) + { + errors.push(Error::ValidationError(format!( + "crl distribution point has an invalid type: {:?}", + point + ))) + } + } + } + Err(e) => errors.push(Error::DecodingError(e.to_string())), + } + + errors +} + +/* The Issuer Alternative Name must be of a type URI or RFC822Name */ +pub fn validate_issuer_alternative_name(bytes: Vec) -> Vec { + let iss_altname = IssuerAltName::from_der(&bytes); + match iss_altname { + Ok(ian) => check_general_name_types(ian.0), + Err(e) => { + vec![Error::DecodingError(e.to_string())] + } + } +} + +/* Basic Constraints must be CA: true, path_len: 0 */ +pub fn validate_basic_constraints(bytes: Vec) -> Vec { + let basic_constraints = BasicConstraints::from_der(&bytes); + match basic_constraints { + Ok(bc) => { + if !bc.path_len_constraint.is_some_and(|path_len| path_len == 0) && bc.ca { + return vec![Error::ValidationError(format!( + "Basic constraints expected to be CA:true, path_len:0, but found: {:?}", + bc + ))]; + } + vec![] + } + Err(e) => { + vec![Error::DecodingError(e.to_string())] + } + } +} + +fn check_general_name_types(names: Vec) -> Vec { + let valid_types: Vec = names + .iter() + .map(|name| { + matches!( + name, + GeneralName::Rfc822Name(_) | GeneralName::UniformResourceIdentifier(_) + ) + }) + .collect(); + + if valid_types.contains(&false) { + vec![Error::ValidationError(format!( + "Invalid type found in GeneralNames: {:?}", + names + ))] + } else { + vec![] + } +} + +pub fn iaca_disallowed_x509_extensions() -> Vec { + vec![ + "2.5.29.30".to_string(), + "2.5.29.33".to_string(), + "2.5.29.36".to_string(), + "2.5.29.46".to_string(), + "2.5.29.54".to_string(), + ] +} + +#[cfg(test)] +pub mod test { + + #[test] + fn test_key_usage() {} +} diff --git a/src/definitions/x509/mod.rs b/src/definitions/x509/mod.rs new file mode 100644 index 00000000..a26c99e8 --- /dev/null +++ b/src/definitions/x509/mod.rs @@ -0,0 +1,6 @@ +pub mod error; +pub mod extensions; +pub mod trust_anchor; +pub mod x5chain; + +pub use x5chain::{Builder, X5Chain}; diff --git a/src/definitions/x509/trust_anchor.rs b/src/definitions/x509/trust_anchor.rs new file mode 100644 index 00000000..8ac9a840 --- /dev/null +++ b/src/definitions/x509/trust_anchor.rs @@ -0,0 +1,379 @@ +use crate::definitions::x509::error::Error as X509Error; +use crate::definitions::x509::extensions::{ + validate_iaca_root_extensions, validate_iaca_signer_extensions, +}; +use crate::definitions::x509::x5chain::X509; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use x509_cert::attr::AttributeTypeAndValue; +use x509_cert::certificate::CertificateInner; +use x509_cert::der::Decode; + +const MDOC_VALUE_EXTENDED_KEY_USAGE: &str = "1.0.18013.5.1.2"; +const READER_VALUE_EXTENDED_KEY_USAGE: &str = "1.0.18013.5.1.6"; + +#[derive(Serialize, Deserialize, Clone)] +pub enum TrustAnchor { + Iaca(X509), + Aamva(X509), + Custom(X509, ValidationRuleSet), + IacaReader(X509), +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct ValidationRuleSet { + pub distinguished_names: Vec, + #[serde(rename = "type")] + pub typ: RuleSetType, +} + +#[derive(Serialize, Deserialize, Clone)] +pub enum RuleSetType { + IACA, + AAMVA, + NamesOnly, + ReaderAuth, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct TrustAnchorRegistry { + pub certificates: Vec, +} + +impl TrustAnchorRegistry { + pub fn iaca_registry_from_str(pem_strings: Vec) -> Result { + let certificates: Vec = pem_strings + .into_iter() + .filter_map(|s| trustanchor_from_str(&s).ok()) + .collect(); + + Ok(TrustAnchorRegistry { certificates }) + } +} + +fn trustanchor_from_str(pem_string: &str) -> Result { + let anchor: TrustAnchor = match pem_rfc7468::decode_vec(pem_string.as_bytes()) { + Ok(b) => TrustAnchor::Iaca(X509 { bytes: b.1 }), + Err(e) => { + return Err(X509Error::DecodingError(format!( + "unable to parse pem: {:?}", + e + ))) + } + }; + Ok(anchor) +} + +pub fn process_validation_outcomes( + leaf_certificate: CertificateInner, + root_certificate: CertificateInner, + rule_set: ValidationRuleSet, +) -> Vec { + let mut errors: Vec = vec![]; + + //execute checks on x509 components + match apply_ruleset(leaf_certificate, root_certificate.clone(), rule_set) { + Ok(mut v) => { + errors.append(&mut v); + } + Err(e) => { + errors.push(e); + } + } + + // make sure that the trust anchor is still valid + errors.append(&mut check_validity_period(&root_certificate)); + + //TODO: check CRL to make sure the certificates have not been revoked + errors +} + +pub fn validate_with_ruleset( + leaf_certificate: CertificateInner, + trust_anchor: TrustAnchor, +) -> Vec { + let mut errors: Vec = vec![]; + + match trust_anchor { + TrustAnchor::Iaca(certificate) => { + let rule_set = ValidationRuleSet { + distinguished_names: vec!["2.5.4.6".to_string(), "2.5.4.8".to_string()], + typ: RuleSetType::IACA, + }; + match x509_cert::Certificate::from_der(&certificate.bytes) { + Ok(root_certificate) => { + errors.append(&mut process_validation_outcomes( + leaf_certificate, + root_certificate, + rule_set, + )); + } + Err(e) => { + errors.push(e.into()); + } + }; + } + TrustAnchor::Aamva(certificate) => { + let rule_set = ValidationRuleSet { + distinguished_names: vec!["2.5.4.6".to_string(), "2.5.4.8".to_string()], + typ: RuleSetType::AAMVA, + }; + //The Aamva ruleset follows the IACA ruleset, but makes the ST value mandatory + match x509_cert::Certificate::from_der(&certificate.bytes) { + Ok(root_certificate) => { + errors.append(&mut process_validation_outcomes( + leaf_certificate, + root_certificate, + rule_set, + )); + } + Err(e) => { + errors.push(e.into()); + } + }; + } + TrustAnchor::IacaReader(certificate) => { + let rule_set = ValidationRuleSet { + distinguished_names: vec!["2.5.4.3".to_string()], + typ: RuleSetType::ReaderAuth, + }; + match x509_cert::Certificate::from_der(&certificate.bytes) { + Ok(root_certificate) => { + errors.append(&mut process_validation_outcomes( + leaf_certificate, + root_certificate, + rule_set, + )); + } + Err(e) => { + errors.push(e.into()); + } + }; + } + TrustAnchor::Custom(_certificate, _ruleset) => { + //TODO + } + } + errors +} + +pub fn validate_with_trust_anchor(leaf_x509: X509, trust_anchor: TrustAnchor) -> Vec { + let mut errors: Vec = vec![]; + let leaf_certificate = x509_cert::Certificate::from_der(&leaf_x509.bytes); + + match leaf_certificate { + Ok(leaf) => { + errors.append(&mut validate_with_ruleset(leaf, trust_anchor)); + } + Err(e) => errors.push(e.into()), + } + errors +} + +pub fn check_validity_period(certificate: &CertificateInner) -> Vec { + let validity = certificate.tbs_certificate.validity; + let mut errors: Vec = vec![]; + if validity.not_after.to_unix_duration().as_secs() + < OffsetDateTime::now_utc().unix_timestamp() as u64 + { + errors.push(X509Error::ValidationError(format!( + "Expired certificate with subject: {:?}", + certificate.tbs_certificate.subject + ))); + }; + if validity.not_before.to_unix_duration().as_secs() + > OffsetDateTime::now_utc().unix_timestamp() as u64 + { + errors.push(X509Error::ValidationError(format!( + "Not yet valid certificate with subject: {:?}", + certificate.tbs_certificate.subject + ))); + }; + + errors +} + +/* Validates: + +- all the correct distinghuished names are present +and match the +- all the correct extensions are present +- the extensions are set to the ruleset values +- */ +fn apply_ruleset( + leaf_certificate: CertificateInner, + root_certificate: CertificateInner, + rule_set: ValidationRuleSet, +) -> Result, X509Error> { + let mut errors: Vec = vec![]; + // collect all the distinguished names in the root certificate that the validation ruleset requires + let root_distinguished_names: Vec = root_certificate + .tbs_certificate + .subject + .0 + .into_iter() + .map(|rdn| { + rdn.0 + .into_vec() + .into_iter() + .filter(|atv| { + rule_set + .distinguished_names + .iter() + .any(|oid| oid == &atv.oid.to_string()) + }) + .collect::>() + }) + .collect::>>() + .into_iter() + .flatten() + .collect(); + + // collect all the distinguished names in the signer certificate that the validation ruleset requires + let leaf_distinguished_names: Vec = leaf_certificate + .tbs_certificate + .issuer + .0 + .into_iter() + .map(|r| { + r.0.into_vec() + .into_iter() + .filter(|atv| { + rule_set + .distinguished_names + .iter() + .any(|oid| oid == &atv.oid.to_string()) + }) + .collect::>() + }) + .collect::>>() + .into_iter() + .flatten() + .collect(); + + // if all the needed distinguished names have been collected, + // there should be the same number of names collected as are present in the ruleset + if root_distinguished_names.len() != rule_set.distinguished_names.len() { + errors.push(X509Error::ValidationError("The configured validation ruleset requires a distinguished name that is not found in the submitted root certificate".to_string())); + } + + if leaf_distinguished_names.len() != rule_set.distinguished_names.len() { + errors.push(X509Error::ValidationError("The configured validation ruleset requires a distinguished name that is not found in the submitted signer certificate".to_string())); + } + + let Some(root_extensions) = root_certificate.tbs_certificate.extensions else { + return Err(X509Error::ValidationError( + "The root certificate is expected to have extensions, but none were found. Skipping all following extension validation checks..".to_string(), + )); + }; + + let Some(leaf_extensions) = leaf_certificate.tbs_certificate.extensions else { + return Err(X509Error::ValidationError( + "The signer certificate is expected to have extensions, but none were found. Skipping all following extension validation checks.. " + .to_string(), + )); + }; + + match rule_set.typ { + //Under the IACA ruleset, the values for S or ST should be the same in subject and issuer if they are present in both + RuleSetType::IACA => { + let mut extension_errors = validate_iaca_root_extensions(root_extensions); + extension_errors.append(&mut validate_iaca_signer_extensions( + leaf_extensions, + MDOC_VALUE_EXTENDED_KEY_USAGE, + )); + for dn in leaf_distinguished_names { + if dn.oid.to_string() == *"2.5.4.8" { + let state_or_province = + root_distinguished_names.iter().find(|r| r.oid == dn.oid); + if let Some(st_or_s) = state_or_province { + if dn != *st_or_s { + return Err(X509Error::ValidationError(format!("Mismatch between supplied certificate issuer attribute: {:?} and the trust anchor registry.", dn))); + } + } + } else { + let Some(_root_dn) = root_distinguished_names.iter().find(|r| r == &&dn) else { + return Err(X509Error::ValidationError(format!("Mismatch between supplied certificate issuer attribute: {:?} and the trust anchor registry.", dn.value))); + }; + } + } + Ok(extension_errors) + } + //Under the AAMVA ruleset, S/ST is mandatory and should be the same in the subject and issuer + RuleSetType::AAMVA => { + let mut extension_errors = validate_iaca_root_extensions(root_extensions); + extension_errors.append(&mut validate_iaca_signer_extensions( + leaf_extensions, + MDOC_VALUE_EXTENDED_KEY_USAGE, + )); + for dn in leaf_distinguished_names { + let Some(_root_dn) = root_distinguished_names.iter().find(|r| r == &&dn) else { + return Err(X509Error::ValidationError(format!("Mismatch between supplied certificate issuer attribute: {:?} and the trust anchor registry.", dn.value))); + }; + } + Ok(extension_errors) + } + RuleSetType::NamesOnly => { + for dn in leaf_distinguished_names { + let Some(_root_dn) = root_distinguished_names.iter().find(|r| r == &&dn) else { + return Err(X509Error::ValidationError(format!("Mismatch between supplied certificate issuer attribute: {:?} and the trust anchor registry.", dn.value))); + }; + } + Ok(vec![]) + } + RuleSetType::ReaderAuth => { + //TODO + + Ok(validate_iaca_signer_extensions( + leaf_extensions, + READER_VALUE_EXTENDED_KEY_USAGE, + )) + } + } +} + +pub fn find_anchor( + leaf_certificate: CertificateInner, + trust_anchor_registry: Option, +) -> Result, X509Error> { + let leaf_issuer = leaf_certificate.tbs_certificate.issuer; + + let Some(root_certificates) = trust_anchor_registry else { + return Ok(None); + }; + let Some(trust_anchor) = root_certificates + .certificates + .into_iter() + .find(|trust_anchor| match trust_anchor { + TrustAnchor::Iaca(certificate) => { + match x509_cert::Certificate::from_der(&certificate.bytes) { + Ok(root_cert) => root_cert.tbs_certificate.subject == leaf_issuer, + Err(_) => false, + } + } + TrustAnchor::Custom(certificate, _ruleset) => { + match x509_cert::Certificate::from_der(&certificate.bytes) { + Ok(root_cert) => root_cert.tbs_certificate.subject == leaf_issuer, + Err(_) => false, + } + } + TrustAnchor::Aamva(certificate) => { + match x509_cert::Certificate::from_der(&certificate.bytes) { + Ok(root_cert) => root_cert.tbs_certificate.subject == leaf_issuer, + Err(_) => false, + } + } + TrustAnchor::IacaReader(certificate) => { + match x509_cert::Certificate::from_der(&certificate.bytes) { + Ok(root_cert) => root_cert.tbs_certificate.subject == leaf_issuer, + Err(_) => false, + } + } + }) + else { + return Err(X509Error::ValidationError( + "The certificate issuer does not match any known trusted issuer".to_string(), + )); + }; + Ok(Some(trust_anchor)) +} diff --git a/src/definitions/x509/x5chain.rs b/src/definitions/x509/x5chain.rs new file mode 100644 index 00000000..72e3d783 --- /dev/null +++ b/src/definitions/x509/x5chain.rs @@ -0,0 +1,304 @@ +use crate::definitions::helpers::NonEmptyVec; +use crate::definitions::x509::error::Error as X509Error; +use crate::definitions::x509::trust_anchor::check_validity_period; +use crate::definitions::x509::trust_anchor::find_anchor; +use crate::definitions::x509::trust_anchor::validate_with_trust_anchor; +use crate::definitions::x509::trust_anchor::TrustAnchorRegistry; +use anyhow::{anyhow, Result}; +use p256::ecdsa::VerifyingKey; + +use const_oid::AssociatedOid; + +use elliptic_curve::{ + sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint}, + AffinePoint, CurveArithmetic, FieldBytesSize, PublicKey, +}; +use p256::NistP256; +use serde::{Deserialize, Serialize}; +use serde_cbor::Value as CborValue; +use signature::Verifier; +use std::collections::HashSet; +use std::hash::Hash; +use std::{fs::File, io::Read}; +use x509_cert::der::Encode; +use x509_cert::{ + certificate::Certificate, + der::{referenced::OwnedToRef, Decode}, +}; + +pub const X5CHAIN_HEADER_LABEL: i128 = 33; + +#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +pub struct X509 { + pub bytes: Vec, +} + +impl X509 { + pub fn public_key(&self) -> Result, X509Error> + where + C: AssociatedOid + CurveArithmetic, + AffinePoint: FromEncodedPoint + ToEncodedPoint, + FieldBytesSize: ModulusSize, + { + let cert = x509_cert::Certificate::from_der(&self.bytes)?; + cert.tbs_certificate + .subject_public_key_info + .owned_to_ref() + .try_into() + .map_err(|e| format!("could not parse public key from pkcs8 spki: {e}")) + .map_err(|_e| { + X509Error::ValidationError("could not parse public key from pkcs8 spki".to_string()) + }) + } + + pub fn from_pem(bytes: &[u8]) -> Result { + let bytes = pem_rfc7468::decode_vec(bytes) + .map_err(|e| anyhow!("unable to parse pem: {}", e))? + .1; + X509::from_der(&bytes) + } + + pub fn from_der(bytes: &[u8]) -> Result { + let _ = Certificate::from_der(bytes) + .map_err(|e| anyhow!("unable to parse certificate from der encoding: {}", e))?; + Ok(X509 { + bytes: bytes.to_vec(), + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct X5Chain(NonEmptyVec); + +impl From> for X5Chain { + fn from(v: NonEmptyVec) -> Self { + Self(v) + } +} + +impl X5Chain { + pub fn builder() -> Builder { + Builder::default() + } + + pub fn into_cbor(&self) -> CborValue { + match &self.0.as_ref() { + &[cert] => CborValue::Bytes(cert.bytes.clone()), + certs => CborValue::Array( + certs + .iter() + .cloned() + .map(|x509| x509.bytes) + .map(CborValue::Bytes) + .collect::>(), + ), + } + } + + pub fn from_cbor(cbor_bytes: CborValue) -> Result { + match cbor_bytes { + CborValue::Bytes(bytes) => { + Self::builder().with_der(&bytes).map_err( + |e| X509Error::DecodingError(e.to_string()) + )?.build().map_err( + |e| X509Error::DecodingError(e.to_string()) + ) + }, + CborValue::Array(x509s) => { + x509s.iter() + .try_fold(Self::builder(), |builder, x509| match x509 { + CborValue::Bytes(bytes) => { + let builder = builder.with_der(bytes).map_err( + |e| X509Error::DecodingError(e.to_string()) + )?; + Ok(builder) + }, + _ => Err(X509Error::ValidationError(format!("Expecting x509 certificate in the x5chain to be a cbor encoded bytestring, but received: {x509:?}"))) + })? + .build() + .map_err(|e| X509Error::DecodingError(e.to_string()) + ) + }, + _ => Err(X509Error::ValidationError(format!("Expecting x509 certificate in the x5chain to be a cbor encoded bytestring, but received: {cbor_bytes:?}"))) + } + } + + pub fn get_signer_key(&self) -> Result { + let leaf = self.0.first().ok_or(X509Error::CborDecodingError)?; + leaf.public_key().map(|key| key.into()) + } + + pub fn validate(&self, trust_anchor_registry: Option) -> Vec { + let x5chain = self.0.as_ref(); + let mut errors: Vec = vec![]; + + if !has_unique_elements(x5chain) { + errors.push(X509Error::ValidationError( + "x5chain contains duplicate certificates".to_string(), + )) + }; + + x5chain.windows(2).for_each(|chain_link| { + let target = &chain_link[0]; + let issuer = &chain_link[1]; + if check_signature(target, issuer).is_err() { + errors.push(X509Error::ValidationError(format!( + "invalid signature for target: {:?}", + target + ))); + } + }); + + //make sure all submitted certificates are valid + for x509 in x5chain { + let cert = x509_cert::Certificate::from_der(&x509.bytes); + match cert { + Ok(c) => { + errors.append(&mut check_validity_period(&c)); + } + Err(e) => errors.push(e.into()), + } + } + + //validate the last certificate in the chain against trust anchor + if let Some(x509) = x5chain.last() { + match x509_cert::Certificate::from_der(&x509.bytes) { + Ok(cert) => { + // if the issuer of the signer certificate is known in the trust anchor registry, do the validation. + // otherwise, report an error and skip. + match find_anchor(cert, trust_anchor_registry) { + Ok(anchor) => { + if let Some(trust_anchor) = anchor { + errors.append(&mut validate_with_trust_anchor( + x509.clone(), + trust_anchor, + )); + } else { + errors.push(X509Error::ValidationError( + "No matching trust anchor found".to_string(), + )); + } + } + Err(e) => errors.push(e), + } + } + Err(e) => errors.push(e.into()), + } + } else { + errors.push(X509Error::ValidationError( + "Empty certificate chain".to_string(), + )) + } + + errors + } +} + +pub fn check_signature(target: &X509, issuer: &X509) -> Result<(), X509Error> { + let parent_public_key = ecdsa::VerifyingKey::from(issuer.public_key()?); + let child_cert = x509_cert::Certificate::from_der(&target.bytes)?; + let sig: ecdsa::Signature = + ecdsa::Signature::from_der(child_cert.signature.raw_bytes())?; + let bytes = child_cert.tbs_certificate.to_der()?; + Ok(parent_public_key.verify(&bytes, &sig)?) +} + +fn has_unique_elements(iter: T) -> bool +where + T: IntoIterator, + T::Item: Eq + Hash, +{ + let mut uniq = HashSet::new(); + iter.into_iter().all(move |x| uniq.insert(x)) +} + +#[derive(Default, Debug, Clone)] +pub struct Builder { + certs: Vec, +} + +impl Builder { + pub fn with_pem(mut self, data: &[u8]) -> Result { + let x509 = X509::from_pem(data)?; + self.certs.push(x509); + Ok(self) + } + pub fn with_der(mut self, data: &[u8]) -> Result { + let x509 = X509::from_der(data)?; + self.certs.push(x509); + Ok(self) + } + pub fn with_pem_from_file(self, mut f: File) -> Result { + let mut data: Vec = vec![]; + f.read_to_end(&mut data)?; + self.with_pem(&data) + } + pub fn with_der_from_file(self, mut f: File) -> Result { + let mut data: Vec = vec![]; + f.read_to_end(&mut data)?; + self.with_der(&data) + } + pub fn build(self) -> Result { + Ok(X5Chain(self.certs.try_into().map_err(|_| { + anyhow!("at least one certificate must be given to the builder") + })?)) + } +} + +#[cfg(test)] +pub mod test { + use super::*; + + static CERT_256: &[u8] = include_bytes!("../../../test/issuance/256-cert.pem"); + static CERT_384: &[u8] = include_bytes!("../../../test/issuance/384-cert.pem"); + static CERT_521: &[u8] = include_bytes!("../../../test/issuance/521-cert.pem"); + + #[test] + pub fn self_signed_es256() { + let _x5chain = X5Chain::builder() + .with_pem(CERT_256) + .expect("unable to add cert") + .build() + .expect("unable to build x5chain"); + } + + #[test] + pub fn self_signed_es384() { + let _x5chain = X5Chain::builder() + .with_pem(CERT_384) + .expect("unable to add cert") + .build() + .expect("unable to build x5chain"); + } + + #[test] + pub fn self_signed_es512() { + let _x5chain = X5Chain::builder() + .with_pem(CERT_521) + .expect("unable to add cert") + .build() + .expect("unable to build x5chain"); + } + + #[test] + pub fn correct_signature() { + let target = include_bytes!("../../../test/presentation/isomdl_iaca_signer.pem"); + let issuer = include_bytes!("../../../test/presentation/isomdl_iaca_root_cert.pem"); + check_signature( + &X509::from_pem(target).unwrap(), + &X509::from_pem(issuer).unwrap(), + ) + .expect("issuer did not sign target cert") + } + + #[test] + pub fn incorrect_signature() { + let issuer = include_bytes!("../../../test/presentation/isomdl_iaca_signer.pem"); + let target = include_bytes!("../../../test/presentation/isomdl_iaca_root_cert.pem"); + check_signature( + &X509::from_pem(target).unwrap(), + &X509::from_pem(issuer).unwrap(), + ) + .expect_err("issuer did sign target cert"); + } +} diff --git a/src/issuance/mdoc.rs b/src/issuance/mdoc.rs index dceecc1b..75c541ee 100644 --- a/src/issuance/mdoc.rs +++ b/src/issuance/mdoc.rs @@ -1,10 +1,10 @@ use crate::{ + definitions::x509::x5chain::{X5Chain, X5CHAIN_HEADER_LABEL}, definitions::{ helpers::{NonEmptyMap, NonEmptyVec, Tag24}, issuer_signed::{IssuerNamespaces, IssuerSignedItemBytes}, DeviceKeyInfo, DigestAlgorithm, DigestId, DigestIds, IssuerSignedItem, Mso, ValidityInfo, }, - issuance::x5chain::{X5Chain, X5CHAIN_HEADER_LABEL}, }; use anyhow::{anyhow, Result}; use async_signature::AsyncSigner; diff --git a/src/issuance/mod.rs b/src/issuance/mod.rs index 27129fdc..218bfe73 100644 --- a/src/issuance/mod.rs +++ b/src/issuance/mod.rs @@ -1,5 +1,3 @@ pub mod mdoc; -pub mod x5chain; pub use mdoc::{Mdoc, Namespaces}; -pub use x5chain::{Builder, X5Chain}; diff --git a/src/issuance/x5chain.rs b/src/issuance/x5chain.rs deleted file mode 100644 index 90cf782e..00000000 --- a/src/issuance/x5chain.rs +++ /dev/null @@ -1,277 +0,0 @@ -use crate::presentation::reader::find_anchor; - -use crate::presentation::reader::Error; -use crate::presentation::trust_anchor::validate_with_trust_anchor; -use crate::presentation::trust_anchor::TrustAnchorRegistry; -use crate::{definitions::helpers::NonEmptyVec, presentation::trust_anchor::check_validity_period}; -use anyhow::{anyhow, Result}; - -use const_oid::AssociatedOid; - -use elliptic_curve::{ - sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint}, - AffinePoint, CurveArithmetic, FieldBytesSize, PublicKey, -}; -use p256::NistP256; -use serde::{Deserialize, Serialize}; -use serde_cbor::Value as CborValue; -use signature::Verifier; -use std::{fs::File, io::Read}; -use x509_cert::der::Encode; -use x509_cert::{ - certificate::Certificate, - der::{referenced::OwnedToRef, Decode}, -}; - -pub const X5CHAIN_HEADER_LABEL: i128 = 33; - -#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] -pub struct X509 { - pub bytes: Vec, -} - -impl X509 { - pub fn public_key(&self) -> Result, Error> - where - C: AssociatedOid + CurveArithmetic, - AffinePoint: FromEncodedPoint + ToEncodedPoint, - FieldBytesSize: ModulusSize, - { - let cert = x509_cert::Certificate::from_der(&self.bytes)?; - cert.tbs_certificate - .subject_public_key_info - .owned_to_ref() - .try_into() - .map_err(|e| format!("could not parse public key from pkcs8 spki: {e}")) - .map_err(|_e| Error::MdocAuth("could not parse public key from pkcs8 spki".to_string())) - } -} - -#[derive(Debug, Clone)] -pub struct X5Chain(NonEmptyVec); - -impl From> for X5Chain { - fn from(v: NonEmptyVec) -> Self { - Self(v) - } -} - -impl X5Chain { - pub fn builder() -> Builder { - Builder::default() - } - - pub fn into_cbor(&self) -> CborValue { - match &self.0.as_ref() { - &[cert] => CborValue::Bytes(cert.bytes.clone()), - certs => CborValue::Array( - certs - .iter() - .cloned() - .map(|x509| x509.bytes) - .map(CborValue::Bytes) - .collect::>(), - ), - } - } - - pub fn validate( - &self, - trust_anchor_registry: Option, - ) -> Result, Error> { - let x5chain = self.0.as_ref(); - let mut results: Vec> = x5chain - .windows(2) - .map(|chain_link| { - let target = &chain_link[0]; - let issuer = &chain_link[1]; - check_signature(target, issuer) - }) - .collect(); - - for x509 in x5chain { - let cert = x509_cert::Certificate::from_der(&x509.bytes)?; - results.push(check_validity_period(&cert)) - } - - let mut errors: Vec = vec![]; - - //validate the last certificate in the chain against trust anchor - let last_in_chain = x5chain.to_vec().pop(); - if let Some(x509) = last_in_chain { - let inner = x509_cert::Certificate::from_der(&x509.bytes)?; - if let Some(trust_anchor) = find_anchor(inner, trust_anchor_registry)? { - errors.append(&mut validate_with_trust_anchor(x509, trust_anchor)?); - } else { - errors.push(Error::MdocAuth( - "No matching trust anchor found".to_string(), - )); - }; - } else { - errors.push(Error::MdocAuth("Empty certificate chain".to_string())) - } - - let mut sig_errors = results - .into_iter() - .filter(|result| result.is_err()) - .collect::>>() - .into_iter() - .map(|e| e.expect_err("something went wrong")) - .collect::>(); - - errors.append(&mut sig_errors); - Ok(errors) - } -} - -pub fn check_signature(target: &X509, issuer: &X509) -> Result<(), Error> { - let parent_public_key = ecdsa::VerifyingKey::from(issuer.public_key()?); - let child_cert = x509_cert::Certificate::from_der(&target.bytes)?; - let sig: ecdsa::Signature = - ecdsa::Signature::from_der(child_cert.signature.raw_bytes())?; - let bytes = child_cert.tbs_certificate.to_der()?; - Ok(parent_public_key.verify(&bytes, &sig)?) -} - -#[derive(Default, Debug, Clone)] -pub struct Builder { - certs: Vec, -} - -impl Builder { - pub fn with_pem(mut self, data: &[u8]) -> Result { - let bytes = pem_rfc7468::decode_vec(data) - .map_err(|e| anyhow!("unable to parse pem: {}", e))? - .1; - let cert: Certificate = Certificate::from_der(&bytes) - .map_err(|e| anyhow!("unable to parse certificate from der: {}", e))?; - let x509 = X509 { - bytes: cert - .encode_to_vec(&mut vec![])? - .to_der() - .map_err(|e| anyhow!("unable to convert certificate to bytes: {}", e))?, - }; - self.certs.push(x509); - Ok(self) - } - pub fn with_der(mut self, data: &[u8]) -> Result { - let cert: Certificate = Certificate::from_der(data) - .map_err(|e| anyhow!("unable to parse certificate from der encoding: {}", e))?; - let x509 = X509 { - bytes: cert - .encode_to_vec(&mut vec![])? - .to_der() - .map_err(|e| anyhow!("unable to convert certificate to bytes: {}", e))?, - }; - self.certs.push(x509); - Ok(self) - } - pub fn with_pem_from_file(self, mut f: File) -> Result { - let mut data: Vec = vec![]; - f.read_to_end(&mut data)?; - self.with_pem(&data) - } - pub fn with_der_from_file(self, mut f: File) -> Result { - let mut data: Vec = vec![]; - f.read_to_end(&mut data)?; - self.with_der(&data) - } - pub fn build(self) -> Result { - Ok(X5Chain(self.certs.try_into().map_err(|_| { - anyhow!("at least one certificate must be given to the builder") - })?)) - } -} - -#[cfg(test)] -pub mod test { - use super::*; - - static CERT_256: &[u8] = include_bytes!("../../test/issuance/256-cert.pem"); - static CERT_384: &[u8] = include_bytes!("../../test/issuance/384-cert.pem"); - static CERT_521: &[u8] = include_bytes!("../../test/issuance/521-cert.pem"); - - #[test] - pub fn self_signed_es256() { - let _x5chain = X5Chain::builder() - .with_pem(CERT_256) - .expect("unable to add cert") - .build() - .expect("unable to build x5chain"); - - //let self_signed = &x5chain[0]; - - //assert!(self_signed.issued(self_signed) == CertificateVerifyResult::OK); - //assert!(self_signed - // .verify( - // &self_signed - // .public_key() - // .expect("unable to get public key of cert") - // ) - // .expect("unable to verify public key of cert")); - - //assert!(matches!( - // x5chain - // .key_algorithm() - // .expect("unable to retrieve public key algorithm"), - // Algorithm::ES256 - //)); - } - - #[test] - pub fn self_signed_es384() { - let _x5chain = X5Chain::builder() - .with_pem(CERT_384) - .expect("unable to add cert") - .build() - .expect("unable to build x5chain"); - - //let self_signed = &x5chain[0]; - - //assert!(self_signed.issued(self_signed) == CertificateVerifyResult::OK); - //assert!(self_signed - // .verify( - // &self_signed - // .public_key() - // .expect("unable to get public key of cert") - // ) - // .expect("unable to verify public key of cert")); - - //assert!(matches!( - // x5chain - // .key_algorithm() - // .expect("unable to retrieve public key algorithm"), - // Algorithm::ES384 - //)); - } - - #[test] - pub fn self_signed_es512() { - let _x5chain = X5Chain::builder() - .with_pem(CERT_521) - .expect("unable to add cert") - .build() - .expect("unable to build x5chain"); - - //let self_signed = &x5chain[0]; - - //assert!(self_signed.issued(self_signed) == CertificateVerifyResult::OK); - //assert!(self_signed - // .verify( - // &self_signed - // .public_key() - // .expect("unable to get public key of cert") - // ) - // .expect("unable to verify public key of cert")); - - //assert!(matches!( - // x5chain - // .key_algorithm() - // .expect("unable to retrieve public key algorithm"), - // Algorithm::ES512 - //)); - } - - #[test] - pub fn validate_x5chain() {} -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 706c066d..00000000 --- a/src/main.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::collections::BTreeMap; - -use anyhow::{Context, Error}; -use clap::Parser; -use clap_stdin::MaybeStdin; -use isomdl::presentation::{device::Document, Stringify}; - -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - #[command(subcommand)] - action: Action, -} - -#[derive(Debug, clap::Subcommand)] -enum Action { - /// Print the namespaces and element identifiers used in an mDL. - GetNamespaces { - /// Base64 encoded mDL in the format used in the issuance module of this crate. - mdl: MaybeStdin, - }, -} - -fn main() -> Result<(), Error> { - match Args::parse().action { - Action::GetNamespaces { mdl } => print_namespaces(mdl.to_string()), - } -} - -fn print_namespaces(mdl: String) -> Result<(), Error> { - let claims = Document::parse(mdl) - .context("could not parse mdl")? - .namespaces - .into_inner() - .into_iter() - .map(|(ns, inner)| (ns, inner.into_inner().into_keys().collect())) - .collect::>>(); - println!("{}", serde_json::to_string_pretty(&claims)?); - Ok(()) -} - -#[cfg(test)] -mod test { - #[test] - fn print_namespaces() { - super::print_namespaces(include_str!("../test/stringified-mdl.txt").to_string()).unwrap() - } -} diff --git a/src/presentation/device.rs b/src/presentation/device.rs index 95e82855..afb77b9b 100644 --- a/src/presentation/device.rs +++ b/src/presentation/device.rs @@ -1,3 +1,8 @@ +use crate::definitions::validated_request::Status as ValidationStatus; +use crate::definitions::validated_request::ValidatedRequest; +use crate::definitions::x509::error::Error as X509Error; +use crate::definitions::x509::trust_anchor::TrustAnchorRegistry; +use crate::definitions::x509::X5Chain; use crate::definitions::IssuerSignedItem; use crate::{ definitions::{ @@ -21,10 +26,13 @@ use cose_rs::sign1::{CoseSign1, PreparedCoseSign1}; use p256::FieldBytes; use serde::{Deserialize, Serialize}; use serde_cbor::Value as CborValue; +use serde_json::json; use session::SessionTranscript180135; use std::collections::BTreeMap; use std::num::ParseIntError; use uuid::Uuid; +use x509_cert::attr::AttributeTypeAndValue; +use x509_cert::der::Decode; #[derive(Serialize, Deserialize)] pub struct SessionManagerInit { @@ -50,6 +58,7 @@ pub struct SessionManager { sk_reader: [u8; 32], reader_message_counter: u32, state: State, + trusted_verifiers: Option, } #[derive(Clone, Debug, Default, Serialize, Deserialize)] @@ -76,6 +85,16 @@ pub enum Error { ParsingError(#[from] ParseIntError), #[error("age_over element identifier is malformed")] PrefixError, + #[error("error decoding reader authentication certificate")] + CertificateError, + #[error("error while validating reader authentication certificate")] + ValidationError, +} + +impl From for Error { + fn from(_value: x509_cert::der::Error) -> Self { + Error::CertificateError + } } pub type Documents = NonEmptyMap; @@ -167,7 +186,8 @@ impl SessionManagerEngaged { pub fn process_session_establishment( self, session_establishment: SessionEstablishment, - ) -> anyhow::Result<(SessionManager, RequestedItems)> { + trusted_verifiers: Option, + ) -> anyhow::Result<(SessionManager, ValidatedRequest)> { let e_reader_key = session_establishment.e_reader_key; let session_transcript = SessionTranscript180135(self.device_engagement, e_reader_key.clone(), self.handover); @@ -191,14 +211,15 @@ impl SessionManagerEngaged { sk_reader, reader_message_counter: 0, state: State::AwaitingRequest, + trusted_verifiers, }; - let requested_data = sm.handle_decoded_request(SessionData { + let validated_request = sm.handle_decoded_request(SessionData { data: Some(session_establishment.data), status: None, - })?; + }); - Ok((sm, requested_data)) + Ok((sm, validated_request)) } } @@ -215,24 +236,43 @@ impl SessionManager { }) } - fn validate_request( - &self, - request: DeviceRequest, - ) -> Result, PreparedDeviceResponse> { + fn validate_request(&self, request: DeviceRequest) -> ValidatedRequest { + let items_requests: Vec = request + .doc_requests + .clone() + .into_inner() + .into_iter() + .map(|DocRequest { items_request, .. }| items_request.into_inner()) + .collect(); + + let mut validated_request = ValidatedRequest { + items_requests, + common_name: None, + reader_authentication: ValidationStatus::Unchecked, + errors: BTreeMap::new(), + }; + if request.version != DeviceRequest::VERSION { // tracing::error!( // "unsupported DeviceRequest version: {} ({} is supported)", // request.version, // DeviceRequest::VERSION // ); - return Err(PreparedDeviceResponse::empty(Status::GeneralError)); + validated_request.errors.insert( + "parsing_errors".to_string(), + json!(vec!["unsupported DeviceRequest version".to_string()]), + ); } - Ok(request - .doc_requests - .into_inner() - .into_iter() - .map(|DocRequest { items_request, .. }| items_request.into_inner()) - .collect()) + if let Some(doc_request) = request.doc_requests.first() { + let (validation_errors, common_name) = self.reader_authentication(doc_request.clone()); + if validation_errors.is_empty() { + validated_request.reader_authentication = ValidationStatus::Valid; + } + + validated_request.common_name = common_name; + } + + validated_request } pub fn prepare_response(&mut self, requests: &RequestedItems, permitted: PermittedItems) { @@ -240,36 +280,59 @@ impl SessionManager { self.state = State::Signing(prepared_response); } - fn handle_decoded_request(&mut self, request: SessionData) -> anyhow::Result { - let data = request.data.ok_or_else(|| { - anyhow::anyhow!("no mdoc requests received, assume session can be terminated") - })?; - let decrypted_request = session::decrypt_reader_data( + fn handle_decoded_request(&mut self, request: SessionData) -> ValidatedRequest { + let mut validated_request = ValidatedRequest::default(); + let data = match request.data { + Some(d) => d, + None => { + validated_request.errors.insert( + "parsing_errors".to_string(), + json!(vec![ + "no mdoc requests received, assume session can be terminated".to_string() + ]), + ); + return validated_request; + } + }; + let decrypted_request = match session::decrypt_reader_data( &self.sk_reader.into(), data.as_ref(), &mut self.reader_message_counter, ) - .map_err(|e| anyhow::anyhow!("unable to decrypt request: {}", e))?; - let request = match self.parse_request(&decrypted_request) { - Ok(r) => r, + .map_err(|e| anyhow::anyhow!("unable to decrypt request: {}", e)) + { + Ok(decrypted) => decrypted, Err(e) => { - self.state = State::Signing(e); - return Ok(Default::default()); + validated_request + .errors + .insert("decryption_errors".to_string(), json!(vec![e.to_string()])); + return validated_request; } }; - let request = match self.validate_request(request) { + + let request = match self.parse_request(&decrypted_request) { Ok(r) => r, Err(e) => { self.state = State::Signing(e); - return Ok(Default::default()); + return ValidatedRequest::default(); } }; - Ok(request) + + self.validate_request(request) } /// Handle a request from the reader. - pub fn handle_request(&mut self, request: &[u8]) -> anyhow::Result { - let session_data: SessionData = serde_cbor::from_slice(request)?; + pub fn handle_request(&mut self, request: &[u8]) -> ValidatedRequest { + let mut validated_request = ValidatedRequest::default(); + let session_data: SessionData = match serde_cbor::from_slice(request) { + Ok(sd) => sd, + Err(e) => { + validated_request + .errors + .insert("parsing_errors".to_string(), json!(vec![e.to_string()])); + return validated_request; + } + }; self.handle_decoded_request(session_data) } @@ -338,6 +401,76 @@ impl SessionManager { None } } + + pub fn reader_authentication( + &self, + doc_request: DocRequest, + ) -> (Vec, Option) { + //TODO validate the reader authentication. This code only grabs the CN from the x5chain + let mut validation_errors: Vec = vec![]; + if let Some(reader_auth) = doc_request.reader_auth { + if let Some(x5chain_cbor) = reader_auth.unprotected().get_i(33) { + let x5c = x5chain_cbor; + + let x5chain = + X5Chain::from_cbor(x5chain_cbor.clone()).map_err(|_| Error::CertificateError); + match x5chain { + Ok(x5c) => { + if let Some(trusted_verifiers) = &self.trusted_verifiers { + validation_errors + .append(&mut x5c.validate(Some(trusted_verifiers.clone()))); + } + } + Err(e) => { + validation_errors.push(X509Error::ValidationError(e.to_string())); + } + } + + match x5c { + CborValue::Bytes(x509) => { + match x509_cert::Certificate::from_der(x509) { + Ok(cert) => { + let distinguished_names: Vec = cert + .tbs_certificate + .subject + .0 + .into_iter() + .map(|rdn| { + rdn.0 + .into_vec() + .into_iter() + .filter(|atv| { + //common name + atv.oid.to_string() == *"2.5.4.3" + }) + .collect::>() + }) + .collect::>>() + .into_iter() + .flatten() + .collect(); + + if let Some(common_name) = distinguished_names.first() { + (validation_errors, Some(common_name.to_string())) + } else { + (validation_errors, None) + } + } + Err(e) => { + validation_errors.push(X509Error::ValidationError(e.to_string())); + (validation_errors, None) + } + } + } + _ => (validation_errors, None), + } + } else { + (validation_errors, None) + } + } else { + (validation_errors, None) + } + } } impl PreparedDeviceResponse { diff --git a/src/presentation/mdoc_auth.rs b/src/presentation/mdoc_auth.rs index ee38d7f2..c48403c5 100644 --- a/src/presentation/mdoc_auth.rs +++ b/src/presentation/mdoc_auth.rs @@ -1,5 +1,6 @@ use crate::definitions::device_response::Document; use crate::definitions::issuer_signed; +use crate::definitions::x509::X5Chain; use crate::definitions::DeviceAuth; use crate::definitions::Mso; use crate::definitions::{ @@ -12,17 +13,15 @@ use elliptic_curve::generic_array::GenericArray; use issuer_signed::IssuerSigned; use p256::ecdsa::Signature; use p256::ecdsa::VerifyingKey; -use p256::pkcs8::DecodePublicKey; -use serde_cbor::Value as CborValue; use ssi_jwk::Params; use ssi_jwk::JWK as SsiJwk; -use x509_cert::der::Decode; -pub fn issuer_authentication(x5chain: CborValue, issuer_signed: IssuerSigned) -> Result<(), Error> { - let signer_key = get_signer_key(&x5chain)?; - let issuer_auth = issuer_signed.issuer_auth; +pub fn issuer_authentication(x5chain: X5Chain, issuer_signed: &IssuerSigned) -> Result<(), Error> { + let signer_key = x5chain.get_signer_key()?; let verification_result: cose_rs::sign1::VerificationResult = - issuer_auth.verify::(&signer_key, None, None); + issuer_signed + .issuer_auth + .verify::(&signer_key, None, None); if !verification_result.success() { Err(ReaderError::ParsingError)? } else { @@ -31,10 +30,15 @@ pub fn issuer_authentication(x5chain: CborValue, issuer_signed: IssuerSigned) -> } pub fn device_authentication( - mso: Tag24, - document: Document, + document: &Document, session_transcript: SessionTranscript180135, ) -> Result<(), Error> { + let mso_bytes = document + .issuer_signed + .issuer_auth + .payload() + .ok_or(Error::DetachedIssuerAuth)?; + let mso: Tag24 = serde_cbor::from_slice(mso_bytes).map_err(|_| Error::MSOParsing)?; let device_key = mso.into_inner().device_key_info.device_key; let jwk = SsiJwk::try_from(device_key)?; match jwk.params { @@ -52,16 +56,16 @@ pub fn device_authentication( false, ); let verifying_key = VerifyingKey::from_encoded_point(&encoded_point)?; - let namespaces_bytes = document.device_signed.namespaces; - let device_auth: DeviceAuth = document.device_signed.device_auth; + let namespaces_bytes = &document.device_signed.namespaces; + let device_auth: &DeviceAuth = &document.device_signed.device_auth; //TODO: fix for attended use case: match device_auth { DeviceAuth::Signature { device_signature } => { let detached_payload = Tag24::new(DeviceAuthentication::new( session_transcript, - document.doc_type, - namespaces_bytes, + document.doc_type.clone(), + namespaces_bytes.clone(), )) .map_err(|_| ReaderError::CborDecodingError)?; let external_aad = None; @@ -86,39 +90,3 @@ pub fn device_authentication( _ => Err(Error::MdocAuth("Unsupported device_key type".to_string())), } } - -fn get_signer_key(x5chain: &CborValue) -> Result { - let signer = match x5chain { - CborValue::Text(t) => { - let x509 = x509_cert::Certificate::from_der(t.as_bytes())?; - - x509.tbs_certificate - .subject_public_key_info - .subject_public_key - } - CborValue::Array(a) => match a.first() { - Some(CborValue::Text(t)) => { - let x509 = x509_cert::Certificate::from_der(t.as_bytes())?; - - x509.tbs_certificate - .subject_public_key_info - .subject_public_key - } - _ => return Err(ReaderError::CborDecodingError)?, - }, - CborValue::Bytes(b) => { - let x509 = x509_cert::Certificate::from_der(b)?; - - x509.tbs_certificate - .subject_public_key_info - .subject_public_key - } - _ => { - return Err(ReaderError::MdocAuth(format!( - "Unexpected type for x5chain header: {:?} ", - x5chain - ))) - } - }; - Ok(VerifyingKey::from_public_key_der(signer.raw_bytes())?) -} diff --git a/src/presentation/mod.rs b/src/presentation/mod.rs index 0ee4040f..d9097360 100644 --- a/src/presentation/mod.rs +++ b/src/presentation/mod.rs @@ -1,7 +1,6 @@ pub mod device; pub mod mdoc_auth; pub mod reader; -pub mod trust_anchor; use anyhow::Result; use base64::{decode, encode}; diff --git a/src/presentation/reader.rs b/src/presentation/reader.rs index 90076e75..0759aedf 100644 --- a/src/presentation/reader.rs +++ b/src/presentation/reader.rs @@ -1,16 +1,16 @@ -use super::{ - mdoc_auth::device_authentication, mdoc_auth::issuer_authentication, - trust_anchor::ValidationRuleSet, -}; +use super::{mdoc_auth::device_authentication, mdoc_auth::issuer_authentication}; use crate::definitions::device_key::cose_key::Error as CoseError; -use crate::definitions::Mso; -use crate::issuance::x5chain::X509; +use crate::definitions::x509::trust_anchor::TrustAnchorRegistry; +use crate::definitions::x509::x5chain::X5CHAIN_HEADER_LABEL; +use crate::definitions::x509::X5Chain; +use crate::definitions::{Status, ValidatedResponse}; +use crate::presentation::reader::device_request::ItemsRequestBytes; use crate::presentation::reader::Error as ReaderError; -use crate::presentation::trust_anchor::TrustAnchorRegistry; use crate::{ definitions::{ device_engagement::DeviceRetrievalMethod, device_request::{self, DeviceRequest, DocRequest, ItemsRequest}, + device_response::Document, helpers::{non_empty_vec, NonEmptyVec, Tag24}, session::{ self, create_p256_ephemeral_keys, derive_session_key, get_shared_secret, Handover, @@ -18,52 +18,48 @@ use crate::{ }, }, definitions::{DeviceEngagement, DeviceResponse, SessionData, SessionTranscript180135}, - issuance::X5Chain, - presentation::trust_anchor::TrustAnchor, }; +use aes::cipher::{generic_array::GenericArray, typenum::U32}; use anyhow::{anyhow, Result}; +use cose_rs::algorithm::Algorithm; +use cose_rs::sign1::HeaderMap; +use cose_rs::CoseSign1; +use p256::ecdsa::SigningKey; +use sec1::DecodeEcPrivateKey; use serde::{Deserialize, Serialize}; use serde_cbor::Value as CborValue; use serde_json::json; use serde_json::Value; use std::collections::BTreeMap; -use std::collections::HashSet; -use std::hash::Hash; use uuid::Uuid; -use x509_cert::{certificate::CertificateInner, der::Decode}; - -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Clone)] pub struct SessionManager { session_transcript: SessionTranscript180135, sk_device: [u8; 32], device_message_counter: u32, sk_reader: [u8; 32], reader_message_counter: u32, - validation_ruleset: Option, trust_anchor_registry: Option, + reader_auth_key: [u8; 32], + reader_x5chain: X5Chain, } -pub struct ValidatedResponse { - pub response: BTreeMap, - pub issuer_authentication: Status, - pub device_authentication: Status, - pub errors: ValidationErrors, -} - -pub struct ValidationErrors(pub BTreeMap>); - #[derive(Serialize, Deserialize)] -pub enum Status { - Unchecked, - Invalid, - Valid, -} +pub struct ReaderAuthentication( + pub String, + pub SessionTranscript180135, + pub ItemsRequestBytes, +); -#[derive(Debug, thiserror::Error)] +#[derive(Debug, thiserror::Error, Serialize, Deserialize)] pub enum Error { - #[error("the qr code had the wrong prefix or the contained data could not be decoded: {0}")] - InvalidQrCode(anyhow::Error), + #[error("Received IssuerAuth had a detached payload.")] + DetachedIssuerAuth, + #[error("Could not parse MSO.")] + MSOParsing, + #[error("the qr code had the wrong prefix or the contained data could not be decoded")] + InvalidQrCode, #[error("Device did not transmit any data.")] DeviceTransmissionError, #[error("Device did not transmit an mDL.")] @@ -88,6 +84,8 @@ pub enum Error { MdocAuth(String), #[error("Currently unsupported format")] Unsupported, + #[error("No x5chain found for mdoc authentication")] + X5Chain, } impl From for Error { @@ -96,6 +94,12 @@ impl From for Error { } } +impl From for Error { + fn from(value: crate::definitions::x509::error::Error) -> Self { + Error::MdocAuth(value.to_string()) + } +} + impl From for Error { fn from(_: serde_json::Error) -> Self { Error::JsonError @@ -143,10 +147,11 @@ impl SessionManager { qr_code: String, namespaces: device_request::Namespaces, trust_anchor_registry: Option, - validation_ruleset: Option, + reader_x5chain: X5Chain, + reader_key: &str, ) -> Result<(Self, Vec, [u8; 16])> { let device_engagement_bytes = - Tag24::::from_qr_code_uri(&qr_code).map_err(Error::InvalidQrCode)?; + Tag24::::from_qr_code_uri(&qr_code).map_err(|e| anyhow!(e))?; //generate own keys let key_pair = create_p256_ephemeral_keys()?; @@ -179,14 +184,18 @@ impl SessionManager { let sk_device = derive_session_key(&shared_secret, &session_transcript_bytes, false)?.into(); + let reader_signing_key: SigningKey = ecdsa::SigningKey::from_sec1_pem(reader_key)?; + let reader_auth_key: GenericArray = reader_signing_key.to_bytes(); + let mut session_manager = Self { session_transcript, sk_device, device_message_counter: 0, sk_reader, reader_message_counter: 0, - validation_ruleset, trust_anchor_registry, + reader_auth_key: reader_auth_key.into(), + reader_x5chain, }; let request = session_manager.build_request(namespaces)?; @@ -238,8 +247,33 @@ impl SessionManager { namespaces, request_info: None, }; + + //the certificate should be supplied by the reader + //let certificate_cbor = serde_cbor::to_vec(&self.reader_cert_bytes)?; + let mut header_map = HeaderMap::default(); + header_map.insert_i(33, self.reader_x5chain.into_cbor()); + + let algorithm = Algorithm::ES256; + let payload = ReaderAuthentication( + "ReaderAuthentication".to_string(), + self.session_transcript.clone(), + Tag24::new(items_request.clone())?, + ); + + let reader_signing_key = SigningKey::from_slice(&self.reader_auth_key)?; //SigningKey::from_bytes(self.reader_auth_key.to_vec()); + let signature = reader_signing_key.sign_recoverable(&serde_cbor::to_vec(&payload)?)?; + let prepared_cosesign = CoseSign1::builder() + .detached() + .signature_algorithm(algorithm) + .payload(serde_cbor::to_vec(&payload)?) + .unprotected(header_map) + .prepare() + .unwrap(); + + let cose_sign1 = prepared_cosesign.finalize(signature.0.to_vec()); + let doc_request = DocRequest { - reader_auth: None, + reader_auth: Some(cose_sign1), items_request: Tag24::new(items_request)?, }; let device_request = DeviceRequest { @@ -255,11 +289,7 @@ impl SessionManager { .map_err(|e| anyhow!("unable to encrypt request: {}", e)) } - pub fn handle_response( - &mut self, - response: &[u8], - session_transcript: SessionTranscript180135, - ) -> Result { + fn decrypt_response(&mut self, response: &[u8]) -> Result { let session_data: SessionData = serde_cbor::from_slice(response)?; let encrypted_response = match session_data.data { None => return Err(Error::HolderError), @@ -271,215 +301,109 @@ impl SessionManager { &mut self.device_message_counter, ) .map_err(|_e| Error::DecryptionError)?; - let mut core_namespace = BTreeMap::::new(); - let mut aamva_namespace = BTreeMap::::new(); - let device_response: DeviceResponse = serde_cbor::from_slice(&decrypted_response)?; + Ok(device_response) + } - let document = device_response - .documents - .clone() - .ok_or(ReaderError::DeviceTransmissionError)? - .into_inner() - .into_iter() - .find(|doc| doc.doc_type == "org.iso.18013.5.1.mDL") - .ok_or(ReaderError::DocumentTypeError)?; - - let issuer_signed = document.issuer_signed.clone(); - - let mso_bytes = issuer_signed - .issuer_auth - .payload() - .expect("expected a COSE_Sign1 with attached payload, found detached payload"); - let mso: Tag24 = - serde_cbor::from_slice(mso_bytes).expect("unable to parse payload as Mso"); + pub fn handle_response(&mut self, response: &[u8]) -> ValidatedResponse { + let mut validated_response = ValidatedResponse::default(); - let header = issuer_signed.issuer_auth.unprotected(); - let Some(x5chain) = header.get_i(33) else { - return Err(ReaderError::MdocAuth("Missing x5chain header".to_string())); + let device_response = match self.decrypt_response(response) { + Ok(device_response) => device_response, + Err(e) => { + validated_response.decryption = Status::Invalid; + validated_response + .errors + .insert("decryption_errors".to_string(), json!(vec![e])); + return validated_response; + } }; + validated_response.decryption = Status::Valid; - let mut parsed_response = BTreeMap::::new(); - let mut namespaces = device_response - .documents - .ok_or(Error::DeviceTransmissionError)? - .into_inner() - .into_iter() - .find(|doc| doc.doc_type == "org.iso.18013.5.1.mDL") - .ok_or(Error::DocumentTypeError)? - .issuer_signed - .namespaces - .ok_or(Error::NoMdlDataTransmission)? - .into_inner(); - - namespaces - .remove("org.iso.18013.5.1") - .ok_or(Error::IncorrectNamespace)? - .into_inner() - .into_iter() - .map(|item| item.into_inner()) - .for_each(|item| { - let value = parse_response(item.element_value.clone()); - if let Ok(val) = value { - core_namespace.insert(item.element_identifier, val); - } - }); - - parsed_response.insert( - "org.iso.18013.5.1".to_string(), - serde_json::to_value(core_namespace)?, - ); - - if let Some(aamva_response) = namespaces.remove("org.iso.18013.5.1.aamva") { - aamva_response - .into_inner() - .into_iter() - .map(|item| item.into_inner()) - .for_each(|item| { - let value = parse_response(item.element_value.clone()); - if let Ok(val) = value { - aamva_namespace.insert(item.element_identifier, val); - } - }); - - parsed_response.insert( - "org.iso.18013.5.1.aamva".to_string(), - serde_json::to_value(aamva_namespace)?, - ); - } - - let mut validated_response = ValidatedResponse { - response: parsed_response, - issuer_authentication: Status::Unchecked, - device_authentication: Status::Unchecked, - errors: ValidationErrors(BTreeMap::new()), + let (document, x5chain, _parsed_response) = match parse(&device_response) { + Ok(res) => res, + Err(e) => { + validated_response.parsing = Status::Invalid; + validated_response + .errors + .insert("parsing_errors".to_string(), json!(vec![e])); + return validated_response; + } }; - - let certificate_errors = - validate_x5chain(x5chain.to_owned(), self.trust_anchor_registry.clone()); - - match certificate_errors { - Ok(r) => { + match parse_namespaces(&device_response) { + Ok(parsed_response) => { + self.validate_response(x5chain, document.clone(), parsed_response) + } + Err(e) => { validated_response .errors - .0 - .insert("certificate_errors".to_string(), r); - let valid_issuer_authentication = - issuer_authentication(x5chain.clone(), issuer_signed); - match valid_issuer_authentication { - Ok(_r) => { - validated_response.issuer_authentication = Status::Valid; - } - Err(e) => { - validated_response.issuer_authentication = Status::Invalid; - validated_response - .errors - .0 - .insert("issuer_authentication_errors".to_string(), vec![e]); - } - } + .insert("parsing_errors".to_string(), json!(vec![e])); + validated_response } - Err(_e) => validated_response.issuer_authentication = Status::Invalid, } + } - let valid_device_authentication = device_authentication(mso, document, session_transcript); - match valid_device_authentication { - Ok(_r) => { + pub fn validate_response( + &mut self, + x5chain: X5Chain, + document: Document, + parsed_response: BTreeMap, + ) -> ValidatedResponse { + let mut validated_response = ValidatedResponse { + response: parsed_response, + ..Default::default() + }; + + match device_authentication(&document, self.session_transcript.clone()) { + Ok(_) => { validated_response.device_authentication = Status::Valid; } Err(e) => { validated_response.device_authentication = Status::Invalid; validated_response .errors - .0 - .insert("device_authentication_errors".to_string(), vec![e]); + .insert("device_authentication_errors".to_string(), json!(vec![e])); } } - Ok(validated_response) - } -} - -pub fn find_anchor( - leaf_certificate: CertificateInner, - trust_anchor_registry: Option, -) -> Result, Error> { - let leaf_issuer = leaf_certificate.tbs_certificate.issuer; - - let Some(root_certificates) = trust_anchor_registry else { - return Ok(None); - }; - let Some(trust_anchor) = root_certificates - .certificates - .into_iter() - .find(|trust_anchor| match trust_anchor { - TrustAnchor::Iaca(certificate) => { - match x509_cert::Certificate::from_der(&certificate.bytes) { - Ok(root_cert) => root_cert.tbs_certificate.subject == leaf_issuer, - Err(_) => false, + let validation_errors = x5chain.validate(self.trust_anchor_registry.clone()); + if validation_errors.is_empty() { + match issuer_authentication(x5chain, &document.issuer_signed) { + Ok(_) => { + validated_response.issuer_authentication = Status::Valid; } - } - TrustAnchor::Custom(certificate, _ruleset) => { - match x509_cert::Certificate::from_der(&certificate.bytes) { - Ok(root_cert) => root_cert.tbs_certificate.subject == leaf_issuer, - Err(_) => false, + Err(e) => { + validated_response.issuer_authentication = Status::Invalid; + validated_response + .errors + .insert("issuer_authentication_errors".to_string(), json!(vec![e])); } } - TrustAnchor::Aamva(certificate) => { - match x509_cert::Certificate::from_der(&certificate.bytes) { - Ok(root_cert) => root_cert.tbs_certificate.subject == leaf_issuer, - Err(_) => false, - } - } - }) - else { - return Err(Error::MdocAuth( - "The certificate issuer does not match any known trusted issuer".to_string(), - )); - }; - Ok(Some(trust_anchor)) -} - -// In 18013-5 the TrustAnchorRegistry is also referred to as the Verified Issuer Certificate Authority List (VICAL) -pub fn validate_x5chain( - x5chain: CborValue, - trust_anchor_registry: Option, -) -> Result, Error> { - match x5chain { - CborValue::Bytes(bytes) => { - let chain: Vec = vec![X509 { - bytes: serde_cbor::from_slice(&bytes)?, - }]; - let x5chain = X5Chain::from(NonEmptyVec::try_from(chain)?); - x5chain.validate(trust_anchor_registry) - } - CborValue::Array(x509s) => { - let mut chain = vec![]; - for x509 in x509s { - match x509 { - CborValue::Bytes(bytes) => { - chain.push(X509{bytes: serde_cbor::from_slice(&bytes)?}) - - }, - _ => return Err(Error::MdocAuth(format!("Expecting x509 certificate in the x5chain to be a cbor encoded bytestring, but received: {:?}", x509))) - } - } - - if !has_unique_elements(chain.clone()) { - return Err(Error::MdocAuth( - "x5chain header contains at least one duplicate certificate".to_string(), - )); - } + } else { + validated_response + .errors + .insert("certificate_errors".to_string(), json!(validation_errors)); + validated_response.issuer_authentication = Status::Invalid + }; - let x5chain = X5Chain::from(NonEmptyVec::try_from(chain)?); - x5chain.validate(trust_anchor_registry) - } - _ => { - Err(Error::MdocAuth(format!("Expecting x509 certificate in the x5chain to be a cbor encoded bytestring, but received: {:?}", x5chain))) - } + validated_response } } +fn parse( + device_response: &DeviceResponse, +) -> Result<(&Document, X5Chain, BTreeMap), Error> { + let document = get_document(device_response)?; + let header = document.issuer_signed.issuer_auth.unprotected(); + let x5chain = header + .get_i(X5CHAIN_HEADER_LABEL) + .cloned() + .ok_or(Error::X5Chain) + .map(X5Chain::from_cbor)??; + let parsed_response = parse_namespaces(device_response)?; + Ok((document, x5chain, parsed_response)) +} + fn parse_response(value: CborValue) -> Result { match value { CborValue::Text(s) => Ok(Value::String(s)), @@ -516,6 +440,16 @@ fn parse_response(value: CborValue) -> Result { } } +fn get_document(device_response: &DeviceResponse) -> Result<&Document, Error> { + device_response + .documents + .as_ref() + .ok_or(ReaderError::DeviceTransmissionError)? + .iter() + .find(|doc| doc.doc_type == "org.iso.18013.5.1.mDL") + .ok_or(ReaderError::DocumentTypeError) +} + fn _validate_request(namespaces: device_request::Namespaces) -> Result { // Check if request follows ISO18013-5 restrictions // A valid mdoc request can contain a maximum of 2 age_over_NN fields @@ -535,33 +469,83 @@ fn _validate_request(namespaces: device_request::Namespaces) -> Result(iter: T) -> bool -where - T: IntoIterator, - T::Item: Eq + Hash, -{ - let mut uniq = HashSet::new(); - iter.into_iter().all(move |x| uniq.insert(x)) + +// TODO: Support other namespaces. +fn parse_namespaces( + device_response: &DeviceResponse, +) -> Result, Error> { + let mut core_namespace = BTreeMap::::new(); + let mut aamva_namespace = BTreeMap::::new(); + let mut parsed_response = BTreeMap::::new(); + let mut namespaces = device_response + .documents + .as_ref() + .ok_or(Error::DeviceTransmissionError)? + .iter() + .find(|doc| doc.doc_type == "org.iso.18013.5.1.mDL") + .ok_or(Error::DocumentTypeError)? + .issuer_signed + .namespaces + .as_ref() + .ok_or(Error::NoMdlDataTransmission)? + .clone() + .into_inner(); + + namespaces + .remove("org.iso.18013.5.1") + .ok_or(Error::IncorrectNamespace)? + .into_inner() + .into_iter() + .map(|item| item.into_inner()) + .for_each(|item| { + let value = parse_response(item.element_value.clone()); + if let Ok(val) = value { + core_namespace.insert(item.element_identifier, val); + } + }); + + parsed_response.insert( + "org.iso.18013.5.1".to_string(), + serde_json::to_value(core_namespace)?, + ); + + if let Some(aamva_response) = namespaces.remove("org.iso.18013.5.1.aamva") { + aamva_response + .into_inner() + .into_iter() + .map(|item| item.into_inner()) + .for_each(|item| { + let value = parse_response(item.element_value.clone()); + if let Ok(val) = value { + aamva_namespace.insert(item.element_identifier, val); + } + }); + + parsed_response.insert( + "org.iso.18013.5.1.aamva".to_string(), + serde_json::to_value(aamva_namespace)?, + ); + } + Ok(parsed_response) } #[cfg(test)] pub mod test { use super::*; - use crate::presentation::reader::validate_x5chain; use crate::{ - issuance::x5chain::X509, - presentation::trust_anchor::{TrustAnchor, TrustAnchorRegistry}, + definitions::x509::trust_anchor::{TrustAnchor, TrustAnchorRegistry}, + definitions::x509::{error::Error as X509Error, x5chain::X509, X5Chain}, }; use anyhow::anyhow; static IACA_ROOT: &[u8] = include_bytes!("../../test/presentation/isomdl_iaca_root_cert.pem"); //TODO fix this cert to contain issuer alternative name - static IACA_INTERMEDIATE: &[u8] = - include_bytes!("../../test/presentation/isomdl_iaca_intermediate.pem"); + // static IACA_INTERMEDIATE: &[u8] = + // include_bytes!("../../test/presentation/isomdl_iaca_intermediate.pem"); // signed by the intermediate certificate //TODO fix this cert to contain issuer alternative name - static IACA_LEAF_SIGNER: &[u8] = - include_bytes!("../../test/presentation/isomdl_iaca_leaf_signer.pem"); + // static IACA_LEAF_SIGNER: &[u8] = + // include_bytes!("../../test/presentation/isomdl_iaca_leaf_signer.pem"); // signed directly by the root certificate static IACA_SIGNER: &[u8] = include_bytes!("../../test/presentation/isomdl_iaca_signer.pem"); static INCORRECT_IACA_SIGNER: &[u8] = @@ -591,76 +575,65 @@ pub mod test { ); assert_eq!(json, expected) } - #[test] - fn validate_x509_with_trust_anchor() { - let root_bytes = pem_rfc7468::decode_vec(IACA_ROOT) - .map_err(|e| anyhow!("unable to parse pem: {}", e)) - .unwrap() + + fn validate(signer: &[u8], root: &[u8]) -> Result, anyhow::Error> { + let root_bytes = pem_rfc7468::decode_vec(root) + .map_err(|e| anyhow!("unable to parse pem: {}", e))? .1; let trust_anchor = TrustAnchor::Iaca(X509 { bytes: root_bytes }); let trust_anchor_registry = TrustAnchorRegistry { certificates: vec![trust_anchor], }; - let bytes = pem_rfc7468::decode_vec(IACA_SIGNER) - .map_err(|e| anyhow!("unable to parse pem: {}", e)) - .unwrap() + let bytes = pem_rfc7468::decode_vec(signer) + .map_err(|e| anyhow!("unable to parse pem: {}", e))? .1; - let x5chain: serde_cbor::Value = - serde_cbor::Value::Bytes(serde_cbor::to_vec(&bytes).unwrap()); + let x5chain_cbor: serde_cbor::Value = serde_cbor::Value::Bytes(bytes); + + let x5chain = X5Chain::from_cbor(x5chain_cbor)?; - let result = validate_x5chain(x5chain, Some(trust_anchor_registry)); - println!("result: {:?}", result) + Ok(x5chain.validate(Some(trust_anchor_registry))) } #[test] - fn validate_incorrect_x509_with_trust_anchor() { - let root_bytes = pem_rfc7468::decode_vec(IACA_ROOT) - .map_err(|e| anyhow!("unable to parse pem: {}", e)) - .unwrap() - .1; - let trust_anchor = TrustAnchor::Iaca(X509 { bytes: root_bytes }); - let trust_anchor_registry = TrustAnchorRegistry { - certificates: vec![trust_anchor], - }; - let bytes = pem_rfc7468::decode_vec(INCORRECT_IACA_SIGNER) - .map_err(|e| anyhow!("unable to parse pem: {}", e)) - .unwrap() - .1; - let x5chain: serde_cbor::Value = - serde_cbor::Value::Bytes(serde_cbor::to_vec(&bytes).unwrap()); - - let result = validate_x5chain(x5chain, Some(trust_anchor_registry)); - println!("result: {:?}", result) + fn validate_x509_with_trust_anchor() { + let result = validate(IACA_SIGNER, IACA_ROOT).unwrap(); + assert!(result.is_empty(), "{result:?}"); } #[test] - fn validate_x5chain_with_trust_anchor() { - let root_bytes = pem_rfc7468::decode_vec(IACA_ROOT) - .map_err(|e| anyhow!("unable to parse pem: {}", e)) - .unwrap() - .1; - let trust_anchor = TrustAnchor::Iaca(X509 { bytes: root_bytes }); - let trust_anchor_registry = TrustAnchorRegistry { - certificates: vec![trust_anchor], - }; - - let intermediate_bytes = pem_rfc7468::decode_vec(IACA_INTERMEDIATE) - .map_err(|e| anyhow!("unable to parse pem: {}", e)) - .unwrap() - .1; - - let leaf_signer_bytes = pem_rfc7468::decode_vec(IACA_LEAF_SIGNER) - .map_err(|e| anyhow!("unable to parse pem: {}", e)) - .unwrap() - .1; - - let intermediate_b = - serde_cbor::Value::Bytes(serde_cbor::to_vec(&intermediate_bytes).unwrap()); - let leaf_signer_b = - serde_cbor::Value::Bytes(serde_cbor::to_vec(&leaf_signer_bytes).unwrap()); - - let x5chain = serde_cbor::Value::Array(vec![leaf_signer_b, intermediate_b]); - let result = validate_x5chain(x5chain, Some(trust_anchor_registry)); - println!("result: {:?}", result) + fn validate_incorrect_x509_with_trust_anchor() { + let result = validate(INCORRECT_IACA_SIGNER, IACA_ROOT).unwrap(); + assert!(!result.is_empty(), "{result:?}"); } + + // TODO: Fix test -- intermediate and leaf are not in a chain. + // #[test] + // fn validate_x5chain_with_trust_anchor() { + // let root_bytes = pem_rfc7468::decode_vec(IACA_ROOT) + // .map_err(|e| anyhow!("unable to parse pem: {}", e)) + // .unwrap() + // .1; + // let trust_anchor = TrustAnchor::Iaca(X509 { bytes: root_bytes }); + // let trust_anchor_registry = TrustAnchorRegistry { + // certificates: vec![trust_anchor], + // }; + + // let intermediate_bytes = pem_rfc7468::decode_vec(IACA_INTERMEDIATE) + // .map(|(_, bytes)| bytes) + // .map(serde_cbor::Value::Bytes) + // .expect("unable to parse pem"); + + // let leaf_signer_bytes = pem_rfc7468::decode_vec(IACA_LEAF_SIGNER) + // .map(|(_, bytes)| bytes) + // .map(serde_cbor::Value::Bytes) + // .expect("unable to parse pem"); + + // let x5chain_cbor: serde_cbor::Value = + // serde_cbor::Value::Array(vec![leaf_signer_bytes, intermediate_bytes]); + + // let x5chain = X5Chain::from_cbor(x5chain_cbor).unwrap(); + + // let result = x5chain.validate(Some(trust_anchor_registry)); + // assert!(result.len() == 0, "{result:?}") + // } } diff --git a/src/presentation/trust_anchor.rs b/src/presentation/trust_anchor.rs deleted file mode 100644 index 408c4328..00000000 --- a/src/presentation/trust_anchor.rs +++ /dev/null @@ -1,546 +0,0 @@ -use crate::issuance::x5chain::{check_signature, X509}; -use crate::presentation::reader::Error; -use asn1_rs::{Any, BitString, FromDer, Oid, SequenceOf}; -use serde::{Deserialize, Serialize}; -use std::collections::BTreeMap; -use time::OffsetDateTime; -use x509_cert::attr::AttributeTypeAndValue; -use x509_cert::certificate::CertificateInner; -use x509_cert::ext::Extension; -use x509_cert::{der::Decode, Certificate}; -// -- DISTINGUISHED NAMES AND OID OVERVIEW -- // -// CN commonName 2.5.4.3 -// SN surname 2.5.4.4 -// SERIALNUMBER serialNumber 2.5.4.5 -// C countryName 2.5.4.6 -// L localityName 2.5.4.7 -// ST or S stateOrProvinceName 2.5.4.8 -// STREET streetAddress 2.5.4.9 -// O organizationName 2.5.4.10 -// OU organizationalUnit 2.5.4.11 -// T or TITLE title 2.5.4.12 -// G or GN givenName 2.5.4.42 -// initials initials 2.5.4.43 -// generationQualifier generation qualifier 2.5.4.44 -// dnQualifier distinguished name qualifier 2.5.4.46 -// pseudonym pseudonym 2.5.4.65 - -// -- IACA/AAMVA DISALLOWED EXTENSION OIDs -- // -// Policy Mappings 2.5.29.33 -// NameConstraints 2.5.29.30 -// PolicyConstraints 2.5.29.36 -// InhibitAnyPolicy 2.5.29.54 -// FreshestCRL 2.5.29.46 - -// -- IACA X509 Extension OIDs -- // -const OID_KEY_USAGE: &str = "2.5.29.15"; -const OID_ISSUER_ALTERNATIVE_NAME: &str = "2.5.29.18"; -const OID_BASIC_CONSTRAINTS: &str = "2.5.29.19"; -const OID_CRL_DISTRIBUTION_POINTS: &str = "2.5.29.31"; -const OID_EXTENDED_KEY_USAGE: &str = "2.5.29.37"; - -// -- 18013-5 IACA SPECIFIC ROOT EXTENSION VALUE CHECKS -- // -// Key Usage: 5, 6 (keyCertSign, crlSign) -// Basic Constraints: Pathlen:0 -// CRL Distribution Points must have tag 0 -// Issuer Alternative Name must be of type rfc822Name or a URI (tag 1 and tag 6) - -// -- 18013-5 IACA SPECIFIC LEAF EXTENSION VALUE CHECKS -- // -// Extended Key Usage: 1.0.18013.5.1.2 -// Key Usage: 0 (digitalSignature) -// CRL Distribution Points must have tag 0 -// Issuer Alternative Name must be of type rfc822Name or a URI (tag 1 and tag 6) -// - -const EXTENDED_KEY_USAGE: &str = "1.0.18013.5.1.2"; -const ROOT_KEY_USAGE: [usize; 2] = [5, 6]; -const LEAF_KEY_USAGE: [usize; 1] = [0]; -const BASIC_CONSTRAINTS: [u32; 1] = [0]; -const CRL_DISTRIBUTION_POINT: [u32; 1] = [0]; -const ISSUER_ALTERNATIVE_NAME: [u32; 2] = [1, 6]; - -#[derive(Serialize, Deserialize, Clone)] -pub enum TrustAnchor { - Iaca(X509), - Aamva(X509), - Custom(X509, ValidationRuleSet), -} - -#[derive(Serialize, Deserialize, Clone)] -pub struct ValidationRuleSet { - pub distinguished_names: Vec, - #[serde(rename = "type")] - pub typ: RuleSetType, -} - -#[derive(Serialize, Deserialize, Clone)] -pub enum RuleSetType { - IACA, - AAMVA, - Custom, - ReaderAuth, -} - -#[derive(Serialize, Deserialize, Clone)] -pub struct TrustAnchorRegistry { - pub certificates: Vec, -} - -fn iaca_disallowed_x509_extensions() -> Vec { - vec![ - "2.5.29.30".to_string(), - "2.5.29.33".to_string(), - "2.5.29.36".to_string(), - "2.5.29.46".to_string(), - "2.5.29.54".to_string(), - ] -} - -pub fn iaca_root_extension_rules() -> BTreeMap> { - BTreeMap::from([ - ("2.5.29.15".to_string(), vec![5, 6]), - ("2.5.29.19".to_string(), vec![0]), - ("2.5.29.18".to_string(), vec![1, 6]), - ("2.5.29.37".to_string(), vec![0]), - ]) -} - -pub fn validate_with_trust_anchor( - leaf_x509: X509, - trust_anchor: TrustAnchor, -) -> Result, Error> { - let leaf_certificate = x509_cert::Certificate::from_der(&leaf_x509.bytes)?; - let mut results: Vec = vec![]; - match trust_anchor { - //TODO: AAMVA TrustAnchor rules - TrustAnchor::Iaca(certificate) => { - // 18013-5 specifies checks that shall be performed for IACA certificates - let rule_set = ValidationRuleSet { - distinguished_names: vec!["2.5.4.6".to_string(), "2.5.4.8".to_string()], - typ: RuleSetType::IACA, - }; - - let root_certificate = x509_cert::Certificate::from_der(&certificate.bytes)?; - results.append(&mut apply_ruleset( - leaf_certificate, - root_certificate.clone(), - rule_set, - )?); - check_validity_period(&root_certificate)?; - check_signature(&leaf_x509, &certificate)?; - Ok(results) - } - TrustAnchor::Aamva(certificate) => { - //The Aamva ruleset follows the IACA ruleset, but makes the ST value mandatory - let rule_set = ValidationRuleSet { - distinguished_names: vec!["2.5.4.6".to_string(), "2.5.4.8".to_string()], - typ: RuleSetType::IACA, - }; - let root_certificate = x509_cert::Certificate::from_der(&certificate.bytes)?; - results.append(&mut apply_ruleset( - leaf_certificate, - root_certificate.clone(), - rule_set, - )?); - check_validity_period(&root_certificate)?; - check_signature(&leaf_x509, &certificate)?; - Ok(results) - } - TrustAnchor::Custom(certificate, _ruleset) => { - let _root_certificate = x509_cert::Certificate::from_der(&certificate.bytes)?; - Ok(results) - } - } -} - -pub fn check_validity_period(certificate: &Certificate) -> Result<(), Error> { - let validity = certificate.tbs_certificate.validity; - if validity.not_after.to_unix_duration().as_secs() - < OffsetDateTime::now_utc().unix_timestamp() as u64 - { - return Err(Error::MdocAuth(format!( - "Expired certificate with subject: {:?}", - certificate.tbs_certificate.subject - ))); - }; - if validity.not_before.to_unix_duration().as_secs() - > OffsetDateTime::now_utc().unix_timestamp() as u64 - { - return Err(Error::MdocAuth(format!( - "Not yet valid certificate with subject: {:?}", - certificate.tbs_certificate.subject - ))); - }; - - Ok(()) -} - -fn apply_ruleset( - leaf_certificate: CertificateInner, - root_certificate: CertificateInner, - rule_set: ValidationRuleSet, -) -> Result, Error> { - let root_distinguished_names: Vec = root_certificate - .tbs_certificate - .subject - .0 - .into_iter() - .map(|rdn| { - rdn.0 - .into_vec() - .into_iter() - .filter(|atv| { - rule_set - .distinguished_names - .iter() - .any(|oid| oid == &atv.oid.to_string()) - }) - .collect::>() - }) - .collect::>>() - .into_iter() - .flatten() - .collect(); - - let leaf_distinguished_names: Vec = leaf_certificate - .tbs_certificate - .issuer - .0 - .into_iter() - .map(|r| { - r.0.into_vec() - .into_iter() - .filter(|atv| { - rule_set - .distinguished_names - .iter() - .any(|oid| oid == &atv.oid.to_string()) - }) - .collect::>() - }) - .collect::>>() - .into_iter() - .flatten() - .collect(); - - // fix this - if root_distinguished_names.len() != rule_set.distinguished_names.len() { - return Err(Error::MdocAuth("The congifured validation ruleset requires a distinguished name that is not found in the submitted root certificate".to_string())); - } - - let Some(root_extensions) = root_certificate.tbs_certificate.extensions else { - return Err(Error::MdocAuth( - "The root certificate is expected to have extensions, but none were found".to_string(), - )); - }; - - let Some(leaf_extensions) = leaf_certificate.tbs_certificate.extensions else { - return Err(Error::MdocAuth( - "The signer certificate is expected to have extensions, but none were found" - .to_string(), - )); - }; - - match rule_set.typ { - RuleSetType::IACA => { - let mut root_extension_errors = validate_iaca_root_extensions(root_extensions)?; - let mut signer_extension_errors = validate_iaca_signer_extensions(leaf_extensions)?; - root_extension_errors.append(&mut signer_extension_errors); - for dn in leaf_distinguished_names { - let disallowed = iaca_disallowed_x509_extensions(); - if let Some(disallowed_extension) = - disallowed.iter().find(|oid| dn.oid.to_string() == **oid) - { - return Err(Error::MdocAuth(format!("The extension with oid: {:?} is not allowed in the IACA certificate profile", disallowed_extension))); - } - - //Under the IACA ruleset, the values for S or ST should be the same in subject and issuer if they are present in both - if dn.oid.to_string() == *"2.5.4.8" { - let state_or_province = - root_distinguished_names.iter().find(|r| r.oid == dn.oid); - if let Some(st_or_s) = state_or_province { - if dn != *st_or_s { - return Err(Error::MdocAuth(format!("Mismatch between supplied certificate issuer attribute: {:?} and the trust anchor registry.", dn))); - } - } - } else { - let Some(_root_dn) = root_distinguished_names.iter().find(|r| r == &&dn) else { - return Err(Error::MdocAuth(format!("Mismatch between supplied certificate issuer attribute: {:?} and the trust anchor registry.", dn.value))); - }; - } - } - Ok(root_extension_errors) - } - RuleSetType::AAMVA => { - let mut root_extension_errors = validate_iaca_root_extensions(root_extensions)?; - let mut signer_extension_errors = validate_iaca_signer_extensions(leaf_extensions)?; - root_extension_errors.append(&mut signer_extension_errors); - for dn in leaf_distinguished_names { - let disallowed = iaca_disallowed_x509_extensions(); - if let Some(disallowed_extension) = - disallowed.iter().find(|oid| dn.oid.to_string() == **oid) - { - return Err(Error::MdocAuth(format!("The extension with oid: {:?} is not allowed in the IACA certificate profile", disallowed_extension))); - } - - let Some(_root_dn) = root_distinguished_names.iter().find(|r| r == &&dn) else { - return Err(Error::MdocAuth(format!("Mismatch between supplied certificate issuer attribute: {:?} and the trust anchor registry.", dn.value))); - }; - } - Ok(root_extension_errors) - } - RuleSetType::Custom => { - //TODO - Err(Error::MdocAuth("Unimplemented ruleset".to_string())) - } - RuleSetType::ReaderAuth => { - //TODO - Err(Error::MdocAuth("Unimplemented ruleset".to_string())) - } - } -} - -pub fn validate_iaca_root_extensions(root_extensions: Vec) -> Result, Error> { - let disallowed = iaca_disallowed_x509_extensions(); - let mut errors: Vec = vec![]; - for extension in root_extensions.clone() { - if let Some(disallowed_extension) = disallowed - .iter() - .find(|oid| extension.extn_id.to_string() == **oid) - { - errors.push(Error::MdocAuth(format!( - "The extension with oid: {:?} is not allowed in the IACA certificate profile", - disallowed_extension - ))); - } - } - - let root_crit_extensions: Vec<&Extension> = - root_extensions.iter().filter(|ext| ext.critical).collect(); - - // Key Usage 2.5.29.15 - let Some(key_usage) = root_crit_extensions - .iter() - .find(|ext| ext.extn_id.to_string() == *OID_KEY_USAGE) - else { - return Err(Error::MdocAuth( - "The root certificate is expected to have its key usage limited to keyCertSign and crlSign, but no restrictions were specified".to_string(), - )); - }; - - let decoded_key_usage_value: (_, BitString) = - FromDer::from_der(key_usage.extn_value.as_bytes()) - .map_err(|e| Error::MdocAuth(e.to_string()))?; - let Some(bitslice) = decoded_key_usage_value.1.as_bitslice() else { - return Err(Error::MdocAuth( - "Error decoding extension value as a bitslice".to_string(), - )); - }; - - if bitslice.iter_ones().collect::>().as_slice() != ROOT_KEY_USAGE { - errors.push(Error::MdocAuth( - "the root certificate key usage extension is invalid".to_string(), - )); - } - - // Basic Constraints 2.5.29.19 - let Some(basic_constraints) = root_crit_extensions - .iter() - .find(|ext| ext.extn_id.to_string() == *OID_BASIC_CONSTRAINTS) - else { - return Err(Error::MdocAuth( - "The root certificate is expected to have critical basic constraints specificied, but the extensions was not found".to_string() - )); - }; - - let decoded_basic_constraints: (_, SequenceOf) = - FromDer::from_der(basic_constraints.extn_value.as_bytes()) - .map_err(|e| Error::MdocAuth(e.to_string()))?; - let mut iter = decoded_basic_constraints.1.iter(); - let Some(ca) = iter.next() else { - return Err(Error::MdocAuth( - "The root certificate is expected to contain CA=true in the Basic Constraints, but found an empty sequence".to_string() - )); - }; - - if !ca.as_boolean()?.bool() { - errors.push(Error::MdocAuth(format!("The root certificate is expected to contain Basic Constraints CA=true, but found: {:?}", ca))); - } - let Some(path_len) = iter.next() else { - return Err(Error::MdocAuth("The root certificate is expected to contain pathLen:0 in the Basic Constraints, but it was not found".to_string())); - }; - - if [path_len.as_integer()?.as_u32()?] != BASIC_CONSTRAINTS { - errors.push(Error::MdocAuth(format!("The root certificate is expected to contain Basic Constraints pathLen=0, but found: {:?}", path_len))); - } - - //CRL Distribution Points 2.5.29.31 - let Some(crl_distribution_point) = root_extensions - .iter() - .find(|ext| ext.extn_id.to_string() == *OID_CRL_DISTRIBUTION_POINTS) - else { - return Err(Error::MdocAuth("The root certificate is expected to have a crl distribution point specificied, but the extensions was not found".to_string())); - }; - - let crl_dp: (_, SequenceOf) = - FromDer::from_der(crl_distribution_point.extn_value.as_bytes()) - .map_err(|e| Error::MdocAuth(e.to_string()))?; - let Some(distribution_points) = crl_dp.1.iter().next() else { - return Err(Error::MdocAuth( - "The root certificate is expected to have a crl distribution point specificied, but the extension value was not found" - .to_string(), - )); - }; - let dp: (_, Any) = - FromDer::from_der(distribution_points.data).map_err(|e| Error::MdocAuth(e.to_string()))?; - if dp.1.tag().0 != 0 { - errors.push(Error::MdocAuth( - "reason and crlIssuer fields shall not be used in the crl distribution point" - .to_string(), - )); - } - - // Issuer Alternative Name 2.5.29.18 - let Some(issuer_alternative_name) = root_extensions - .iter() - .find(|ext| ext.extn_id.to_string() == *OID_ISSUER_ALTERNATIVE_NAME) - else { - return Err(Error::MdocAuth( - "The root certificate is expected to have issuer alternative name specificied, but the extensions was not found".to_string() - )); - }; - - let ian: (_, SequenceOf) = - FromDer::from_der(issuer_alternative_name.extn_value.as_bytes()) - .map_err(|e| Error::MdocAuth(e.to_string()))?; - for item in ian.1.iter() { - if item.tag().0 != 1 && item.tag().0 != 6 { - errors.push(Error::MdocAuth(format!( - "issuer alternative name is expected to be an rfc822name or a URI, but found: {:?}", - item - ))); - } - } - - Ok(errors) -} - -pub fn validate_iaca_signer_extensions( - leaf_extensions: Vec, -) -> Result, Error> { - let disallowed = iaca_disallowed_x509_extensions(); - let mut errors: Vec = vec![]; - for extension in leaf_extensions.clone() { - if let Some(disallowed_extension) = disallowed - .iter() - .find(|oid| extension.extn_id.to_string() == **oid) - { - errors.push(Error::MdocAuth(format!( - "The extension with oid: {:?} is not allowed in the IACA certificate profile", - disallowed_extension - ))); - } - } - - let leaf_crit_extensions: Vec<&Extension> = - leaf_extensions.iter().filter(|ext| ext.critical).collect(); - // Key Usage 2.5.29.15 - let Some(key_usage) = leaf_crit_extensions - .iter() - .find(|ext| ext.extn_id.to_string() == *OID_KEY_USAGE) - else { - return Err(Error::MdocAuth( - "Missing critical KeyUsage extension in the signer certificate".to_string(), - )); - }; - - let decoded_key_usage_value: (_, BitString) = - FromDer::from_der(key_usage.extn_value.as_bytes()) - .map_err(|e| Error::MdocAuth(e.to_string()))?; - let Some(bitslice) = decoded_key_usage_value.1.as_bitslice() else { - return Err(Error::MdocAuth( - "Error decoding extension value as a bitslice".to_string(), - )); - }; - - let leaf_key_usage_bit: Vec = bitslice.iter_ones().collect(); - if leaf_key_usage_bit.as_slice() != LEAF_KEY_USAGE { - errors.push(Error::MdocAuth( - "the signer certificate key usage extension is invalid".to_string(), - )); - } - - // Extended Key Usage 2.5.29.37 - let Some(extended_key_usage) = leaf_crit_extensions - .iter() - .find(|ext| ext.extn_id.to_string() == *OID_EXTENDED_KEY_USAGE) - else { - return Err(Error::MdocAuth( - "Missing critical ExtendedKeyUsage extension in the signer certificate".to_string(), - )); - }; - - let ext_ku: (_, SequenceOf) = FromDer::from_der(extended_key_usage.extn_value.as_bytes()) - .map_err(|e| Error::MdocAuth(e.to_string()))?; - let Some(eku) = ext_ku.1.iter().next() else { - return Err(Error::MdocAuth( - "missing critical ExtendedKeyUsage value".to_string(), - )); - }; - - if eku.to_id_string() != *EXTENDED_KEY_USAGE { - errors.push(Error::MdocAuth( - "Invalid value for Extended Key Usage in signer certificate to sign mDLs".to_string(), - )); - } - - //CRL Distribution Points 2.5.29.31 - let Some(crl_distribution_point) = leaf_extensions - .iter() - .find(|ext| ext.extn_id.to_string() == *OID_CRL_DISTRIBUTION_POINTS) - else { - return Err(Error::MdocAuth( - "The leaf certificate is expected to have a crl distribution point specificied, but the extensions was not found".to_string(), - )); - }; - - let crl_dp: (_, SequenceOf) = - FromDer::from_der(crl_distribution_point.extn_value.as_bytes()) - .map_err(|e| Error::MdocAuth(e.to_string()))?; - let Some(distribution_points) = crl_dp.1.iter().next() else { - return Err(Error::MdocAuth( - "The leaf certificate is expected to have a crl distribution point specificied, but the extensions was not found".to_string(), - )); - }; - - let dp: (_, Any) = - FromDer::from_der(distribution_points.data).map_err(|e| Error::MdocAuth(e.to_string()))?; - if !CRL_DISTRIBUTION_POINT.contains(&dp.1.tag().0) { - errors.push(Error::MdocAuth( - "reason and crlIssuer fields shall not be used in the crl distribution point" - .to_string(), - )); - } - - // Issuer Alternative Name 2.5.29.18 - let Some(issuer_alternative_name) = leaf_extensions - .iter() - .find(|ext| ext.extn_id.to_string() == *OID_ISSUER_ALTERNATIVE_NAME) - else { - return Err(Error::MdocAuth("The leaf certificate is expected to have issuer alternative name specificied, but the extensions was not found".to_string())); - }; - - let ian: (_, SequenceOf) = - FromDer::from_der(issuer_alternative_name.extn_value.as_bytes()) - .map_err(|e| Error::MdocAuth(e.to_string()))?; - for item in ian.1.iter() { - if !ISSUER_ALTERNATIVE_NAME.contains(&item.tag().0) { - errors.push(Error::MdocAuth(format!( - "issuer alternative name is expected to be an rfc822name or a URI, but found: {:?}", - item - ))); - } - } - - Ok(errors) -} diff --git a/src/x509/mod.rs b/src/x509/mod.rs new file mode 100644 index 00000000..e69de29b diff --git a/test/presentation/reader_auth.pem b/test/presentation/reader_auth.pem new file mode 100644 index 00000000..3d7ed57c --- /dev/null +++ b/test/presentation/reader_auth.pem @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE----- +MIICfzCCAiWgAwIBAgIUa1sPN12Jdv6KwSjG3DJeK6DCm0cwCgYIKoZIzj0EAwIw +bjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMSAwHgYDVQQKDBdJU09tREwgVGVz +dCBSZWFkZXIgUm9vdDEwMC4GA1UEAwwnSVNPMTgwMTMtNSBUZXN0IENlcnRpZmlj +YXRlIFJlYWRlciBSb290MB4XDTIzMTEyMDEzMjYwMloXDTI0MDUyNDEzMjYwMlow +VDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAk5ZMRcwFQYDVQQKDA5TcHJ1Y2UgU3lz +dGVtczEfMB0GA1UEAwwWSVNPMTgwMTMtNSBUZXN0IFJlYWRlcjBZMBMGByqGSM49 +AgEGCCqGSM49AwEHA0IABCm4PuNX0645fokw5XwZ5MpMtY0G4z+b1PvE/5Zx8As5 +4c9VAeVHb1Mlw59GPNBGU2xzccPZF8qsInT1JBd4cqOjgbowgbcwHQYDVR0OBBYE +FCyPAvWShVVL9dkiTlZQuL7kOtSjMB8GA1UdIwQYMBaAFFhiV3bwFCly/JtNCDvK +NUQxvDVmMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUBAf8ECzAJBgcogYxdBQEGMB0G +A1UdEgQWMBSBEmV4YW1wbGVAaXNvbWRsLmNvbTAvBgNVHR8EKDAmMCSgIqAghh5o +dHRwczovL2V4YW1wbGUuY29tL0lTT21ETC5jcmwwCgYIKoZIzj0EAwIDSAAwRQIg +XB7Y464ffTiQr32lfm/30S6HuvIsghovj1NFWcBGuCECIQCxGGShlVzrjTDsfahx +3LPTEI8prVIfLclczAvOOMq30A== +-----END CERTIFICATE----- diff --git a/test/presentation/reader_key.pem b/test/presentation/reader_key.pem new file mode 100644 index 00000000..c4b18090 --- /dev/null +++ b/test/presentation/reader_key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIFkWRnOYuhN/3iJLTcBXZdvwbnWWAppQSZvc5OlzROK6oAoGCCqGSM49 +AwEHoUQDQgAEKbg+41fTrjl+iTDlfBnkyky1jQbjP5vU+8T/lnHwCznhz1UB5Udv +UyXDn0Y80EZTbHNxw9kXyqwidPUkF3hyow== +-----END EC PRIVATE KEY----- From 80854462f4a0a60a19974918530a875a49297ac0 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Sat, 16 Nov 2024 13:27:03 -0800 Subject: [PATCH 03/12] Update src/definitions/x509/extensions.rs Co-authored-by: Jacob Signed-off-by: Ryan Tate --- src/definitions/x509/extensions.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/definitions/x509/extensions.rs b/src/definitions/x509/extensions.rs index 472aa1f7..1325cb0a 100644 --- a/src/definitions/x509/extensions.rs +++ b/src/definitions/x509/extensions.rs @@ -58,7 +58,7 @@ pub fn validate_iaca_root_extensions(root_extensions: Vec) -> Vec) -> Vec { /* A root certificate should have KeyCertSign and CRLSign set for key usage, but no other key usages are allowed */ -pub fn validate_root_key_usage(bytes: Vec) -> Vec { +pub fn validate_root_key_usage(bytes: &[u8]) -> Vec { let mut errors = vec![]; let key_usage = KeyUsage::from_der(&bytes); match key_usage { From 0ae09b690e687a6d0125b5aa813dc751b5dcde1d Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 6 Dec 2024 11:38:26 +0000 Subject: [PATCH 04/12] Linting --- src/cose/mac0.rs | 2 -- src/cose/serialized_as_cbor_value.rs | 2 +- src/cose/sign1.rs | 4 ---- src/definitions/device_request.rs | 1 - src/definitions/x509/extensions.rs | 4 ++-- tests/common.rs | 2 +- 6 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/cose/mac0.rs b/src/cose/mac0.rs index 0d99eed6..b00f4ddb 100644 --- a/src/cose/mac0.rs +++ b/src/cose/mac0.rs @@ -218,8 +218,6 @@ mod hmac { use super::super::SignatureAlgorithm; - /// Implement [`SignatureAlgorithm`]. - impl SignatureAlgorithm for Hmac { fn algorithm(&self) -> iana::Algorithm { iana::Algorithm::HMAC_256_256 diff --git a/src/cose/serialized_as_cbor_value.rs b/src/cose/serialized_as_cbor_value.rs index 04dc815b..ee07ef21 100644 --- a/src/cose/serialized_as_cbor_value.rs +++ b/src/cose/serialized_as_cbor_value.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; /// implement `Serialize`/`Deserialize` but only `AsCborValue`. pub struct SerializedAsCborValue(pub T); -impl<'a, T: Clone + AsCborValue> Serialize for SerializedAsCborValue<&'a T> { +impl Serialize for SerializedAsCborValue<&T> { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, diff --git a/src/cose/sign1.rs b/src/cose/sign1.rs index e3e8e51d..b47483b0 100644 --- a/src/cose/sign1.rs +++ b/src/cose/sign1.rs @@ -231,8 +231,6 @@ mod p256 { use crate::cose::SignatureAlgorithm; - /// Implement [`SignatureAlgorithm`]. - impl SignatureAlgorithm for SigningKey { fn algorithm(&self) -> iana::Algorithm { iana::Algorithm::ES256 @@ -252,8 +250,6 @@ mod p384 { use crate::cose::SignatureAlgorithm; - /// Implement [`SignatureAlgorithm`]. - impl SignatureAlgorithm for SigningKey { fn algorithm(&self) -> iana::Algorithm { iana::Algorithm::ES384 diff --git a/src/definitions/device_request.rs b/src/definitions/device_request.rs index 03470aaf..3935f5ef 100644 --- a/src/definitions/device_request.rs +++ b/src/definitions/device_request.rs @@ -17,7 +17,6 @@ pub type ReaderAuth = MaybeTagged; /// Represents a device request. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] - pub struct DeviceRequest { /// The version of the device request. pub version: String, diff --git a/src/definitions/x509/extensions.rs b/src/definitions/x509/extensions.rs index 1325cb0a..ac29a6fd 100644 --- a/src/definitions/x509/extensions.rs +++ b/src/definitions/x509/extensions.rs @@ -223,7 +223,7 @@ pub fn validate_signer_key_usage(bytes: Vec) -> Vec { but no other key usages are allowed */ pub fn validate_root_key_usage(bytes: &[u8]) -> Vec { let mut errors = vec![]; - let key_usage = KeyUsage::from_der(&bytes); + let key_usage = KeyUsage::from_der(bytes); match key_usage { Ok(ku) => { if !ku.crl_sign() { @@ -328,7 +328,7 @@ pub fn validate_basic_constraints(bytes: Vec) -> Vec { let basic_constraints = BasicConstraints::from_der(&bytes); match basic_constraints { Ok(bc) => { - if !bc.path_len_constraint.is_some_and(|path_len| path_len == 0) && bc.ca { + if bc.path_len_constraint.is_none_or(|path_len| path_len != 0) && bc.ca { return vec![Error::ValidationError(format!( "Basic constraints expected to be CA:true, path_len:0, but found: {:?}", bc diff --git a/tests/common.rs b/tests/common.rs index 3f40788e..b65f3eaa 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -108,7 +108,7 @@ impl Device { )] .into_iter() .collect(); - session_manager.prepare_response(&requested_items, permitted_items); + session_manager.prepare_response(requested_items, permitted_items); let (_, sign_payload) = session_manager.get_next_signature_payload().unwrap(); let signature: p256::ecdsa::Signature = key.sign(sign_payload); session_manager From 9afdfac33ae246e35113374e4019bde26465c442 Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 6 Dec 2024 14:16:49 +0000 Subject: [PATCH 05/12] Tidy up "Validated*" structs - Moved and renamed to 'src/presentation/authentication'. - Added doc comments. - Removed 'decryption' and 'parsing' from ResponseAuthenticationOutcome, as these are errors, not part of validating the response. --- src/definitions/mod.rs | 3 - src/definitions/validated_request.rs | 18 ----- src/definitions/validated_response.rs | 23 ------ .../{mdoc_auth.rs => authentication/mdoc.rs} | 0 src/presentation/authentication/mod.rs | 48 +++++++++++++ src/presentation/device.rs | 44 ++++++------ src/presentation/mod.rs | 2 +- src/presentation/reader.rs | 72 +++++++++---------- tests/common.rs | 7 +- 9 files changed, 106 insertions(+), 111 deletions(-) delete mode 100644 src/definitions/validated_request.rs delete mode 100644 src/definitions/validated_response.rs rename src/presentation/{mdoc_auth.rs => authentication/mdoc.rs} (100%) create mode 100644 src/presentation/authentication/mod.rs diff --git a/src/definitions/mod.rs b/src/definitions/mod.rs index 6c2a4c36..e5b3a185 100644 --- a/src/definitions/mod.rs +++ b/src/definitions/mod.rs @@ -10,8 +10,6 @@ pub mod mso; pub mod namespaces; pub mod session; pub mod traits; -pub mod validated_request; -pub mod validated_response; pub mod validity_info; pub mod x509; @@ -26,5 +24,4 @@ pub use device_signed::{DeviceAuth, DeviceSigned}; pub use issuer_signed::{IssuerSigned, IssuerSignedItem}; pub use mso::{DigestAlgorithm, DigestId, DigestIds, Mso}; pub use session::{SessionData, SessionEstablishment, SessionTranscript180135}; -pub use validated_response::{Status, ValidatedResponse, ValidationErrors}; pub use validity_info::ValidityInfo; diff --git a/src/definitions/validated_request.rs b/src/definitions/validated_request.rs deleted file mode 100644 index 529bae7e..00000000 --- a/src/definitions/validated_request.rs +++ /dev/null @@ -1,18 +0,0 @@ -use crate::{definitions::ValidationErrors, presentation::device::RequestedItems}; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Default)] -pub struct ValidatedRequest { - pub items_request: RequestedItems, - pub common_name: Option, - pub reader_authentication: Status, - pub errors: ValidationErrors, -} - -#[derive(Serialize, Deserialize, Default)] -pub enum Status { - #[default] - Unchecked, - Invalid, - Valid, -} diff --git a/src/definitions/validated_response.rs b/src/definitions/validated_response.rs deleted file mode 100644 index c21f7695..00000000 --- a/src/definitions/validated_response.rs +++ /dev/null @@ -1,23 +0,0 @@ -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::collections::BTreeMap; - -#[derive(Debug, Serialize, Deserialize, Default)] -pub struct ValidatedResponse { - pub response: BTreeMap, - pub decryption: Status, - pub parsing: Status, - pub issuer_authentication: Status, - pub device_authentication: Status, - pub errors: ValidationErrors, -} - -pub type ValidationErrors = BTreeMap; - -#[derive(Debug, Serialize, Deserialize, Default)] -pub enum Status { - #[default] - Unchecked, - Invalid, - Valid, -} diff --git a/src/presentation/mdoc_auth.rs b/src/presentation/authentication/mdoc.rs similarity index 100% rename from src/presentation/mdoc_auth.rs rename to src/presentation/authentication/mdoc.rs diff --git a/src/presentation/authentication/mod.rs b/src/presentation/authentication/mod.rs new file mode 100644 index 00000000..b990c359 --- /dev/null +++ b/src/presentation/authentication/mod.rs @@ -0,0 +1,48 @@ +use std::collections::BTreeMap; + +use crate::presentation::device::RequestedItems; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +/// Module containing functions to perform mdoc authentication. +pub mod mdoc; + +/// The outcome of the holder device authenticating the device request. +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct RequestAuthenticationOutcome { + /// The requested items from the mDL namespace. + pub items_request: RequestedItems, + /// The common name from the certificate that signed this request, if available. + /// This value can be used to display to the user who the reader is, however + /// caution should be exercised if reader authentication was not successful. + pub common_name: Option, + /// Outcome of reader authentication. + pub reader_authentication: AuthenticationStatus, + /// Errors that occurred during request processing. + pub errors: Errors, +} + +/// The outcome of the reader device authenticating the device response. +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct ResponseAuthenticationOutcome { + /// The values sent back from the holder device, serialized as JSON. + pub response: BTreeMap, + /// Outcome of issuer authentication. + pub issuer_authentication: AuthenticationStatus, + /// Outcome of device authentication. + pub device_authentication: AuthenticationStatus, + /// Errors that occurred during response processing. + pub errors: Errors, +} + +/// The outcome of authenticity checks. +#[derive(Debug, Serialize, Deserialize, Default)] +pub enum AuthenticationStatus { + #[default] + Unchecked, + Invalid, + Valid, +} + +/// Errors that occur during request/response processing. +pub type Errors = BTreeMap; diff --git a/src/presentation/device.rs b/src/presentation/device.rs index e97c5576..9fc2f4b9 100644 --- a/src/presentation/device.rs +++ b/src/presentation/device.rs @@ -16,19 +16,9 @@ //! //! You can view examples in `tests` directory in `simulated_device_and_reader.rs`, for a basic example and //! `simulated_device_and_reader_state.rs` which uses `State` pattern, `Arc` and `Mutex`. -use crate::cose::mac0::PreparedCoseMac0; -use crate::cose::sign1::PreparedCoseSign1; -use crate::cose::MaybeTagged; -use crate::definitions::device_signed::DeviceAuthType; -use crate::definitions::validated_request::Status as ValidationStatus; -use crate::definitions::validated_request::ValidatedRequest; -use crate::definitions::x509::error::Error as X509Error; -use crate::definitions::x509::trust_anchor::TrustAnchorRegistry; -use crate::definitions::x509::x5chain::X5CHAIN_HEADER_LABEL; -use crate::definitions::x509::X5Chain; -use crate::definitions::IssuerSignedItem; use crate::{ cbor, + cose::{mac0::PreparedCoseMac0, sign1::PreparedCoseSign1, MaybeTagged}, definitions::{ device_engagement::{DeviceRetrievalMethod, Security, ServerRetrievalMethods}, device_request::{DeviceRequest, DocRequest, ItemsRequest}, @@ -36,13 +26,19 @@ use crate::{ Document as DeviceResponseDoc, DocumentError, DocumentErrorCode, DocumentErrors, Errors as NamespaceErrors, Status, }, - device_signed::{DeviceAuth, DeviceAuthentication, DeviceNamespacesBytes, DeviceSigned}, + device_signed::{ + DeviceAuth, DeviceAuthType, DeviceAuthentication, DeviceNamespacesBytes, DeviceSigned, + }, helpers::{tag24, NonEmptyMap, NonEmptyVec, Tag24}, issuer_signed::{IssuerSigned, IssuerSignedItemBytes}, session::{ self, derive_session_key, get_shared_secret, Handover, SessionData, SessionTranscript, }, - CoseKey, DeviceEngagement, DeviceResponse, Mso, SessionEstablishment, + x509::{ + error::Error as X509Error, trust_anchor::TrustAnchorRegistry, + x5chain::X5CHAIN_HEADER_LABEL, X5Chain, + }, + CoseKey, DeviceEngagement, DeviceResponse, IssuerSignedItem, Mso, SessionEstablishment, }, issuance::Mdoc, }; @@ -59,6 +55,8 @@ use uuid::Uuid; use x509_cert::attr::AttributeTypeAndValue; use x509_cert::der::Decode; +use super::authentication::{AuthenticationStatus, RequestAuthenticationOutcome}; + /// Initialisation state. /// /// You enter this state using [SessionManagerInit::initialise] method, providing @@ -300,7 +298,7 @@ impl SessionManagerEngaged { self, session_establishment: SessionEstablishment, trusted_verifiers: Option, - ) -> anyhow::Result<(SessionManager, ValidatedRequest)> { + ) -> anyhow::Result<(SessionManager, RequestAuthenticationOutcome)> { let e_reader_key = session_establishment.e_reader_key; let session_transcript = SessionTranscript180135(self.device_engagement, e_reader_key.clone(), self.handover); @@ -350,7 +348,7 @@ impl SessionManager { }) } - fn validate_request(&self, request: DeviceRequest) -> ValidatedRequest { + fn validate_request(&self, request: DeviceRequest) -> RequestAuthenticationOutcome { let items_request: Vec = request .doc_requests .clone() @@ -359,10 +357,10 @@ impl SessionManager { .map(|DocRequest { items_request, .. }| items_request.into_inner()) .collect(); - let mut validated_request = ValidatedRequest { + let mut validated_request = RequestAuthenticationOutcome { items_request, common_name: None, - reader_authentication: ValidationStatus::Unchecked, + reader_authentication: AuthenticationStatus::Unchecked, errors: BTreeMap::new(), }; @@ -380,7 +378,7 @@ impl SessionManager { if let Some(doc_request) = request.doc_requests.first() { let (validation_errors, common_name) = self.reader_authentication(doc_request.clone()); if validation_errors.is_empty() { - validated_request.reader_authentication = ValidationStatus::Valid; + validated_request.reader_authentication = AuthenticationStatus::Valid; } validated_request.common_name = common_name; @@ -409,8 +407,8 @@ impl SessionManager { self.state = State::Signing(prepared_response); } - fn handle_decoded_request(&mut self, request: SessionData) -> ValidatedRequest { - let mut validated_request = ValidatedRequest::default(); + fn handle_decoded_request(&mut self, request: SessionData) -> RequestAuthenticationOutcome { + let mut validated_request = RequestAuthenticationOutcome::default(); let data = match request.data { Some(d) => d, None => { @@ -443,7 +441,7 @@ impl SessionManager { Ok(r) => r, Err(e) => { self.state = State::Signing(e); - return ValidatedRequest::default(); + return RequestAuthenticationOutcome::default(); } }; @@ -457,8 +455,8 @@ impl SessionManager { /// /// This method will return the [ValidatedRequest] struct, which will /// include the items requested by the reader/verifier. - pub fn handle_request(&mut self, request: &[u8]) -> ValidatedRequest { - let mut validated_request = ValidatedRequest::default(); + pub fn handle_request(&mut self, request: &[u8]) -> RequestAuthenticationOutcome { + let mut validated_request = RequestAuthenticationOutcome::default(); let session_data: SessionData = match cbor::from_slice(request) { Ok(sd) => sd, Err(e) => { diff --git a/src/presentation/mod.rs b/src/presentation/mod.rs index a8295145..fca090e6 100644 --- a/src/presentation/mod.rs +++ b/src/presentation/mod.rs @@ -39,8 +39,8 @@ //! //! You can see the example in `simulated_device_and_reader.rs` from `examples` directory or a version that //! uses **State pattern**, `Arc` and `Mutex` `simulated_device_and_reader_state.rs`. +pub mod authentication; pub mod device; -pub mod mdoc_auth; pub mod reader; use anyhow::Result; diff --git a/src/presentation/reader.rs b/src/presentation/reader.rs index f5a33089..841376ff 100644 --- a/src/presentation/reader.rs +++ b/src/presentation/reader.rs @@ -13,19 +13,28 @@ //! //! You can view examples in `tests` directory in `simulated_device_and_reader.rs`, for a basic example and //! `simulated_device_and_reader_state.rs` which uses `State` pattern, `Arc` and `Mutex`. -use super::{mdoc_auth::device_authentication, mdoc_auth::issuer_authentication}; -use crate::cbor; -use crate::cbor::CborError; -use crate::definitions::device_key::cose_key::Error as CoseError; -use crate::definitions::x509::trust_anchor::TrustAnchorRegistry; -use crate::definitions::x509::x5chain::X5CHAIN_HEADER_LABEL; -use crate::definitions::x509::X5Chain; -use crate::definitions::{Status, ValidatedResponse}; -use crate::presentation::reader::device_request::ItemsRequestBytes; -use crate::presentation::reader::Error as ReaderError; +use std::collections::BTreeMap; + +use aes::cipher::{generic_array::GenericArray, typenum::U32}; +use anyhow::{anyhow, Result}; +use coset::{CoseSign1Builder, Header, Label}; +use p256::ecdsa::SigningKey; +use sec1::DecodeEcPrivateKey; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use serde_json::Value; +use uuid::Uuid; + +use super::authentication::{ + mdoc::{device_authentication, issuer_authentication}, + AuthenticationStatus, ResponseAuthenticationOutcome, +}; + use crate::{ + cbor::{self, CborError}, definitions::{ device_engagement::DeviceRetrievalMethod, + device_key::cose_key::Error as CoseError, device_request::{self, DeviceRequest, DocRequest, ItemsRequest}, device_response::Document, helpers::{non_empty_vec, NonEmptyVec, Tag24}, @@ -33,22 +42,11 @@ use crate::{ self, create_p256_ephemeral_keys, derive_session_key, get_shared_secret, Handover, SessionEstablishment, }, + x509::{trust_anchor::TrustAnchorRegistry, x5chain::X5CHAIN_HEADER_LABEL, X5Chain}, + DeviceEngagement, DeviceResponse, SessionData, SessionTranscript180135, }, - definitions::{DeviceEngagement, DeviceResponse, SessionData, SessionTranscript180135}, + presentation::reader::{device_request::ItemsRequestBytes, Error as ReaderError}, }; -use aes::cipher::{generic_array::GenericArray, typenum::U32}; -use anyhow::{anyhow, Result}; -use coset::{CoseSign1Builder, Header, Label}; -// use cose_rs::algorithm::Algorithm; -// use cose_rs::sign1::HeaderMap; -// use cose_rs::CoseSign1; -use p256::ecdsa::SigningKey; -use sec1::DecodeEcPrivateKey; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use serde_json::Value; -use std::collections::BTreeMap; -use uuid::Uuid; /// The main state of the reader. /// @@ -351,17 +349,12 @@ impl SessionManager { Ok(device_response) } - pub fn handle_response(&mut self, response: &[u8]) -> ValidatedResponse { - let mut validated_response = ValidatedResponse::default(); + pub fn handle_response(&mut self, response: &[u8]) -> ResponseAuthenticationOutcome { + let mut validated_response = ResponseAuthenticationOutcome::default(); let device_response = match self.decrypt_response(response) { - Ok(device_response) => { - validated_response.decryption = Status::Valid; - - device_response - } + Ok(device_response) => device_response, Err(e) => { - validated_response.decryption = Status::Invalid; validated_response.errors.insert( "decryption_errors".to_string(), json!(vec![format!("{e:?}")]), @@ -375,7 +368,6 @@ impl SessionManager { self.validate_response(x5chain, document.clone(), namespaces) } Err(e) => { - validated_response.parsing = Status::Invalid; validated_response .errors .insert("parsing_errors".to_string(), json!(vec![format!("{e:?}")])); @@ -389,18 +381,18 @@ impl SessionManager { x5chain: X5Chain, document: Document, namespaces: BTreeMap, - ) -> ValidatedResponse { - let mut validated_response = ValidatedResponse { + ) -> ResponseAuthenticationOutcome { + let mut validated_response = ResponseAuthenticationOutcome { response: namespaces, ..Default::default() }; match device_authentication(&document, self.session_transcript.clone()) { Ok(_) => { - validated_response.device_authentication = Status::Valid; + validated_response.device_authentication = AuthenticationStatus::Valid; } Err(e) => { - validated_response.device_authentication = Status::Invalid; + validated_response.device_authentication = AuthenticationStatus::Invalid; validated_response.errors.insert( "device_authentication_errors".to_string(), json!(vec![format!("{e:?}")]), @@ -412,10 +404,10 @@ impl SessionManager { if validation_errors.is_empty() { match issuer_authentication(x5chain, &document.issuer_signed) { Ok(_) => { - validated_response.issuer_authentication = Status::Valid; + validated_response.issuer_authentication = AuthenticationStatus::Valid; } Err(e) => { - validated_response.issuer_authentication = Status::Invalid; + validated_response.issuer_authentication = AuthenticationStatus::Invalid; validated_response.errors.insert( "issuer_authentication_errors".to_string(), serde_json::json!(vec![format!("{e:?}")]), @@ -426,7 +418,7 @@ impl SessionManager { validated_response .errors .insert("certificate_errors".to_string(), json!(validation_errors)); - validated_response.issuer_authentication = Status::Invalid + validated_response.issuer_authentication = AuthenticationStatus::Invalid }; validated_response diff --git a/tests/common.rs b/tests/common.rs index b65f3eaa..d98c00b5 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -4,12 +4,13 @@ use isomdl::cbor; use isomdl::definitions::device_engagement::{CentralClientMode, DeviceRetrievalMethods}; use isomdl::definitions::device_request::{DataElements, DocType, Namespaces}; use isomdl::definitions::helpers::NonEmptyMap; -use isomdl::definitions::validated_request::ValidatedRequest; use isomdl::definitions::x509::trust_anchor::TrustAnchorRegistry; use isomdl::definitions::x509::X5Chain; use isomdl::definitions::{self, BleOptions, DeviceRetrievalMethod}; use isomdl::presentation::device::{Document, Documents, RequestedItems, SessionManagerEngaged}; -use isomdl::presentation::{device, reader, Stringify}; +use isomdl::presentation::{ + authentication::RequestAuthenticationOutcome, device, reader, Stringify, +}; use signature::Signer; use uuid::Uuid; @@ -80,7 +81,7 @@ impl Device { state: SessionManagerEngaged, request: Vec, trusted_verifiers: Option, - ) -> Result<(device::SessionManager, ValidatedRequest)> { + ) -> Result<(device::SessionManager, RequestAuthenticationOutcome)> { let (session_manager, validated_request) = { let session_establishment: definitions::SessionEstablishment = cbor::from_slice(&request).context("could not deserialize request")?; From 47b544cb7ba19d45bf6466ce6f82ff40c68bd4d5 Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 9 Dec 2024 18:27:32 +0000 Subject: [PATCH 06/12] Clean up TODOs * Remove unnecessary TODO regarding 18013-7: work will be underway soon. * Remove unnecessary TODO regarding reader certificate checks - already implemented. * Ensure that all critical extensions are checked and understood. --- src/bin/utils.rs | 1 - src/bin/x509/mod.rs | 13 +- src/definitions/x509/extensions.rs | 584 ++++++++++++++---------- src/definitions/x509/trust_anchor.rs | 53 ++- src/presentation/authentication/mdoc.rs | 9 +- src/presentation/reader.rs | 12 +- 6 files changed, 375 insertions(+), 297 deletions(-) diff --git a/src/bin/utils.rs b/src/bin/utils.rs index 72c712cf..de900335 100644 --- a/src/bin/utils.rs +++ b/src/bin/utils.rs @@ -36,7 +36,6 @@ enum Action { enum RuleSet { Iaca, Aamva, - NamesOnly, } fn main() -> Result<(), Error> { diff --git a/src/bin/x509/mod.rs b/src/bin/x509/mod.rs index eb767499..a6bb4b94 100644 --- a/src/bin/x509/mod.rs +++ b/src/bin/x509/mod.rs @@ -1,7 +1,7 @@ use anyhow::anyhow; use isomdl::definitions::x509::{ error::Error as X509Error, - trust_anchor::{RuleSetType, TrustAnchor, TrustAnchorRegistry, ValidationRuleSet}, + trust_anchor::{TrustAnchor, TrustAnchorRegistry}, x5chain::X509, X5Chain, }; @@ -17,16 +17,11 @@ pub fn validate( .map_err(|e| anyhow!("unable to parse pem: {}", e))? .1; - let ruleset = ValidationRuleSet { - distinguished_names: vec!["2.5.4.6".to_string(), "2.5.4.8".to_string()], - typ: match rules { - RuleSet::Iaca => RuleSetType::IACA, - RuleSet::Aamva => RuleSetType::AAMVA, - RuleSet::NamesOnly => RuleSetType::NamesOnly, - }, + let trust_anchor = match rules { + RuleSet::Iaca => TrustAnchor::Iaca(X509 { bytes: root_bytes }), + RuleSet::Aamva => TrustAnchor::Aamva(X509 { bytes: root_bytes }), }; - let trust_anchor = TrustAnchor::Custom(X509 { bytes: root_bytes }, ruleset); let trust_anchor_registry = TrustAnchorRegistry { certificates: vec![trust_anchor], }; diff --git a/src/definitions/x509/extensions.rs b/src/definitions/x509/extensions.rs index ac29a6fd..9bcda3fe 100644 --- a/src/definitions/x509/extensions.rs +++ b/src/definitions/x509/extensions.rs @@ -1,4 +1,12 @@ +//! All the checks in this module relate to requirements for IACA x509 certificates as +//! detailed in Annex B of ISO18013-5. Specifically, the requirements for values in +//! root and signer certificates are given in tables B.2 and B.4. + +use std::fmt; +use std::ops::Deref; + use crate::definitions::x509::error::Error; +use const_oid::ObjectIdentifier; use der::Decode; use x509_cert::ext::pkix::name::DistributionPointName; use x509_cert::ext::pkix::name::GeneralName; @@ -14,21 +22,11 @@ const OID_BASIC_CONSTRAINTS: &str = "2.5.29.19"; const OID_CRL_DISTRIBUTION_POINTS: &str = "2.5.29.31"; const OID_EXTENDED_KEY_USAGE: &str = "2.5.29.37"; -// -- 18013-5 IACA SPECIFIC ROOT EXTENSION VALUE CHECKS -- // -// Key Usage: 5, 6 (keyCertSign, crlSign) -// Basic Constraints: Pathlen:0 -// CRL Distribution Points must have tag 0 -// Issuer Alternative Name must be of type rfc822Name or a URI (tag 1 and tag 6) - -// -- 18013-5 IACA SPECIFIC LEAF EXTENSION VALUE CHECKS -- // -// Extended Key Usage: 1.0.18013.5.1.2 -// Key Usage: 0 (digitalSignature) -// CRL Distribution Points must have tag 0 -// Issuer Alternative Name must be of type rfc822Name or a URI (tag 1 and tag 6) - -/* All the checks in this file relate to requirements for IACA x509 certificates as -detailed in Annex B of ISO18013-5. Specifically, the requirements for values in -root and signer certificates are given in tables B.2 and B.4 */ +/// 18013-5 IACA root certificate extension checks +/// * Key Usage: 5, 6 (keyCertSign, crlSign) +/// * Basic Constraints: Pathlen:0 +/// * CRL Distribution Points must have tag 0 +/// * Issuer Alternative Name must be of type rfc822Name or a URI (tag 1 and tag 6) pub fn validate_iaca_root_extensions(root_extensions: Vec) -> Vec { //A specific subset of x509 extensions is not allowed in IACA certificates. //We enter an error for every present disallowed x509 extension @@ -47,299 +45,381 @@ pub fn validate_iaca_root_extensions(root_extensions: Vec) -> Vec = - root_extensions.iter().filter(|ext| ext.critical).collect(); - - //TODO: check for any critical extensions beyond what is expected - - // Key Usage 2.5.29.15 - if let Some(key_usage) = root_crit_extensions - .iter() - .find(|ext| ext.extn_id.to_string() == *OID_KEY_USAGE) - { - x509_errors.append(&mut validate_root_key_usage( - key_usage.extn_value.as_bytes(), - )); - } else { - x509_errors.push(Error::ValidationError( - "The root certificate is expected to have its key usage limited to keyCertSign and crlSign, but no restrictions were specified".to_string(), - )); - }; + let critical_extension_errors = ExtensionValidators::default() + .with(RootKeyUsageValidator) + .with(BasicConstraintsValidator) + .with(CrlDistributionPointsValidator { kind: Kind::Root }) + .with(IssuerAlternativeNameValidator { kind: Kind::Root }) + .validate_critical_extensions(root_extensions.iter()); - // Basic Constraints 2.5.29.19 - if let Some(basic_constraints) = root_crit_extensions - .iter() - .find(|ext| ext.extn_id.to_string() == *OID_BASIC_CONSTRAINTS) - { - x509_errors.append(&mut validate_basic_constraints( - basic_constraints.extn_value.as_bytes().to_vec(), - )); - } else { - x509_errors.push(Error::ValidationError( - "The root certificate is expected to have critical basic constraints specificied, but the extensions was not found".to_string() - )); - }; - - //CRL Distribution Points 2.5.29.31 - if let Some(crl_distribution_point) = root_extensions - .iter() - .find(|ext| ext.extn_id.to_string() == *OID_CRL_DISTRIBUTION_POINTS) - { - x509_errors.append(&mut validate_crl_distribution_point( - crl_distribution_point.extn_value.as_bytes().to_vec(), - )); - } else { - x509_errors.push(Error::ValidationError("The root certificate is expected to have a crl distribution point specificied, but the extensions was not found".to_string())); - }; - - // Issuer Alternative Name 2.5.29.18 - if let Some(issuer_alternative_name) = root_extensions - .iter() - .find(|ext| ext.extn_id.to_string() == *OID_ISSUER_ALTERNATIVE_NAME) - { - x509_errors.append(&mut validate_issuer_alternative_name( - issuer_alternative_name.extn_value.as_bytes().to_vec(), - )); - } else { - x509_errors.push(Error::ValidationError( - "The root certificate is expected to have issuer alternative name specificied, but the extensions was not found".to_string() - )); - }; + x509_errors.extend(critical_extension_errors); x509_errors } +/// 18013-5 IACA leaf certificate extension checks +/// * Extended Key Usage: 1.0.18013.5.1.2 +/// * Key Usage: 0 (digitalSignature) +/// * CRL Distribution Points must have tag 0 +/// * Issuer Alternative Name must be of type rfc822Name or a URI (tag 1 and tag 6) pub fn validate_iaca_signer_extensions( leaf_extensions: Vec, - value_extended_key_usage: &str, + value_extended_key_usage: ObjectIdentifier, ) -> Vec { let disallowed = iaca_disallowed_x509_extensions(); let mut x509_errors: Vec = vec![]; - let mut errors: Vec = vec![]; + for extension in leaf_extensions.clone() { if let Some(disallowed_extension) = disallowed .iter() .find(|oid| extension.extn_id.to_string() == **oid) { - errors.push(Error::ValidationError(format!( + x509_errors.push(Error::ValidationError(format!( "The extension with oid: {:?} is not allowed in the IACA certificate profile", disallowed_extension ))); } } - let leaf_crit_extensions: Vec<&Extension> = - leaf_extensions.iter().filter(|ext| ext.critical).collect(); + let critical_extension_errors = ExtensionValidators::default() + .with(ExtendedKeyUsageValidator { + expected_oid: value_extended_key_usage, + }) + .with(SignerKeyUsageValidator) + .with(CrlDistributionPointsValidator { kind: Kind::Signer }) + .with(IssuerAlternativeNameValidator { kind: Kind::Signer }) + .validate_critical_extensions(leaf_extensions.iter()); + + x509_errors.extend(critical_extension_errors); - // Key Usage 2.5.29.15 - if let Some(key_usage) = leaf_crit_extensions - .iter() - .find(|ext| ext.extn_id.to_string() == *OID_KEY_USAGE) - { - x509_errors.append(&mut validate_signer_key_usage( - key_usage.extn_value.as_bytes().to_vec(), - )); - } else { - x509_errors.push(Error::ValidationError( - "Missing critical KeyUsage extension in the signer certificate".to_string(), - )); + x509_errors +} + +#[derive(Default)] +struct ExtensionValidators(Vec>); + +impl ExtensionValidators { + fn with(mut self, validator: V) -> Self { + self.0.push(Box::new(validator)); + self } +} - // Extended Key Usage 2.5.29.37 - if let Some(extended_key_usage) = leaf_crit_extensions - .iter() - .find(|ext| ext.extn_id.to_string() == OID_EXTENDED_KEY_USAGE) - { - x509_errors.append(&mut validate_extended_key_usage( - extended_key_usage.extn_value.as_bytes().to_vec(), - value_extended_key_usage, - )); - } else { - x509_errors.push(Error::ValidationError( - "Missing critical ExtendedKeyUsage extension in the signer certificate".to_string(), - )); - }; +struct CriticalExtensionValidator { + found: bool, + validator: Box, +} - //CRL Distribution Points 2.5.29.31 - if let Some(crl_distribution_point) = leaf_extensions - .iter() - .find(|ext| ext.extn_id.to_string() == *OID_CRL_DISTRIBUTION_POINTS) - { - x509_errors.append(&mut validate_crl_distribution_point( - crl_distribution_point.extn_value.as_bytes().to_vec(), - )); - } else { - x509_errors.push(Error::ValidationError( - "The leaf certificate is expected to have a crl distribution point specificied, but the extensions was not found".to_string(), - )); - }; +impl CriticalExtensionValidator { + fn new(validator: Box) -> Self { + Self { + found: false, + validator, + } + } +} - // Issuer Alternative Name 2.5.29.18 - if let Some(issuer_alternative_name) = leaf_extensions - .iter() - .find(|ext| ext.extn_id.to_string() == *OID_ISSUER_ALTERNATIVE_NAME) +impl Deref for CriticalExtensionValidator { + type Target = Box; + + fn deref(&self) -> &Self::Target { + &self.validator + } +} + +trait ExtensionValidator { + fn matches(&self, extension: &Extension) -> bool; + fn validate(&self, extension: &Extension) -> Vec; + fn not_found(&self) -> Error; +} + +impl ExtensionValidators { + fn validate_critical_extensions<'a, Extensions>(self, extensions: Extensions) -> Vec + where + Extensions: Iterator, { - x509_errors.append(&mut validate_issuer_alternative_name( - issuer_alternative_name.extn_value.as_bytes().to_vec(), - )); - } else { - x509_errors.push(Error::ValidationError("The leaf certificate is expected to have issuer alternative name specificied, but the extensions was not found".to_string())); - }; + let mut validation_errors = vec![]; + + let mut validators: Vec = self + .0 + .into_iter() + .map(CriticalExtensionValidator::new) + .collect(); + let mut validators_mut = validators.iter_mut(); + + for ext in extensions.filter(|ext| ext.critical) { + if let Some(validator) = validators_mut.find(|validator| validator.matches(ext)) { + validation_errors.extend(validator.validate(ext)); + validator.found = true; + } else { + validation_errors.push(Error::ValidationError(format!( + "certificate contains unknown critical extension: {:?}", + ext.extn_id + ))); + } + } - x509_errors + validation_errors.extend(validators_mut.filter(|v| !v.found).map(|v| v.not_found())); + + validation_errors + } +} + +struct ExtendedKeyUsageValidator { + expected_oid: ObjectIdentifier, } -/* A signer certificate should have digital signatures set for it's key usage, -but not other key usages are allowed */ -pub fn validate_signer_key_usage(bytes: Vec) -> Vec { - let mut errors: Vec = vec![]; - let key_usage = KeyUsage::from_der(&bytes); - - match key_usage { - Ok(ku) => { - if !ku.digital_signature() { - errors.push(Error::ValidationError( - "Signer key usage should be set to digital signature".to_string(), - )) +impl ExtensionValidator for ExtendedKeyUsageValidator { + fn matches(&self, extension: &Extension) -> bool { + extension.extn_id.to_string() == OID_EXTENDED_KEY_USAGE + } + + /* A root certificate should have KeyCertSign and CRLSign set for key usage, + but no other key usages are allowed */ + fn validate(&self, extension: &Extension) -> Vec { + let bytes = extension.extn_value.as_bytes(); + let extended_key_usage = ExtendedKeyUsage::from_der(&bytes); + match extended_key_usage { + Ok(eku) => { + if !eku.0.into_iter().all(|oid| oid == self.expected_oid) { + return vec![Error::ValidationError(format!( + "Invalid extended key usage, expected: {}", + self.expected_oid + ))]; + }; + vec![] } - if ku - .0 - .into_iter() - .any(|flag| flag != KeyUsages::DigitalSignature) - { - errors.push(Error::ValidationError( - "Key usage is set beyond scope of IACA signer certificates".to_string(), - )) + Err(e) => { + vec![Error::DecodingError(e.to_string())] } } - Err(e) => { - errors.push(e.into()); - } - }; - errors + } + + fn not_found(&self) -> Error { + Error::ValidationError( + "Missing critical ExtendedKeyUsage extension in the signer certificate".to_string(), + ) + } } -/* A root certificate should have KeyCertSign and CRLSign set for key usage, -but no other key usages are allowed */ -pub fn validate_root_key_usage(bytes: &[u8]) -> Vec { - let mut errors = vec![]; - let key_usage = KeyUsage::from_der(bytes); - match key_usage { - Ok(ku) => { - if !ku.crl_sign() { - errors.push(Error::ValidationError( - "CrlSign should be set on the root certificate key usage".to_string(), - )) - }; - if !ku.key_cert_sign() { - errors.push(Error::ValidationError( - "KeyCertSign should be set on the root certificate key usage".to_string(), - )) - }; - - if ku - .0 - .into_iter() - .any(|flag| flag != KeyUsages::CRLSign && flag != KeyUsages::KeyCertSign) - { - errors.push(Error::ValidationError(format!("The key usage of the root certificate goes beyond the scope of IACA root certificates {:?}", ku))) - }; - errors - } - Err(e) => { - vec![Error::DecodingError(e.to_string())] - } +struct SignerKeyUsageValidator; + +impl ExtensionValidator for SignerKeyUsageValidator { + fn matches(&self, extension: &Extension) -> bool { + extension.extn_id.to_string() == OID_KEY_USAGE + } + + /* A root certificate should have KeyCertSign and CRLSign set for key usage, + but no other key usages are allowed */ + fn validate(&self, extension: &Extension) -> Vec { + let bytes = extension.extn_value.as_bytes(); + let mut errors: Vec = vec![]; + let key_usage = KeyUsage::from_der(&bytes); + + match key_usage { + Ok(ku) => { + if !ku.digital_signature() { + errors.push(Error::ValidationError( + "Signer key usage should be set to digital signature".to_string(), + )) + } + if ku + .0 + .into_iter() + .any(|flag| flag != KeyUsages::DigitalSignature) + { + errors.push(Error::ValidationError( + "Key usage is set beyond scope of IACA signer certificates".to_string(), + )) + } + } + Err(e) => { + errors.push(e.into()); + } + }; + errors + } + + fn not_found(&self) -> Error { + Error::ValidationError( + "Missing critical KeyUsage extension in the signer certificate".to_string(), + ) } } -/* Extended key usage in the signer certificate should be set to this OID meant specifically for mDL signing. -Note that this value will be different for other types of mdocs */ - -pub fn validate_extended_key_usage(bytes: Vec, value_extended_key_usage: &str) -> Vec { - let extended_key_usage = ExtendedKeyUsage::from_der(&bytes); - match extended_key_usage { - Ok(eku) => { - if !eku - .0 - .into_iter() - .any(|oid| oid.to_string() == value_extended_key_usage) - { - return vec![Error::ValidationError( - "Invalid extended key usage, expected: 1.0.18013.5.1.2".to_string(), - )]; - }; - vec![] - } - Err(e) => { - vec![Error::DecodingError(e.to_string())] +struct RootKeyUsageValidator; + +impl ExtensionValidator for RootKeyUsageValidator { + fn matches(&self, extension: &Extension) -> bool { + extension.extn_id.to_string() == OID_KEY_USAGE + } + + /* A root certificate should have KeyCertSign and CRLSign set for key usage, + but no other key usages are allowed */ + fn validate(&self, extension: &Extension) -> Vec { + let bytes = extension.extn_value.as_bytes(); + let mut errors = vec![]; + let key_usage = KeyUsage::from_der(bytes); + match key_usage { + Ok(ku) => { + if !ku.crl_sign() { + errors.push(Error::ValidationError( + "CrlSign should be set on the root certificate key usage".to_string(), + )) + }; + if !ku.key_cert_sign() { + errors.push(Error::ValidationError( + "KeyCertSign should be set on the root certificate key usage".to_string(), + )) + }; + + if ku + .0 + .into_iter() + .any(|flag| flag != KeyUsages::CRLSign && flag != KeyUsages::KeyCertSign) + { + errors.push(Error::ValidationError(format!("The key usage of the root certificate goes beyond the scope of IACA root certificates {:?}", ku))) + }; + errors + } + Err(e) => { + vec![Error::DecodingError(e.to_string())] + } } } + + fn not_found(&self) -> Error { + Error::ValidationError( + "Missing critical KeyUsage extension in the root certificate".to_string(), + ) + } } -/* The CRL DistributionPoint shall not contain values for crl_issuer and reasons. -Every Distribution Point must be of a type URI or RFC822Name */ -pub fn validate_crl_distribution_point(bytes: Vec) -> Vec { - let mut errors: Vec = vec![]; - let crl_distribution_point = CrlDistributionPoints::from_der(&bytes); - match crl_distribution_point { - Ok(crl_dp) => { - for point in crl_dp.0.into_iter() { - if point.crl_issuer.is_some() || point.reasons.is_some() { - errors.push(Error::ValidationError(format!("crl_issuer and reasons may not be set on CrlDistributionPoints, but is set for: {:?}", point))) - } +struct BasicConstraintsValidator; - if !point - .distribution_point - .clone() - .is_some_and(|dpn| match dpn { - DistributionPointName::FullName(names) => { - let type_errors: Vec = check_general_name_types(names); - type_errors.is_empty() - } - DistributionPointName::NameRelativeToCRLIssuer(_) => false, - }) - { - errors.push(Error::ValidationError(format!( - "crl distribution point has an invalid type: {:?}", - point - ))) +impl ExtensionValidator for BasicConstraintsValidator { + fn matches(&self, extension: &Extension) -> bool { + extension.extn_id.to_string() == OID_BASIC_CONSTRAINTS + } + + fn validate(&self, extension: &Extension) -> Vec { + let bytes = extension.extn_value.as_bytes(); + let basic_constraints = BasicConstraints::from_der(&bytes); + match basic_constraints { + Ok(bc) => { + if bc.path_len_constraint.is_none_or(|path_len| path_len != 0) && bc.ca { + return vec![Error::ValidationError(format!( + "Basic constraints expected to be CA:true, path_len:0, but found: {:?}", + bc + ))]; } + vec![] + } + Err(e) => { + vec![Error::DecodingError(e.to_string())] } } - Err(e) => errors.push(Error::DecodingError(e.to_string())), } - errors + fn not_found(&self) -> Error { + Error::ValidationError( + "The root certificate is expected to have critical basic constraints specificied, but the extensions was not found".to_string() + ) + } } -/* The Issuer Alternative Name must be of a type URI or RFC822Name */ -pub fn validate_issuer_alternative_name(bytes: Vec) -> Vec { - let iss_altname = IssuerAltName::from_der(&bytes); - match iss_altname { - Ok(ian) => check_general_name_types(ian.0), - Err(e) => { - vec![Error::DecodingError(e.to_string())] +struct CrlDistributionPointsValidator { + kind: Kind, +} + +impl ExtensionValidator for CrlDistributionPointsValidator { + fn matches(&self, extension: &Extension) -> bool { + extension.extn_id.to_string() == OID_CRL_DISTRIBUTION_POINTS + } + + fn validate(&self, extension: &Extension) -> Vec { + let bytes = extension.extn_value.as_bytes(); + let mut errors: Vec = vec![]; + let crl_distribution_point = CrlDistributionPoints::from_der(&bytes); + match crl_distribution_point { + Ok(crl_dp) => { + for point in crl_dp.0.into_iter() { + if point.crl_issuer.is_some() || point.reasons.is_some() { + errors.push(Error::ValidationError(format!("crl_issuer and reasons may not be set on CrlDistributionPoints, but is set for: {:?}", point))) + } + + if !point + .distribution_point + .clone() + .is_some_and(|dpn| match dpn { + DistributionPointName::FullName(names) => { + let type_errors: Vec = check_general_name_types(names); + type_errors.is_empty() + } + DistributionPointName::NameRelativeToCRLIssuer(_) => false, + }) + { + errors.push(Error::ValidationError(format!( + "crl distribution point has an invalid type: {:?}", + point + ))) + } + } + } + Err(e) => errors.push(Error::DecodingError(e.to_string())), } + + errors } + + fn not_found(&self) -> Error { + Error::ValidationError(format!("The {} certificate is expected to have a crl distribution point specificied, but the extension was not found", self.kind)) + } +} + +struct IssuerAlternativeNameValidator { + kind: Kind, } -/* Basic Constraints must be CA: true, path_len: 0 */ -pub fn validate_basic_constraints(bytes: Vec) -> Vec { - let basic_constraints = BasicConstraints::from_der(&bytes); - match basic_constraints { - Ok(bc) => { - if bc.path_len_constraint.is_none_or(|path_len| path_len != 0) && bc.ca { - return vec![Error::ValidationError(format!( - "Basic constraints expected to be CA:true, path_len:0, but found: {:?}", - bc - ))]; +impl ExtensionValidator for IssuerAlternativeNameValidator { + fn matches(&self, extension: &Extension) -> bool { + extension.extn_id.to_string() == OID_ISSUER_ALTERNATIVE_NAME + } + + fn validate(&self, extension: &Extension) -> Vec { + let bytes = extension.extn_value.as_bytes(); + let iss_altname = IssuerAltName::from_der(bytes); + match iss_altname { + Ok(ian) => check_general_name_types(ian.0), + Err(e) => { + vec![Error::DecodingError(e.to_string())] } - vec![] - } - Err(e) => { - vec![Error::DecodingError(e.to_string())] } } + + fn not_found(&self) -> Error { + Error::ValidationError(format!( + "The {} certificate is expected to have issuer alternative name specificied, but the extension was not found", self.kind) + ) + } +} + +enum Kind { + Root, + Signer, +} + +impl fmt::Display for Kind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::Root => "root", + Self::Signer => "signer", + } + ) + } } fn check_general_name_types(names: Vec) -> Vec { @@ -363,7 +443,7 @@ fn check_general_name_types(names: Vec) -> Vec { } } -pub fn iaca_disallowed_x509_extensions() -> Vec { +fn iaca_disallowed_x509_extensions() -> Vec { vec![ "2.5.29.30".to_string(), "2.5.29.33".to_string(), diff --git a/src/definitions/x509/trust_anchor.rs b/src/definitions/x509/trust_anchor.rs index 8ac9a840..5c8fb70a 100644 --- a/src/definitions/x509/trust_anchor.rs +++ b/src/definitions/x509/trust_anchor.rs @@ -3,20 +3,17 @@ use crate::definitions::x509::extensions::{ validate_iaca_root_extensions, validate_iaca_signer_extensions, }; use crate::definitions::x509::x5chain::X509; +use const_oid::ObjectIdentifier; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use x509_cert::attr::AttributeTypeAndValue; use x509_cert::certificate::CertificateInner; use x509_cert::der::Decode; -const MDOC_VALUE_EXTENDED_KEY_USAGE: &str = "1.0.18013.5.1.2"; -const READER_VALUE_EXTENDED_KEY_USAGE: &str = "1.0.18013.5.1.6"; - #[derive(Serialize, Deserialize, Clone)] pub enum TrustAnchor { Iaca(X509), Aamva(X509), - Custom(X509, ValidationRuleSet), IacaReader(X509), } @@ -150,9 +147,6 @@ pub fn validate_with_ruleset( } }; } - TrustAnchor::Custom(_certificate, _ruleset) => { - //TODO - } } errors } @@ -280,7 +274,7 @@ fn apply_ruleset( let mut extension_errors = validate_iaca_root_extensions(root_extensions); extension_errors.append(&mut validate_iaca_signer_extensions( leaf_extensions, - MDOC_VALUE_EXTENDED_KEY_USAGE, + mdoc_extended_key_usage_oid(), )); for dn in leaf_distinguished_names { if dn.oid.to_string() == *"2.5.4.8" { @@ -304,7 +298,7 @@ fn apply_ruleset( let mut extension_errors = validate_iaca_root_extensions(root_extensions); extension_errors.append(&mut validate_iaca_signer_extensions( leaf_extensions, - MDOC_VALUE_EXTENDED_KEY_USAGE, + mdoc_extended_key_usage_oid(), )); for dn in leaf_distinguished_names { let Some(_root_dn) = root_distinguished_names.iter().find(|r| r == &&dn) else { @@ -321,14 +315,10 @@ fn apply_ruleset( } Ok(vec![]) } - RuleSetType::ReaderAuth => { - //TODO - - Ok(validate_iaca_signer_extensions( - leaf_extensions, - READER_VALUE_EXTENDED_KEY_USAGE, - )) - } + RuleSetType::ReaderAuth => Ok(validate_iaca_signer_extensions( + leaf_extensions, + reader_extended_key_usage_oid(), + )), } } @@ -351,12 +341,6 @@ pub fn find_anchor( Err(_) => false, } } - TrustAnchor::Custom(certificate, _ruleset) => { - match x509_cert::Certificate::from_der(&certificate.bytes) { - Ok(root_cert) => root_cert.tbs_certificate.subject == leaf_issuer, - Err(_) => false, - } - } TrustAnchor::Aamva(certificate) => { match x509_cert::Certificate::from_der(&certificate.bytes) { Ok(root_cert) => root_cert.tbs_certificate.subject == leaf_issuer, @@ -377,3 +361,26 @@ pub fn find_anchor( }; Ok(Some(trust_anchor)) } + +fn mdoc_extended_key_usage_oid() -> ObjectIdentifier { + // Unwrap safety: unit tested. + ObjectIdentifier::new("1.0.18013.5.1.2").unwrap() +} + +fn reader_extended_key_usage_oid() -> ObjectIdentifier { + // Unwrap safety: unit tested. + ObjectIdentifier::new("1.0.18013.5.1.6").unwrap() +} + +#[cfg(test)] +mod test { + #[test] + fn mdoc_extended_key_usage_oid_doesnt_panic() { + super::mdoc_extended_key_usage_oid(); + } + + #[test] + fn reader_extended_key_usage_oid_doesnt_panic() { + super::reader_extended_key_usage_oid(); + } +} diff --git a/src/presentation/authentication/mdoc.rs b/src/presentation/authentication/mdoc.rs index c8941edb..ffe9e62e 100644 --- a/src/presentation/authentication/mdoc.rs +++ b/src/presentation/authentication/mdoc.rs @@ -24,11 +24,9 @@ pub fn issuer_authentication(x5chain: X5Chain, issuer_signed: &IssuerSigned) -> issuer_signed .issuer_auth .verify::(&signer_key, None, None); - if !verification_result.is_success() { - Err(ReaderError::ParsingError)? - } else { - Ok(()) - } + verification_result + .into_result() + .map_err(ReaderError::IssuerAuthentication) } pub fn device_authentication( @@ -62,7 +60,6 @@ pub fn device_authentication( let namespaces_bytes = &document.device_signed.namespaces; let device_auth: &DeviceAuth = &document.device_signed.device_auth; - //TODO: fix for attended use case: match device_auth { DeviceAuth::Signature { device_signature } => { let detached_payload = Tag24::new(DeviceAuthentication::new( diff --git a/src/presentation/reader.rs b/src/presentation/reader.rs index 841376ff..81707a48 100644 --- a/src/presentation/reader.rs +++ b/src/presentation/reader.rs @@ -107,8 +107,8 @@ pub enum Error { /// Not a valid JSON input. #[error("not a valid JSON input.")] JsonError, - /// Unexpected date type for data_element. - #[error("Unexpected date type for data_element.")] + /// Unexpected data type for data element. + #[error("Unexpected data type for data element.")] ParsingError, /// Request for data is invalid. #[error("Request for data is invalid.")] @@ -117,10 +117,10 @@ pub enum Error { MdocAuth(String), #[error("Currently unsupported format")] Unsupported, - #[error("No x5chain found for mdoc authentication")] + #[error("No x5chain found for issuer authentication")] X5Chain, - #[error("Could not serialize to cbor: {0}")] - CborError(CborError), + #[error("issuer authentication failed: {0}")] + IssuerAuthentication(String), } impl From for Error { @@ -376,7 +376,7 @@ impl SessionManager { } } - pub fn validate_response( + fn validate_response( &mut self, x5chain: X5Chain, document: Document, From 556f1444a29fccc3848f86c3eaf2b4b9c65ea17f Mon Sep 17 00:00:00 2001 From: Jacob Date: Thu, 12 Dec 2024 14:49:45 +0000 Subject: [PATCH 07/12] Fix extensions validation and tests --- Cargo.toml | 8 +- src/bin/x509/mod.rs | 13 +- src/definitions/x509/extensions.rs | 197 ++++++++----- src/definitions/x509/mod.rs | 171 ++++++++++++ src/definitions/x509/trust_anchor.rs | 304 ++++++++++----------- src/definitions/x509/x5chain.rs | 104 +++---- src/presentation/device.rs | 3 +- src/presentation/reader.rs | 159 ++--------- src/x509/mod.rs | 0 tests/common.rs | 19 +- tests/data/sec1.pem | 2 +- tests/simulated_device_and_reader.rs | 22 +- tests/simulated_device_and_reader_state.rs | 18 +- 13 files changed, 553 insertions(+), 467 deletions(-) delete mode 100644 src/x509/mod.rs diff --git a/Cargo.toml b/Cargo.toml index b840b1cc..ac657e9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,14 +16,14 @@ path = "src/bin/utils.rs" [dependencies] anyhow = "1.0" -ecdsa = { version = "0.16.0", features = ["serde", "verifying"] } +ecdsa = { version = "0.16.9", features = ["serde", "verifying"] } p256 = { version = "0.13.0", features = ["serde", "ecdh"] } p384 = { version = "0.13.0", features = ["serde", "ecdh"] } rand = { version = "0.8.5", features = ["getrandom"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_bytes = "0.11.0" -sha2 = "0.10.6" +sha2 = { version = "0.10.8", features = ["oid"] } thiserror = "1.0" elliptic-curve = "0.13.1" hkdf = "0.12.3" @@ -56,8 +56,12 @@ strum_macros = "0.24" coset = "0.3.8" ciborium = "0.2.2" digest = "0.10.7" +tracing = "0.1.41" [dev-dependencies] hex = "0.4.3" p256 = "0.13.0" +rstest = "0.23.0" serde_json = "*" +test-log = { version = "0.2.16", features = ["trace"] } +x509-cert = { version = "0.2.4", features = ["pem", "builder", "hazmat"] } diff --git a/src/bin/x509/mod.rs b/src/bin/x509/mod.rs index a6bb4b94..8bb8ebea 100644 --- a/src/bin/x509/mod.rs +++ b/src/bin/x509/mod.rs @@ -1,10 +1,11 @@ use anyhow::anyhow; +use der::DecodePem; use isomdl::definitions::x509::{ error::Error as X509Error, trust_anchor::{TrustAnchor, TrustAnchorRegistry}, - x5chain::X509, X5Chain, }; +use x509_cert::Certificate; use crate::RuleSet; @@ -13,13 +14,11 @@ pub fn validate( signer: &[u8], root: &[u8], ) -> Result, anyhow::Error> { - let root_bytes = pem_rfc7468::decode_vec(root) - .map_err(|e| anyhow!("unable to parse pem: {}", e))? - .1; + let root = Certificate::from_pem(root)?; let trust_anchor = match rules { - RuleSet::Iaca => TrustAnchor::Iaca(X509 { bytes: root_bytes }), - RuleSet::Aamva => TrustAnchor::Aamva(X509 { bytes: root_bytes }), + RuleSet::Iaca => TrustAnchor::Iaca(root), + RuleSet::Aamva => TrustAnchor::Aamva(root), }; let trust_anchor_registry = TrustAnchorRegistry { @@ -32,5 +31,5 @@ pub fn validate( let x5chain = X5Chain::from_cbor(x5chain_cbor)?; - Ok(x5chain.validate(Some(trust_anchor_registry))) + Ok(x5chain.validate(Some(&trust_anchor_registry))) } diff --git a/src/definitions/x509/extensions.rs b/src/definitions/x509/extensions.rs index 9bcda3fe..e33fbe21 100644 --- a/src/definitions/x509/extensions.rs +++ b/src/definitions/x509/extensions.rs @@ -6,7 +6,9 @@ use std::fmt; use std::ops::Deref; use crate::definitions::x509::error::Error; +use const_oid::AssociatedOid; use const_oid::ObjectIdentifier; +use der::flagset::FlagSet; use der::Decode; use x509_cert::ext::pkix::name::DistributionPointName; use x509_cert::ext::pkix::name::GeneralName; @@ -15,25 +17,19 @@ use x509_cert::ext::pkix::{ }; use x509_cert::ext::Extension; -// -- IACA X509 Extension OIDs -- // -const OID_KEY_USAGE: &str = "2.5.29.15"; -const OID_ISSUER_ALTERNATIVE_NAME: &str = "2.5.29.18"; -const OID_BASIC_CONSTRAINTS: &str = "2.5.29.19"; -const OID_CRL_DISTRIBUTION_POINTS: &str = "2.5.29.31"; -const OID_EXTENDED_KEY_USAGE: &str = "2.5.29.37"; - /// 18013-5 IACA root certificate extension checks /// * Key Usage: 5, 6 (keyCertSign, crlSign) /// * Basic Constraints: Pathlen:0 /// * CRL Distribution Points must have tag 0 /// * Issuer Alternative Name must be of type rfc822Name or a URI (tag 1 and tag 6) -pub fn validate_iaca_root_extensions(root_extensions: Vec) -> Vec { +pub fn validate_iaca_root_extensions(root_extensions: &[Extension]) -> Vec { + tracing::debug!("validating root certificate extensions..."); //A specific subset of x509 extensions is not allowed in IACA certificates. //We enter an error for every present disallowed x509 extension let disallowed = iaca_disallowed_x509_extensions(); let mut x509_errors: Vec = vec![]; - for extension in root_extensions.clone() { + for extension in root_extensions { if let Some(disallowed_extension) = disallowed .iter() .find(|oid| extension.extn_id.to_string() == **oid) @@ -45,14 +41,14 @@ pub fn validate_iaca_root_extensions(root_extensions: Vec) -> Vec) -> Vec, + leaf_extensions: &[Extension], value_extended_key_usage: ObjectIdentifier, ) -> Vec { + tracing::debug!("validating signer certificate extensions..."); + let disallowed = iaca_disallowed_x509_extensions(); let mut x509_errors: Vec = vec![]; - for extension in leaf_extensions.clone() { + for extension in leaf_extensions { if let Some(disallowed_extension) = disallowed .iter() .find(|oid| extension.extn_id.to_string() == **oid) @@ -81,16 +79,16 @@ pub fn validate_iaca_signer_extensions( } } - let critical_extension_errors = ExtensionValidators::default() + let extension_errors = ExtensionValidators::default() .with(ExtendedKeyUsageValidator { expected_oid: value_extended_key_usage, }) .with(SignerKeyUsageValidator) .with(CrlDistributionPointsValidator { kind: Kind::Signer }) .with(IssuerAlternativeNameValidator { kind: Kind::Signer }) - .validate_critical_extensions(leaf_extensions.iter()); + .validate_extensions(leaf_extensions.iter()); - x509_errors.extend(critical_extension_errors); + x509_errors.extend(extension_errors); x509_errors } @@ -105,12 +103,12 @@ impl ExtensionValidators { } } -struct CriticalExtensionValidator { +struct RequiredExtension { found: bool, validator: Box, } -impl CriticalExtensionValidator { +impl RequiredExtension { fn new(validator: Box) -> Self { Self { found: false, @@ -119,7 +117,7 @@ impl CriticalExtensionValidator { } } -impl Deref for CriticalExtensionValidator { +impl Deref for RequiredExtension { type Target = Box; fn deref(&self) -> &Self::Target { @@ -134,32 +132,43 @@ trait ExtensionValidator { } impl ExtensionValidators { - fn validate_critical_extensions<'a, Extensions>(self, extensions: Extensions) -> Vec + fn validate_extensions<'a, Extensions>(self, extensions: Extensions) -> Vec where Extensions: Iterator, { let mut validation_errors = vec![]; - let mut validators: Vec = self - .0 - .into_iter() - .map(CriticalExtensionValidator::new) - .collect(); - let mut validators_mut = validators.iter_mut(); + let mut validators: Vec = + self.0.into_iter().map(RequiredExtension::new).collect(); - for ext in extensions.filter(|ext| ext.critical) { - if let Some(validator) = validators_mut.find(|validator| validator.matches(ext)) { + for ext in extensions { + if let Some(validator) = validators.iter_mut().find(|validator| { + tracing::debug!("searching for ext: '{}'", ext.extn_id); + validator.matches(ext) + }) { + tracing::debug!("validating required extension: {}", ext.extn_id); validation_errors.extend(validator.validate(ext)); validator.found = true; - } else { + } else if ext.critical { + tracing::debug!( + "critical, non-required extension causing an error: {}", + ext.extn_id + ); validation_errors.push(Error::ValidationError(format!( - "certificate contains unknown critical extension: {:?}", + "certificate contains unknown critical extension: {}", ext.extn_id ))); + } else { + tracing::debug!("non-critical, non-required extension ignored: {ext:?}") } } - validation_errors.extend(validators_mut.filter(|v| !v.found).map(|v| v.not_found())); + validation_errors.extend( + validators + .iter() + .filter(|v| !v.found) + .map(|v| v.not_found()), + ); validation_errors } @@ -171,7 +180,7 @@ struct ExtendedKeyUsageValidator { impl ExtensionValidator for ExtendedKeyUsageValidator { fn matches(&self, extension: &Extension) -> bool { - extension.extn_id.to_string() == OID_EXTENDED_KEY_USAGE + extension.extn_id == ExtendedKeyUsage::OID } /* A root certificate should have KeyCertSign and CRLSign set for key usage, @@ -179,6 +188,13 @@ impl ExtensionValidator for ExtendedKeyUsageValidator { fn validate(&self, extension: &Extension) -> Vec { let bytes = extension.extn_value.as_bytes(); let extended_key_usage = ExtendedKeyUsage::from_der(&bytes); + + if !extension.critical { + tracing::warn!( + "expected ExtendedKeyUsage extension to be critical on signer certificate", + ) + } + match extended_key_usage { Ok(eku) => { if !eku.0.into_iter().all(|oid| oid == self.expected_oid) { @@ -206,7 +222,7 @@ struct SignerKeyUsageValidator; impl ExtensionValidator for SignerKeyUsageValidator { fn matches(&self, extension: &Extension) -> bool { - extension.extn_id.to_string() == OID_KEY_USAGE + extension.extn_id == KeyUsage::OID } /* A root certificate should have KeyCertSign and CRLSign set for key usage, @@ -216,20 +232,16 @@ impl ExtensionValidator for SignerKeyUsageValidator { let mut errors: Vec = vec![]; let key_usage = KeyUsage::from_der(&bytes); + if !extension.critical { + tracing::warn!("expected KeyUsage extension to be critical on signer certificate",) + } + match key_usage { Ok(ku) => { - if !ku.digital_signature() { - errors.push(Error::ValidationError( - "Signer key usage should be set to digital signature".to_string(), - )) - } - if ku - .0 - .into_iter() - .any(|flag| flag != KeyUsages::DigitalSignature) - { + let expected_flagset: FlagSet = KeyUsages::DigitalSignature.into(); + if ku.0 != expected_flagset { errors.push(Error::ValidationError( - "Key usage is set beyond scope of IACA signer certificates".to_string(), + "Signer KeyUsage should be set to DigitalSignature only".into(), )) } } @@ -251,7 +263,7 @@ struct RootKeyUsageValidator; impl ExtensionValidator for RootKeyUsageValidator { fn matches(&self, extension: &Extension) -> bool { - extension.extn_id.to_string() == OID_KEY_USAGE + extension.extn_id == KeyUsage::OID } /* A root certificate should have KeyCertSign and CRLSign set for key usage, @@ -260,6 +272,11 @@ impl ExtensionValidator for RootKeyUsageValidator { let bytes = extension.extn_value.as_bytes(); let mut errors = vec![]; let key_usage = KeyUsage::from_der(bytes); + + if !extension.critical { + tracing::warn!("expected KeyUsage extension to be critical on root certificate",) + } + match key_usage { Ok(ku) => { if !ku.crl_sign() { @@ -297,33 +314,52 @@ impl ExtensionValidator for RootKeyUsageValidator { struct BasicConstraintsValidator; +impl BasicConstraintsValidator { + fn check(constraints: BasicConstraints) -> Option { + if constraints + .path_len_constraint + .is_none_or(|path_len| path_len != 0) + || !constraints.ca + { + Some(Error::ValidationError(format!( + "Basic constraints expected to be CA:true, path_len:0, but found: {:?}", + constraints + ))) + } else { + None + } + } +} + impl ExtensionValidator for BasicConstraintsValidator { fn matches(&self, extension: &Extension) -> bool { - extension.extn_id.to_string() == OID_BASIC_CONSTRAINTS + extension.extn_id == BasicConstraints::OID } fn validate(&self, extension: &Extension) -> Vec { + let mut errors = vec![]; + + if !extension.critical { + tracing::warn!("expected BasicConstraints extension to be critical on root certificate",) + } + let bytes = extension.extn_value.as_bytes(); let basic_constraints = BasicConstraints::from_der(&bytes); match basic_constraints { Ok(bc) => { - if bc.path_len_constraint.is_none_or(|path_len| path_len != 0) && bc.ca { - return vec![Error::ValidationError(format!( - "Basic constraints expected to be CA:true, path_len:0, but found: {:?}", - bc - ))]; + if let Some(e) = Self::check(bc) { + errors.push(e); } - vec![] - } - Err(e) => { - vec![Error::DecodingError(e.to_string())] } + Err(e) => errors.push(Error::DecodingError(e.to_string())), } + + errors } fn not_found(&self) -> Error { Error::ValidationError( - "The root certificate is expected to have critical basic constraints specificied, but the extensions was not found".to_string() + "The root certificate is expected to have BasicConstraints, but the extension was not found".to_string() ) } } @@ -334,7 +370,7 @@ struct CrlDistributionPointsValidator { impl ExtensionValidator for CrlDistributionPointsValidator { fn matches(&self, extension: &Extension) -> bool { - extension.extn_id.to_string() == OID_CRL_DISTRIBUTION_POINTS + extension.extn_id == CrlDistributionPoints::OID } fn validate(&self, extension: &Extension) -> Vec { @@ -343,14 +379,25 @@ impl ExtensionValidator for CrlDistributionPointsValidator { let crl_distribution_point = CrlDistributionPoints::from_der(&bytes); match crl_distribution_point { Ok(crl_dp) => { + if crl_dp.0.is_empty() { + errors.push(Error::ValidationError( + "expected one or more CRL distribution points".into(), + )); + } for point in crl_dp.0.into_iter() { - if point.crl_issuer.is_some() || point.reasons.is_some() { - errors.push(Error::ValidationError(format!("crl_issuer and reasons may not be set on CrlDistributionPoints, but is set for: {:?}", point))) + if point.crl_issuer.is_some() { + errors.push(Error::ValidationError(format!("crl_issuer may not be set on CrlDistributionPoints, but is set for: {point:?}"))) + } + + if point.reasons.is_some() { + errors.push(Error::ValidationError(format!( + "reasons may not be set on CrlDistributionPoints, but is set for: {point:?}", + ))) } if !point .distribution_point - .clone() + .as_ref() .is_some_and(|dpn| match dpn { DistributionPointName::FullName(names) => { let type_errors: Vec = check_general_name_types(names); @@ -373,7 +420,7 @@ impl ExtensionValidator for CrlDistributionPointsValidator { } fn not_found(&self) -> Error { - Error::ValidationError(format!("The {} certificate is expected to have a crl distribution point specificied, but the extension was not found", self.kind)) + Error::ValidationError(format!("The {} certificate is expected to have CRLDistributionPoints, but the extension was not found", self.kind)) } } @@ -383,14 +430,14 @@ struct IssuerAlternativeNameValidator { impl ExtensionValidator for IssuerAlternativeNameValidator { fn matches(&self, extension: &Extension) -> bool { - extension.extn_id.to_string() == OID_ISSUER_ALTERNATIVE_NAME + extension.extn_id == IssuerAltName::OID } fn validate(&self, extension: &Extension) -> Vec { let bytes = extension.extn_value.as_bytes(); let iss_altname = IssuerAltName::from_der(bytes); match iss_altname { - Ok(ian) => check_general_name_types(ian.0), + Ok(ian) => check_general_name_types(&ian.0), Err(e) => { vec![Error::DecodingError(e.to_string())] } @@ -399,7 +446,7 @@ impl ExtensionValidator for IssuerAlternativeNameValidator { fn not_found(&self) -> Error { Error::ValidationError(format!( - "The {} certificate is expected to have issuer alternative name specificied, but the extension was not found", self.kind) + "The {} certificate is expected to have issuer alternative name specified, but the extension was not found", self.kind) ) } } @@ -422,7 +469,7 @@ impl fmt::Display for Kind { } } -fn check_general_name_types(names: Vec) -> Vec { +fn check_general_name_types(names: &[GeneralName]) -> Vec { let valid_types: Vec = names .iter() .map(|name| { @@ -455,7 +502,19 @@ fn iaca_disallowed_x509_extensions() -> Vec { #[cfg(test)] pub mod test { - - #[test] - fn test_key_usage() {} + use rstest::rstest; + use x509_cert::ext::pkix::BasicConstraints; + + use super::BasicConstraintsValidator; + + #[rstest] + #[case::ok(BasicConstraints { ca: true, path_len_constraint: Some(0) }, true)] + #[case::ca_false(BasicConstraints { ca: false, path_len_constraint: Some(0) }, false)] + #[case::path_none(BasicConstraints { ca: true, path_len_constraint: None }, false)] + #[case::path_too_long(BasicConstraints { ca: true, path_len_constraint: Some(1) }, false)] + #[case::both_wrong(BasicConstraints { ca: false, path_len_constraint: None }, false)] + fn basic_constraints(#[case] bc: BasicConstraints, #[case] valid: bool) { + let outcome = BasicConstraintsValidator::check(bc); + assert_eq!(outcome.is_none(), valid) + } } diff --git a/src/definitions/x509/mod.rs b/src/definitions/x509/mod.rs index a26c99e8..f6e28172 100644 --- a/src/definitions/x509/mod.rs +++ b/src/definitions/x509/mod.rs @@ -4,3 +4,174 @@ pub mod trust_anchor; pub mod x5chain; pub use x5chain::{Builder, X5Chain}; + +#[cfg(test)] +mod test { + use std::time::Duration; + + use const_oid::ObjectIdentifier; + use p256::NistP256; + use rand::random; + use sec1::pkcs8::EncodePublicKey; + use signature::{Keypair, KeypairRef, Signer}; + use x509_cert::{ + builder::{Builder, CertificateBuilder}, + ext::pkix::{ + crl::dp::DistributionPoint, + name::{DistributionPointName, GeneralName}, + BasicConstraints, CrlDistributionPoints, ExtendedKeyUsage, IssuerAltName, KeyUsage, + KeyUsages, + }, + name::Name, + spki::{ + DynSignatureAlgorithmIdentifier, SignatureBitStringEncoding, SubjectPublicKeyInfoOwned, + }, + time::Validity, + Certificate, + }; + + fn prepare_root_certificate<'s, S>(root_key: &'s S, issuer: Name) -> CertificateBuilder<'s, S> + where + S: KeypairRef + DynSignatureAlgorithmIdentifier, + S::VerifyingKey: EncodePublicKey, + { + let mut builder = CertificateBuilder::new( + x509_cert::builder::Profile::Manual { issuer: None }, + random::().into(), + Validity::from_now(Duration::from_secs(600)).unwrap(), + issuer, + SubjectPublicKeyInfoOwned::from_key(root_key.verifying_key()).unwrap(), + root_key, + ) + .unwrap(); + + builder + .add_extension(&KeyUsage(KeyUsages::KeyCertSign | KeyUsages::CRLSign)) + .unwrap(); + + builder + .add_extension(&BasicConstraints { + ca: true, + path_len_constraint: Some(0), + }) + .unwrap(); + + builder + .add_extension(&IssuerAltName(vec![GeneralName::Rfc822Name( + "test@example.com".to_string().try_into().unwrap(), + )])) + .unwrap(); + + builder + .add_extension(&CrlDistributionPoints(vec![DistributionPoint { + distribution_point: Some(DistributionPointName::FullName(vec![ + GeneralName::UniformResourceIdentifier( + "http://example.com".to_string().try_into().unwrap(), + ), + ])), + reasons: None, + crl_issuer: None, + }])) + .unwrap(); + + builder + } + + fn prepare_signer_certificate<'s, S>( + signer_key: &'s S, + root_key: &'s S, + issuer: Name, + ) -> CertificateBuilder<'s, S> + where + S: KeypairRef + DynSignatureAlgorithmIdentifier, + S::VerifyingKey: EncodePublicKey, + { + let mut builder = CertificateBuilder::new( + x509_cert::builder::Profile::Manual { + issuer: Some(issuer), + }, + random::().into(), + Validity::from_now(Duration::from_secs(600)).unwrap(), + Name::default(), + SubjectPublicKeyInfoOwned::from_key(signer_key.verifying_key()).unwrap(), + root_key, + ) + .unwrap(); + + builder + .add_extension(&KeyUsage(KeyUsages::DigitalSignature.into())) + .unwrap(); + + builder + .add_extension(&IssuerAltName(vec![GeneralName::Rfc822Name( + "test@example.com".to_string().try_into().unwrap(), + )])) + .unwrap(); + + builder + .add_extension(&CrlDistributionPoints(vec![DistributionPoint { + distribution_point: Some(DistributionPointName::FullName(vec![ + GeneralName::UniformResourceIdentifier( + "http://example.com".to_string().try_into().unwrap(), + ), + ])), + reasons: None, + crl_issuer: None, + }])) + .unwrap(); + + builder + .add_extension(&ExtendedKeyUsage(vec![ObjectIdentifier::new_unwrap( + "1.0.18013.5.1.2", + )])) + .unwrap(); + + builder + } + + fn setup() -> (Certificate, Certificate) { + let root_key = p256::ecdsa::SigningKey::random(&mut rand::thread_rng()); + let signer_key = p256::ecdsa::SigningKey::random(&mut rand::thread_rng()); + + let issuer = Name::default(); + + let mut prepared_root_certificate = prepare_root_certificate(&root_key, issuer.clone()); + let signature: ecdsa::der::Signature = + root_key.sign(&prepared_root_certificate.finalize().unwrap()); + let root_certificate: Certificate = prepared_root_certificate + .assemble(signature.to_bitstring().unwrap()) + .unwrap(); + + let mut prepared_signer_certificate = + prepare_signer_certificate(&signer_key, &root_key, issuer.clone()); + let signature: ecdsa::der::Signature = + root_key.sign(&prepared_signer_certificate.finalize().unwrap()); + let signer_certificate: Certificate = prepared_signer_certificate + .assemble(signature.to_bitstring().unwrap()) + .unwrap(); + + (root_certificate, signer_certificate) + } + + mod iaca { + use crate::definitions::x509::{ + trust_anchor::{TrustAnchor, TrustAnchorRegistry}, + X5Chain, + }; + + #[test_log::test] + fn valid_certificate_chain_is_validated() { + let (root, signer) = super::setup(); + let trust_anchor_registry = TrustAnchorRegistry { + certificates: vec![TrustAnchor::Iaca(root)], + }; + let x5chain = X5Chain::builder() + .with_certificate(signer) + .unwrap() + .build() + .unwrap(); + let errors = x5chain.validate(Some(&trust_anchor_registry)); + assert!(errors.is_empty(), "{errors:?}"); + } + } +} diff --git a/src/definitions/x509/trust_anchor.rs b/src/definitions/x509/trust_anchor.rs index 5c8fb70a..9238a547 100644 --- a/src/definitions/x509/trust_anchor.rs +++ b/src/definitions/x509/trust_anchor.rs @@ -1,20 +1,76 @@ -use crate::definitions::x509::error::Error as X509Error; -use crate::definitions::x509::extensions::{ - validate_iaca_root_extensions, validate_iaca_signer_extensions, +use crate::definitions::x509::{ + error::Error as X509Error, + extensions::{validate_iaca_root_extensions, validate_iaca_signer_extensions}, }; -use crate::definitions::x509::x5chain::X509; +use anyhow::Error; use const_oid::ObjectIdentifier; +use der::{DecodePem, EncodePem}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; -use x509_cert::attr::AttributeTypeAndValue; -use x509_cert::certificate::CertificateInner; -use x509_cert::der::Decode; +use x509_cert::{attr::AttributeTypeAndValue, Certificate}; -#[derive(Serialize, Deserialize, Clone)] +#[derive(Debug, Clone)] pub enum TrustAnchor { - Iaca(X509), - Aamva(X509), - IacaReader(X509), + Iaca(Certificate), + Aamva(Certificate), + IacaReader(Certificate), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +enum PemTrustAnchor { + Iaca(String), + Aamva(String), + IacaReader(String), +} + +impl<'l> TryFrom<&'l TrustAnchor> for PemTrustAnchor { + type Error = Error; + + fn try_from(value: &'l TrustAnchor) -> Result { + Ok(match value { + TrustAnchor::Iaca(c) => PemTrustAnchor::Iaca(c.to_pem(Default::default())?), + TrustAnchor::Aamva(c) => PemTrustAnchor::Aamva(c.to_pem(Default::default())?), + TrustAnchor::IacaReader(c) => PemTrustAnchor::IacaReader(c.to_pem(Default::default())?), + }) + } +} + +impl TryFrom for TrustAnchor { + type Error = Error; + + fn try_from(value: PemTrustAnchor) -> Result { + Ok(match value { + PemTrustAnchor::Iaca(c) => TrustAnchor::Iaca(Certificate::from_pem(&c)?), + PemTrustAnchor::Aamva(c) => TrustAnchor::Aamva(Certificate::from_pem(&c)?), + PemTrustAnchor::IacaReader(c) => TrustAnchor::IacaReader(Certificate::from_pem(&c)?), + }) + } +} + +impl Serialize for TrustAnchor { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::Error; + + PemTrustAnchor::try_from(self) + .map_err(S::Error::custom)? + .serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for TrustAnchor { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::Error; + + PemTrustAnchor::deserialize(deserializer)? + .try_into() + .map_err(D::Error::custom) + } } #[derive(Serialize, Deserialize, Clone)] @@ -32,44 +88,32 @@ pub enum RuleSetType { ReaderAuth, } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Clone, Serialize, Deserialize)] pub struct TrustAnchorRegistry { pub certificates: Vec, } impl TrustAnchorRegistry { - pub fn iaca_registry_from_str(pem_strings: Vec) -> Result { - let certificates: Vec = pem_strings - .into_iter() - .filter_map(|s| trustanchor_from_str(&s).ok()) - .collect(); - - Ok(TrustAnchorRegistry { certificates }) + pub fn from_pem_iaca_certificates(certs: Vec) -> Result { + Ok(Self { + certificates: certs + .into_iter() + .map(PemTrustAnchor::Iaca) + .map(TrustAnchor::try_from) + .collect::>()?, + }) } } -fn trustanchor_from_str(pem_string: &str) -> Result { - let anchor: TrustAnchor = match pem_rfc7468::decode_vec(pem_string.as_bytes()) { - Ok(b) => TrustAnchor::Iaca(X509 { bytes: b.1 }), - Err(e) => { - return Err(X509Error::DecodingError(format!( - "unable to parse pem: {:?}", - e - ))) - } - }; - Ok(anchor) -} - -pub fn process_validation_outcomes( - leaf_certificate: CertificateInner, - root_certificate: CertificateInner, +fn process_validation_outcomes( + leaf_certificate: &Certificate, + root_certificate: &Certificate, rule_set: ValidationRuleSet, ) -> Vec { let mut errors: Vec = vec![]; //execute checks on x509 components - match apply_ruleset(leaf_certificate, root_certificate.clone(), rule_set) { + match apply_ruleset(leaf_certificate, root_certificate, rule_set) { Ok(mut v) => { errors.append(&mut v); } @@ -79,92 +123,57 @@ pub fn process_validation_outcomes( } // make sure that the trust anchor is still valid - errors.append(&mut check_validity_period(&root_certificate)); + errors.append(&mut check_validity_period(root_certificate)); //TODO: check CRL to make sure the certificates have not been revoked errors } pub fn validate_with_ruleset( - leaf_certificate: CertificateInner, - trust_anchor: TrustAnchor, + leaf_certificate: &Certificate, + trust_anchor: &TrustAnchor, ) -> Vec { let mut errors: Vec = vec![]; match trust_anchor { - TrustAnchor::Iaca(certificate) => { + TrustAnchor::Iaca(root_certificate) => { let rule_set = ValidationRuleSet { distinguished_names: vec!["2.5.4.6".to_string(), "2.5.4.8".to_string()], typ: RuleSetType::IACA, }; - match x509_cert::Certificate::from_der(&certificate.bytes) { - Ok(root_certificate) => { - errors.append(&mut process_validation_outcomes( - leaf_certificate, - root_certificate, - rule_set, - )); - } - Err(e) => { - errors.push(e.into()); - } - }; + errors.append(&mut process_validation_outcomes( + leaf_certificate, + root_certificate, + rule_set, + )); } - TrustAnchor::Aamva(certificate) => { + TrustAnchor::Aamva(root_certificate) => { let rule_set = ValidationRuleSet { distinguished_names: vec!["2.5.4.6".to_string(), "2.5.4.8".to_string()], typ: RuleSetType::AAMVA, }; - //The Aamva ruleset follows the IACA ruleset, but makes the ST value mandatory - match x509_cert::Certificate::from_der(&certificate.bytes) { - Ok(root_certificate) => { - errors.append(&mut process_validation_outcomes( - leaf_certificate, - root_certificate, - rule_set, - )); - } - Err(e) => { - errors.push(e.into()); - } - }; + errors.append(&mut process_validation_outcomes( + leaf_certificate, + root_certificate, + rule_set, + )); } - TrustAnchor::IacaReader(certificate) => { + TrustAnchor::IacaReader(root_certificate) => { let rule_set = ValidationRuleSet { distinguished_names: vec!["2.5.4.3".to_string()], typ: RuleSetType::ReaderAuth, }; - match x509_cert::Certificate::from_der(&certificate.bytes) { - Ok(root_certificate) => { - errors.append(&mut process_validation_outcomes( - leaf_certificate, - root_certificate, - rule_set, - )); - } - Err(e) => { - errors.push(e.into()); - } - }; - } - } - errors -} - -pub fn validate_with_trust_anchor(leaf_x509: X509, trust_anchor: TrustAnchor) -> Vec { - let mut errors: Vec = vec![]; - let leaf_certificate = x509_cert::Certificate::from_der(&leaf_x509.bytes); - - match leaf_certificate { - Ok(leaf) => { - errors.append(&mut validate_with_ruleset(leaf, trust_anchor)); + errors.append(&mut process_validation_outcomes( + leaf_certificate, + root_certificate, + rule_set, + )); } - Err(e) => errors.push(e.into()), } errors } -pub fn check_validity_period(certificate: &CertificateInner) -> Vec { +pub fn check_validity_period(certificate: &Certificate) -> Vec { let validity = certificate.tbs_certificate.validity; let mut errors: Vec = vec![]; if validity.not_after.to_unix_duration().as_secs() @@ -195,54 +204,41 @@ and match the - the extensions are set to the ruleset values - */ fn apply_ruleset( - leaf_certificate: CertificateInner, - root_certificate: CertificateInner, + leaf_certificate: &Certificate, + root_certificate: &Certificate, rule_set: ValidationRuleSet, ) -> Result, X509Error> { let mut errors: Vec = vec![]; // collect all the distinguished names in the root certificate that the validation ruleset requires - let root_distinguished_names: Vec = root_certificate + let root_distinguished_names: Vec<&AttributeTypeAndValue> = root_certificate .tbs_certificate .subject .0 - .into_iter() - .map(|rdn| { - rdn.0 - .into_vec() - .into_iter() - .filter(|atv| { - rule_set - .distinguished_names - .iter() - .any(|oid| oid == &atv.oid.to_string()) - }) - .collect::>() + .iter() + .flat_map(|rdn| { + rdn.0.as_slice().iter().filter(|atv| { + rule_set + .distinguished_names + .iter() + .any(|oid| oid == &atv.oid.to_string()) + }) }) - .collect::>>() - .into_iter() - .flatten() .collect(); // collect all the distinguished names in the signer certificate that the validation ruleset requires - let leaf_distinguished_names: Vec = leaf_certificate + let leaf_distinguished_names: Vec<&AttributeTypeAndValue> = leaf_certificate .tbs_certificate .issuer .0 - .into_iter() - .map(|r| { - r.0.into_vec() - .into_iter() - .filter(|atv| { - rule_set - .distinguished_names - .iter() - .any(|oid| oid == &atv.oid.to_string()) - }) - .collect::>() + .iter() + .flat_map(|rdn| { + rdn.0.as_slice().iter().filter(|atv| { + rule_set + .distinguished_names + .iter() + .any(|oid| oid == &atv.oid.to_string()) + }) }) - .collect::>>() - .into_iter() - .flatten() .collect(); // if all the needed distinguished names have been collected, @@ -255,13 +251,13 @@ fn apply_ruleset( errors.push(X509Error::ValidationError("The configured validation ruleset requires a distinguished name that is not found in the submitted signer certificate".to_string())); } - let Some(root_extensions) = root_certificate.tbs_certificate.extensions else { + let Some(root_extensions) = root_certificate.tbs_certificate.extensions.as_ref() else { return Err(X509Error::ValidationError( "The root certificate is expected to have extensions, but none were found. Skipping all following extension validation checks..".to_string(), )); }; - let Some(leaf_extensions) = leaf_certificate.tbs_certificate.extensions else { + let Some(leaf_extensions) = leaf_certificate.tbs_certificate.extensions.as_ref() else { return Err(X509Error::ValidationError( "The signer certificate is expected to have extensions, but none were found. Skipping all following extension validation checks.. " .to_string(), @@ -277,11 +273,11 @@ fn apply_ruleset( mdoc_extended_key_usage_oid(), )); for dn in leaf_distinguished_names { - if dn.oid.to_string() == *"2.5.4.8" { - let state_or_province = - root_distinguished_names.iter().find(|r| r.oid == dn.oid); - if let Some(st_or_s) = state_or_province { - if dn != *st_or_s { + if dn.oid == const_oid::db::rfc2256::STATE_OR_PROVINCE_NAME { + if let Some(&root_state_or_province) = + root_distinguished_names.iter().find(|r| r.oid == dn.oid) + { + if dn != root_state_or_province { return Err(X509Error::ValidationError(format!("Mismatch between supplied certificate issuer attribute: {:?} and the trust anchor registry.", dn))); } } @@ -322,38 +318,26 @@ fn apply_ruleset( } } -pub fn find_anchor( - leaf_certificate: CertificateInner, - trust_anchor_registry: Option, -) -> Result, X509Error> { - let leaf_issuer = leaf_certificate.tbs_certificate.issuer; +pub fn find_anchor<'l>( + leaf_certificate: &Certificate, + trust_anchor_registry: Option<&'l TrustAnchorRegistry>, +) -> Result, X509Error> { + let leaf_issuer = &leaf_certificate.tbs_certificate.issuer; let Some(root_certificates) = trust_anchor_registry else { return Ok(None); }; - let Some(trust_anchor) = root_certificates - .certificates - .into_iter() - .find(|trust_anchor| match trust_anchor { - TrustAnchor::Iaca(certificate) => { - match x509_cert::Certificate::from_der(&certificate.bytes) { - Ok(root_cert) => root_cert.tbs_certificate.subject == leaf_issuer, - Err(_) => false, - } - } - TrustAnchor::Aamva(certificate) => { - match x509_cert::Certificate::from_der(&certificate.bytes) { - Ok(root_cert) => root_cert.tbs_certificate.subject == leaf_issuer, - Err(_) => false, + let Some(trust_anchor) = + root_certificates + .certificates + .iter() + .find(|trust_anchor| match trust_anchor { + TrustAnchor::Iaca(certificate) + | TrustAnchor::Aamva(certificate) + | TrustAnchor::IacaReader(certificate) => { + &certificate.tbs_certificate.subject == leaf_issuer } - } - TrustAnchor::IacaReader(certificate) => { - match x509_cert::Certificate::from_der(&certificate.bytes) { - Ok(root_cert) => root_cert.tbs_certificate.subject == leaf_issuer, - Err(_) => false, - } - } - }) + }) else { return Err(X509Error::ValidationError( "The certificate issuer does not match any known trusted issuer".to_string(), diff --git a/src/definitions/x509/x5chain.rs b/src/definitions/x509/x5chain.rs index ee836620..df93661f 100644 --- a/src/definitions/x509/x5chain.rs +++ b/src/definitions/x509/x5chain.rs @@ -2,7 +2,6 @@ use crate::definitions::helpers::NonEmptyVec; use crate::definitions::x509::error::Error as X509Error; use crate::definitions::x509::trust_anchor::check_validity_period; use crate::definitions::x509::trust_anchor::find_anchor; -use crate::definitions::x509::trust_anchor::validate_with_trust_anchor; use crate::definitions::x509::trust_anchor::TrustAnchorRegistry; use anyhow::{anyhow, Result}; use p256::ecdsa::VerifyingKey; @@ -15,10 +14,8 @@ use elliptic_curve::{ AffinePoint, CurveArithmetic, FieldBytesSize, PublicKey, }; use p256::NistP256; -use serde::{Deserialize, Serialize}; use signature::Verifier; use std::collections::HashSet; -use std::hash::Hash; use std::{fs::File, io::Read}; use x509_cert::der::Encode; use x509_cert::{ @@ -26,12 +23,15 @@ use x509_cert::{ der::{referenced::OwnedToRef, Decode}, }; +use super::trust_anchor::validate_with_ruleset; + /// See: https://www.iana.org/assignments/cose/cose.xhtml#header-parameters pub const X5CHAIN_HEADER_LABEL: i64 = 0x21; -#[derive(Debug, Clone, Serialize, Deserialize, Hash, Eq, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct X509 { - pub bytes: Vec, + pub inner: Certificate, + der: Vec, } impl X509 { @@ -41,8 +41,8 @@ impl X509 { AffinePoint: FromEncodedPoint + ToEncodedPoint, FieldBytesSize: ModulusSize, { - let cert = x509_cert::Certificate::from_der(&self.bytes)?; - cert.tbs_certificate + self.inner + .tbs_certificate .subject_public_key_info .owned_to_ref() .try_into() @@ -60,15 +60,24 @@ impl X509 { } pub fn from_der(bytes: &[u8]) -> Result { - let _ = Certificate::from_der(bytes) + let inner = Certificate::from_der(bytes) .map_err(|e| anyhow!("unable to parse certificate from der encoding: {}", e))?; - Ok(X509 { - bytes: bytes.to_vec(), + Ok(Self { + inner, + der: bytes.to_vec(), + }) + } + + pub fn from_cert(certificate: Certificate) -> Result { + let der = certificate.to_der()?; + Ok(Self { + inner: certificate, + der, }) } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct X5Chain(NonEmptyVec); impl From> for X5Chain { @@ -84,12 +93,11 @@ impl X5Chain { pub fn into_cbor(&self) -> CborValue { match &self.0.as_ref() { - &[cert] => CborValue::Bytes(cert.bytes.clone()), + &[cert] => CborValue::Bytes(cert.der.clone()), certs => CborValue::Array( certs .iter() - .cloned() - .map(|x509| x509.bytes) + .map(|x509| x509.der.clone()) .map(CborValue::Bytes) .collect::>(), ), @@ -129,11 +137,11 @@ impl X5Chain { leaf.public_key().map(|key| key.into()) } - pub fn validate(&self, trust_anchor_registry: Option) -> Vec { + pub fn validate(&self, trust_anchor_registry: Option<&TrustAnchorRegistry>) -> Vec { let x5chain = self.0.as_ref(); let mut errors: Vec = vec![]; - if !has_unique_elements(x5chain) { + if !self.has_unique_elements() { errors.push(X509Error::ValidationError( "x5chain contains duplicate certificates".to_string(), )) @@ -152,38 +160,25 @@ impl X5Chain { //make sure all submitted certificates are valid for x509 in x5chain { - let cert = x509_cert::Certificate::from_der(&x509.bytes); - match cert { - Ok(c) => { - errors.append(&mut check_validity_period(&c)); - } - Err(e) => errors.push(e.into()), - } + errors.append(&mut check_validity_period(&x509.inner)); } //validate the last certificate in the chain against trust anchor if let Some(x509) = x5chain.last() { - match x509_cert::Certificate::from_der(&x509.bytes) { - Ok(cert) => { - // if the issuer of the signer certificate is known in the trust anchor registry, do the validation. - // otherwise, report an error and skip. - match find_anchor(cert, trust_anchor_registry) { - Ok(anchor) => { - if let Some(trust_anchor) = anchor { - errors.append(&mut validate_with_trust_anchor( - x509.clone(), - trust_anchor, - )); - } else { - errors.push(X509Error::ValidationError( - "No matching trust anchor found".to_string(), - )); - } - } - Err(e) => errors.push(e), + let cert = &x509.inner; + // if the issuer of the signer certificate is known in the trust anchor registry, do the validation. + // otherwise, report an error and skip. + match find_anchor(cert, trust_anchor_registry) { + Ok(anchor) => { + if let Some(trust_anchor) = anchor { + errors.append(&mut validate_with_ruleset(cert, trust_anchor)); + } else { + errors.push(X509Error::ValidationError( + "No matching trust anchor found".to_string(), + )); } } - Err(e) => errors.push(e.into()), + Err(e) => errors.push(e), } } else { errors.push(X509Error::ValidationError( @@ -193,32 +188,37 @@ impl X5Chain { errors } + + fn has_unique_elements(&self) -> bool { + let mut uniq = HashSet::new(); + self.0.iter().all(move |x| uniq.insert(&x.der)) + } } pub fn check_signature(target: &X509, issuer: &X509) -> Result<(), X509Error> { let parent_public_key = ecdsa::VerifyingKey::from(issuer.public_key()?); - let child_cert = x509_cert::Certificate::from_der(&target.bytes)?; + let child_cert = &target.inner; let sig: ecdsa::Signature = ecdsa::Signature::from_der(child_cert.signature.raw_bytes())?; let bytes = child_cert.tbs_certificate.to_der()?; Ok(parent_public_key.verify(&bytes, &sig)?) } -fn has_unique_elements(iter: T) -> bool -where - T: IntoIterator, - T::Item: Eq + Hash, -{ - let mut uniq = HashSet::new(); - iter.into_iter().all(move |x| uniq.insert(x)) -} - #[derive(Default, Debug, Clone)] pub struct Builder { certs: Vec, } impl Builder { + pub fn with_certificate(mut self, cert: Certificate) -> Result { + let x509 = X509::from_cert(cert)?; + self.certs.push(x509); + Ok(self) + } + pub fn with_x509(mut self, x509: X509) -> Builder { + self.certs.push(x509); + self + } pub fn with_pem(mut self, data: &[u8]) -> Result { let x509 = X509::from_pem(data)?; self.certs.push(x509); diff --git a/src/presentation/device.rs b/src/presentation/device.rs index 9fc2f4b9..0d3a290e 100644 --- a/src/presentation/device.rs +++ b/src/presentation/device.rs @@ -591,8 +591,7 @@ impl SessionManager { match x5chain { Ok(x5c) => { if let Some(trusted_verifiers) = &self.trusted_verifiers { - validation_errors - .append(&mut x5c.validate(Some(trusted_verifiers.clone()))); + validation_errors.append(&mut x5c.validate(Some(&trusted_verifiers))); } } Err(e) => { diff --git a/src/presentation/reader.rs b/src/presentation/reader.rs index 81707a48..26d07fc5 100644 --- a/src/presentation/reader.rs +++ b/src/presentation/reader.rs @@ -15,11 +15,9 @@ //! `simulated_device_and_reader_state.rs` which uses `State` pattern, `Arc` and `Mutex`. use std::collections::BTreeMap; -use aes::cipher::{generic_array::GenericArray, typenum::U32}; +use anyhow::Context; use anyhow::{anyhow, Result}; -use coset::{CoseSign1Builder, Header, Label}; -use p256::ecdsa::SigningKey; -use sec1::DecodeEcPrivateKey; +use coset::Label; use serde::{Deserialize, Serialize}; use serde_json::json; use serde_json::Value; @@ -62,8 +60,6 @@ pub struct SessionManager { sk_reader: [u8; 32], reader_message_counter: u32, trust_anchor_registry: Option, - reader_auth_key: [u8; 32], - reader_x5chain: X5Chain, } #[derive(Serialize, Deserialize)] @@ -187,29 +183,30 @@ impl SessionManager { qr_code: String, namespaces: device_request::Namespaces, trust_anchor_registry: Option, - reader_x5chain: X5Chain, - reader_key: &str, ) -> Result<(Self, Vec, [u8; 16])> { - let device_engagement_bytes = - Tag24::::from_qr_code_uri(&qr_code).map_err(|e| anyhow!(e))?; + let device_engagement_bytes = Tag24::::from_qr_code_uri(&qr_code) + .context("failed to construct QR code")?; //generate own keys - let key_pair = create_p256_ephemeral_keys()?; + let key_pair = create_p256_ephemeral_keys().context("failed to generate ephemeral key")?; let e_reader_key_private = key_pair.0; - let e_reader_key_public = Tag24::new(key_pair.1)?; + let e_reader_key_public = + Tag24::new(key_pair.1).context("failed to encode public cose key")?; //decode device_engagement let device_engagement = device_engagement_bytes.as_ref(); let e_device_key = &device_engagement.security.1; // calculate ble Ident value - let ble_ident = super::calculate_ble_ident(e_device_key)?; + let ble_ident = + super::calculate_ble_ident(e_device_key).context("failed to calculate BLE Ident")?; // derive shared secret let shared_secret = get_shared_secret( e_device_key.clone().into_inner(), &e_reader_key_private.into(), - )?; + ) + .context("failed to derive shared session secret")?; let session_transcript = SessionTranscript180135( device_engagement_bytes, @@ -217,15 +214,16 @@ impl SessionManager { Handover::QR, ); - let session_transcript_bytes = Tag24::new(session_transcript.clone())?; + let session_transcript_bytes = Tag24::new(session_transcript.clone()) + .context("failed to encode session transcript")?; //derive session keys - let sk_reader = derive_session_key(&shared_secret, &session_transcript_bytes, true)?.into(); - let sk_device = - derive_session_key(&shared_secret, &session_transcript_bytes, false)?.into(); - - let reader_signing_key: SigningKey = ecdsa::SigningKey::from_sec1_pem(reader_key)?; - let reader_auth_key: GenericArray = reader_signing_key.to_bytes(); + let sk_reader = derive_session_key(&shared_secret, &session_transcript_bytes, true) + .context("failed to derive reader session key")? + .into(); + let sk_device = derive_session_key(&shared_secret, &session_transcript_bytes, false) + .context("failed to derive device session key")? + .into(); let mut session_manager = Self { session_transcript, @@ -234,16 +232,17 @@ impl SessionManager { sk_reader, reader_message_counter: 0, trust_anchor_registry, - reader_auth_key: reader_auth_key.into(), - reader_x5chain, }; - let request = session_manager.build_request(namespaces)?; + let request = session_manager + .build_request(namespaces) + .context("failed to build device request")?; let session = SessionEstablishment { data: request.into(), e_reader_key: e_reader_key_public, }; - let session_request = cbor::to_vec(&session)?; + let session_request = + cbor::to_vec(&session).context("failed to encode session establishment")?; Ok((session_manager, session_request, ble_ident)) } @@ -289,35 +288,8 @@ impl SessionManager { request_info: None, }; - //the certificate should be supplied by the reader - //let certificate_cbor = serde_cbor::to_vec(&self.reader_cert_bytes)?; - let mut header_map = Header::default(); - header_map.rest.push(( - Label::Int(X5CHAIN_HEADER_LABEL), - self.reader_x5chain.into_cbor(), - )); - - let payload = ReaderAuthentication( - "ReaderAuthentication".to_string(), - self.session_transcript.clone(), - Tag24::new(items_request.clone())?, - ); - - let reader_signing_key = SigningKey::from_slice(&self.reader_auth_key)?; //SigningKey::from_bytes(self.reader_auth_key.to_vec()); - let signature = reader_signing_key.sign_recoverable(&cbor::to_vec(&payload)?)?; - let cose_sign1 = CoseSign1Builder::new() - .unprotected(header_map) - .payload(cbor::to_vec(&payload)?) - .signature(signature.0.to_vec()) - .build(); - let doc_request = DocRequest { - reader_auth: Some(crate::cose::MaybeTagged { - // NOTE: COSE_Sign1 is tagged with 18 - // see: https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml - tagged: true, - inner: cose_sign1, - }), + reader_auth: None, items_request: Tag24::new(items_request)?, }; let device_request = DeviceRequest { @@ -400,7 +372,7 @@ impl SessionManager { } } - let validation_errors = x5chain.validate(self.trust_anchor_registry.clone()); + let validation_errors = x5chain.validate(self.trust_anchor_registry.as_ref()); if validation_errors.is_empty() { match issuer_authentication(x5chain, &document.issuer_signed) { Ok(_) => { @@ -569,24 +541,6 @@ fn parse_namespaces( #[cfg(test)] pub mod test { use super::*; - use crate::{ - definitions::x509::trust_anchor::{TrustAnchor, TrustAnchorRegistry}, - definitions::x509::{error::Error as X509Error, x5chain::X509, X5Chain}, - }; - use anyhow::anyhow; - - static IACA_ROOT: &[u8] = include_bytes!("../../test/presentation/isomdl_iaca_root_cert.pem"); - //TODO fix this cert to contain issuer alternative name - // static IACA_INTERMEDIATE: &[u8] = - // include_bytes!("../../test/presentation/isomdl_iaca_intermediate.pem"); - // signed by the intermediate certificate - //TODO fix this cert to contain issuer alternative name - // static IACA_LEAF_SIGNER: &[u8] = - // include_bytes!("../../test/presentation/isomdl_iaca_leaf_signer.pem"); - // signed directly by the root certificate - static IACA_SIGNER: &[u8] = include_bytes!("../../test/presentation/isomdl_iaca_signer.pem"); - static INCORRECT_IACA_SIGNER: &[u8] = - include_bytes!("../../test/presentation/isomdl_incorrect_iaca_signer.pem"); #[test] fn nested_response_values() { @@ -612,65 +566,4 @@ pub mod test { ); assert_eq!(json, expected) } - - fn validate(signer: &[u8], root: &[u8]) -> Result, anyhow::Error> { - let root_bytes = pem_rfc7468::decode_vec(root) - .map_err(|e| anyhow!("unable to parse pem: {}", e))? - .1; - let trust_anchor = TrustAnchor::Iaca(X509 { bytes: root_bytes }); - let trust_anchor_registry = TrustAnchorRegistry { - certificates: vec![trust_anchor], - }; - let bytes = pem_rfc7468::decode_vec(signer) - .map_err(|e| anyhow!("unable to parse pem: {}", e))? - .1; - let x5chain_cbor: ciborium::Value = ciborium::Value::Bytes(bytes); - - let x5chain = X5Chain::from_cbor(x5chain_cbor)?; - - Ok(x5chain.validate(Some(trust_anchor_registry))) - } - - #[test] - fn validate_x509_with_trust_anchor() { - let result = validate(IACA_SIGNER, IACA_ROOT).unwrap(); - assert!(result.is_empty(), "{result:?}"); - } - - #[test] - fn validate_incorrect_x509_with_trust_anchor() { - let result = validate(INCORRECT_IACA_SIGNER, IACA_ROOT).unwrap(); - assert!(!result.is_empty(), "{result:?}"); - } - - // TODO: Fix test -- intermediate and leaf are not in a chain. - // #[test] - // fn validate_x5chain_with_trust_anchor() { - // let root_bytes = pem_rfc7468::decode_vec(IACA_ROOT) - // .map_err(|e| anyhow!("unable to parse pem: {}", e)) - // .unwrap() - // .1; - // let trust_anchor = TrustAnchor::Iaca(X509 { bytes: root_bytes }); - // let trust_anchor_registry = TrustAnchorRegistry { - // certificates: vec![trust_anchor], - // }; - - // let intermediate_bytes = pem_rfc7468::decode_vec(IACA_INTERMEDIATE) - // .map(|(_, bytes)| bytes) - // .map(serde_cbor::Value::Bytes) - // .expect("unable to parse pem"); - - // let leaf_signer_bytes = pem_rfc7468::decode_vec(IACA_LEAF_SIGNER) - // .map(|(_, bytes)| bytes) - // .map(serde_cbor::Value::Bytes) - // .expect("unable to parse pem"); - - // let x5chain_cbor: serde_cbor::Value = - // serde_cbor::Value::Array(vec![leaf_signer_bytes, intermediate_bytes]); - - // let x5chain = X5Chain::from_cbor(x5chain_cbor).unwrap(); - - // let result = x5chain.validate(Some(trust_anchor_registry)); - // assert!(result.len() == 0, "{result:?}") - // } } diff --git a/src/x509/mod.rs b/src/x509/mod.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/common.rs b/tests/common.rs index d98c00b5..8648320e 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -5,7 +5,6 @@ use isomdl::definitions::device_engagement::{CentralClientMode, DeviceRetrievalM use isomdl::definitions::device_request::{DataElements, DocType, Namespaces}; use isomdl::definitions::helpers::NonEmptyMap; use isomdl::definitions::x509::trust_anchor::TrustAnchorRegistry; -use isomdl::definitions::x509::X5Chain; use isomdl::definitions::{self, BleOptions, DeviceRetrievalMethod}; use isomdl::presentation::device::{Document, Documents, RequestedItems, SessionManagerEngaged}; use isomdl::presentation::{ @@ -57,22 +56,10 @@ impl Device { ); let trust_anchor = None; - let reader_x5chain = - // NOTE: Should we be using a different certificate here for the reader? - // I didn't see one in the test data. - X5Chain::builder().with_der(include_bytes!("../test/issuance/256-cert.der"))?.build()?; - // TODO: We should be using a typed key to pass to establish the session below instead of &str. - // let reader_key = p256::ecdsa::SigningKey::from_sec1_pem(include_str!("data/sec1.pem"))?; - let reader_key = include_str!("data/sec1.pem"); - let (reader_sm, session_request, _ble_ident) = reader::SessionManager::establish_session( - qr, - requested_elements, - trust_anchor, - reader_x5chain, - reader_key, - ) - .context("failed to establish reader session")?; + let (reader_sm, session_request, _ble_ident) = + reader::SessionManager::establish_session(qr, requested_elements, trust_anchor) + .context("failed to establish reader session")?; Ok((reader_sm, session_request)) } diff --git a/tests/data/sec1.pem b/tests/data/sec1.pem index 5bbe5ee9..32d96261 100644 --- a/tests/data/sec1.pem +++ b/tests/data/sec1.pem @@ -2,4 +2,4 @@ MGsCAQEEIBAKimXWUID1bY5RAX89iLRxvKFyDjXpzsUXj7ajkONsoUQDQgAEwON4 CNiG/PqiiuGBDnzQkYjToch2gi4ALynYR0vsusKRt6CdsbnV2oMyim3H71HWuMdI /M0df6A+epqZvORmzg== ------END EC PRIVATE KEY----- +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/tests/simulated_device_and_reader.rs b/tests/simulated_device_and_reader.rs index 9682c1cb..790567c1 100644 --- a/tests/simulated_device_and_reader.rs +++ b/tests/simulated_device_and_reader.rs @@ -2,32 +2,32 @@ mod common; use crate::common::{Device, Reader}; -use anyhow::Result; - #[test] -pub fn simulated_device_and_reader_interaction() -> Result<()> { +pub fn simulated_device_and_reader_interaction() { let key: p256::ecdsa::SigningKey = - p256::SecretKey::from_sec1_pem(include_str!("data/sec1.pem"))?.into(); + p256::SecretKey::from_sec1_pem(include_str!("data/sec1.pem")) + .unwrap() + .into(); // Device initialization and engagement - let (engaged_state, qr_code_uri) = Device::initialise_session()?; + let (engaged_state, qr_code_uri) = Device::initialise_session().unwrap(); // Reader processing QR and requesting the necessary fields - let (mut reader_session_manager, request) = Device::establish_reader_session(qr_code_uri)?; + let (mut reader_session_manager, request) = + Device::establish_reader_session(qr_code_uri).unwrap(); // Device accepting request let (device_session_manager, validated_request) = - Device::handle_request(engaged_state, request, None)?; + Device::handle_request(engaged_state, request, None).unwrap(); // Prepare response with required elements let response = Device::create_response( device_session_manager, &validated_request.items_request, &key, - )?; + ) + .unwrap(); // Reader Processing mDL data - Reader::reader_handle_device_response(&mut reader_session_manager, response)?; - - Ok(()) + Reader::reader_handle_device_response(&mut reader_session_manager, response).unwrap(); } diff --git a/tests/simulated_device_and_reader_state.rs b/tests/simulated_device_and_reader_state.rs index 674bf751..b1e8119a 100644 --- a/tests/simulated_device_and_reader_state.rs +++ b/tests/simulated_device_and_reader_state.rs @@ -4,7 +4,6 @@ use anyhow::{Context, Result}; use isomdl::cbor; use isomdl::definitions::device_engagement::{CentralClientMode, DeviceRetrievalMethods}; use isomdl::definitions::device_request::{DataElements, Namespaces}; -use isomdl::definitions::x509::X5Chain; use isomdl::definitions::{self, BleOptions, DeviceRetrievalMethod}; use isomdl::presentation::device::{Documents, RequestedItems}; use isomdl::presentation::{device, reader}; @@ -98,19 +97,10 @@ fn establish_reader_session(qr: String) -> Result<(reader::SessionManager, Vec,; - let reader_x5chain = X5Chain::builder() - .with_der(include_bytes!("../test/issuance/256-cert.der"))? - .build()?; - let reader_key = include_str!("data/sec1.pem"); - - let (reader_sm, session_request, _ble_ident) = reader::SessionManager::establish_session( - qr, - requested_elements, - trust_anchor_registry, - reader_x5chain, - reader_key, - ) - .context("failed to establish reader session")?; + + let (reader_sm, session_request, _ble_ident) = + reader::SessionManager::establish_session(qr, requested_elements, trust_anchor_registry) + .context("failed to establish reader session")?; Ok((reader_sm, session_request)) } From b8c6e8e18a2a2e95af68f64cc2023396e23b2161 Mon Sep 17 00:00:00 2001 From: Jacob Date: Thu, 12 Dec 2024 14:51:20 +0000 Subject: [PATCH 08/12] Linting --- src/definitions/x509/extensions.rs | 8 ++++---- src/definitions/x509/mod.rs | 2 +- src/presentation/device.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/definitions/x509/extensions.rs b/src/definitions/x509/extensions.rs index e33fbe21..dd453ada 100644 --- a/src/definitions/x509/extensions.rs +++ b/src/definitions/x509/extensions.rs @@ -187,7 +187,7 @@ impl ExtensionValidator for ExtendedKeyUsageValidator { but no other key usages are allowed */ fn validate(&self, extension: &Extension) -> Vec { let bytes = extension.extn_value.as_bytes(); - let extended_key_usage = ExtendedKeyUsage::from_der(&bytes); + let extended_key_usage = ExtendedKeyUsage::from_der(bytes); if !extension.critical { tracing::warn!( @@ -230,7 +230,7 @@ impl ExtensionValidator for SignerKeyUsageValidator { fn validate(&self, extension: &Extension) -> Vec { let bytes = extension.extn_value.as_bytes(); let mut errors: Vec = vec![]; - let key_usage = KeyUsage::from_der(&bytes); + let key_usage = KeyUsage::from_der(bytes); if !extension.critical { tracing::warn!("expected KeyUsage extension to be critical on signer certificate",) @@ -344,7 +344,7 @@ impl ExtensionValidator for BasicConstraintsValidator { } let bytes = extension.extn_value.as_bytes(); - let basic_constraints = BasicConstraints::from_der(&bytes); + let basic_constraints = BasicConstraints::from_der(bytes); match basic_constraints { Ok(bc) => { if let Some(e) = Self::check(bc) { @@ -376,7 +376,7 @@ impl ExtensionValidator for CrlDistributionPointsValidator { fn validate(&self, extension: &Extension) -> Vec { let bytes = extension.extn_value.as_bytes(); let mut errors: Vec = vec![]; - let crl_distribution_point = CrlDistributionPoints::from_der(&bytes); + let crl_distribution_point = CrlDistributionPoints::from_der(bytes); match crl_distribution_point { Ok(crl_dp) => { if crl_dp.0.is_empty() { diff --git a/src/definitions/x509/mod.rs b/src/definitions/x509/mod.rs index f6e28172..f0d723df 100644 --- a/src/definitions/x509/mod.rs +++ b/src/definitions/x509/mod.rs @@ -30,7 +30,7 @@ mod test { Certificate, }; - fn prepare_root_certificate<'s, S>(root_key: &'s S, issuer: Name) -> CertificateBuilder<'s, S> + fn prepare_root_certificate(root_key: &S, issuer: Name) -> CertificateBuilder<'_, S> where S: KeypairRef + DynSignatureAlgorithmIdentifier, S::VerifyingKey: EncodePublicKey, diff --git a/src/presentation/device.rs b/src/presentation/device.rs index 0d3a290e..9771635b 100644 --- a/src/presentation/device.rs +++ b/src/presentation/device.rs @@ -591,7 +591,7 @@ impl SessionManager { match x5chain { Ok(x5c) => { if let Some(trusted_verifiers) = &self.trusted_verifiers { - validation_errors.append(&mut x5c.validate(Some(&trusted_verifiers))); + validation_errors.append(&mut x5c.validate(Some(trusted_verifiers))); } } Err(e) => { From cd332c67102c5bfbfc66906d2771cc051ef6942c Mon Sep 17 00:00:00 2001 From: Jacob Date: Thu, 12 Dec 2024 14:57:23 +0000 Subject: [PATCH 09/12] Remove unreference file and fix doc issues --- src/definitions/x509/x5chain.rs | 2 +- src/issuance/x5chain.rs | 306 -------------------------------- src/presentation/device.rs | 2 +- 3 files changed, 2 insertions(+), 308 deletions(-) delete mode 100644 src/issuance/x5chain.rs diff --git a/src/definitions/x509/x5chain.rs b/src/definitions/x509/x5chain.rs index df93661f..c1b59500 100644 --- a/src/definitions/x509/x5chain.rs +++ b/src/definitions/x509/x5chain.rs @@ -25,7 +25,7 @@ use x509_cert::{ use super::trust_anchor::validate_with_ruleset; -/// See: https://www.iana.org/assignments/cose/cose.xhtml#header-parameters +/// See: pub const X5CHAIN_HEADER_LABEL: i64 = 0x21; #[derive(Debug, Clone, Eq, PartialEq)] diff --git a/src/issuance/x5chain.rs b/src/issuance/x5chain.rs deleted file mode 100644 index e68e55d2..00000000 --- a/src/issuance/x5chain.rs +++ /dev/null @@ -1,306 +0,0 @@ -//! This module provides functionality for working with `X.509`` certificate chains. -//! -//! The [X5Chain] struct represents a chain of `X.509`` certificates. It can be built using -//! the [Builder] struct, which allows adding certificates in either `PEM`` or `DER`` format. -//! The resulting [X5Chain] can be converted to `CBOR`` format using the [X5Chain::into_cbor] method. -//! -//! # Examples -//! -//! ```ignore -//! use crate::isomdl::issuance::x5chain::{X5Chain, Builder}; -//! -//! // Create an X5Chain using the Builder -//! let pem_data = include_bytes!("../../test/issuance/256-cert.pem"); -//! let x5chain = X5Chain::builder() -//! .with_pem(&pem_data) -//! .expect("Failed to add certificate") -//! .build() -//! .expect("Failed to build X5Chain"); -//! -//! // Convert the X5Chain to CBOR format -//! let cbor_value = x5chain.into_cbor(); -//! ``` -//! -//! The [Builder] struct provides methods for adding certificates to the chain. Certificates can be added -//! either from PEM or DER data, or from files containing PEM or DER data. -//! -//! # Examples -//! -//! ```ignore -//! use std::fs::File; -//! use crate::isomdl::issuance::x5chain::Builder; -//! -//! // Create a Builder and add a certificate from PEM data -//! let pem_data = include_bytes!("../../test/issuance/256-cert.pem"); -//! let builder = Builder::default() -//! .with_pem(pem_data) -//! .expect("Failed to add certificate"); -//! -//! // Add a certificate from DER data -//! let der_data = include_bytes!("../../test/issuance/256-cert.der"); -//! let builder = builder.with_der(der_data) -//! .expect("Failed to add certificate"); -//! -//! // Add a certificate from a PEM file -//! let pem_file = File::open("256-cert.pem").unwrap(); -//! let builder = builder.with_pem_from_file(pem_file) -//! .expect("Failed to add certificate"); -//! -//! // Add a certificate from a DER file -//! let der_file = File::open("256-cert.der").unwrap(); -//! let builder = builder.with_der_from_file(der_file) -//! .expect("Failed to add certificate"); -//! -//! // Build the X5Chain -//! let x5chain = builder.build() -//! .expect("Failed to build X5Chain"); -//! ``` -//! -//! The [X5Chain] struct also provides a [X5Chain::builder] method for creating a new [Builder] instance. -use crate::definitions::helpers::NonEmptyVec; -use anyhow::{anyhow, Result}; -use std::{fs::File, io::Read}; -use x509_cert::{ - certificate::Certificate, - der::{Decode, Encode}, -}; - -pub const X5CHAIN_HEADER_LABEL: i128 = 33; - -/// Represents an X509 certificate. -#[derive(Debug, Clone)] -pub struct X509 { - bytes: Vec, -} - -/// Represents a chain of [X509] certificates. -#[derive(Debug, Clone)] -pub struct X5Chain(NonEmptyVec); - -impl From> for X5Chain { - fn from(v: NonEmptyVec) -> Self { - Self(v) - } -} - -/// Implements the [X5Chain] struct. -/// -/// This struct provides methods for building and converting the X5Chain object. -impl X5Chain { - /// Creates a new [Builder] instance for [X5Chain]. - pub fn builder() -> Builder { - Builder::default() - } - - /// Converts the [X5Chain] object into a [`ciborium::Value`]. - pub fn into_cbor(self) -> ciborium::Value { - match &self.0.as_ref() { - &[cert] => ciborium::Value::Bytes(cert.bytes.clone()), - certs => ciborium::Value::Array( - certs - .iter() - .cloned() - .map(|x509| x509.bytes) - .map(ciborium::Value::Bytes) - .collect::>(), - ), - } - } -} - -/// Builder for creating an [X5Chain]. -/// -/// This struct is used to build an [X5Chain] by providing a vector of [X509] certificates. -/// The [X5Chain] represents a chain of `X.509`` certificates used for issuance. -/// -/// # Note -/// -/// The `Builder` struct is typically used in the context of the `issuance` module. -#[derive(Default, Debug, Clone)] -pub struct Builder { - certs: Vec, -} - -impl Builder { - /// Adds a `PEM-encoded`` certificate to the builder. - /// - /// # Errors - /// - /// Returns an error if the `PEM`` cannot be parsed or the certificate - /// cannot be converted to bytes. - /// - /// # Returns - /// - /// Returns a [Result] containing the updated [Builder] if successful. - pub fn with_pem(mut self, data: &[u8]) -> Result { - let bytes = pem_rfc7468::decode_vec(data) - .map_err(|e| anyhow!("unable to parse pem: {}", e))? - .1; - let cert: Certificate = Certificate::from_der(&bytes) - .map_err(|e| anyhow!("unable to parse certificate from der: {}", e))?; - let x509 = X509 { - bytes: cert - .to_vec() - .map_err(|e| anyhow!("unable to convert certificate to bytes: {}", e))?, - }; - self.certs.push(x509); - Ok(self) - } - - /// Adds a `DER`-encoded certificate to the builder. - /// - /// # Errors - /// - /// Returns an error if the certificate cannot be parsed from `DER` encoding - /// or cannot be converted to bytes. - /// - /// # Returns - /// - /// Returns a [Result] containing the updated [Builder] if successful. - pub fn with_der(mut self, data: &[u8]) -> Result { - let cert: Certificate = Certificate::from_der(data) - .map_err(|e| anyhow!("unable to parse certificate from der encoding: {}", e))?; - let x509 = X509 { - bytes: cert - .to_vec() - .map_err(|e| anyhow!("unable to convert certificate to bytes: {}", e))?, - }; - self.certs.push(x509); - Ok(self) - } - - /// Adds a `PEM`-encoded certificate from a file to the builder. - /// - /// # Errors - /// - /// Returns an error if the file cannot be read or the certificate cannot be parsed or converted to bytes. - /// - /// # Returns - /// - /// Returns a [Result] containing the updated [Builder] if successful. - pub fn with_pem_from_file(self, mut f: File) -> Result { - let mut data: Vec = vec![]; - f.read_to_end(&mut data)?; - self.with_pem(&data) - } - - /// Adds a `DER`-encoded certificate from a file to the builder. - /// - /// # Errors - /// - /// Returns an error if the file cannot be read or the certificate cannot be parsed or converted to bytes. - /// - /// # Returns - /// - /// Returns a [Result] containing the updated [Builder] if successful. - pub fn with_der_from_file(self, mut f: File) -> Result { - let mut data: Vec = vec![]; - f.read_to_end(&mut data)?; - self.with_der(&data) - } - - /// Builds the [X5Chain] from the added certificates. - /// - /// # Errors - /// - /// Returns an error if at least one certificate is not added to the builder. - /// - /// # Returns - /// - /// Returns a [Result] containing the built [X5Chain] if successful. - pub fn build(self) -> Result { - Ok(X5Chain(self.certs.try_into().map_err(|_| { - anyhow!("at least one certificate must be given to the builder") - })?)) - } -} - -#[cfg(test)] -pub mod test { - use super::*; - - static CERT_256: &[u8] = include_bytes!("../../test/issuance/256-cert.pem"); - static CERT_384: &[u8] = include_bytes!("../../test/issuance/384-cert.pem"); - static CERT_521: &[u8] = include_bytes!("../../test/issuance/521-cert.pem"); - - #[test] - pub fn self_signed_es256() { - let _x5chain = X5Chain::builder() - .with_pem(CERT_256) - .expect("unable to add cert") - .build() - .expect("unable to build x5chain"); - - //let self_signed = &x5chain[0]; - - //assert!(self_signed.issued(self_signed) == CertificateVerifyResult::OK); - //assert!(self_signed - // .verify( - // &self_signed - // .public_key() - // .expect("unable to get public key of cert") - // ) - // .expect("unable to verify public key of cert")); - - //assert!(matches!( - // x5chain - // .key_algorithm() - // .expect("unable to retrieve public key algorithm"), - // Algorithm::ES256 - //)); - } - - #[test] - pub fn self_signed_es384() { - let _x5chain = X5Chain::builder() - .with_pem(CERT_384) - .expect("unable to add cert") - .build() - .expect("unable to build x5chain"); - - //let self_signed = &x5chain[0]; - - //assert!(self_signed.issued(self_signed) == CertificateVerifyResult::OK); - //assert!(self_signed - // .verify( - // &self_signed - // .public_key() - // .expect("unable to get public key of cert") - // ) - // .expect("unable to verify public key of cert")); - - //assert!(matches!( - // x5chain - // .key_algorithm() - // .expect("unable to retrieve public key algorithm"), - // Algorithm::ES384 - //)); - } - - #[test] - pub fn self_signed_es512() { - let _x5chain = X5Chain::builder() - .with_pem(CERT_521) - .expect("unable to add cert") - .build() - .expect("unable to build x5chain"); - - //let self_signed = &x5chain[0]; - - //assert!(self_signed.issued(self_signed) == CertificateVerifyResult::OK); - //assert!(self_signed - // .verify( - // &self_signed - // .public_key() - // .expect("unable to get public key of cert") - // ) - // .expect("unable to verify public key of cert")); - - //assert!(matches!( - // x5chain - // .key_algorithm() - // .expect("unable to retrieve public key algorithm"), - // Algorithm::ES512 - //)); - } -} diff --git a/src/presentation/device.rs b/src/presentation/device.rs index 9771635b..c0fd3c73 100644 --- a/src/presentation/device.rs +++ b/src/presentation/device.rs @@ -453,7 +453,7 @@ impl SessionManager { /// The request is expected to be a CBOR encoded /// and encrypted [SessionData]. /// - /// This method will return the [ValidatedRequest] struct, which will + /// This method will return the [RequestAuthenticationOutcome] struct, which will /// include the items requested by the reader/verifier. pub fn handle_request(&mut self, request: &[u8]) -> RequestAuthenticationOutcome { let mut validated_request = RequestAuthenticationOutcome::default(); From 218654965fcd5842e1fd70c272c4e8443b2ded0d Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 18 Dec 2024 11:55:49 +0000 Subject: [PATCH 10/12] Refactor x5chain validation code. Refactored the x5chain validation code to help debug an issue with the chain validation. Added an e2e positive test for DS->IACA validation. --- Cargo.toml | 1 + src/bin/x509/mod.rs | 33 +- src/definitions/x509/error.rs | 57 -- src/definitions/x509/extensions.rs | 520 ------------------ src/definitions/x509/mod.rs | 86 ++- src/definitions/x509/trust_anchor.rs | 362 ++---------- src/definitions/x509/util.rs | 62 +++ src/definitions/x509/validation/error.rs | 75 +++ .../extensions/basic_constraints.rs | 68 +++ .../extensions/crl_distribution_points.rs | 177 ++++++ .../extensions/extended_key_usage.rs | 95 ++++ .../extensions/issuer_alternative_name.rs | 57 ++ .../x509/validation/extensions/key_usage.rs | 106 ++++ .../x509/validation/extensions/mod.rs | 260 +++++++++ .../extensions/subject_key_identifier.rs | 101 ++++ src/definitions/x509/validation/mod.rs | 218 ++++++++ src/definitions/x509/validation/names.rs | 154 ++++++ src/definitions/x509/validation/signature.rs | 69 +++ src/definitions/x509/validation/validity.rs | 27 + src/definitions/x509/x5chain.rs | 223 ++------ src/issuance/mdoc.rs | 8 +- src/presentation/authentication/mdoc.rs | 15 +- src/presentation/device.rs | 205 ++++--- src/presentation/reader.rs | 30 +- tests/common.rs | 6 +- tests/simulated_device_and_reader.rs | 2 +- tests/simulated_device_and_reader_state.rs | 5 +- 27 files changed, 1807 insertions(+), 1215 deletions(-) delete mode 100644 src/definitions/x509/error.rs delete mode 100644 src/definitions/x509/extensions.rs create mode 100644 src/definitions/x509/util.rs create mode 100644 src/definitions/x509/validation/error.rs create mode 100644 src/definitions/x509/validation/extensions/basic_constraints.rs create mode 100644 src/definitions/x509/validation/extensions/crl_distribution_points.rs create mode 100644 src/definitions/x509/validation/extensions/extended_key_usage.rs create mode 100644 src/definitions/x509/validation/extensions/issuer_alternative_name.rs create mode 100644 src/definitions/x509/validation/extensions/key_usage.rs create mode 100644 src/definitions/x509/validation/extensions/mod.rs create mode 100644 src/definitions/x509/validation/extensions/subject_key_identifier.rs create mode 100644 src/definitions/x509/validation/mod.rs create mode 100644 src/definitions/x509/validation/names.rs create mode 100644 src/definitions/x509/validation/signature.rs create mode 100644 src/definitions/x509/validation/validity.rs diff --git a/Cargo.toml b/Cargo.toml index ac657e9b..0d3587d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ coset = "0.3.8" ciborium = "0.2.2" digest = "0.10.7" tracing = "0.1.41" +sha1 = "0.10.6" [dev-dependencies] hex = "0.4.3" diff --git a/src/bin/x509/mod.rs b/src/bin/x509/mod.rs index 8bb8ebea..4ed285fc 100644 --- a/src/bin/x509/mod.rs +++ b/src/bin/x509/mod.rs @@ -1,35 +1,32 @@ -use anyhow::anyhow; use der::DecodePem; use isomdl::definitions::x509::{ - error::Error as X509Error, - trust_anchor::{TrustAnchor, TrustAnchorRegistry}, + trust_anchor::{TrustAnchor, TrustAnchorRegistry, TrustPurpose}, + validation::ValidationRuleset, X5Chain, }; use x509_cert::Certificate; use crate::RuleSet; -pub fn validate( - rules: RuleSet, - signer: &[u8], - root: &[u8], -) -> Result, anyhow::Error> { +pub fn validate(rules: RuleSet, signer: &[u8], root: &[u8]) -> Result, anyhow::Error> { let root = Certificate::from_pem(root)?; - let trust_anchor = match rules { - RuleSet::Iaca => TrustAnchor::Iaca(root), - RuleSet::Aamva => TrustAnchor::Aamva(root), + let trust_anchor = TrustAnchor { + certificate: root, + purpose: TrustPurpose::Iaca, }; let trust_anchor_registry = TrustAnchorRegistry { - certificates: vec![trust_anchor], + anchors: vec![trust_anchor], }; - let bytes = pem_rfc7468::decode_vec(signer) - .map_err(|e| anyhow!("unable to parse pem: {}", e))? - .1; - let x5chain_cbor: ciborium::Value = ciborium::Value::Bytes(bytes); - let x5chain = X5Chain::from_cbor(x5chain_cbor)?; + let x5chain = X5Chain::builder().with_pem_certificate(signer)?.build()?; - Ok(x5chain.validate(Some(&trust_anchor_registry))) + let outcome = match rules { + RuleSet::Iaca => ValidationRuleset::Mdl, + RuleSet::Aamva => ValidationRuleset::AamvaMdl, + } + .validate(&x5chain, &trust_anchor_registry); + + Ok(outcome.errors) } diff --git a/src/definitions/x509/error.rs b/src/definitions/x509/error.rs deleted file mode 100644 index 8f6bf0cc..00000000 --- a/src/definitions/x509/error.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::definitions::device_key::cose_key::Error as CoseError; -use crate::definitions::helpers::non_empty_vec; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize, thiserror::Error)] -pub enum Error { - #[error("Error occurred while validating x509 certificate: {0}")] - ValidationError(String), - #[error("Error occurred while decoding a x509 certificate: {0}")] - DecodingError(String), - #[error("Error decoding cbor")] - CborDecodingError, - #[error("Error decoding json")] - JsonError, -} - -impl From for Error { - fn from(_: serde_json::Error) -> Self { - Error::JsonError - } -} - -impl From for Error { - fn from(value: x509_cert::der::Error) -> Self { - Error::ValidationError(value.to_string()) - } -} - -impl From for Error { - fn from(value: p256::ecdsa::Error) -> Self { - Error::ValidationError(value.to_string()) - } -} - -impl From for Error { - fn from(value: x509_cert::spki::Error) -> Self { - Error::ValidationError(value.to_string()) - } -} - -impl From for Error { - fn from(value: CoseError) -> Self { - Error::ValidationError(value.to_string()) - } -} - -impl From for Error { - fn from(value: non_empty_vec::Error) -> Self { - Error::ValidationError(value.to_string()) - } -} - -impl From for Error { - fn from(value: asn1_rs::Error) -> Self { - Error::ValidationError(value.to_string()) - } -} diff --git a/src/definitions/x509/extensions.rs b/src/definitions/x509/extensions.rs deleted file mode 100644 index dd453ada..00000000 --- a/src/definitions/x509/extensions.rs +++ /dev/null @@ -1,520 +0,0 @@ -//! All the checks in this module relate to requirements for IACA x509 certificates as -//! detailed in Annex B of ISO18013-5. Specifically, the requirements for values in -//! root and signer certificates are given in tables B.2 and B.4. - -use std::fmt; -use std::ops::Deref; - -use crate::definitions::x509::error::Error; -use const_oid::AssociatedOid; -use const_oid::ObjectIdentifier; -use der::flagset::FlagSet; -use der::Decode; -use x509_cert::ext::pkix::name::DistributionPointName; -use x509_cert::ext::pkix::name::GeneralName; -use x509_cert::ext::pkix::{ - BasicConstraints, CrlDistributionPoints, ExtendedKeyUsage, IssuerAltName, KeyUsage, KeyUsages, -}; -use x509_cert::ext::Extension; - -/// 18013-5 IACA root certificate extension checks -/// * Key Usage: 5, 6 (keyCertSign, crlSign) -/// * Basic Constraints: Pathlen:0 -/// * CRL Distribution Points must have tag 0 -/// * Issuer Alternative Name must be of type rfc822Name or a URI (tag 1 and tag 6) -pub fn validate_iaca_root_extensions(root_extensions: &[Extension]) -> Vec { - tracing::debug!("validating root certificate extensions..."); - //A specific subset of x509 extensions is not allowed in IACA certificates. - //We enter an error for every present disallowed x509 extension - let disallowed = iaca_disallowed_x509_extensions(); - let mut x509_errors: Vec = vec![]; - - for extension in root_extensions { - if let Some(disallowed_extension) = disallowed - .iter() - .find(|oid| extension.extn_id.to_string() == **oid) - { - x509_errors.push(Error::ValidationError(format!( - "The extension with oid: {:?} is not allowed in the IACA certificate profile", - disallowed_extension - ))); - } - } - - let extension_errors = ExtensionValidators::default() - .with(RootKeyUsageValidator) - .with(BasicConstraintsValidator) - .with(CrlDistributionPointsValidator { kind: Kind::Root }) - .with(IssuerAlternativeNameValidator { kind: Kind::Root }) - .validate_extensions(root_extensions.iter()); - - x509_errors.extend(extension_errors); - - x509_errors -} - -/// 18013-5 IACA leaf certificate extension checks -/// * Extended Key Usage: 1.0.18013.5.1.2 -/// * Key Usage: 0 (digitalSignature) -/// * CRL Distribution Points must have tag 0 -/// * Issuer Alternative Name must be of type rfc822Name or a URI (tag 1 and tag 6) -pub fn validate_iaca_signer_extensions( - leaf_extensions: &[Extension], - value_extended_key_usage: ObjectIdentifier, -) -> Vec { - tracing::debug!("validating signer certificate extensions..."); - - let disallowed = iaca_disallowed_x509_extensions(); - let mut x509_errors: Vec = vec![]; - - for extension in leaf_extensions { - if let Some(disallowed_extension) = disallowed - .iter() - .find(|oid| extension.extn_id.to_string() == **oid) - { - x509_errors.push(Error::ValidationError(format!( - "The extension with oid: {:?} is not allowed in the IACA certificate profile", - disallowed_extension - ))); - } - } - - let extension_errors = ExtensionValidators::default() - .with(ExtendedKeyUsageValidator { - expected_oid: value_extended_key_usage, - }) - .with(SignerKeyUsageValidator) - .with(CrlDistributionPointsValidator { kind: Kind::Signer }) - .with(IssuerAlternativeNameValidator { kind: Kind::Signer }) - .validate_extensions(leaf_extensions.iter()); - - x509_errors.extend(extension_errors); - - x509_errors -} - -#[derive(Default)] -struct ExtensionValidators(Vec>); - -impl ExtensionValidators { - fn with(mut self, validator: V) -> Self { - self.0.push(Box::new(validator)); - self - } -} - -struct RequiredExtension { - found: bool, - validator: Box, -} - -impl RequiredExtension { - fn new(validator: Box) -> Self { - Self { - found: false, - validator, - } - } -} - -impl Deref for RequiredExtension { - type Target = Box; - - fn deref(&self) -> &Self::Target { - &self.validator - } -} - -trait ExtensionValidator { - fn matches(&self, extension: &Extension) -> bool; - fn validate(&self, extension: &Extension) -> Vec; - fn not_found(&self) -> Error; -} - -impl ExtensionValidators { - fn validate_extensions<'a, Extensions>(self, extensions: Extensions) -> Vec - where - Extensions: Iterator, - { - let mut validation_errors = vec![]; - - let mut validators: Vec = - self.0.into_iter().map(RequiredExtension::new).collect(); - - for ext in extensions { - if let Some(validator) = validators.iter_mut().find(|validator| { - tracing::debug!("searching for ext: '{}'", ext.extn_id); - validator.matches(ext) - }) { - tracing::debug!("validating required extension: {}", ext.extn_id); - validation_errors.extend(validator.validate(ext)); - validator.found = true; - } else if ext.critical { - tracing::debug!( - "critical, non-required extension causing an error: {}", - ext.extn_id - ); - validation_errors.push(Error::ValidationError(format!( - "certificate contains unknown critical extension: {}", - ext.extn_id - ))); - } else { - tracing::debug!("non-critical, non-required extension ignored: {ext:?}") - } - } - - validation_errors.extend( - validators - .iter() - .filter(|v| !v.found) - .map(|v| v.not_found()), - ); - - validation_errors - } -} - -struct ExtendedKeyUsageValidator { - expected_oid: ObjectIdentifier, -} - -impl ExtensionValidator for ExtendedKeyUsageValidator { - fn matches(&self, extension: &Extension) -> bool { - extension.extn_id == ExtendedKeyUsage::OID - } - - /* A root certificate should have KeyCertSign and CRLSign set for key usage, - but no other key usages are allowed */ - fn validate(&self, extension: &Extension) -> Vec { - let bytes = extension.extn_value.as_bytes(); - let extended_key_usage = ExtendedKeyUsage::from_der(bytes); - - if !extension.critical { - tracing::warn!( - "expected ExtendedKeyUsage extension to be critical on signer certificate", - ) - } - - match extended_key_usage { - Ok(eku) => { - if !eku.0.into_iter().all(|oid| oid == self.expected_oid) { - return vec![Error::ValidationError(format!( - "Invalid extended key usage, expected: {}", - self.expected_oid - ))]; - }; - vec![] - } - Err(e) => { - vec![Error::DecodingError(e.to_string())] - } - } - } - - fn not_found(&self) -> Error { - Error::ValidationError( - "Missing critical ExtendedKeyUsage extension in the signer certificate".to_string(), - ) - } -} - -struct SignerKeyUsageValidator; - -impl ExtensionValidator for SignerKeyUsageValidator { - fn matches(&self, extension: &Extension) -> bool { - extension.extn_id == KeyUsage::OID - } - - /* A root certificate should have KeyCertSign and CRLSign set for key usage, - but no other key usages are allowed */ - fn validate(&self, extension: &Extension) -> Vec { - let bytes = extension.extn_value.as_bytes(); - let mut errors: Vec = vec![]; - let key_usage = KeyUsage::from_der(bytes); - - if !extension.critical { - tracing::warn!("expected KeyUsage extension to be critical on signer certificate",) - } - - match key_usage { - Ok(ku) => { - let expected_flagset: FlagSet = KeyUsages::DigitalSignature.into(); - if ku.0 != expected_flagset { - errors.push(Error::ValidationError( - "Signer KeyUsage should be set to DigitalSignature only".into(), - )) - } - } - Err(e) => { - errors.push(e.into()); - } - }; - errors - } - - fn not_found(&self) -> Error { - Error::ValidationError( - "Missing critical KeyUsage extension in the signer certificate".to_string(), - ) - } -} - -struct RootKeyUsageValidator; - -impl ExtensionValidator for RootKeyUsageValidator { - fn matches(&self, extension: &Extension) -> bool { - extension.extn_id == KeyUsage::OID - } - - /* A root certificate should have KeyCertSign and CRLSign set for key usage, - but no other key usages are allowed */ - fn validate(&self, extension: &Extension) -> Vec { - let bytes = extension.extn_value.as_bytes(); - let mut errors = vec![]; - let key_usage = KeyUsage::from_der(bytes); - - if !extension.critical { - tracing::warn!("expected KeyUsage extension to be critical on root certificate",) - } - - match key_usage { - Ok(ku) => { - if !ku.crl_sign() { - errors.push(Error::ValidationError( - "CrlSign should be set on the root certificate key usage".to_string(), - )) - }; - if !ku.key_cert_sign() { - errors.push(Error::ValidationError( - "KeyCertSign should be set on the root certificate key usage".to_string(), - )) - }; - - if ku - .0 - .into_iter() - .any(|flag| flag != KeyUsages::CRLSign && flag != KeyUsages::KeyCertSign) - { - errors.push(Error::ValidationError(format!("The key usage of the root certificate goes beyond the scope of IACA root certificates {:?}", ku))) - }; - errors - } - Err(e) => { - vec![Error::DecodingError(e.to_string())] - } - } - } - - fn not_found(&self) -> Error { - Error::ValidationError( - "Missing critical KeyUsage extension in the root certificate".to_string(), - ) - } -} - -struct BasicConstraintsValidator; - -impl BasicConstraintsValidator { - fn check(constraints: BasicConstraints) -> Option { - if constraints - .path_len_constraint - .is_none_or(|path_len| path_len != 0) - || !constraints.ca - { - Some(Error::ValidationError(format!( - "Basic constraints expected to be CA:true, path_len:0, but found: {:?}", - constraints - ))) - } else { - None - } - } -} - -impl ExtensionValidator for BasicConstraintsValidator { - fn matches(&self, extension: &Extension) -> bool { - extension.extn_id == BasicConstraints::OID - } - - fn validate(&self, extension: &Extension) -> Vec { - let mut errors = vec![]; - - if !extension.critical { - tracing::warn!("expected BasicConstraints extension to be critical on root certificate",) - } - - let bytes = extension.extn_value.as_bytes(); - let basic_constraints = BasicConstraints::from_der(bytes); - match basic_constraints { - Ok(bc) => { - if let Some(e) = Self::check(bc) { - errors.push(e); - } - } - Err(e) => errors.push(Error::DecodingError(e.to_string())), - } - - errors - } - - fn not_found(&self) -> Error { - Error::ValidationError( - "The root certificate is expected to have BasicConstraints, but the extension was not found".to_string() - ) - } -} - -struct CrlDistributionPointsValidator { - kind: Kind, -} - -impl ExtensionValidator for CrlDistributionPointsValidator { - fn matches(&self, extension: &Extension) -> bool { - extension.extn_id == CrlDistributionPoints::OID - } - - fn validate(&self, extension: &Extension) -> Vec { - let bytes = extension.extn_value.as_bytes(); - let mut errors: Vec = vec![]; - let crl_distribution_point = CrlDistributionPoints::from_der(bytes); - match crl_distribution_point { - Ok(crl_dp) => { - if crl_dp.0.is_empty() { - errors.push(Error::ValidationError( - "expected one or more CRL distribution points".into(), - )); - } - for point in crl_dp.0.into_iter() { - if point.crl_issuer.is_some() { - errors.push(Error::ValidationError(format!("crl_issuer may not be set on CrlDistributionPoints, but is set for: {point:?}"))) - } - - if point.reasons.is_some() { - errors.push(Error::ValidationError(format!( - "reasons may not be set on CrlDistributionPoints, but is set for: {point:?}", - ))) - } - - if !point - .distribution_point - .as_ref() - .is_some_and(|dpn| match dpn { - DistributionPointName::FullName(names) => { - let type_errors: Vec = check_general_name_types(names); - type_errors.is_empty() - } - DistributionPointName::NameRelativeToCRLIssuer(_) => false, - }) - { - errors.push(Error::ValidationError(format!( - "crl distribution point has an invalid type: {:?}", - point - ))) - } - } - } - Err(e) => errors.push(Error::DecodingError(e.to_string())), - } - - errors - } - - fn not_found(&self) -> Error { - Error::ValidationError(format!("The {} certificate is expected to have CRLDistributionPoints, but the extension was not found", self.kind)) - } -} - -struct IssuerAlternativeNameValidator { - kind: Kind, -} - -impl ExtensionValidator for IssuerAlternativeNameValidator { - fn matches(&self, extension: &Extension) -> bool { - extension.extn_id == IssuerAltName::OID - } - - fn validate(&self, extension: &Extension) -> Vec { - let bytes = extension.extn_value.as_bytes(); - let iss_altname = IssuerAltName::from_der(bytes); - match iss_altname { - Ok(ian) => check_general_name_types(&ian.0), - Err(e) => { - vec![Error::DecodingError(e.to_string())] - } - } - } - - fn not_found(&self) -> Error { - Error::ValidationError(format!( - "The {} certificate is expected to have issuer alternative name specified, but the extension was not found", self.kind) - ) - } -} - -enum Kind { - Root, - Signer, -} - -impl fmt::Display for Kind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - match self { - Self::Root => "root", - Self::Signer => "signer", - } - ) - } -} - -fn check_general_name_types(names: &[GeneralName]) -> Vec { - let valid_types: Vec = names - .iter() - .map(|name| { - matches!( - name, - GeneralName::Rfc822Name(_) | GeneralName::UniformResourceIdentifier(_) - ) - }) - .collect(); - - if valid_types.contains(&false) { - vec![Error::ValidationError(format!( - "Invalid type found in GeneralNames: {:?}", - names - ))] - } else { - vec![] - } -} - -fn iaca_disallowed_x509_extensions() -> Vec { - vec![ - "2.5.29.30".to_string(), - "2.5.29.33".to_string(), - "2.5.29.36".to_string(), - "2.5.29.46".to_string(), - "2.5.29.54".to_string(), - ] -} - -#[cfg(test)] -pub mod test { - use rstest::rstest; - use x509_cert::ext::pkix::BasicConstraints; - - use super::BasicConstraintsValidator; - - #[rstest] - #[case::ok(BasicConstraints { ca: true, path_len_constraint: Some(0) }, true)] - #[case::ca_false(BasicConstraints { ca: false, path_len_constraint: Some(0) }, false)] - #[case::path_none(BasicConstraints { ca: true, path_len_constraint: None }, false)] - #[case::path_too_long(BasicConstraints { ca: true, path_len_constraint: Some(1) }, false)] - #[case::both_wrong(BasicConstraints { ca: false, path_len_constraint: None }, false)] - fn basic_constraints(#[case] bc: BasicConstraints, #[case] valid: bool) { - let outcome = BasicConstraintsValidator::check(bc); - assert_eq!(outcome.is_none(), valid) - } -} diff --git a/src/definitions/x509/mod.rs b/src/definitions/x509/mod.rs index f0d723df..c3918282 100644 --- a/src/definitions/x509/mod.rs +++ b/src/definitions/x509/mod.rs @@ -1,6 +1,6 @@ -pub mod error; -pub mod extensions; pub mod trust_anchor; +mod util; +pub mod validation; pub mod x5chain; pub use x5chain::{Builder, X5Chain}; @@ -10,17 +10,19 @@ mod test { use std::time::Duration; use const_oid::ObjectIdentifier; + use der::asn1::OctetString; use p256::NistP256; use rand::random; use sec1::pkcs8::EncodePublicKey; + use sha1::{Digest, Sha1}; use signature::{Keypair, KeypairRef, Signer}; use x509_cert::{ builder::{Builder, CertificateBuilder}, ext::pkix::{ crl::dp::DistributionPoint, name::{DistributionPointName, GeneralName}, - BasicConstraints, CrlDistributionPoints, ExtendedKeyUsage, IssuerAltName, KeyUsage, - KeyUsages, + AuthorityKeyIdentifier, BasicConstraints, CrlDistributionPoints, ExtendedKeyUsage, + IssuerAltName, KeyUsage, KeyUsages, SubjectKeyIdentifier, }, name::Name, spki::{ @@ -30,21 +32,31 @@ mod test { Certificate, }; + use super::validation; + fn prepare_root_certificate(root_key: &S, issuer: Name) -> CertificateBuilder<'_, S> where S: KeypairRef + DynSignatureAlgorithmIdentifier, S::VerifyingKey: EncodePublicKey, { + let spki = SubjectPublicKeyInfoOwned::from_key(root_key.verifying_key()).unwrap(); + let ski_digest = Sha1::digest(spki.subject_public_key.raw_bytes()); + let ski_digest_octet = OctetString::new(ski_digest.to_vec()).unwrap(); + let mut builder = CertificateBuilder::new( x509_cert::builder::Profile::Manual { issuer: None }, random::().into(), Validity::from_now(Duration::from_secs(600)).unwrap(), issuer, - SubjectPublicKeyInfoOwned::from_key(root_key.verifying_key()).unwrap(), + spki, root_key, ) .unwrap(); + builder + .add_extension(&SubjectKeyIdentifier(ski_digest_octet)) + .unwrap(); + builder .add_extension(&KeyUsage(KeyUsages::KeyCertSign | KeyUsages::CRLSign)) .unwrap(); @@ -86,18 +98,37 @@ mod test { S: KeypairRef + DynSignatureAlgorithmIdentifier, S::VerifyingKey: EncodePublicKey, { + let spki = SubjectPublicKeyInfoOwned::from_key(signer_key.verifying_key()).unwrap(); + let ski_digest = Sha1::digest(spki.subject_public_key.raw_bytes()); + let ski_digest_octet = OctetString::new(ski_digest.to_vec()).unwrap(); + + let apki = SubjectPublicKeyInfoOwned::from_key(root_key.verifying_key()).unwrap(); + let aki_digest = Sha1::digest(apki.subject_public_key.raw_bytes()); + let aki_digest_octet = OctetString::new(aki_digest.to_vec()).unwrap(); + let mut builder = CertificateBuilder::new( x509_cert::builder::Profile::Manual { issuer: Some(issuer), }, random::().into(), Validity::from_now(Duration::from_secs(600)).unwrap(), - Name::default(), - SubjectPublicKeyInfoOwned::from_key(signer_key.verifying_key()).unwrap(), + "CN=subject,C=US".parse().unwrap(), + spki, root_key, ) .unwrap(); + builder + .add_extension(&SubjectKeyIdentifier(ski_digest_octet)) + .unwrap(); + + builder + .add_extension(&AuthorityKeyIdentifier { + key_identifier: Some(aki_digest_octet), + ..Default::default() + }) + .unwrap(); + builder .add_extension(&KeyUsage(KeyUsages::DigitalSignature.into())) .unwrap(); @@ -133,45 +164,66 @@ mod test { let root_key = p256::ecdsa::SigningKey::random(&mut rand::thread_rng()); let signer_key = p256::ecdsa::SigningKey::random(&mut rand::thread_rng()); - let issuer = Name::default(); + let issuer: Name = "CN=issuer,C=US".parse().unwrap(); let mut prepared_root_certificate = prepare_root_certificate(&root_key, issuer.clone()); - let signature: ecdsa::der::Signature = + let signature: ecdsa::Signature = root_key.sign(&prepared_root_certificate.finalize().unwrap()); let root_certificate: Certificate = prepared_root_certificate - .assemble(signature.to_bitstring().unwrap()) + .assemble(signature.to_der().to_bitstring().unwrap()) .unwrap(); let mut prepared_signer_certificate = prepare_signer_certificate(&signer_key, &root_key, issuer.clone()); - let signature: ecdsa::der::Signature = + let signature: ecdsa::Signature = root_key.sign(&prepared_signer_certificate.finalize().unwrap()); let signer_certificate: Certificate = prepared_signer_certificate - .assemble(signature.to_bitstring().unwrap()) + .assemble(signature.to_der().to_bitstring().unwrap()) .unwrap(); + assert!(validation::signature::issuer_signed_subject( + &signer_certificate, + &root_certificate + )); + (root_certificate, signer_certificate) } mod iaca { + use der::EncodePem; + use crate::definitions::x509::{ - trust_anchor::{TrustAnchor, TrustAnchorRegistry}, + trust_anchor::{TrustAnchor, TrustAnchorRegistry, TrustPurpose}, + validation::ValidationRuleset, X5Chain, }; #[test_log::test] - fn valid_certificate_chain_is_validated() { + fn valid_mdoc_issuer_certificate_chain_is_validated() { let (root, signer) = super::setup(); + + tracing::debug!( + "issuer certificate:\n{}", + root.to_pem(Default::default()).unwrap() + ); + tracing::debug!( + "signer certificate:\n{}", + signer.to_pem(Default::default()).unwrap() + ); + let trust_anchor_registry = TrustAnchorRegistry { - certificates: vec![TrustAnchor::Iaca(root)], + anchors: vec![TrustAnchor { + certificate: root, + purpose: TrustPurpose::Iaca, + }], }; let x5chain = X5Chain::builder() .with_certificate(signer) .unwrap() .build() .unwrap(); - let errors = x5chain.validate(Some(&trust_anchor_registry)); - assert!(errors.is_empty(), "{errors:?}"); + let outcome = ValidationRuleset::Mdl.validate(&x5chain, &trust_anchor_registry); + assert!(outcome.success(), "{outcome:?}"); } } } diff --git a/src/definitions/x509/trust_anchor.rs b/src/definitions/x509/trust_anchor.rs index 9238a547..e31ebf8e 100644 --- a/src/definitions/x509/trust_anchor.rs +++ b/src/definitions/x509/trust_anchor.rs @@ -1,36 +1,57 @@ -use crate::definitions::x509::{ - error::Error as X509Error, - extensions::{validate_iaca_root_extensions, validate_iaca_signer_extensions}, -}; use anyhow::Error; -use const_oid::ObjectIdentifier; use der::{DecodePem, EncodePem}; use serde::{Deserialize, Serialize}; -use time::OffsetDateTime; -use x509_cert::{attr::AttributeTypeAndValue, Certificate}; +use x509_cert::Certificate; +/// A collection of roots of trust, each with a specific purpose. +#[derive(Clone, Serialize, Deserialize, Default)] +pub struct TrustAnchorRegistry { + /// The roots of trust in this registry. + pub anchors: Vec, +} + +/// A root of trust for a specific purpose. #[derive(Debug, Clone)] -pub enum TrustAnchor { - Iaca(Certificate), - Aamva(Certificate), - IacaReader(Certificate), +pub struct TrustAnchor { + pub certificate: Certificate, + pub purpose: TrustPurpose, +} + +/// Identifies what purpose the certificate is trusted for. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +pub enum TrustPurpose { + /// Issuer Authority Certificate Authority as defined in 18013-5. + Iaca, + /// Reader Certificate Authority as defined in 18013-5. + ReaderCa, } +/// PEM representation of a TrustAnchor, used for serialization and deserialization only. #[derive(Debug, Clone, Serialize, Deserialize)] -enum PemTrustAnchor { - Iaca(String), - Aamva(String), - IacaReader(String), +pub struct PemTrustAnchor { + pub certificate_pem: String, + pub purpose: TrustPurpose, +} + +impl TrustAnchorRegistry { + /// Build a trust anchor registry from PEM certificates. + pub fn from_pem_certificates(certs: Vec) -> Result { + Ok(Self { + anchors: certs + .into_iter() + .map(TrustAnchor::try_from) + .collect::>()?, + }) + } } impl<'l> TryFrom<&'l TrustAnchor> for PemTrustAnchor { type Error = Error; fn try_from(value: &'l TrustAnchor) -> Result { - Ok(match value { - TrustAnchor::Iaca(c) => PemTrustAnchor::Iaca(c.to_pem(Default::default())?), - TrustAnchor::Aamva(c) => PemTrustAnchor::Aamva(c.to_pem(Default::default())?), - TrustAnchor::IacaReader(c) => PemTrustAnchor::IacaReader(c.to_pem(Default::default())?), + Ok(Self { + certificate_pem: value.certificate.to_pem(Default::default())?, + purpose: value.purpose, }) } } @@ -39,10 +60,9 @@ impl TryFrom for TrustAnchor { type Error = Error; fn try_from(value: PemTrustAnchor) -> Result { - Ok(match value { - PemTrustAnchor::Iaca(c) => TrustAnchor::Iaca(Certificate::from_pem(&c)?), - PemTrustAnchor::Aamva(c) => TrustAnchor::Aamva(Certificate::from_pem(&c)?), - PemTrustAnchor::IacaReader(c) => TrustAnchor::IacaReader(Certificate::from_pem(&c)?), + Ok(Self { + certificate: Certificate::from_pem(&value.certificate_pem)?, + purpose: value.purpose, }) } } @@ -72,299 +92,3 @@ impl<'de> Deserialize<'de> for TrustAnchor { .map_err(D::Error::custom) } } - -#[derive(Serialize, Deserialize, Clone)] -pub struct ValidationRuleSet { - pub distinguished_names: Vec, - #[serde(rename = "type")] - pub typ: RuleSetType, -} - -#[derive(Serialize, Deserialize, Clone)] -pub enum RuleSetType { - IACA, - AAMVA, - NamesOnly, - ReaderAuth, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct TrustAnchorRegistry { - pub certificates: Vec, -} - -impl TrustAnchorRegistry { - pub fn from_pem_iaca_certificates(certs: Vec) -> Result { - Ok(Self { - certificates: certs - .into_iter() - .map(PemTrustAnchor::Iaca) - .map(TrustAnchor::try_from) - .collect::>()?, - }) - } -} - -fn process_validation_outcomes( - leaf_certificate: &Certificate, - root_certificate: &Certificate, - rule_set: ValidationRuleSet, -) -> Vec { - let mut errors: Vec = vec![]; - - //execute checks on x509 components - match apply_ruleset(leaf_certificate, root_certificate, rule_set) { - Ok(mut v) => { - errors.append(&mut v); - } - Err(e) => { - errors.push(e); - } - } - - // make sure that the trust anchor is still valid - errors.append(&mut check_validity_period(root_certificate)); - - //TODO: check CRL to make sure the certificates have not been revoked - errors -} - -pub fn validate_with_ruleset( - leaf_certificate: &Certificate, - trust_anchor: &TrustAnchor, -) -> Vec { - let mut errors: Vec = vec![]; - - match trust_anchor { - TrustAnchor::Iaca(root_certificate) => { - let rule_set = ValidationRuleSet { - distinguished_names: vec!["2.5.4.6".to_string(), "2.5.4.8".to_string()], - typ: RuleSetType::IACA, - }; - errors.append(&mut process_validation_outcomes( - leaf_certificate, - root_certificate, - rule_set, - )); - } - TrustAnchor::Aamva(root_certificate) => { - let rule_set = ValidationRuleSet { - distinguished_names: vec!["2.5.4.6".to_string(), "2.5.4.8".to_string()], - typ: RuleSetType::AAMVA, - }; - errors.append(&mut process_validation_outcomes( - leaf_certificate, - root_certificate, - rule_set, - )); - } - TrustAnchor::IacaReader(root_certificate) => { - let rule_set = ValidationRuleSet { - distinguished_names: vec!["2.5.4.3".to_string()], - typ: RuleSetType::ReaderAuth, - }; - errors.append(&mut process_validation_outcomes( - leaf_certificate, - root_certificate, - rule_set, - )); - } - } - errors -} - -pub fn check_validity_period(certificate: &Certificate) -> Vec { - let validity = certificate.tbs_certificate.validity; - let mut errors: Vec = vec![]; - if validity.not_after.to_unix_duration().as_secs() - < OffsetDateTime::now_utc().unix_timestamp() as u64 - { - errors.push(X509Error::ValidationError(format!( - "Expired certificate with subject: {:?}", - certificate.tbs_certificate.subject - ))); - }; - if validity.not_before.to_unix_duration().as_secs() - > OffsetDateTime::now_utc().unix_timestamp() as u64 - { - errors.push(X509Error::ValidationError(format!( - "Not yet valid certificate with subject: {:?}", - certificate.tbs_certificate.subject - ))); - }; - - errors -} - -/* Validates: - -- all the correct distinghuished names are present -and match the -- all the correct extensions are present -- the extensions are set to the ruleset values -- */ -fn apply_ruleset( - leaf_certificate: &Certificate, - root_certificate: &Certificate, - rule_set: ValidationRuleSet, -) -> Result, X509Error> { - let mut errors: Vec = vec![]; - // collect all the distinguished names in the root certificate that the validation ruleset requires - let root_distinguished_names: Vec<&AttributeTypeAndValue> = root_certificate - .tbs_certificate - .subject - .0 - .iter() - .flat_map(|rdn| { - rdn.0.as_slice().iter().filter(|atv| { - rule_set - .distinguished_names - .iter() - .any(|oid| oid == &atv.oid.to_string()) - }) - }) - .collect(); - - // collect all the distinguished names in the signer certificate that the validation ruleset requires - let leaf_distinguished_names: Vec<&AttributeTypeAndValue> = leaf_certificate - .tbs_certificate - .issuer - .0 - .iter() - .flat_map(|rdn| { - rdn.0.as_slice().iter().filter(|atv| { - rule_set - .distinguished_names - .iter() - .any(|oid| oid == &atv.oid.to_string()) - }) - }) - .collect(); - - // if all the needed distinguished names have been collected, - // there should be the same number of names collected as are present in the ruleset - if root_distinguished_names.len() != rule_set.distinguished_names.len() { - errors.push(X509Error::ValidationError("The configured validation ruleset requires a distinguished name that is not found in the submitted root certificate".to_string())); - } - - if leaf_distinguished_names.len() != rule_set.distinguished_names.len() { - errors.push(X509Error::ValidationError("The configured validation ruleset requires a distinguished name that is not found in the submitted signer certificate".to_string())); - } - - let Some(root_extensions) = root_certificate.tbs_certificate.extensions.as_ref() else { - return Err(X509Error::ValidationError( - "The root certificate is expected to have extensions, but none were found. Skipping all following extension validation checks..".to_string(), - )); - }; - - let Some(leaf_extensions) = leaf_certificate.tbs_certificate.extensions.as_ref() else { - return Err(X509Error::ValidationError( - "The signer certificate is expected to have extensions, but none were found. Skipping all following extension validation checks.. " - .to_string(), - )); - }; - - match rule_set.typ { - //Under the IACA ruleset, the values for S or ST should be the same in subject and issuer if they are present in both - RuleSetType::IACA => { - let mut extension_errors = validate_iaca_root_extensions(root_extensions); - extension_errors.append(&mut validate_iaca_signer_extensions( - leaf_extensions, - mdoc_extended_key_usage_oid(), - )); - for dn in leaf_distinguished_names { - if dn.oid == const_oid::db::rfc2256::STATE_OR_PROVINCE_NAME { - if let Some(&root_state_or_province) = - root_distinguished_names.iter().find(|r| r.oid == dn.oid) - { - if dn != root_state_or_province { - return Err(X509Error::ValidationError(format!("Mismatch between supplied certificate issuer attribute: {:?} and the trust anchor registry.", dn))); - } - } - } else { - let Some(_root_dn) = root_distinguished_names.iter().find(|r| r == &&dn) else { - return Err(X509Error::ValidationError(format!("Mismatch between supplied certificate issuer attribute: {:?} and the trust anchor registry.", dn.value))); - }; - } - } - Ok(extension_errors) - } - //Under the AAMVA ruleset, S/ST is mandatory and should be the same in the subject and issuer - RuleSetType::AAMVA => { - let mut extension_errors = validate_iaca_root_extensions(root_extensions); - extension_errors.append(&mut validate_iaca_signer_extensions( - leaf_extensions, - mdoc_extended_key_usage_oid(), - )); - for dn in leaf_distinguished_names { - let Some(_root_dn) = root_distinguished_names.iter().find(|r| r == &&dn) else { - return Err(X509Error::ValidationError(format!("Mismatch between supplied certificate issuer attribute: {:?} and the trust anchor registry.", dn.value))); - }; - } - Ok(extension_errors) - } - RuleSetType::NamesOnly => { - for dn in leaf_distinguished_names { - let Some(_root_dn) = root_distinguished_names.iter().find(|r| r == &&dn) else { - return Err(X509Error::ValidationError(format!("Mismatch between supplied certificate issuer attribute: {:?} and the trust anchor registry.", dn.value))); - }; - } - Ok(vec![]) - } - RuleSetType::ReaderAuth => Ok(validate_iaca_signer_extensions( - leaf_extensions, - reader_extended_key_usage_oid(), - )), - } -} - -pub fn find_anchor<'l>( - leaf_certificate: &Certificate, - trust_anchor_registry: Option<&'l TrustAnchorRegistry>, -) -> Result, X509Error> { - let leaf_issuer = &leaf_certificate.tbs_certificate.issuer; - - let Some(root_certificates) = trust_anchor_registry else { - return Ok(None); - }; - let Some(trust_anchor) = - root_certificates - .certificates - .iter() - .find(|trust_anchor| match trust_anchor { - TrustAnchor::Iaca(certificate) - | TrustAnchor::Aamva(certificate) - | TrustAnchor::IacaReader(certificate) => { - &certificate.tbs_certificate.subject == leaf_issuer - } - }) - else { - return Err(X509Error::ValidationError( - "The certificate issuer does not match any known trusted issuer".to_string(), - )); - }; - Ok(Some(trust_anchor)) -} - -fn mdoc_extended_key_usage_oid() -> ObjectIdentifier { - // Unwrap safety: unit tested. - ObjectIdentifier::new("1.0.18013.5.1.2").unwrap() -} - -fn reader_extended_key_usage_oid() -> ObjectIdentifier { - // Unwrap safety: unit tested. - ObjectIdentifier::new("1.0.18013.5.1.6").unwrap() -} - -#[cfg(test)] -mod test { - #[test] - fn mdoc_extended_key_usage_oid_doesnt_panic() { - super::mdoc_extended_key_usage_oid(); - } - - #[test] - fn reader_extended_key_usage_oid_doesnt_panic() { - super::reader_extended_key_usage_oid(); - } -} diff --git a/src/definitions/x509/util.rs b/src/definitions/x509/util.rs new file mode 100644 index 00000000..1a02fe00 --- /dev/null +++ b/src/definitions/x509/util.rs @@ -0,0 +1,62 @@ +use anyhow::{Context, Error}; +use const_oid::{db::rfc4519::COMMON_NAME, AssociatedOid}; +use der::{ + asn1::{Ia5StringRef, PrintableStringRef, TeletexStringRef, Utf8StringRef}, + referenced::OwnedToRef, + Tag, Tagged, +}; +use ecdsa::{PrimeCurve, VerifyingKey}; +use elliptic_curve::{ + sec1::{FromEncodedPoint, ToEncodedPoint}, + AffinePoint, CurveArithmetic, FieldBytesSize, PublicKey, +}; +use sec1::point::ModulusSize; +use x509_cert::{attr::AttributeValue, Certificate}; + +/// Get the public key from a certificate for verification. +pub fn public_key(certificate: &Certificate) -> Result, Error> +where + C: AssociatedOid + CurveArithmetic + PrimeCurve, + AffinePoint: FromEncodedPoint + ToEncodedPoint, + FieldBytesSize: ModulusSize, +{ + certificate + .tbs_certificate + .subject_public_key_info + .owned_to_ref() + .try_into() + .map(|key: PublicKey| key.into()) + .context("could not parse public key from PKCS8 SPKI") +} + +/// Get the first CommonName of the X.509 certificate, or return "Unknown". +pub fn common_name_or_unknown(certificate: &Certificate) -> &str { + common_name(certificate).unwrap_or("Unknown") +} + +fn common_name(certificate: &Certificate) -> Option<&str> { + certificate + .tbs_certificate + .subject + .0 + .iter() + .flat_map(|rdn| rdn.0.iter()) + .filter_map(|attribute| { + if attribute.oid == COMMON_NAME { + attribute_value_to_str(&attribute.value) + } else { + None + } + }) + .next() +} + +pub fn attribute_value_to_str(av: &AttributeValue) -> Option<&str> { + match av.tag() { + Tag::PrintableString => PrintableStringRef::try_from(av).ok().map(|s| s.as_str()), + Tag::Utf8String => Utf8StringRef::try_from(av).ok().map(|s| s.as_str()), + Tag::Ia5String => Ia5StringRef::try_from(av).ok().map(|s| s.as_str()), + Tag::TeletexString => TeletexStringRef::try_from(av).ok().map(|s| s.as_str()), + _ => None, + } +} diff --git a/src/definitions/x509/validation/error.rs b/src/definitions/x509/validation/error.rs new file mode 100644 index 00000000..16c53334 --- /dev/null +++ b/src/definitions/x509/validation/error.rs @@ -0,0 +1,75 @@ +use std::fmt; + +#[derive(Debug, Clone, Copy)] +pub struct ErrorWithContext { + context: ErrorContext, + error: E, +} + +impl ErrorWithContext { + pub fn comparison(error: E) -> String { + Self { + context: ErrorContext::Comparison, + error, + } + .to_string() + } + + pub fn ds(error: E) -> String { + Self { + context: ErrorContext::DocumentSigner, + error, + } + .to_string() + } + + pub fn iaca(error: E) -> String { + Self { + context: ErrorContext::Iaca, + error, + } + .to_string() + } + + pub fn reader(error: E) -> String { + Self { + context: ErrorContext::Reader, + error, + } + .to_string() + } + + pub fn reader_ca(error: E) -> String { + Self { + context: ErrorContext::ReaderCa, + error, + } + .to_string() + } +} + +impl fmt::Display for ErrorWithContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} error: {}", + match self.context { + ErrorContext::Comparison => "Comparison", + ErrorContext::DocumentSigner => "DS certificate", + ErrorContext::Iaca => "IACA certificate", + ErrorContext::Reader => "Reader certificate", + ErrorContext::ReaderCa => "Reader CA certificate", + }, + self.error, + ) + } +} + +#[derive(Debug, Clone, Copy)] +enum ErrorContext { + Comparison, + DocumentSigner, + Iaca, + Reader, + ReaderCa, +} diff --git a/src/definitions/x509/validation/extensions/basic_constraints.rs b/src/definitions/x509/validation/extensions/basic_constraints.rs new file mode 100644 index 00000000..7fd67b80 --- /dev/null +++ b/src/definitions/x509/validation/extensions/basic_constraints.rs @@ -0,0 +1,68 @@ +use const_oid::AssociatedOid; +use der::Decode; +use x509_cert::ext::{pkix::BasicConstraints, Extension}; + +use super::{Error, ExtensionValidator}; + +/// BasicConstraints validation for IACA certificate. +pub struct BasicConstraintsValidator; + +impl BasicConstraintsValidator { + fn check(constraints: BasicConstraints) -> Option { + if constraints + .path_len_constraint + .is_none_or(|path_len| path_len != 0) + || !constraints.ca + { + Some(format!( + "expected to be CA:true, path_len:0, but found: {:?}", + constraints + )) + } else { + None + } + } +} + +impl ExtensionValidator for BasicConstraintsValidator { + fn oid(&self) -> const_oid::ObjectIdentifier { + BasicConstraints::OID + } + + fn ext_name(&self) -> &'static str { + "BasicConstraints" + } + + fn validate(&self, extension: &Extension) -> Vec { + let mut errors = vec![]; + + if !extension.critical { + tracing::warn!("expected BasicConstraints extension to be critical",) + } + + let bytes = extension.extn_value.as_bytes(); + let basic_constraints = BasicConstraints::from_der(bytes); + match basic_constraints { + Ok(bc) => { + if let Some(e) = Self::check(bc) { + errors.push(e); + } + } + Err(e) => errors.push(format!("failed to decode: {e}")), + } + + errors + } +} + +#[cfg(test)] +#[rstest::rstest] +#[case::ok(BasicConstraints { ca: true, path_len_constraint: Some(0) }, true)] +#[case::ca_false(BasicConstraints { ca: false, path_len_constraint: Some(0) }, false)] +#[case::path_none(BasicConstraints { ca: true, path_len_constraint: None }, false)] +#[case::path_too_long(BasicConstraints { ca: true, path_len_constraint: Some(1) }, false)] +#[case::both_wrong(BasicConstraints { ca: false, path_len_constraint: None }, false)] +fn test(#[case] bc: BasicConstraints, #[case] valid: bool) { + let outcome = BasicConstraintsValidator::check(bc); + assert_eq!(outcome.is_none(), valid) +} diff --git a/src/definitions/x509/validation/extensions/crl_distribution_points.rs b/src/definitions/x509/validation/extensions/crl_distribution_points.rs new file mode 100644 index 00000000..76d29544 --- /dev/null +++ b/src/definitions/x509/validation/extensions/crl_distribution_points.rs @@ -0,0 +1,177 @@ +use const_oid::AssociatedOid; +use der::Decode; +use x509_cert::ext::{ + pkix::{ + name::{DistributionPointName, GeneralName}, + CrlDistributionPoints, + }, + Extension, +}; + +use super::{Error, ExtensionValidator}; + +/// CRLDistributionPoints validation for all certificate profiles. +pub struct CrlDistributionPointsValidator; + +impl CrlDistributionPointsValidator { + fn check(crl_distribution_points: CrlDistributionPoints) -> Vec { + if crl_distribution_points.0.is_empty() { + return vec!["expected one or more distribution points".into()]; + } + let mut errors = vec![]; + for point in crl_distribution_points.0.into_iter() { + if point.crl_issuer.is_some() { + errors.push(format!( + "crl_issuer cannot be set, but is set for: {point:?}" + )) + } + + if point.reasons.is_some() { + errors.push(format!("reasons cannot be set, but is set for: {point:?}",)) + } + + if !point + .distribution_point + .as_ref() + .is_some_and(|dpn| match dpn { + DistributionPointName::FullName(names) => names + .iter() + .any(|gn| matches!(gn, GeneralName::UniformResourceIdentifier(_))), + DistributionPointName::NameRelativeToCRLIssuer(_) => false, + }) + { + errors.push(format!("point is invalid: {point:?}",)) + } + } + errors + } +} + +impl ExtensionValidator for CrlDistributionPointsValidator { + fn oid(&self) -> const_oid::ObjectIdentifier { + CrlDistributionPoints::OID + } + + fn ext_name(&self) -> &'static str { + "CrlDistributionPoints" + } + + fn validate(&self, extension: &Extension) -> Vec { + let bytes = extension.extn_value.as_bytes(); + let crl_distribution_points = CrlDistributionPoints::from_der(bytes); + match crl_distribution_points { + Ok(crl_dps) => Self::check(crl_dps), + Err(e) => vec![format!("failed to decode: {e}")], + } + } +} + +#[cfg(test)] +use der::flagset::FlagSet; +#[cfg(test)] +use x509_cert::ext::pkix::crl::dp::DistributionPoint; + +#[cfg(test)] +#[rstest::rstest] +#[case::ok( + CrlDistributionPoints( + vec![ + DistributionPoint { + distribution_point: Some( + DistributionPointName::FullName( + vec![ + GeneralName::UniformResourceIdentifier( + "http://example.com".to_string().try_into().unwrap() + ) + ] + ) + ), + reasons: None, + crl_issuer: None, + } + ] + ), + true +)] +#[case::empty(CrlDistributionPoints(vec![]), false)] +#[case::one_good_one_bad( + CrlDistributionPoints( + vec![ + DistributionPoint { + distribution_point: Some( + DistributionPointName::FullName( + vec![ + GeneralName::UniformResourceIdentifier( + "http://example.com".to_string().try_into().unwrap() + ) + ] + ) + ), + reasons: None, + crl_issuer: None, + }, + DistributionPoint { + distribution_point: None, + reasons: None, + crl_issuer: None, + } + ] + ), + false +)] +#[case::no_dp( + CrlDistributionPoints( + vec![ + DistributionPoint { + distribution_point: None, + reasons: None, + crl_issuer: None, + } + ] + ), + false +)] +#[case::good_with_reasons( + CrlDistributionPoints( + vec![ + DistributionPoint { + distribution_point: Some( + DistributionPointName::FullName( + vec![ + GeneralName::UniformResourceIdentifier( + "http://example.com".to_string().try_into().unwrap() + ) + ] + ) + ), + reasons: Some(FlagSet::default()), + crl_issuer: None, + }, + ] + ), + false +)] +#[case::good_with_issuer( + CrlDistributionPoints( + vec![ + DistributionPoint { + distribution_point: Some( + DistributionPointName::FullName( + vec![ + GeneralName::UniformResourceIdentifier( + "http://example.com".to_string().try_into().unwrap() + ) + ] + ) + ), + reasons: None, + crl_issuer: Some(vec![]), + }, + ] + ), + false +)] +fn test(#[case] crl_dps: CrlDistributionPoints, #[case] valid: bool) { + let outcome = CrlDistributionPointsValidator::check(crl_dps); + assert_eq!(outcome.is_empty(), valid) +} diff --git a/src/definitions/x509/validation/extensions/extended_key_usage.rs b/src/definitions/x509/validation/extensions/extended_key_usage.rs new file mode 100644 index 00000000..db0e38a9 --- /dev/null +++ b/src/definitions/x509/validation/extensions/extended_key_usage.rs @@ -0,0 +1,95 @@ +use const_oid::AssociatedOid; +use const_oid::ObjectIdentifier; +use der::Decode; +use x509_cert::ext::{pkix::ExtendedKeyUsage, Extension}; + +use super::Error; +use super::ExtensionValidator; + +/// ExtendedKeyUsage validation for document signer and mdoc reader certificates. +pub struct ExtendedKeyUsageValidator { + pub expected_oid: ObjectIdentifier, +} + +impl ExtendedKeyUsageValidator { + fn check(&self, eku: ExtendedKeyUsage) -> Option { + if !eku.0.iter().all(|oid| *oid == self.expected_oid) { + Some(format!( + "expected '{}', found '{:?}'", + self.expected_oid, eku.0 + )) + } else if eku.0.is_empty() { + Some(format!("expected '{}', found '[]'", self.expected_oid)) + } else { + None + } + } +} + +impl ExtensionValidator for ExtendedKeyUsageValidator { + fn oid(&self) -> const_oid::ObjectIdentifier { + ExtendedKeyUsage::OID + } + + fn ext_name(&self) -> &'static str { + "ExtendedKeyUsage" + } + + fn validate(&self, extension: &Extension) -> Vec { + let bytes = extension.extn_value.as_bytes(); + let extended_key_usage = ExtendedKeyUsage::from_der(bytes); + + if !extension.critical { + tracing::warn!("expected ExtendedKeyUsage extension to be critical",) + } + + match extended_key_usage { + Ok(eku) => { + if let Some(e) = self.check(eku) { + vec![e] + } else { + vec![] + } + } + Err(e) => { + vec![format!("failed to decode: {e}")] + } + } + } +} + +pub const fn document_signer_extended_key_usage_oid() -> ObjectIdentifier { + // Unwrap safety: unit tested. + ObjectIdentifier::new_unwrap("1.0.18013.5.1.2") +} + +pub const fn mdoc_reader_extended_key_usage_oid() -> ObjectIdentifier { + // Unwrap safety: unit tested. + ObjectIdentifier::new_unwrap("1.0.18013.5.1.6") +} + +#[cfg(test)] +#[rstest::rstest] +#[case::ok(ExtendedKeyUsage(vec![ObjectIdentifier::new_unwrap("1.1.1")]), true)] +#[case::wrong(ExtendedKeyUsage(vec![ObjectIdentifier::new_unwrap("1.1.0")]), false)] +#[case::missing(ExtendedKeyUsage(vec![]), false)] +#[case::good_and_bad(ExtendedKeyUsage(vec![ObjectIdentifier::new_unwrap("1.1.1"), ObjectIdentifier::new_unwrap("1.1.0")]), false)] +fn test(#[case] eku: ExtendedKeyUsage, #[case] valid: bool) { + let outcome = ExtendedKeyUsageValidator { + expected_oid: ObjectIdentifier::new_unwrap("1.1.1"), + } + .check(eku); + assert_eq!(outcome.is_none(), valid) +} + +#[cfg(test)] +#[test] +fn test_document_signer_extended_key_usage_oid() { + document_signer_extended_key_usage_oid(); +} + +#[cfg(test)] +#[test] +fn test_mdoc_reader_extended_key_usage_oid() { + mdoc_reader_extended_key_usage_oid(); +} diff --git a/src/definitions/x509/validation/extensions/issuer_alternative_name.rs b/src/definitions/x509/validation/extensions/issuer_alternative_name.rs new file mode 100644 index 00000000..1053aa82 --- /dev/null +++ b/src/definitions/x509/validation/extensions/issuer_alternative_name.rs @@ -0,0 +1,57 @@ +use const_oid::AssociatedOid; +use const_oid::ObjectIdentifier; +use der::Decode; +use x509_cert::ext::{ + pkix::{name::GeneralName, IssuerAltName}, + Extension, +}; + +use super::Error; +use super::ExtensionValidator; + +pub struct IssuerAlternativeNameValidator; + +impl IssuerAlternativeNameValidator { + fn check(ian: IssuerAltName) -> Option { + if !ian.0.iter().all(|gn| { + matches!( + gn, + GeneralName::Rfc822Name(_) | GeneralName::UniformResourceIdentifier(_) + ) + }) { + Some(format!( + "invalid type in found in general names: {:?}", + ian.0 + )) + } else { + None + } + } +} + +impl ExtensionValidator for IssuerAlternativeNameValidator { + fn oid(&self) -> ObjectIdentifier { + IssuerAltName::OID + } + + fn ext_name(&self) -> &'static str { + "IssuerAlternativeName" + } + + fn validate(&self, extension: &Extension) -> Vec { + let bytes = extension.extn_value.as_bytes(); + let iss_altname = IssuerAltName::from_der(bytes); + match iss_altname { + Ok(ian) => { + if let Some(e) = Self::check(ian) { + vec![e] + } else { + vec![] + } + } + Err(e) => { + vec![format!("failed to decode: {e}")] + } + } + } +} diff --git a/src/definitions/x509/validation/extensions/key_usage.rs b/src/definitions/x509/validation/extensions/key_usage.rs new file mode 100644 index 00000000..4e3c6bcc --- /dev/null +++ b/src/definitions/x509/validation/extensions/key_usage.rs @@ -0,0 +1,106 @@ +use const_oid::AssociatedOid; +use der::{flagset::FlagSet, Decode}; +use x509_cert::ext::{ + pkix::{KeyUsage, KeyUsages}, + Extension, +}; + +use super::{Error, ExtensionValidator}; + +/// KeyUsage validation for all certificate profiles. +pub struct KeyUsageValidator { + expected_flagset: FlagSet, +} + +impl KeyUsageValidator { + pub fn document_signer() -> Self { + Self { + expected_flagset: KeyUsages::DigitalSignature.into(), + } + } + + pub fn mdoc_reader() -> Self { + Self { + expected_flagset: KeyUsages::DigitalSignature.into(), + } + } + + pub fn iaca() -> Self { + Self { + expected_flagset: KeyUsages::CRLSign | KeyUsages::KeyCertSign, + } + } + + fn check(&self, ku: KeyUsage) -> Option { + if ku.0 != self.expected_flagset { + Some(format!( + "unexpected usage: {:?}", + ku.0.into_iter().collect::>() + )) + } else { + None + } + } +} + +impl ExtensionValidator for KeyUsageValidator { + fn oid(&self) -> const_oid::ObjectIdentifier { + KeyUsage::OID + } + + fn ext_name(&self) -> &'static str { + "KeyUsage" + } + + fn validate(&self, extension: &Extension) -> Vec { + let bytes = extension.extn_value.as_bytes(); + let key_usage = KeyUsage::from_der(bytes); + + if !extension.critical { + tracing::warn!("expected KeyUsage extension to be critical",) + } + + match key_usage { + Ok(ku) => { + if let Some(e) = self.check(ku) { + vec![e] + } else { + vec![] + } + } + Err(e) => { + vec![format!("failed to decode: {e}")] + } + } + } +} + +#[cfg(test)] +#[rstest::rstest] +#[case::ds_ok(KeyUsageValidator::document_signer(), KeyUsage(KeyUsages::DigitalSignature.into()), true)] +#[case::iaca_ok(KeyUsageValidator::iaca(), KeyUsage(KeyUsages::CRLSign | KeyUsages::KeyCertSign), true)] +#[case::ds_extra(KeyUsageValidator::iaca(), KeyUsage( KeyUsages::KeyCertSign | KeyUsages::DigitalSignature), false)] +#[case::iaca_extra(KeyUsageValidator::iaca(), KeyUsage(KeyUsages::CRLSign | KeyUsages::KeyCertSign | KeyUsages::DigitalSignature), false)] +#[case::ds_missing(KeyUsageValidator::iaca(), KeyUsage(FlagSet::default()), false)] +#[case::iaca_missing(KeyUsageValidator::iaca(), KeyUsage(KeyUsages::KeyCertSign.into()), false)] +fn test(#[case] kuv: KeyUsageValidator, #[case] ku: KeyUsage, #[case] valid: bool) { + let outcome = kuv.check(ku); + assert_eq!(outcome.is_none(), valid) +} + +#[cfg(test)] +#[test] +fn test_flagsets() { + assert!(KeyUsageValidator::document_signer() + .expected_flagset + .contains(KeyUsages::DigitalSignature)); + assert!(KeyUsageValidator::mdoc_reader() + .expected_flagset + .contains(KeyUsages::DigitalSignature)); + assert!(KeyUsageValidator::iaca() + .expected_flagset + .contains(KeyUsages::CRLSign)); + assert!(KeyUsageValidator::iaca() + .expected_flagset + .contains(KeyUsages::KeyCertSign)); +} diff --git a/src/definitions/x509/validation/extensions/mod.rs b/src/definitions/x509/validation/extensions/mod.rs new file mode 100644 index 00000000..98827f00 --- /dev/null +++ b/src/definitions/x509/validation/extensions/mod.rs @@ -0,0 +1,260 @@ +//! All the checks in this module relate to requirements for X.509 certificates as detailed in +//! Annex B of ISO18013-5. Specifically, the requirements for extensions in IACA and mdoc signer +//! certificates are given in tables B.2 and B.4 respectively. + +mod basic_constraints; +mod crl_distribution_points; +mod extended_key_usage; +mod issuer_alternative_name; +mod key_usage; +mod subject_key_identifier; + +use std::ops::Deref; + +use basic_constraints::BasicConstraintsValidator; +use const_oid::db; +use const_oid::AssociatedOid; +use const_oid::ObjectIdentifier; +use crl_distribution_points::CrlDistributionPointsValidator; +use der::Decode; +use extended_key_usage::document_signer_extended_key_usage_oid; +use extended_key_usage::mdoc_reader_extended_key_usage_oid; +use extended_key_usage::ExtendedKeyUsageValidator; +use issuer_alternative_name::IssuerAlternativeNameValidator; +use key_usage::KeyUsageValidator; +use subject_key_identifier::SubjectKeyIdentifierValidator; +use x509_cert::ext::{ + pkix::{ + AuthorityKeyIdentifier, FreshestCrl, InhibitAnyPolicy, NameConstraints, PolicyConstraints, + PolicyMappings, SubjectKeyIdentifier, + }, + Extension, +}; +use x509_cert::Certificate; + +type Error = String; + +/// Validate that the subject key identifier of the issuer and the authority key identifier of the +/// subject are present and equal. +pub fn key_identifier_check<'a, E>(issuer_extensions: E, subject_extensions: E) -> bool +where + E: Iterator + Clone, +{ + let issuer_skis = issuer_extensions.filter_map(|ext| { + if ext.extn_id == SubjectKeyIdentifier::OID { + SubjectKeyIdentifier::from_der(ext.extn_value.as_bytes()) + .inspect_err(|e| tracing::warn!("failed to parse SubjectKeyIdentifier: {e}")) + .ok() + } else { + None + } + }); + + subject_extensions + .filter_map(|ext| { + if ext.extn_id == AuthorityKeyIdentifier::OID { + AuthorityKeyIdentifier::from_der(ext.extn_value.as_bytes()) + .inspect_err(|e| tracing::warn!("failed to parse AuthorityKeyIdentifier: {e}")) + .ok() + } else { + None + } + }) + .filter_map(|aki| aki.key_identifier) + .any(|ki| { + issuer_skis.clone().any(|ski| { + tracing::debug!("comparing key identifiers:\n\t{ki:?}\n\t{:?}", ski.0); + ki == ski.0 + }) + }) +} + +/// Validate IACA extensions according to 18013-5 Annex B. +pub fn validate_iaca_extensions(certificate: &Certificate) -> Vec { + tracing::debug!("validating IACA extensions..."); + + let extensions = certificate.tbs_certificate.extensions.iter().flatten(); + + let mut errors: Vec = check_for_disallowed_x509_extensions(extensions.clone()); + + errors.extend( + ExtensionValidators::default() + .with(SubjectKeyIdentifierValidator::from_certificate(certificate)) + .with(KeyUsageValidator::iaca()) + .with(BasicConstraintsValidator) + .with(CrlDistributionPointsValidator) + .with(IssuerAlternativeNameValidator) + .validate_extensions(extensions), + ); + + errors +} + +/// Validate document signer extensions according to 18013-5 Annex B. +pub fn validate_document_signer_certificate_extensions(certificate: &Certificate) -> Vec { + tracing::debug!("validating document signer certificate extensions..."); + + let extensions = certificate.tbs_certificate.extensions.iter().flatten(); + + let mut errors: Vec = check_for_disallowed_x509_extensions(extensions.clone()); + + errors.extend( + ExtensionValidators::default() + .with(SubjectKeyIdentifierValidator::from_certificate(certificate)) + .with(ExtendedKeyUsageValidator { + expected_oid: document_signer_extended_key_usage_oid(), + }) + .with(KeyUsageValidator::document_signer()) + .with(CrlDistributionPointsValidator) + .with(IssuerAlternativeNameValidator) + .validate_extensions(extensions), + ); + + errors +} + +/// Validate mdoc reader extensions according to 18013-5 Annex B. +pub fn validate_mdoc_reader_certificate_extensions(certificate: &Certificate) -> Vec { + tracing::debug!("validating mdoc_reader certificate extensions..."); + + let extensions = certificate.tbs_certificate.extensions.iter().flatten(); + + let mut errors: Vec = check_for_disallowed_x509_extensions(extensions.clone()); + + errors.extend( + ExtensionValidators::default() + .with(SubjectKeyIdentifierValidator::from_certificate(certificate)) + .with(ExtendedKeyUsageValidator { + expected_oid: mdoc_reader_extended_key_usage_oid(), + }) + .with(KeyUsageValidator::mdoc_reader()) + .with(CrlDistributionPointsValidator) + .with(IssuerAlternativeNameValidator) + .validate_extensions(extensions), + ); + + errors +} + +#[derive(Default)] +struct ExtensionValidators(Vec>); + +impl ExtensionValidators { + fn with(mut self, validator: V) -> Self { + self.0.push(Box::new(validator)); + self + } +} + +struct RequiredExtension { + found: bool, + validator: Box, +} + +impl RequiredExtension { + fn new(validator: Box) -> Self { + Self { + found: false, + validator, + } + } +} + +impl Deref for RequiredExtension { + type Target = Box; + + fn deref(&self) -> &Self::Target { + &self.validator + } +} + +trait ExtensionValidator { + fn oid(&self) -> ObjectIdentifier; + fn ext_name(&self) -> &'static str; + fn validate(&self, extension: &Extension) -> Vec; +} + +impl ExtensionValidators { + fn validate_extensions<'a, Extensions>(self, extensions: Extensions) -> Vec + where + Extensions: IntoIterator, + { + let mut validation_errors = vec![]; + + let mut validators: Vec = + self.0.into_iter().map(RequiredExtension::new).collect(); + + for ext in extensions { + if let Some(validator) = validators.iter_mut().find(|validator| { + tracing::debug!("searching for ext: '{}'", ext.extn_id); + validator.oid() == ext.extn_id + }) { + tracing::debug!("validating required extension: {}", ext.extn_id); + validation_errors.extend( + validator + .validate(ext) + .into_iter() + .map(|e| format!("{}: {e}", validator.ext_name())), + ); + validator.found = true; + } else if ext.critical { + tracing::debug!( + "critical, non-required extension causing an error: {}", + ext.extn_id + ); + validation_errors.push(format!( + "contains unknown critical extension: {}", + ext.extn_id + )); + } else { + tracing::debug!("non-critical, non-required extension ignored: {ext:?}") + } + } + + validation_errors.extend( + validators + .iter() + .filter(|v| !v.found) + .map(|v| format!("{}: required extension not found", v.ext_name())), + ); + + validation_errors + } +} + +/// As identified in 18013-5 Annex B, section B.1.1. +/// +/// The specification is unclear as to which certificates this restriction applies to, so it is +/// assumed in this library to apply to all and only to certificate profiles defined in Annex B. +fn check_for_disallowed_x509_extensions<'a, E>(extensions: E) -> Vec +where + E: Iterator + Clone, +{ + let disallowed_extensions = [ + PolicyMappings::OID, + NameConstraints::OID, + PolicyConstraints::OID, + InhibitAnyPolicy::OID, + FreshestCrl::OID, + ]; + + extensions + .map(|e| e.extn_id) + .filter_map(|id| { + if disallowed_extensions + .iter() + .any(|disallowed_id| *disallowed_id == id) + { + Some(format!( + "extension is not allowed: {}", + db::DB + .by_oid(&id) + .map(|s| s.to_string()) + .unwrap_or(id.to_string()) + )) + } else { + None + } + }) + .collect() +} diff --git a/src/definitions/x509/validation/extensions/subject_key_identifier.rs b/src/definitions/x509/validation/extensions/subject_key_identifier.rs new file mode 100644 index 00000000..840ae9db --- /dev/null +++ b/src/definitions/x509/validation/extensions/subject_key_identifier.rs @@ -0,0 +1,101 @@ +use const_oid::{AssociatedOid, ObjectIdentifier}; +use der::Decode; +use sha1::{Digest, Sha1}; +use x509_cert::{ + ext::{pkix::SubjectKeyIdentifier, Extension}, + Certificate, +}; + +use super::Error; +use super::ExtensionValidator; + +pub struct SubjectKeyIdentifierValidator { + subject_public_key_bitstring_raw_bytes: Vec, +} + +impl SubjectKeyIdentifierValidator { + pub fn from_certificate(certificate: &Certificate) -> Self { + Self { + subject_public_key_bitstring_raw_bytes: certificate + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes() + .to_owned(), + } + } + + fn check(&self, ski: SubjectKeyIdentifier) -> Option { + let expected_digest = ski.0.as_bytes(); + let digest = Sha1::digest(&self.subject_public_key_bitstring_raw_bytes); + + if digest.as_slice() != expected_digest { + Some("public key digest did not match the expected value".into()) + } else { + None + } + } +} + +impl ExtensionValidator for SubjectKeyIdentifierValidator { + fn oid(&self) -> ObjectIdentifier { + SubjectKeyIdentifier::OID + } + + fn ext_name(&self) -> &'static str { + "SubjectKeyIdentifier" + } + + fn validate(&self, extension: &Extension) -> Vec { + let bytes = extension.extn_value.as_bytes(); + let ski = SubjectKeyIdentifier::from_der(bytes); + match ski { + Ok(ski) => { + if let Some(e) = self.check(ski) { + vec![e] + } else { + vec![] + } + } + Err(e) => { + vec![format!("failed to decode: {e}")] + } + } + } +} + +#[cfg(test)] +#[rstest::rstest] +#[case::ok( + include_str!("../../../../../test/issuance/256-cert.pem"), + include_str!("../../../../../test/issuance/256-cert.pem"), + true +)] +#[case::different_cert_ski_ext( + include_str!("../../../../../test/issuance/256-cert.pem"), + include_str!("../../../../../test/issuance/384-cert.pem"), + false +)] +fn test( + #[case] public_key_from_certificate_pem: &'static str, + #[case] ski_ext_from_certificate_pem: &'static str, + #[case] valid: bool, +) { + use der::DecodePem; + + let certificate = Certificate::from_pem(public_key_from_certificate_pem).unwrap(); + let skiv = SubjectKeyIdentifierValidator::from_certificate(&certificate); + + let certificate = Certificate::from_pem(ski_ext_from_certificate_pem).unwrap(); + let outcome = skiv.validate( + certificate + .tbs_certificate + .extensions + .iter() + .flatten() + .filter(|ext| ext.extn_id == skiv.oid()) + .next() + .unwrap(), + ); + assert_eq!(outcome.is_empty(), valid) +} diff --git a/src/definitions/x509/validation/mod.rs b/src/definitions/x509/validation/mod.rs new file mode 100644 index 00000000..df2d1db5 --- /dev/null +++ b/src/definitions/x509/validation/mod.rs @@ -0,0 +1,218 @@ +use const_oid::db::rfc2256::STATE_OR_PROVINCE_NAME; +use error::ErrorWithContext; +use extensions::{ + key_identifier_check, validate_document_signer_certificate_extensions, + validate_iaca_extensions, validate_mdoc_reader_certificate_extensions, +}; +use names::{country_name_matches, has_rdn, state_or_province_name_matches}; +use serde::Serialize; +use signature::issuer_signed_subject; +use validity::check_validity_period; +use x509_cert::Certificate; + +use super::{ + trust_anchor::{TrustAnchorRegistry, TrustPurpose}, + X5Chain, +}; + +mod error; +mod extensions; +mod names; +pub(super) mod signature; +mod validity; + +/// Ruleset for X5Chain validation. +#[derive(Debug, Clone, Copy)] +pub enum ValidationRuleset { + /// Validate the certificate chain according to the 18013-5 rules for mDL IACA and Document + /// Signer certificates. + Mdl, + /// Validate the certificate chain according to the AAMVA rules for mDL IACA and Document + /// Signer certificates. + AamvaMdl, + /// Validate the certificate chain according to the 18013-5 rules for mDL Reader certificates. + /// + /// Only validates the leaf certificate in the x5chain against the trust anchor registry. + MdlReaderOneStep, +} + +#[derive(Debug, Clone, Serialize, Default)] +pub struct ValidationOutcome { + pub errors: Vec, +} + +impl ValidationOutcome { + pub fn success(&self) -> bool { + self.errors.is_empty() + } +} + +impl ValidationRuleset { + pub fn validate( + self, + x5chain: &X5Chain, + trust_anchors: &TrustAnchorRegistry, + ) -> ValidationOutcome { + match self { + Self::Mdl => mdl_validate(x5chain, trust_anchors), + Self::AamvaMdl => aamva_mdl_validate(x5chain, trust_anchors), + Self::MdlReaderOneStep => mdl_reader_one_step_validate(x5chain, trust_anchors), + } + } +} + +fn mdl_validate_inner<'a: 'b, 'b>( + x5chain: &'a X5Chain, + trust_anchors: &'b TrustAnchorRegistry, +) -> Result<(ValidationOutcome, &'a Certificate, &'b Certificate), ValidationOutcome> { + let mut outcome = ValidationOutcome::default(); + + // As we are validating using the IACA rules in 18013-5, we don't need to verify the whole + // chain. We can simply take the first certificate in the chain as the document signer + // certificate (NOTE 1 in B.1.1). + let document_signer = x5chain.end_entity_certificate(); + + let validity_errors = check_validity_period(document_signer) + .into_iter() + .map(ErrorWithContext::ds); + outcome.errors.extend(validity_errors); + + let ds_extension_errors = validate_document_signer_certificate_extensions(document_signer) + .into_iter() + .map(ErrorWithContext::ds); + outcome.errors.extend(ds_extension_errors); + + let mut trust_anchor_candidates = + find_trust_anchor_candidates(document_signer, trust_anchors, TrustPurpose::Iaca); + + let Some(iaca) = trust_anchor_candidates.next() else { + outcome + .errors + .push(ErrorWithContext::iaca("no valid trust anchor found")); + return Err(outcome); + }; + + if trust_anchor_candidates.next().is_some() { + tracing::warn!("more than one trust anchor candidate found, using the first one"); + } + + if let Some(error) = country_name_matches(document_signer, iaca) { + outcome.errors.push(ErrorWithContext::comparison(error)) + } + + let iaca_extension_errors = validate_iaca_extensions(iaca) + .into_iter() + .map(ErrorWithContext::iaca); + outcome.errors.extend(iaca_extension_errors); + + // TODO: CRL check on DS and IACA. + + Ok((outcome, document_signer, iaca)) +} + +fn mdl_validate(x5chain: &X5Chain, trust_anchors: &TrustAnchorRegistry) -> ValidationOutcome { + match mdl_validate_inner(x5chain, trust_anchors) { + Ok((mut outcome, ds, iaca)) => { + if has_rdn(ds, STATE_OR_PROVINCE_NAME) || has_rdn(iaca, STATE_OR_PROVINCE_NAME) { + if let Some(error) = state_or_province_name_matches(ds, iaca) { + outcome.errors.push(ErrorWithContext::comparison(error)) + } + } + + outcome + } + Err(outcome) => outcome, + } +} + +fn aamva_mdl_validate(x5chain: &X5Chain, trust_anchors: &TrustAnchorRegistry) -> ValidationOutcome { + match mdl_validate_inner(x5chain, trust_anchors) { + Ok((mut outcome, ds, iaca)) => { + if let Some(error) = state_or_province_name_matches(ds, iaca) { + outcome.errors.push(ErrorWithContext::comparison(error)) + } + + outcome + } + Err(outcome) => outcome, + } +} + +fn mdl_reader_one_step_validate( + x5chain: &X5Chain, + trust_anchors: &TrustAnchorRegistry, +) -> ValidationOutcome { + let mut outcome = ValidationOutcome::default(); + + let reader = x5chain.end_entity_certificate(); + + let validity_errors = check_validity_period(reader) + .into_iter() + .map(ErrorWithContext::reader); + outcome.errors.extend(validity_errors); + + let reader_extension_errors = validate_mdoc_reader_certificate_extensions(reader) + .into_iter() + .map(ErrorWithContext::reader); + outcome.errors.extend(reader_extension_errors); + + let mut trust_anchor_candidates = + find_trust_anchor_candidates(reader, trust_anchors, TrustPurpose::ReaderCa); + + let Some(_reader_ca) = trust_anchor_candidates.next() else { + outcome + .errors + .push(ErrorWithContext::reader_ca("no valid trust anchor found")); + return outcome; + }; + + if trust_anchor_candidates.next().is_some() { + tracing::warn!("more than one trust anchor candidate found, using the first one"); + } + + // TODO: CRL or OCSP check on reader and reader CA. + + outcome +} + +fn find_trust_anchor_candidates<'a: 'b, 'b>( + subject: &'a Certificate, + trust_anchors: &'b TrustAnchorRegistry, + trust_purpose: TrustPurpose, +) -> impl Iterator { + trust_anchors + .anchors + .iter() + .filter_map(move |anchor| { + if trust_purpose == anchor.purpose { + Some(&anchor.certificate) + } else { + None + } + }) + .filter(|candidate| candidate.tbs_certificate.subject == subject.tbs_certificate.issuer) + .filter(|candidate| { + let valid = key_identifier_check( + candidate.tbs_certificate.extensions.iter().flatten(), + subject.tbs_certificate.extensions.iter().flatten(), + ); + if !valid { + tracing::warn!("key identifier extensions did not match"); + } + valid + }) + .filter(|candidate| { + let valid = issuer_signed_subject(subject, candidate); + if !valid { + tracing::warn!("issuer did not sign subject"); + } + valid + }) + .filter(|candidate| { + let errors = check_validity_period(candidate); + if !errors.is_empty() { + tracing::warn!("certificate is not valid: {errors:?}"); + } + errors.is_empty() + }) +} diff --git a/src/definitions/x509/validation/names.rs b/src/definitions/x509/validation/names.rs new file mode 100644 index 00000000..5a51abb2 --- /dev/null +++ b/src/definitions/x509/validation/names.rs @@ -0,0 +1,154 @@ +use const_oid::{ + db::{self, rfc2256::STATE_OR_PROVINCE_NAME, rfc4519::COUNTRY_NAME}, + ObjectIdentifier, +}; +use x509_cert::{attr::AttributeValue, Certificate}; + +use crate::definitions::x509::util::{attribute_value_to_str, common_name_or_unknown}; + +#[derive(Debug, Copy, Clone, thiserror::Error)] +pub enum Error<'l> { + #[error("'{certificate_common_name}' has no subject '{name}'")] + Missing { + certificate_common_name: &'l str, + name: &'static str, + }, + #[error("'{certificate_common_name}' has multiple subject '{name}'s")] + Multiple { + certificate_common_name: &'l str, + name: &'static str, + }, + #[error("subject '{name}' does not match: {this} != {that}")] + Mismatch { + name: &'static str, + this: &'l str, + that: &'l str, + }, +} + +#[allow(dead_code)] +/// Checks that countryName in the certificate has only one countryName in the subject and that it +/// matches the expected value. +pub fn country_name_is<'a: 'c, 'b: 'c, 'c>( + certificate: &'a Certificate, + expected_country_name: &'b str, +) -> Option> { + let name = "countryName"; + + let mut cs = get_rdns(certificate, COUNTRY_NAME); + + let Some(c) = cs.next() else { + return Some(Error::Missing { + certificate_common_name: common_name_or_unknown(certificate), + name, + }); + }; + + if cs.next().is_some() { + return Some(Error::Multiple { + certificate_common_name: common_name_or_unknown(certificate), + name, + }); + } + + let c = attribute_value_to_str(c).unwrap_or("unknown"); + + if c != expected_country_name { + return Some(Error::Mismatch { + name, + this: c, + that: expected_country_name, + }); + } + + None +} + +/// Checks that each certificate has only one countryName in the subject, and that they match. +pub fn country_name_matches<'a: 'c, 'b: 'c, 'c>( + this: &'a Certificate, + that: &'b Certificate, +) -> Option> { + name_matches(COUNTRY_NAME, this, that) +} + +/// Checks that each certificate has only one stateOrProvinceName in the subject, and that they match. +pub fn state_or_province_name_matches<'a: 'c, 'b: 'c, 'c>( + this: &'a Certificate, + that: &'b Certificate, +) -> Option> { + name_matches(STATE_OR_PROVINCE_NAME, this, that) +} + +fn name_matches<'a: 'c, 'b: 'c, 'c>( + name_oid: ObjectIdentifier, + this: &'a Certificate, + that: &'b Certificate, +) -> Option> { + let name = db::DB.by_oid(&name_oid).unwrap_or("unknown"); + + let mut this_cs = get_rdns(this, name_oid); + let mut that_cs = get_rdns(that, name_oid); + + let Some(this_c) = this_cs.next() else { + return Some(Error::Missing { + certificate_common_name: common_name_or_unknown(this), + name, + }); + }; + + let Some(that_c) = that_cs.next() else { + return Some(Error::Missing { + certificate_common_name: common_name_or_unknown(that), + name, + }); + }; + + if this_cs.next().is_some() { + return Some(Error::Multiple { + certificate_common_name: common_name_or_unknown(that), + name, + }); + } + + if that_cs.next().is_some() { + return Some(Error::Multiple { + certificate_common_name: common_name_or_unknown(this), + name, + }); + } + + if this_c != that_c { + return Some(Error::Mismatch { + name, + this: attribute_value_to_str(this_c).unwrap_or("Unknown"), + that: attribute_value_to_str(that_c).unwrap_or("Unknown"), + }); + } + + None +} + +/// Check whether the certificate has a particular RelativeDistinguished name in the subject. +pub fn has_rdn(certificate: &Certificate, oid: ObjectIdentifier) -> bool { + get_rdns(certificate, oid).next().is_some() +} + +fn get_rdns( + certificate: &Certificate, + oid: ObjectIdentifier, +) -> impl Iterator { + certificate + .tbs_certificate + .subject + .0 + .iter() + .flat_map(|rdn| rdn.0.iter()) + .filter_map(move |attribute| { + if attribute.oid == oid { + Some(&attribute.value) + } else { + None + } + }) +} diff --git a/src/definitions/x509/validation/signature.rs b/src/definitions/x509/validation/signature.rs new file mode 100644 index 00000000..8af3d8a0 --- /dev/null +++ b/src/definitions/x509/validation/signature.rs @@ -0,0 +1,69 @@ +use der::Encode; +use ecdsa::{signature::Verifier, Signature, VerifyingKey}; +use p256::NistP256; +use x509_cert::Certificate; + +use crate::definitions::x509::util::public_key; + +/// Check that the issuer certificate signed the subject certificate. +pub fn issuer_signed_subject(subject: &Certificate, issuer: &Certificate) -> bool { + // TODO: Support curves other than P-256. + let issuer_public_key: VerifyingKey = match public_key(issuer) { + Ok(pk) => pk, + Err(e) => { + tracing::error!("failed to decode issuer public key: {e:?}"); + return false; + } + }; + + let sig: Signature = match Signature::from_der(subject.signature.raw_bytes()) { + Ok(sig) => sig, + Err(e) => { + tracing::error!("failed to parse subject signature: {e:?}"); + return false; + } + }; + + let tbs = match subject.tbs_certificate.to_der() { + Ok(tbs) => tbs, + Err(e) => { + tracing::error!("failed to parse subject tbs: {e:?}"); + return false; + } + }; + + match issuer_public_key.verify(&tbs, &sig) { + Ok(()) => true, + Err(e) => { + tracing::info!("subject certificate signature could not be validated: {e:?}"); + false + } + } +} + +#[cfg(test)] +mod test { + use crate::definitions::x509::x5chain::CertificateWithDer; + + use super::issuer_signed_subject; + + #[test] + pub fn correct_signature() { + let target = include_bytes!("../../../../test/presentation/isomdl_iaca_signer.pem"); + let issuer = include_bytes!("../../../../test/presentation/isomdl_iaca_root_cert.pem"); + assert!(issuer_signed_subject( + &CertificateWithDer::from_pem(target).unwrap().inner, + &CertificateWithDer::from_pem(issuer).unwrap().inner, + )) + } + + #[test] + pub fn incorrect_signature() { + let issuer = include_bytes!("../../../../test/presentation/isomdl_iaca_signer.pem"); + let target = include_bytes!("../../../../test/presentation/isomdl_iaca_root_cert.pem"); + assert!(!issuer_signed_subject( + &CertificateWithDer::from_pem(target).unwrap().inner, + &CertificateWithDer::from_pem(issuer).unwrap().inner, + )) + } +} diff --git a/src/definitions/x509/validation/validity.rs b/src/definitions/x509/validation/validity.rs new file mode 100644 index 00000000..fc4629c5 --- /dev/null +++ b/src/definitions/x509/validation/validity.rs @@ -0,0 +1,27 @@ +use time::OffsetDateTime; +use x509_cert::Certificate; + +pub fn check_validity_period(certificate: &Certificate) -> Vec { + let validity = certificate.tbs_certificate.validity; + let mut errors: Vec = vec![]; + if validity.not_after.to_unix_duration().as_secs() + < OffsetDateTime::now_utc().unix_timestamp() as u64 + { + errors.push(Error::Expired); + }; + if validity.not_before.to_unix_duration().as_secs() + > OffsetDateTime::now_utc().unix_timestamp() as u64 + { + errors.push(Error::NotYetValid); + }; + + errors +} + +#[derive(Debug, Clone, Copy, thiserror::Error)] +pub enum Error { + #[error("expired")] + Expired, + #[error("not yet valid")] + NotYetValid, +} diff --git a/src/definitions/x509/x5chain.rs b/src/definitions/x509/x5chain.rs index c1b59500..d3f44bc0 100644 --- a/src/definitions/x509/x5chain.rs +++ b/src/definitions/x509/x5chain.rs @@ -1,67 +1,43 @@ +use std::io::Read; + use crate::definitions::helpers::NonEmptyVec; -use crate::definitions::x509::error::Error as X509Error; -use crate::definitions::x509::trust_anchor::check_validity_period; -use crate::definitions::x509::trust_anchor::find_anchor; -use crate::definitions::x509::trust_anchor::TrustAnchorRegistry; -use anyhow::{anyhow, Result}; -use p256::ecdsa::VerifyingKey; + +use anyhow::{anyhow, bail, Context, Error, Result}; use const_oid::AssociatedOid; use ciborium::Value as CborValue; +use ecdsa::{PrimeCurve, VerifyingKey}; use elliptic_curve::{ sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint}, - AffinePoint, CurveArithmetic, FieldBytesSize, PublicKey, + AffinePoint, CurveArithmetic, FieldBytesSize, }; -use p256::NistP256; -use signature::Verifier; -use std::collections::HashSet; -use std::{fs::File, io::Read}; use x509_cert::der::Encode; -use x509_cert::{ - certificate::Certificate, - der::{referenced::OwnedToRef, Decode}, -}; +use x509_cert::{certificate::Certificate, der::Decode}; -use super::trust_anchor::validate_with_ruleset; +use super::util::{common_name_or_unknown, public_key}; /// See: -pub const X5CHAIN_HEADER_LABEL: i64 = 0x21; +pub const X5CHAIN_COSE_HEADER_LABEL: i64 = 0x21; +/// X.509 certificate with the DER representation held in memory for ease of serialization. #[derive(Debug, Clone, Eq, PartialEq)] -pub struct X509 { +pub struct CertificateWithDer { pub inner: Certificate, der: Vec, } -impl X509 { - pub fn public_key(&self) -> Result, X509Error> - where - C: AssociatedOid + CurveArithmetic, - AffinePoint: FromEncodedPoint + ToEncodedPoint, - FieldBytesSize: ModulusSize, - { - self.inner - .tbs_certificate - .subject_public_key_info - .owned_to_ref() - .try_into() - .map_err(|e| format!("could not parse public key from pkcs8 spki: {e}")) - .map_err(|_e| { - X509Error::ValidationError("could not parse public key from pkcs8 spki".to_string()) - }) - } - +impl CertificateWithDer { pub fn from_pem(bytes: &[u8]) -> Result { let bytes = pem_rfc7468::decode_vec(bytes) - .map_err(|e| anyhow!("unable to parse pem: {}", e))? + .map_err(|e| anyhow!("unable to parse certificate from PEM encoding: {e}"))? .1; - X509::from_der(&bytes) + CertificateWithDer::from_der(&bytes) } pub fn from_der(bytes: &[u8]) -> Result { let inner = Certificate::from_der(bytes) - .map_err(|e| anyhow!("unable to parse certificate from der encoding: {}", e))?; + .context("unable to parse certificate from DER encoding")?; Ok(Self { inner, der: bytes.to_vec(), @@ -78,10 +54,10 @@ impl X509 { } #[derive(Debug, Clone)] -pub struct X5Chain(NonEmptyVec); +pub struct X5Chain(NonEmptyVec); -impl From> for X5Chain { - fn from(v: NonEmptyVec) -> Self { +impl From> for X5Chain { + fn from(v: NonEmptyVec) -> Self { Self(v) } } @@ -104,145 +80,86 @@ impl X5Chain { } } - pub fn from_cbor(cbor_bytes: CborValue) -> Result { - match cbor_bytes { + pub fn from_cbor(cbor: CborValue) -> Result { + match cbor { CborValue::Bytes(bytes) => { - Self::builder().with_der(&bytes).map_err( - |e| X509Error::DecodingError(e.to_string()) - )?.build().map_err( - |e| X509Error::DecodingError(e.to_string()) - ) + Self::builder().with_der_certificate(&bytes)?.build() }, CborValue::Array(x509s) => { x509s.iter() - .try_fold(Self::builder(), |builder, x509| match x509 { + .try_fold(Self::builder(), |mut builder, x509| match x509 { CborValue::Bytes(bytes) => { - let builder = builder.with_der(bytes).map_err( - |e| X509Error::DecodingError(e.to_string()) - )?; + builder = builder.with_der_certificate(bytes)?; Ok(builder) }, - _ => Err(X509Error::ValidationError(format!("Expecting x509 certificate in the x5chain to be a cbor encoded bytestring, but received: {x509:?}"))) + _ => bail!("expected x509 certificate in the x5chain to be a cbor encoded bytestring, but received: {x509:?}") })? - .build() - .map_err(|e| X509Error::DecodingError(e.to_string()) - ) + .build() }, - _ => Err(X509Error::ValidationError(format!("Expecting x509 certificate in the x5chain to be a cbor encoded bytestring, but received: {cbor_bytes:?}"))) + _ => bail!("expected x5chain to be a cbor encoded bytestring or array, but received: {cbor:?}") } } - pub fn get_signer_key(&self) -> Result { - let leaf = self.0.first().ok_or(X509Error::CborDecodingError)?; - leaf.public_key().map(|key| key.into()) + /// Retrieve the end-entity certificate. + pub fn end_entity_certificate(&self) -> &Certificate { + &self.0[0].inner } - pub fn validate(&self, trust_anchor_registry: Option<&TrustAnchorRegistry>) -> Vec { - let x5chain = self.0.as_ref(); - let mut errors: Vec = vec![]; - - if !self.has_unique_elements() { - errors.push(X509Error::ValidationError( - "x5chain contains duplicate certificates".to_string(), - )) - }; - - x5chain.windows(2).for_each(|chain_link| { - let target = &chain_link[0]; - let issuer = &chain_link[1]; - if check_signature(target, issuer).is_err() { - errors.push(X509Error::ValidationError(format!( - "invalid signature for target: {:?}", - target - ))); - } - }); - - //make sure all submitted certificates are valid - for x509 in x5chain { - errors.append(&mut check_validity_period(&x509.inner)); - } - - //validate the last certificate in the chain against trust anchor - if let Some(x509) = x5chain.last() { - let cert = &x509.inner; - // if the issuer of the signer certificate is known in the trust anchor registry, do the validation. - // otherwise, report an error and skip. - match find_anchor(cert, trust_anchor_registry) { - Ok(anchor) => { - if let Some(trust_anchor) = anchor { - errors.append(&mut validate_with_ruleset(cert, trust_anchor)); - } else { - errors.push(X509Error::ValidationError( - "No matching trust anchor found".to_string(), - )); - } - } - Err(e) => errors.push(e), - } - } else { - errors.push(X509Error::ValidationError( - "Empty certificate chain".to_string(), - )) - } - - errors + /// Retrieve the public key of the end-entity certificate. + pub fn end_entity_public_key(&self) -> Result, Error> + where + C: AssociatedOid + CurveArithmetic + PrimeCurve, + AffinePoint: FromEncodedPoint + ToEncodedPoint, + FieldBytesSize: ModulusSize, + { + public_key(self.end_entity_certificate()) } - fn has_unique_elements(&self) -> bool { - let mut uniq = HashSet::new(); - self.0.iter().all(move |x| uniq.insert(&x.der)) + /// Retrieve the public key of the end-entity certificate. + pub fn end_entity_common_name(&self) -> &str { + common_name_or_unknown(self.end_entity_certificate()) } } -pub fn check_signature(target: &X509, issuer: &X509) -> Result<(), X509Error> { - let parent_public_key = ecdsa::VerifyingKey::from(issuer.public_key()?); - let child_cert = &target.inner; - let sig: ecdsa::Signature = - ecdsa::Signature::from_der(child_cert.signature.raw_bytes())?; - let bytes = child_cert.tbs_certificate.to_der()?; - Ok(parent_public_key.verify(&bytes, &sig)?) -} - #[derive(Default, Debug, Clone)] pub struct Builder { - certs: Vec, + certs: Vec, } impl Builder { pub fn with_certificate(mut self, cert: Certificate) -> Result { - let x509 = X509::from_cert(cert)?; + let x509 = CertificateWithDer::from_cert(cert)?; self.certs.push(x509); Ok(self) } - pub fn with_x509(mut self, x509: X509) -> Builder { + pub fn with_certificate_and_der(mut self, x509: CertificateWithDer) -> Builder { self.certs.push(x509); self } - pub fn with_pem(mut self, data: &[u8]) -> Result { - let x509 = X509::from_pem(data)?; + pub fn with_pem_certificate(mut self, data: &[u8]) -> Result { + let x509 = CertificateWithDer::from_pem(data)?; self.certs.push(x509); Ok(self) } - pub fn with_der(mut self, data: &[u8]) -> Result { - let x509 = X509::from_der(data)?; + pub fn with_der_certificate(mut self, data: &[u8]) -> Result { + let x509 = CertificateWithDer::from_der(data)?; self.certs.push(x509); Ok(self) } - pub fn with_pem_from_file(self, mut f: File) -> Result { + pub fn with_pem_certificate_from_io(self, mut io: R) -> Result { let mut data: Vec = vec![]; - f.read_to_end(&mut data)?; - self.with_pem(&data) + io.read_to_end(&mut data)?; + self.with_pem_certificate(&data) } - pub fn with_der_from_file(self, mut f: File) -> Result { + pub fn with_der_certificate_from_io(self, mut io: R) -> Result { let mut data: Vec = vec![]; - f.read_to_end(&mut data)?; - self.with_der(&data) + io.read_to_end(&mut data)?; + self.with_der_certificate(&data) } pub fn build(self) -> Result { - Ok(X5Chain(self.certs.try_into().map_err(|_| { - anyhow!("at least one certificate must be given to the builder") - })?)) + Ok(X5Chain(self.certs.try_into().context( + "at least one certificate must be given to the builder", + )?)) } } @@ -257,7 +174,7 @@ pub mod test { #[test] pub fn self_signed_es256() { let _x5chain = X5Chain::builder() - .with_pem(CERT_256) + .with_pem_certificate(CERT_256) .expect("unable to add cert") .build() .expect("unable to build x5chain"); @@ -266,7 +183,7 @@ pub mod test { #[test] pub fn self_signed_es384() { let _x5chain = X5Chain::builder() - .with_pem(CERT_384) + .with_pem_certificate(CERT_384) .expect("unable to add cert") .build() .expect("unable to build x5chain"); @@ -275,31 +192,9 @@ pub mod test { #[test] pub fn self_signed_es512() { let _x5chain = X5Chain::builder() - .with_pem(CERT_521) + .with_pem_certificate(CERT_521) .expect("unable to add cert") .build() .expect("unable to build x5chain"); } - - #[test] - pub fn correct_signature() { - let target = include_bytes!("../../../test/presentation/isomdl_iaca_signer.pem"); - let issuer = include_bytes!("../../../test/presentation/isomdl_iaca_root_cert.pem"); - check_signature( - &X509::from_pem(target).unwrap(), - &X509::from_pem(issuer).unwrap(), - ) - .expect("issuer did not sign target cert") - } - - #[test] - pub fn incorrect_signature() { - let issuer = include_bytes!("../../../test/presentation/isomdl_iaca_signer.pem"); - let target = include_bytes!("../../../test/presentation/isomdl_iaca_root_cert.pem"); - check_signature( - &X509::from_pem(target).unwrap(), - &X509::from_pem(issuer).unwrap(), - ) - .expect_err("issuer did sign target cert"); - } } diff --git a/src/issuance/mdoc.rs b/src/issuance/mdoc.rs index 7f09b6de..9feb1451 100644 --- a/src/issuance/mdoc.rs +++ b/src/issuance/mdoc.rs @@ -12,7 +12,7 @@ use signature::{SignatureEncoding, Signer}; use crate::cose::sign1::PreparedCoseSign1; use crate::cose::{MaybeTagged, SignatureAlgorithm}; use crate::{ - definitions::x509::x5chain::{X5Chain, X5CHAIN_HEADER_LABEL}, + definitions::x509::x5chain::{X5Chain, X5CHAIN_COSE_HEADER_LABEL}, definitions::{ helpers::{NonEmptyMap, NonEmptyVec, Tag24}, issuer_signed::{IssuerNamespaces, IssuerSignedItemBytes}, @@ -196,7 +196,7 @@ impl PreparedMdoc { .inner .unprotected .rest - .push((Label::Int(X5CHAIN_HEADER_LABEL), x5chain.into_cbor())); + .push((Label::Int(X5CHAIN_COSE_HEADER_LABEL), x5chain.into_cbor())); Mdoc { doc_type, mso, @@ -629,7 +629,7 @@ pub mod test { let mdoc_builder = minimal_test_mdoc_builder(); let x5chain = X5Chain::builder() - .with_pem(ISSUER_CERT) + .with_pem_certificate(ISSUER_CERT) .unwrap() .build() .unwrap(); @@ -646,7 +646,7 @@ pub mod test { fn decoy_digests() { let mdoc_builder = minimal_test_mdoc_builder(); let x5chain = X5Chain::builder() - .with_pem(ISSUER_CERT) + .with_pem_certificate(ISSUER_CERT) .unwrap() .build() .unwrap(); diff --git a/src/presentation/authentication/mdoc.rs b/src/presentation/authentication/mdoc.rs index ffe9e62e..01418f4b 100644 --- a/src/presentation/authentication/mdoc.rs +++ b/src/presentation/authentication/mdoc.rs @@ -9,7 +9,6 @@ use crate::definitions::{ device_signed::DeviceAuthentication, helpers::Tag24, SessionTranscript180135, }; use crate::presentation::reader::Error; -use crate::presentation::reader::Error as ReaderError; use anyhow::Result; use elliptic_curve::generic_array::GenericArray; use issuer_signed::IssuerSigned; @@ -19,14 +18,16 @@ use ssi_jwk::Params; use ssi_jwk::JWK as SsiJwk; pub fn issuer_authentication(x5chain: X5Chain, issuer_signed: &IssuerSigned) -> Result<(), Error> { - let signer_key = x5chain.get_signer_key()?; + let signer_key = x5chain + .end_entity_public_key() + .map_err(Error::IssuerPublicKey)?; let verification_result: cose::sign1::VerificationResult = issuer_signed .issuer_auth .verify::(&signer_key, None, None); verification_result .into_result() - .map_err(ReaderError::IssuerAuthentication) + .map_err(Error::IssuerAuthentication) } pub fn device_authentication( @@ -47,7 +48,7 @@ pub fn device_authentication( let x_coordinate = p.x_coordinate.clone(); let y_coordinate = p.y_coordinate.clone(); let (Some(x), Some(y)) = (x_coordinate, y_coordinate) else { - return Err(ReaderError::MdocAuth( + return Err(Error::MdocAuth( "device key jwk is missing coordinates".to_string(), )); }; @@ -67,7 +68,7 @@ pub fn device_authentication( document.doc_type.clone(), namespaces_bytes.clone(), )) - .map_err(|_| ReaderError::CborDecodingError)?; + .map_err(|_| Error::CborDecodingError)?; let external_aad = None; let cbor_payload = cbor::to_vec(&detached_payload)?; let result = device_signature.verify::( @@ -76,13 +77,13 @@ pub fn device_authentication( external_aad, ); if !result.is_success() { - Err(ReaderError::ParsingError)? + Err(Error::ParsingError)? } else { Ok(()) } } DeviceAuth::Mac { .. } => { - Err(ReaderError::Unsupported) + Err(Error::Unsupported) // send not yet supported error } } diff --git a/src/presentation/device.rs b/src/presentation/device.rs index c0fd3c73..1ce2e158 100644 --- a/src/presentation/device.rs +++ b/src/presentation/device.rs @@ -35,27 +35,27 @@ use crate::{ self, derive_session_key, get_shared_secret, Handover, SessionData, SessionTranscript, }, x509::{ - error::Error as X509Error, trust_anchor::TrustAnchorRegistry, - x5chain::X5CHAIN_HEADER_LABEL, X5Chain, + self, trust_anchor::TrustAnchorRegistry, x5chain::X5CHAIN_COSE_HEADER_LABEL, X5Chain, }, CoseKey, DeviceEngagement, DeviceResponse, IssuerSignedItem, Mso, SessionEstablishment, }, issuance::Mdoc, }; -use ciborium::Value as CborValue; use coset::Label; use coset::{CoseMac0Builder, CoseSign1, CoseSign1Builder}; -use p256::FieldBytes; +use ecdsa::VerifyingKey; +use p256::{FieldBytes, NistP256}; use serde::{Deserialize, Serialize}; use serde_json::json; use session::SessionTranscript180135; use std::collections::BTreeMap; use std::num::ParseIntError; use uuid::Uuid; -use x509_cert::attr::AttributeTypeAndValue; -use x509_cert::der::Decode; -use super::authentication::{AuthenticationStatus, RequestAuthenticationOutcome}; +use super::{ + authentication::{AuthenticationStatus, RequestAuthenticationOutcome}, + reader::ReaderAuthentication, +}; /// Initialisation state. /// @@ -111,7 +111,7 @@ pub struct SessionManager { sk_reader: [u8; 32], reader_message_counter: u32, state: State, - trusted_verifiers: Option, + trusted_verifiers: TrustAnchorRegistry, device_auth_type: DeviceAuthType, } @@ -205,6 +205,12 @@ pub struct PreparedDocument { pub errors: Option, } +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct ReaderAuthOutcome { + pub common_name: Option, + pub errors: Vec, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum PreparedCose { Sign1(PreparedCoseSign1), @@ -297,7 +303,7 @@ impl SessionManagerEngaged { pub fn process_session_establishment( self, session_establishment: SessionEstablishment, - trusted_verifiers: Option, + trusted_verifiers: TrustAnchorRegistry, ) -> anyhow::Result<(SessionManager, RequestAuthenticationOutcome)> { let e_reader_key = session_establishment.e_reader_key; let session_transcript = @@ -337,13 +343,13 @@ impl SessionManagerEngaged { impl SessionManager { fn parse_request(&self, request: &[u8]) -> Result { - let request: ciborium::Value = cbor::from_slice(request).map_err(|_| { - // tracing::error!("unable to decode DeviceRequest bytes as cbor: {}", error); + let request: ciborium::Value = cbor::from_slice(request).map_err(|error| { + tracing::error!("unable to decode DeviceRequest bytes as cbor: {}", error); PreparedDeviceResponse::empty(Status::CborDecodingError) })?; - cbor::from_value(request).map_err(|_| { - // tracing::error!("unable to validate DeviceRequest cbor: {}", error); + cbor::from_value(request).map_err(|error| { + tracing::error!("unable to validate DeviceRequest cbor: {}", error); PreparedDeviceResponse::empty(Status::CborValidationError) }) } @@ -365,23 +371,26 @@ impl SessionManager { }; if request.version != DeviceRequest::VERSION { - // tracing::error!( - // "unsupported DeviceRequest version: {} ({} is supported)", - // request.version, - // DeviceRequest::VERSION - // ); + tracing::error!( + "unsupported DeviceRequest version: {} ({} is supported)", + request.version, + DeviceRequest::VERSION + ); validated_request.errors.insert( "parsing_errors".to_string(), json!(vec!["unsupported DeviceRequest version".to_string()]), ); } if let Some(doc_request) = request.doc_requests.first() { - let (validation_errors, common_name) = self.reader_authentication(doc_request.clone()); - if validation_errors.is_empty() { + let outcome = self.reader_authentication(doc_request.clone()); + if outcome.errors.is_empty() { validated_request.reader_authentication = AuthenticationStatus::Valid; + } else { + validated_request.reader_authentication = AuthenticationStatus::Invalid; + tracing::error!("Reader authentication errors: {:#?}", outcome.errors); } - validated_request.common_name = common_name; + validated_request.common_name = outcome.common_name; } validated_request @@ -570,79 +579,95 @@ impl SessionManager { } } - pub fn reader_authentication( - &self, - doc_request: DocRequest, - ) -> (Vec, Option) { - //TODO validate the reader authentication. This code only grabs the CN from the x5chain - let mut validation_errors: Vec = vec![]; - if let Some(reader_auth) = doc_request.reader_auth { - if let Some(x5chain_cbor) = reader_auth - .unprotected - .rest - .iter() - .find(|(label, _)| label == &Label::Int(X5CHAIN_HEADER_LABEL)) - .map(|(_, value)| value) - { - let x5c = x5chain_cbor; - - let x5chain = - X5Chain::from_cbor(x5chain_cbor.clone()).map_err(|_| Error::CertificateError); - match x5chain { - Ok(x5c) => { - if let Some(trusted_verifiers) = &self.trusted_verifiers { - validation_errors.append(&mut x5c.validate(Some(trusted_verifiers))); - } - } - Err(e) => { - validation_errors.push(X509Error::ValidationError(e.to_string())); - } - } + pub fn reader_authentication(&self, doc_request: DocRequest) -> ReaderAuthOutcome { + let mut outcome = ReaderAuthOutcome::default(); - match x5c { - CborValue::Bytes(x509) => { - match x509_cert::Certificate::from_der(x509) { - Ok(cert) => { - let distinguished_names: Vec = cert - .tbs_certificate - .subject - .0 - .into_iter() - .map(|rdn| { - rdn.0 - .into_vec() - .into_iter() - .filter(|atv| { - //common name - atv.oid.to_string() == *"2.5.4.3" - }) - .collect::>() - }) - .collect::>>() - .into_iter() - .flatten() - .collect(); + let Some(reader_auth) = doc_request.reader_auth else { + outcome + .errors + .push("Processing: request does not contain reader auth".into()); + return outcome; + }; - if let Some(common_name) = distinguished_names.first() { - (validation_errors, Some(common_name.to_string())) - } else { - (validation_errors, None) - } - } - Err(e) => { - validation_errors.push(X509Error::ValidationError(e.to_string())); - (validation_errors, None) - } - } - } - _ => (validation_errors, None), - } - } else { - (validation_errors, None) + let Some(x5chain_cbor) = reader_auth + .unprotected + .rest + .iter() + .find(|(label, _)| label == &Label::Int(X5CHAIN_COSE_HEADER_LABEL)) + .map(|(_, value)| value) + else { + outcome + .errors + .push("Processing: reader auth does not contain x5chain".into()); + return outcome; + }; + + let x5chain = match X5Chain::from_cbor(x5chain_cbor.clone()) { + Ok(x5c) => x5c, + Err(e) => { + outcome + .errors + .push(format!("Processing: x5chain cannot be decoded: {e}")); + return outcome; } - } else { - (validation_errors, None) + }; + + outcome.common_name = Some(x5chain.end_entity_common_name().to_string()); + + let x5chain_validation_outcome = x509::validation::ValidationRuleset::MdlReaderOneStep + .validate(&x5chain, &self.trusted_verifiers); + + outcome.errors.extend(x5chain_validation_outcome.errors); + + // TODO: Support more than P-256. + let verifier: VerifyingKey = match x5chain.end_entity_public_key() { + Ok(verifier) => verifier, + Err(e) => { + outcome.errors.push(format!( + "Processing: reader public key cannot be decoded: {e}" + )); + return outcome; + } + }; + + let detached_payload = match Tag24::new(ReaderAuthentication( + "ReaderAuthentication".into(), + self.session_transcript.clone(), + doc_request.items_request, + )) { + Ok(tagged) => tagged, + Err(e) => { + outcome.errors.push(format!( + "Processing: failed to construct reader auth payload: {e}" + )); + return outcome; + } + }; + + let detached_payload = match cbor::to_vec(&detached_payload) { + Ok(bytes) => bytes, + Err(e) => { + outcome.errors.push(format!( + "Processing: failed to encode reader auth payload: {e}" + )); + return outcome; + } + }; + + let verification_outcome = reader_auth + .verify::, p256::ecdsa::Signature>( + &verifier, + Some(&detached_payload), + None, + ); + + if let Err(e) = verification_outcome.into_result() { + outcome.errors.push(format!( + "Verification: failed to verify reader auth signature: {e}" + )) } + + outcome } } diff --git a/src/presentation/reader.rs b/src/presentation/reader.rs index 26d07fc5..41bbdf0d 100644 --- a/src/presentation/reader.rs +++ b/src/presentation/reader.rs @@ -28,6 +28,7 @@ use super::authentication::{ AuthenticationStatus, ResponseAuthenticationOutcome, }; +use crate::definitions::x509; use crate::{ cbor::{self, CborError}, definitions::{ @@ -40,7 +41,7 @@ use crate::{ self, create_p256_ephemeral_keys, derive_session_key, get_shared_secret, Handover, SessionEstablishment, }, - x509::{trust_anchor::TrustAnchorRegistry, x5chain::X5CHAIN_HEADER_LABEL, X5Chain}, + x509::{trust_anchor::TrustAnchorRegistry, x5chain::X5CHAIN_COSE_HEADER_LABEL, X5Chain}, DeviceEngagement, DeviceResponse, SessionData, SessionTranscript180135, }, presentation::reader::{device_request::ItemsRequestBytes, Error as ReaderError}, @@ -59,7 +60,7 @@ pub struct SessionManager { device_message_counter: u32, sk_reader: [u8; 32], reader_message_counter: u32, - trust_anchor_registry: Option, + trust_anchor_registry: TrustAnchorRegistry, } #[derive(Serialize, Deserialize)] @@ -114,9 +115,13 @@ pub enum Error { #[error("Currently unsupported format")] Unsupported, #[error("No x5chain found for issuer authentication")] - X5Chain, + X5ChainMissing, + #[error("Failed to parse x5chain: {0}")] + X5ChainParsing(anyhow::Error), #[error("issuer authentication failed: {0}")] IssuerAuthentication(String), + #[error("Unable to parse issuer public key")] + IssuerPublicKey(anyhow::Error), } impl From for Error { @@ -125,12 +130,6 @@ impl From for Error { } } -impl From for Error { - fn from(value: crate::definitions::x509::error::Error) -> Self { - Error::MdocAuth(value.to_string()) - } -} - impl From for Error { fn from(_: serde_json::Error) -> Self { Error::JsonError @@ -182,7 +181,7 @@ impl SessionManager { pub fn establish_session( qr_code: String, namespaces: device_request::Namespaces, - trust_anchor_registry: Option, + trust_anchor_registry: TrustAnchorRegistry, ) -> Result<(Self, Vec, [u8; 16])> { let device_engagement_bytes = Tag24::::from_qr_code_uri(&qr_code) .context("failed to construct QR code")?; @@ -372,7 +371,9 @@ impl SessionManager { } } - let validation_errors = x5chain.validate(self.trust_anchor_registry.as_ref()); + let validation_errors = x509::validation::ValidationRuleset::Mdl + .validate(&x5chain, &self.trust_anchor_registry) + .errors; if validation_errors.is_empty() { match issuer_authentication(x5chain, &document.issuer_signed) { Ok(_) => { @@ -405,10 +406,11 @@ fn parse( let x5chain = header .rest .iter() - .find(|(label, _)| label == &Label::Int(X5CHAIN_HEADER_LABEL)) + .find(|(label, _)| label == &Label::Int(X5CHAIN_COSE_HEADER_LABEL)) .map(|(_, value)| value.to_owned()) .map(X5Chain::from_cbor) - .ok_or(Error::X5Chain)??; + .ok_or(Error::X5ChainMissing)? + .map_err(Error::X5ChainParsing)?; let parsed_response = parse_namespaces(device_response)?; Ok((document, x5chain, parsed_response)) } @@ -460,6 +462,8 @@ fn get_document(device_response: &DeviceResponse) -> Result<&Document, Error> { } fn _validate_request(namespaces: device_request::Namespaces) -> Result { + // TODO: Check country name of certificate matches mdl + // Check if request follows ISO18013-5 restrictions // A valid mdoc request can contain a maximum of 2 age_over_NN fields let age_over_nn_requested: Vec<(String, bool)> = namespaces diff --git a/tests/common.rs b/tests/common.rs index 8648320e..d9f49c95 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -55,10 +55,10 @@ impl Device { DataElements::new(AGE_OVER_21_ELEMENT.to_string(), false), ); - let trust_anchor = None; + let trust_anchors = TrustAnchorRegistry::default(); let (reader_sm, session_request, _ble_ident) = - reader::SessionManager::establish_session(qr, requested_elements, trust_anchor) + reader::SessionManager::establish_session(qr, requested_elements, trust_anchors) .context("failed to establish reader session")?; Ok((reader_sm, session_request)) } @@ -67,7 +67,7 @@ impl Device { pub fn handle_request( state: SessionManagerEngaged, request: Vec, - trusted_verifiers: Option, + trusted_verifiers: TrustAnchorRegistry, ) -> Result<(device::SessionManager, RequestAuthenticationOutcome)> { let (session_manager, validated_request) = { let session_establishment: definitions::SessionEstablishment = diff --git a/tests/simulated_device_and_reader.rs b/tests/simulated_device_and_reader.rs index 790567c1..b6b99fa9 100644 --- a/tests/simulated_device_and_reader.rs +++ b/tests/simulated_device_and_reader.rs @@ -18,7 +18,7 @@ pub fn simulated_device_and_reader_interaction() { // Device accepting request let (device_session_manager, validated_request) = - Device::handle_request(engaged_state, request, None).unwrap(); + Device::handle_request(engaged_state, request, Default::default()).unwrap(); // Prepare response with required elements let response = Device::create_response( diff --git a/tests/simulated_device_and_reader_state.rs b/tests/simulated_device_and_reader_state.rs index b1e8119a..56c44817 100644 --- a/tests/simulated_device_and_reader_state.rs +++ b/tests/simulated_device_and_reader_state.rs @@ -4,6 +4,7 @@ use anyhow::{Context, Result}; use isomdl::cbor; use isomdl::definitions::device_engagement::{CentralClientMode, DeviceRetrievalMethods}; use isomdl::definitions::device_request::{DataElements, Namespaces}; +use isomdl::definitions::x509::trust_anchor::TrustAnchorRegistry; use isomdl::definitions::{self, BleOptions, DeviceRetrievalMethod}; use isomdl::presentation::device::{Documents, RequestedItems}; use isomdl::presentation::{device, reader}; @@ -96,7 +97,7 @@ fn establish_reader_session(qr: String) -> Result<(reader::SessionManager, Vec,; + let trust_anchor_registry = TrustAnchorRegistry::default(); let (reader_sm, session_request, _ble_ident) = reader::SessionManager::establish_session(qr, requested_elements, trust_anchor_registry) @@ -117,7 +118,7 @@ fn handle_request( state .0 .clone() - .process_session_establishment(session_establishment, None) + .process_session_establishment(session_establishment, Default::default()) .context("could not process process session establishment")? }; let session_manager = Arc::new(SessionManager { From b316f323e5af55a6981bbd9bad08cdca74c3f824 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 18 Dec 2024 12:08:49 +0000 Subject: [PATCH 11/12] Lint --- .../x509/validation/extensions/subject_key_identifier.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/definitions/x509/validation/extensions/subject_key_identifier.rs b/src/definitions/x509/validation/extensions/subject_key_identifier.rs index 840ae9db..ba4d8934 100644 --- a/src/definitions/x509/validation/extensions/subject_key_identifier.rs +++ b/src/definitions/x509/validation/extensions/subject_key_identifier.rs @@ -93,8 +93,7 @@ fn test( .extensions .iter() .flatten() - .filter(|ext| ext.extn_id == skiv.oid()) - .next() + .find(|ext| ext.extn_id == skiv.oid()) .unwrap(), ); assert_eq!(outcome.is_empty(), valid) From 5bd86c99659783b26a214d8ea649e38b30c8cf90 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 18 Dec 2024 15:12:22 +0000 Subject: [PATCH 12/12] Combine ExtensionValidator impl blocks --- src/definitions/x509/validation/extensions/mod.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/definitions/x509/validation/extensions/mod.rs b/src/definitions/x509/validation/extensions/mod.rs index 98827f00..e88b5947 100644 --- a/src/definitions/x509/validation/extensions/mod.rs +++ b/src/definitions/x509/validation/extensions/mod.rs @@ -139,13 +139,6 @@ pub fn validate_mdoc_reader_certificate_extensions(certificate: &Certificate) -> #[derive(Default)] struct ExtensionValidators(Vec>); -impl ExtensionValidators { - fn with(mut self, validator: V) -> Self { - self.0.push(Box::new(validator)); - self - } -} - struct RequiredExtension { found: bool, validator: Box, @@ -175,6 +168,11 @@ trait ExtensionValidator { } impl ExtensionValidators { + fn with(mut self, validator: V) -> Self { + self.0.push(Box::new(validator)); + self + } + fn validate_extensions<'a, Extensions>(self, extensions: Extensions) -> Vec where Extensions: IntoIterator,