diff --git a/Cargo.toml b/Cargo.toml index 0ffaa348..0e7f0774 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,13 @@ documentation = "https://docs.rs/isomdl" license = "Apache-2.0 OR MIT" 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"] } @@ -36,13 +40,16 @@ 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.4", features = ["pem", "builder", "std"] } ssi-jwk = "0.2.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"] } +reqwest = "0.11.22" strum = "0.24" strum_macros = "0.24" 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..eb767499 --- /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: ciborium::Value = ciborium::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 9edf7e0c..cdf0c24e 100644 --- a/src/definitions/device_engagement.rs +++ b/src/definitions/device_engagement.rs @@ -1,6 +1,6 @@ //! This module contains the definitions for the [DeviceEngagement] struct and related types. //! -//! The [DeviceEngagement] struct represents a device engagement object, which contains information about a device's engagement with a server. +//! The [DeviceEngagement] struct represents a device engagement object, which contains information about a device's engagement with a server. //! It includes fields such as the `version`, `security details, `device retrieval methods, `server retrieval methods, and `protocol information. //! //! The module also provides implementations for conversions between [DeviceEngagement] and [ciborium::Value], as well as other utility functions. @@ -76,7 +76,7 @@ pub enum DeviceRetrievalMethod { /// Represents the BLE options for device engagement. /// - /// This struct is used to configure the BLE options for device engagement. + /// This struct is used to configure the BLE options for device engagement. /// It contains the necessary parameters and settings for BLE communication. BLE(BleOptions), diff --git a/src/definitions/device_engagement/error.rs b/src/definitions/device_engagement/error.rs index 365cc731..65be542c 100644 --- a/src/definitions/device_engagement/error.rs +++ b/src/definitions/device_engagement/error.rs @@ -5,7 +5,7 @@ use crate::definitions::helpers::tag24::Error as Tag24Error; /// 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/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/mod.rs b/src/definitions/mod.rs index cf8954c2..6c2a4c36 100644 --- a/src/definitions/mod.rs +++ b/src/definitions/mod.rs @@ -10,7 +10,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, @@ -23,4 +26,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/namespaces/latin1.rs b/src/definitions/namespaces/latin1.rs index d7fb62fa..c94b8d21 100644 --- a/src/definitions/namespaces/latin1.rs +++ b/src/definitions/namespaces/latin1.rs @@ -95,12 +95,12 @@ mod test { fn upper_latin() { #[allow(clippy::invisible_characters)] let upper_latin_chars = vec![ - ' ', '¡', '¢', '£', '¤', '¥', '¦', '§', '¨', '©', 'ª', '«', '¬', '­', '®', '¯', '°', - '±', '²', '³', '´', 'µ', '¶', '·', '¸', '¹', 'º', '»', '¼', '½', '¾', '¿', 'À', 'Á', - 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ð', 'Ñ', 'Ò', - 'Ó', 'Ô', 'Õ', 'Ö', '×', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'Þ', 'ß', 'à', 'á', 'â', 'ã', - 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ð', 'ñ', 'ò', 'ó', 'ô', - 'õ', 'ö', '÷', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ', 'ÿ', + ' ', '¡', '¢', '£', '¤', '¥', '¦', '§', '¨', '©', 'ª', '«', '¬', '\u{AD}', '®', '¯', + '°', '±', '²', '³', '´', 'µ', '¶', '·', '¸', '¹', 'º', '»', '¼', '½', '¾', '¿', 'À', + 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ð', 'Ñ', + 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', '×', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'Þ', 'ß', 'à', 'á', 'â', + 'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ð', 'ñ', 'ò', 'ó', + 'ô', 'õ', 'ö', '÷', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ', 'ÿ', ]; assert!(upper_latin_chars.iter().all(is_upper_latin)); } diff --git a/src/definitions/validated_request.rs b/src/definitions/validated_request.rs new file mode 100644 index 00000000..529bae7e --- /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_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 new file mode 100644 index 00000000..c21f7695 --- /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(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/definitions/x509/crl.rs b/src/definitions/x509/crl.rs new file mode 100644 index 00000000..94606d7b --- /dev/null +++ b/src/definitions/x509/crl.rs @@ -0,0 +1,309 @@ +use asn1_rs::{FromDer, Sequence}; +use const_oid::{AssociatedOid, ObjectIdentifier}; +use der::{Any, Decode, SliceReader}; +use signature::Verifier; +use thiserror::Error; +use x509_cert::{ + crl::{CertificateList, RevokedCert, TbsCertList}, + ext::pkix::{ + name::{DistributionPointName, GeneralName}, + CrlDistributionPoints, CrlReason, + }, + spki::AlgorithmIdentifierOwned, + TbsCertificate, +}; + +/// All CRL parsing and revocation errors +#[derive(Error, Debug)] +pub enum Error { + #[error("Cert was revoked: Reason:{0:?}")] + CertRevoked(Option), + + #[error("Unable to parse CRL component: {0} {1}")] + ParsingCrlComponent(&'static str, der::Error), + #[error("Distrbution point malformed: {0}")] + DistributionPointMalformed(&'static str), + #[error("Unable to fetch CRL: {0}")] + FetchingCrl(#[from] reqwest::Error), + #[error("Unable to reach distribution point")] + ReachingDistributionPoint, + #[error("Issuer mismatch between cert and CRL")] + IssuerMismatchBetweenCertAndCrl, + #[error("Unknown signature algorithm: {0:?}")] + UnknownSignatureAlgorithm(Box), + #[error("Signature type mismatch: {0:?} {0:?}")] + SignatureTypeMismatch(Box, Box), + #[error("Unable to extract CRL sequence signature")] + ExtractCrlSequenceSignature(asn1_rs::Err), + #[error("Missing curve")] + MissingCurve, + #[error("Unknown curve extension: {0}")] + UnknownCurveExt(ObjectIdentifier), + #[error("Missing public key")] + MissingPublicKey, + #[error("Signature check of CRL failed")] + SignatureCheckOfCrlFailed, + #[error("Invalid public key format")] + InvalidPublicKeyFormat, + #[error("Missing signature")] + MissingSignature, + #[error("Signature in wrong format")] + SignatureInWrongFormat, +} + +pub const OID_EC_CURVE_P256: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7"); +pub const OID_EC_CURVE_P384: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.132.0.34"); +pub const OID_PUBLIC_KEY_ELLIPTIC_CURVE: ObjectIdentifier = + ObjectIdentifier::new_unwrap("1.2.840.10045.2.1"); + +const OID_EXTENSION_REASON_CODE: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.21"); + +/// Given a cert, download and verify the associated crl listed in the cert, and verify the crl +/// against the cert's metadata and public key. +pub async fn fetch_and_validate_crl( + crl_signing_cert: &TbsCertificate, +) -> Result, Error> { + let distribution_points = match read_distribution_points(crl_signing_cert)? { + None => return Ok(vec![]), + Some(distribution_points) => distribution_points, + }; + + let mut cert_lists = vec![]; + + for distribution_point in distribution_points.0.iter() { + let distribution_point_name = distribution_point.distribution_point.as_ref().ok_or( + Error::DistributionPointMalformed("missing distributionPoint name"), + )?; + + let urls = distribution_point_urls(distribution_point_name)?; + + for url in urls.iter() { + let crl_bytes = fetch_crl(url).await?; + + cert_lists.push(validate_crl(crl_signing_cert, &crl_bytes)?); + } + } + + Ok(cert_lists) +} + +fn distribution_point_urls(name: &DistributionPointName) -> Result, Error> { + match name { + DistributionPointName::FullName(uris) => { + let uris: Result, Error> = uris + .iter() + .map(|general_name| match general_name { + GeneralName::UniformResourceIdentifier(s) => Ok(s.to_string()), + _ => Err(Error::DistributionPointMalformed( + "distribution point name something other than URI", + )), + }) + .collect(); + + uris + } + DistributionPointName::NameRelativeToCRLIssuer(_) => Err( + Error::DistributionPointMalformed("contained relative to issuer name"), + ), + } +} + +async fn fetch_crl(url: &str) -> Result, Error> { + let bytes = reqwest::get(url) + .await + .map_err(Error::FetchingCrl)? + .bytes() + .await?; + + Ok(bytes.to_vec()) +} + +fn read_distribution_points(cert: &TbsCertificate) -> Result, Error> { + let extensions = match cert.extensions.as_ref() { + None => return Ok(None), + Some(extensions) => extensions, + }; + + let crl_extension = match extensions + .iter() + .find(|ext| ext.extn_id == CrlDistributionPoints::OID) + { + None => return Ok(None), + Some(crl_extension) => crl_extension, + }; + + let mut der_reader = SliceReader::new(crl_extension.extn_value.as_bytes()) + .map_err(|err| Error::ParsingCrlComponent("extension", err))?; + + let distribution_points = CrlDistributionPoints::decode(&mut der_reader) + .map_err(|err| Error::ParsingCrlComponent("extension", err))?; + + Ok(Some(distribution_points)) +} + +fn validate_crl(cert: &TbsCertificate, crl_bytes: &[u8]) -> Result { + let (crl_raw, crl) = decode_cert_list(crl_bytes)?; + + if cert.issuer != crl.tbs_cert_list.issuer { + return Err(Error::IssuerMismatchBetweenCertAndCrl); + } + + if cert.subject_public_key_info.algorithm != crl.signature_algorithm { + return Err(Error::SignatureTypeMismatch( + Box::new(cert.subject_public_key_info.algorithm.clone()), + Box::new(crl.signature_algorithm), + )); + } + + match crl.signature_algorithm.oid { + OID_PUBLIC_KEY_ELLIPTIC_CURVE => { + let curve = + CurveKind::try_from(cert.subject_public_key_info.algorithm.parameters.as_ref())?; + + let public_key = cert + .subject_public_key_info + .subject_public_key + .as_bytes() + .ok_or(Error::MissingPublicKey)?; + + let mut sec1_public_key = vec![0x01]; + sec1_public_key.extend_from_slice(public_key); + + let signature = crl.signature.as_bytes().ok_or(Error::MissingSignature)?; + + match curve { + CurveKind::P256 => { + let key = p256::ecdsa::VerifyingKey::from_sec1_bytes(&sec1_public_key) + .map_err(|_| Error::InvalidPublicKeyFormat)?; + + let signature = p256::ecdsa::Signature::from_slice(signature) + .map_err(|_| Error::SignatureInWrongFormat)?; + + key.verify(&crl_raw, &signature) + .map_err(|_| Error::SignatureCheckOfCrlFailed)?; + + Ok(crl.tbs_cert_list) + } + CurveKind::P384 => { + let key = p384::ecdsa::VerifyingKey::from_sec1_bytes(&sec1_public_key) + .map_err(|_| Error::InvalidPublicKeyFormat)?; + + let signature = p384::ecdsa::Signature::from_slice(signature) + .map_err(|_| Error::SignatureInWrongFormat)?; + + key.verify(&crl_raw, &signature) + .map_err(|_| Error::SignatureCheckOfCrlFailed)?; + + Ok(crl.tbs_cert_list) + } + } + } + _ => Err(Error::UnknownSignatureAlgorithm(Box::new( + crl.signature_algorithm, + ))), + } +} + +fn decode_cert_list(bytes: &[u8]) -> Result<(Vec, CertificateList), Error> { + let (_, top_sequence) = + Sequence::from_der(bytes).map_err(Error::ExtractCrlSequenceSignature)?; + + let top_sequence_content = top_sequence.into_content(); + let top_sequence_bytes = top_sequence_content.as_ref(); + + let (_, cert_list_sequence) = + Sequence::from_der(top_sequence_bytes).map_err(Error::ExtractCrlSequenceSignature)?; + + let cert_list_content = cert_list_sequence.into_content(); + let cert_list_bytes = cert_list_content.as_ref(); + + let cert_list = CertificateList::from_der(bytes) + .map_err(|err| Error::ParsingCrlComponent("cert list", err))?; + + Ok((cert_list_bytes.to_vec(), cert_list)) +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CurveKind { + P256, + P384, +} + +impl TryFrom> for CurveKind { + type Error = Error; + + fn try_from(ext_bit_string: Option<&Any>) -> Result { + let ext_any = ext_bit_string.ok_or(Error::MissingCurve)?; + let obj_id = ObjectIdentifier::from_der(ext_any.value()) + .map_err(|err| Error::ParsingCrlComponent("curve kind extension", err))?; + + let curve = match obj_id { + OID_EC_CURVE_P256 => Self::P256, + OID_EC_CURVE_P384 => Self::P384, + other => return Err(Error::UnknownCurveExt(other)), + }; + + Ok(curve) + } +} + +/// Given revocation `cert_lists`, check if `cert` has been revoked +pub fn check_cert_against_cert_lists( + cert: &TbsCertificate, + cert_lists: &[TbsCertList], +) -> Result<(), Error> { + for cert_list in cert_lists { + if cert.issuer != cert_list.issuer { + return Err(Error::IssuerMismatchBetweenCertAndCrl); + } + + let revoked_certs = match cert_list.revoked_certificates.as_ref() { + Some(revoked) => revoked, + None => return Ok(()), + }; + + for revoked_cert in revoked_certs { + if revoked_cert.serial_number == cert.serial_number { + return Err(Error::CertRevoked(find_reason_code(revoked_cert))); + } + } + } + + Ok(()) +} + +fn find_reason_code(revoked_cert: &RevokedCert) -> Option { + if let Some(exts) = revoked_cert.crl_entry_extensions.as_ref() { + if let Some(reason_code_ext) = exts + .iter() + .find(|ext| ext.extn_id == OID_EXTENSION_REASON_CODE) + { + return CrlReason::from_der(reason_code_ext.extn_value.as_bytes()).ok(); + } + } + + None +} + +#[cfg(test)] +mod tests { + use super::*; + + const TEST_CRL_DER: &[u8] = include_bytes!("./crl/testcrl.der"); + + #[test] + fn parse_crl_signature_components() { + let (raw, cert_list) = decode_cert_list(TEST_CRL_DER).unwrap(); + + assert_eq!(3219, raw.len()); + assert_eq!( + 88, + cert_list + .tbs_cert_list + .revoked_certificates + .as_ref() + .unwrap() + .len() + ); + } +} diff --git a/src/definitions/x509/crl/testcrl.der b/src/definitions/x509/crl/testcrl.der new file mode 100644 index 00000000..0a5c0d18 Binary files /dev/null and b/src/definitions/x509/crl/testcrl.der differ diff --git a/src/definitions/x509/error.rs b/src/definitions/x509/error.rs new file mode 100644 index 00000000..f84dfcc3 --- /dev/null +++ b/src/definitions/x509/error.rs @@ -0,0 +1,59 @@ +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, + #[error("Custom Trust Anchor Not Implemented: {0}")] + CustomTrustAnchorNotImplemented(String), +} + +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..1325cb0a --- /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(), + )); + } 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: &[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())] + } + } +} + +/* 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..ac81c1f8 --- /dev/null +++ b/src/definitions/x509/mod.rs @@ -0,0 +1,7 @@ +pub mod crl; +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..25279425 --- /dev/null +++ b/src/definitions/x509/trust_anchor.rs @@ -0,0 +1,377 @@ +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) => { + errors.push(X509Error::CustomTrustAnchorNotImplemented( + "Custom trust anchor validation not yet implemented".to_string(), + )); + } + } + 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 => 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..ee836620 --- /dev/null +++ b/src/definitions/x509/x5chain.rs @@ -0,0 +1,305 @@ +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 ciborium::Value as CborValue; +use elliptic_curve::{ + sec1::{FromEncodedPoint, ModulusSize, ToEncodedPoint}, + 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::{ + certificate::Certificate, + der::{referenced::OwnedToRef, Decode}, +}; + +/// 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)] +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 737191af..7f09b6de 100644 --- a/src/issuance/mdoc.rs +++ b/src/issuance/mdoc.rs @@ -12,12 +12,12 @@ use signature::{SignatureEncoding, Signer}; use crate::cose::sign1::PreparedCoseSign1; use crate::cose::{MaybeTagged, SignatureAlgorithm}; 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}, }; pub type Namespaces = BTreeMap>; @@ -196,7 +196,7 @@ impl PreparedMdoc { .inner .unprotected .rest - .push((Label::Int(X5CHAIN_HEADER_LABEL as i64), x5chain.into_cbor())); + .push((Label::Int(X5CHAIN_HEADER_LABEL), x5chain.into_cbor())); Mdoc { doc_type, mso, diff --git a/src/issuance/mod.rs b/src/issuance/mod.rs index 263b8012..5f25e8d2 100644 --- a/src/issuance/mod.rs +++ b/src/issuance/mod.rs @@ -2,7 +2,5 @@ //! //! The `issuance` module provides functionality for handling issuance related operations. pub mod mdoc; -pub mod x5chain; pub use mdoc::{Mdoc, Namespaces}; -pub use x5chain::{Builder, 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 4c5964cc..e97c5576 100644 --- a/src/presentation/device.rs +++ b/src/presentation/device.rs @@ -16,11 +16,16 @@ //! //! 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::cbor::CborError; 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, @@ -41,13 +46,18 @@ use crate::{ }, issuance::Mdoc, }; +use ciborium::Value as CborValue; +use coset::Label; use coset::{CoseMac0Builder, CoseSign1, CoseSign1Builder}; use p256::FieldBytes; 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; /// Initialisation state. /// @@ -70,7 +80,7 @@ pub struct SessionManagerInit { /// Engaged state. /// -/// Transition to this state is made with [SessionManagerInit::qr_engagement]. +/// Transition to this state is made with [SessionManagerInit::qr_engagement]. /// That creates the `QR code` that the reader will use to establish the session. #[derive(Clone, Serialize, Deserialize)] pub struct SessionManagerEngaged { @@ -103,6 +113,7 @@ pub struct SessionManager { sk_reader: [u8; 32], reader_message_counter: u32, state: State, + trusted_verifiers: Option, device_auth_type: DeviceAuthType, } @@ -142,10 +153,20 @@ pub enum Error { /// `age_over` element identifier is malformed. #[error("age_over element identifier is malformed")] PrefixError, + #[error("error decoding reader authentication certificate")] + CertificateError, + #[error("error while validating reader authentication certificate")] + ValidationError, #[error("Could not serialize to cbor: {0}")] CborError(coset::CoseError), } +impl From for Error { + fn from(_value: x509_cert::der::Error) -> Self { + Error::CertificateError + } +} + /// The documents the device owns. pub type Documents = NonEmptyMap; type DocType = String; @@ -170,24 +191,24 @@ pub struct Document { /// After those are signed, they are kept in a list of [DeviceResponseDoc] #[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: PreparedCose, - errors: Option, +pub struct PreparedDocument { + pub id: Uuid, + pub doc_type: String, + pub issuer_signed: IssuerSigned, + pub device_namespaces: DeviceNamespacesBytes, + pub prepared_cose: PreparedCose, + pub errors: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] -enum PreparedCose { +pub enum PreparedCose { Sign1(PreparedCoseSign1), Mac0(PreparedCoseMac0), } @@ -278,7 +299,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); @@ -302,15 +324,16 @@ impl SessionManagerEngaged { sk_reader, reader_message_counter: 0, state: State::AwaitingRequest, + trusted_verifiers, device_auth_type: DeviceAuthType::Sign1, }; - 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)) } } @@ -327,24 +350,43 @@ impl SessionManager { }) } - fn validate_request( - &self, - request: DeviceRequest, - ) -> Result, PreparedDeviceResponse> { + fn validate_request(&self, request: DeviceRequest) -> ValidatedRequest { + let items_request: Vec = request + .doc_requests + .clone() + .into_inner() + .into_iter() + .map(|DocRequest { items_request, .. }| items_request.into_inner()) + .collect(); + + let mut validated_request = ValidatedRequest { + items_request, + 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 } /// When the device is ready to respond, it prepares the response specifying the permitted items. @@ -367,42 +409,65 @@ 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 new request from the reader. + /// Handle a request from the reader. /// - /// The request is expected to be a [CBOR](https://cbor.io) - /// encoded [SessionData] and encrypted. - /// It will parse and validate it. + /// The request is expected to be a CBOR encoded + /// and encrypted [SessionData]. /// - /// It returns the requested items by the reader. - pub fn handle_request(&mut self, request: &[u8]) -> anyhow::Result { - let session_data: SessionData = cbor::from_slice(request).map_err(CborError::from)?; + /// 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(); + let session_data: SessionData = match 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) } @@ -506,6 +571,82 @@ 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 + .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.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 { @@ -850,7 +991,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)| { @@ -1063,9 +1204,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..c8941edb --- /dev/null +++ b/src/presentation/mdoc_auth.rs @@ -0,0 +1,95 @@ +use crate::cbor; +use crate::cose; +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::{ + 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 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 verification_result: cose::sign1::VerificationResult = + issuer_signed + .issuer_auth + .verify::(&signer_key, None, None); + if !verification_result.is_success() { + Err(ReaderError::ParsingError)? + } else { + Ok(()) + } +} + +pub fn device_authentication( + document: &Document, + session_transcript: SessionTranscript180135, +) -> Result<(), Error> { + let mso_bytes = document + .issuer_signed + .issuer_auth + .payload + .as_ref() + .ok_or(Error::DetachedIssuerAuth)?; + let mso: Tag24 = 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 { + 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.clone(), + namespaces_bytes.clone(), + )) + .map_err(|_| ReaderError::CborDecodingError)?; + let external_aad = None; + let cbor_payload = cbor::to_vec(&detached_payload)?; + let result = device_signature.verify::( + &verifying_key, + Some(&cbor_payload), + external_aad, + ); + if !result.is_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())), + } +} diff --git a/src/presentation/mod.rs b/src/presentation/mod.rs index 1ae372e1..a8295145 100644 --- a/src/presentation/mod.rs +++ b/src/presentation/mod.rs @@ -40,6 +40,7 @@ //! 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 device; +pub mod mdoc_auth; pub mod reader; use anyhow::Result; diff --git a/src/presentation/reader.rs b/src/presentation/reader.rs index b1c95cc2..f5a33089 100644 --- a/src/presentation/reader.rs +++ b/src/presentation/reader.rs @@ -13,19 +13,37 @@ //! //! 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_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 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 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, + SessionEstablishment, + }, }, - DeviceEngagement, DeviceResponse, SessionData, SessionTranscript180135, + definitions::{DeviceEngagement, DeviceResponse, SessionData, SessionTranscript180135}, }; +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; @@ -38,18 +56,32 @@ use uuid::Uuid; /// for handling the session with the device. /// /// The transition to this state is made by [SessionManager::establish_session]. -#[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, + trust_anchor_registry: Option, + reader_auth_key: [u8; 32], + reader_x5chain: X5Chain, } +#[derive(Serialize, Deserialize)] +pub struct ReaderAuthentication( + pub String, + pub SessionTranscript180135, + pub ItemsRequestBytes, +); + /// Various errors that can occur during the interaction with the device. #[derive(Debug, thiserror::Error)] pub enum Error { + #[error("Received IssuerAuth had a detached payload.")] + DetachedIssuerAuth, + #[error("Could not parse MSO.")] + MSOParsing, /// The QR code had the wrong prefix or the contained data could not be decoded. #[error("the qr code had the wrong prefix or the contained data could not be decoded: {0}")] InvalidQrCode(anyhow::Error), @@ -83,6 +115,12 @@ pub enum Error { /// Request for data is invalid. #[error("Request for data is invalid.")] InvalidRequest, + #[error("Failed mdoc authentication: {0}")] + MdocAuth(String), + #[error("Currently unsupported format")] + Unsupported, + #[error("No x5chain found for mdoc authentication")] + X5Chain, #[error("Could not serialize to cbor: {0}")] CborError(CborError), } @@ -93,12 +131,54 @@ 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 } } +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 { /// Establish a session with the device. /// @@ -108,9 +188,12 @@ impl SessionManager { pub fn establish_session( 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(Error::InvalidQrCode)?; + Tag24::::from_qr_code_uri(&qr_code).map_err(|e| anyhow!(e))?; //generate own keys let key_pair = create_p256_ephemeral_keys()?; @@ -143,12 +226,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, + trust_anchor_registry, + reader_auth_key: reader_auth_key.into(), + reader_x5chain, }; let request = session_manager.build_request(namespaces)?; @@ -201,8 +290,36 @@ 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 = 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: None, + 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, + }), items_request: Tag24::new(items_request)?, }; let device_request = DeviceRequest { @@ -218,16 +335,7 @@ impl SessionManager { .map_err(|e| anyhow!("unable to encrypt request: {}", e)) } - /// Handles a response from the device. - /// - /// The response is expected to be a [CBOR](https://cbor.io) - /// encoded [SessionData] and encrypted. - /// - /// Will return the elements and values grouped by namespace. - pub fn handle_response( - &mut self, - response: &[u8], - ) -> Result>, Error> { + fn decrypt_response(&mut self, response: &[u8]) -> Result { let session_data: SessionData = cbor::from_slice(response)?; let encrypted_response = match session_data.data { None => return Err(Error::HolderError), @@ -239,64 +347,108 @@ impl SessionManager { &mut self.device_message_counter, ) .map_err(|_e| Error::DecryptionError)?; - let response: DeviceResponse = 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 - .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(); - - // Check if at least one of the two namespaces exists - if !namespaces.contains_key("org.iso.18013.5.1") - && !namespaces.contains_key("org.iso.18013.5.1.aamva") - { - return Err(Error::IncorrectNamespace); - } + let device_response: DeviceResponse = cbor::from_slice(&decrypted_response)?; + Ok(device_response) + } + + pub fn handle_response(&mut self, response: &[u8]) -> ValidatedResponse { + let mut validated_response = ValidatedResponse::default(); + + let device_response = match self.decrypt_response(response) { + Ok(device_response) => { + validated_response.decryption = Status::Valid; + + device_response + } + Err(e) => { + validated_response.decryption = Status::Invalid; + validated_response.errors.insert( + "decryption_errors".to_string(), + json!(vec![format!("{e:?}")]), + ); + return validated_response; + } + }; - if let Some(core_response) = namespaces.remove("org.iso.18013.5.1") { - core_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 { - core_namespace.insert(item.element_identifier, val); - } - }); - - parsed_response.insert("org.iso.18013.5.1".to_string(), core_namespace); + match parse(&device_response) { + Ok((document, x5chain, namespaces)) => { + 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:?}")])); + validated_response + } } + } + + pub fn validate_response( + &mut self, + x5chain: X5Chain, + document: Document, + namespaces: BTreeMap, + ) -> ValidatedResponse { + let mut validated_response = ValidatedResponse { + response: namespaces, + ..Default::default() + }; - 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(), aamva_namespace); + 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.insert( + "device_authentication_errors".to_string(), + json!(vec![format!("{e:?}")]), + ); + } } - Ok(parsed_response) + 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; + } + Err(e) => { + validated_response.issuer_authentication = Status::Invalid; + validated_response.errors.insert( + "issuer_authentication_errors".to_string(), + serde_json::json!(vec![format!("{e:?}")]), + ); + } + } + } else { + validated_response + .errors + .insert("certificate_errors".to_string(), json!(validation_errors)); + validated_response.issuer_authentication = Status::Invalid + }; + + 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.clone(); + let x5chain = header + .rest + .iter() + .find(|(label, _)| label == &Label::Int(X5CHAIN_HEADER_LABEL)) + .map(|(_, value)| value.to_owned()) + .map(X5Chain::from_cbor) + .ok_or(Error::X5Chain)??; + let parsed_response = parse_namespaces(device_response)?; + Ok((document, x5chain, parsed_response)) +} + fn parse_response(value: ciborium::Value) -> Result { match value { ciborium::Value::Text(s) => Ok(Value::String(s)), @@ -333,6 +485,16 @@ fn parse_response(value: ciborium::Value) -> 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 @@ -353,9 +515,86 @@ fn _validate_request(namespaces: device_request::Namespaces) -> Result 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)] -mod 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() { @@ -381,4 +620,65 @@ 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 new file mode 100644 index 00000000..e69de29b 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 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----- diff --git a/tests/common.rs b/tests/common.rs index 6cd060af..3f40788e 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -4,6 +4,9 @@ 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}; @@ -51,9 +54,24 @@ impl Device { NAMESPACE.into(), DataElements::new(AGE_OVER_21_ELEMENT.to_string(), false), ); - let (reader_sm, session_request, _ble_ident) = - reader::SessionManager::establish_session(qr, requested_elements) - .context("failed to establish reader session")?; + + 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")?; Ok((reader_sm, session_request)) } @@ -61,24 +79,25 @@ impl Device { pub fn handle_request( state: SessionManagerEngaged, request: Vec, - ) -> Result<(device::SessionManager, RequestedItems)> { - let (session_manager, items_requests) = { + trusted_verifiers: Option, + ) -> Result<(device::SessionManager, ValidatedRequest)> { + let (session_manager, validated_request) = { let session_establishment: definitions::SessionEstablishment = cbor::from_slice(&request).context("could not deserialize request")?; state - .process_session_establishment(session_establishment) + .process_session_establishment(session_establishment, trusted_verifiers) .context("could not process process session establishment")? }; if session_manager.get_next_signature_payload().is_some() { anyhow::bail!("there were errors processing request"); } - Ok((session_manager, items_requests)) + Ok((session_manager, validated_request)) } /// Prepare response with required elements. pub fn create_response( mut session_manager: device::SessionManager, - requested_items: RequestedItems, + requested_items: &RequestedItems, key: &p256::ecdsa::SigningKey, ) -> Result> { let permitted_items = [( @@ -113,8 +132,8 @@ impl Reader { reader_sm: &mut reader::SessionManager, response: Vec, ) -> Result<()> { - let res = reader_sm.handle_response(&response)?; - println!("{:?}", res); + let validated = reader_sm.handle_response(&response); + println!("Validated Response: {validated:?}"); Ok(()) } } diff --git a/tests/simulated_device_and_reader.rs b/tests/simulated_device_and_reader.rs index b7c0ba0a..9682c1cb 100644 --- a/tests/simulated_device_and_reader.rs +++ b/tests/simulated_device_and_reader.rs @@ -1,9 +1,9 @@ mod common; -use anyhow::Result; - use crate::common::{Device, Reader}; +use anyhow::Result; + #[test] pub fn simulated_device_and_reader_interaction() -> Result<()> { let key: p256::ecdsa::SigningKey = @@ -16,10 +16,15 @@ pub fn simulated_device_and_reader_interaction() -> Result<()> { let (mut reader_session_manager, request) = Device::establish_reader_session(qr_code_uri)?; // Device accepting request - let (device_session_manager, requested_items) = Device::handle_request(engaged_state, request)?; + let (device_session_manager, validated_request) = + Device::handle_request(engaged_state, request, None)?; // Prepare response with required elements - let response = Device::create_response(device_session_manager, requested_items, &key)?; + let response = Device::create_response( + device_session_manager, + &validated_request.items_request, + &key, + )?; // Reader Processing mDL data Reader::reader_handle_device_response(&mut reader_session_manager, response)?; diff --git a/tests/simulated_device_and_reader_state.rs b/tests/simulated_device_and_reader_state.rs index 7b663460..674bf751 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::X5Chain; use isomdl::definitions::{self, BleOptions, DeviceRetrievalMethod}; use isomdl::presentation::device::{Documents, RequestedItems}; use isomdl::presentation::{device, reader}; @@ -25,7 +26,7 @@ struct RequestData { struct SessionManager { inner: Mutex, - items_requests: RequestedItems, + items_request: RequestedItems, key: Arc, } @@ -96,9 +97,20 @@ 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")?; Ok((reader_sm, session_request)) } @@ -109,24 +121,24 @@ fn handle_request( request: Vec, key: Arc, ) -> Result> { - let (session_manager, items_requests) = { + let (session_manager, validated_response) = { let session_establishment: definitions::SessionEstablishment = cbor::from_slice(&request).context("could not deserialize request")?; state .0 .clone() - .process_session_establishment(session_establishment) + .process_session_establishment(session_establishment, None) .context("could not process process session establishment")? }; let session_manager = Arc::new(SessionManager { inner: Mutex::new(session_manager), - items_requests: items_requests.clone(), + items_request: validated_response.items_request.clone(), key, }); // Propagate any errors back to the reader if let Ok(Some(response)) = get_errors(session_manager.clone()) { - let res = reader_session_manager.handle_response(&response)?; - println!("Reader: {res:?}"); + let validated_response = reader_session_manager.handle_response(&response); + println!("Reader: {validated_response:?}"); return Ok(None); }; @@ -147,7 +159,7 @@ fn create_response(session_manager: Arc) -> Result> { .inner .lock() .unwrap() - .prepare_response(&session_manager.items_requests, permitted_items); + .prepare_response(&session_manager.items_request, permitted_items); sign_pending_and_retrieve_response(session_manager.clone(), Some(1))? .ok_or_else(|| anyhow::anyhow!("cannot prepare response")) } @@ -185,7 +197,7 @@ fn reader_handle_device_response( reader_sm: &mut reader::SessionManager, response: Vec, ) -> Result<()> { - let res = reader_sm.handle_response(&response)?; - println!("{:?}", res); + let validated_response = reader_sm.handle_response(&response); + println!("Validated Response: {validated_response:?}"); Ok(()) }