From 2337834a2736fb873685cbb6e51031a6a998dcaa Mon Sep 17 00:00:00 2001 From: Arjen van Veen Date: Tue, 2 May 2023 10:26:51 +0200 Subject: [PATCH 01/23] OID4VP ID2 - WIP wip - add id2 mdoc request support id2 WIP fix mdl path matching oid4vp id2 WIP OID4VP ID2 use ssh for isomdl dep clean up update isomdl version --- .cargo/config.toml | 2 + Cargo.toml | 13 +- src/lib.rs | 5 +- src/mdl_request.rs | 293 +++++++++++++++++++++++++++++++++++ src/mdl_response.rs | 152 ++++++++++++++++++ src/presentation_exchange.rs | 84 +++++++--- src/presentment.rs | 27 ++++ src/utils.rs | 45 +++++- 8 files changed, 593 insertions(+), 28 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 src/mdl_request.rs create mode 100644 src/mdl_response.rs create mode 100644 src/presentment.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..c91c3f3 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[net] +git-fetch-with-cli = true diff --git a/Cargo.toml b/Cargo.toml index 57687ad..7d9b3b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,20 +9,25 @@ repository = "https://github.com/spruceid/oidc4vp-rs/" documentation = "https://docs.rs/oidc4vp/" [dependencies] -# openidconnect = { version = "2.4.0", default-features = false } jsonpath-rust = "0.2.0" serde = { version = "1.0.147", features = ["derive"] } serde_json = "1.0.87" ssi = { version = "0.6.0", default-features = false } thiserror = "1.0.37" -# jsonschema = { version = "0.16.1", default-features = false } lazy_static = "1.4.0" -# schemars = { version = "0.8.11", default-features = false } +url = "2.3.1" +rand = "0.8.5" +isomdl = { git = "ssh://git@github.com/spruceid/isomdl.git", rev = "8f87768"} +time = "0.3.21" +p256 = "0.13.2" +base64 = "0.13.1" +x509-cert = "0.2.3" +serde_cbor = "0.11.2" +anyhow = "1.0.71" [dev-dependencies] serde_path_to_error = "0.1.8" - [target.'cfg(target_arch = "wasm32")'.dependencies] uuid = { version = "1.2", features = ["v4", "serde", "js"] } diff --git a/src/lib.rs b/src/lib.rs index 7fa2eb1..26a3acd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,5 @@ +pub mod mdl_request; +pub mod mdl_response; pub mod presentation_exchange; -mod utils; +pub mod utils; +pub mod presentment; diff --git a/src/mdl_request.rs b/src/mdl_request.rs new file mode 100644 index 0000000..4503acc --- /dev/null +++ b/src/mdl_request.rs @@ -0,0 +1,293 @@ +use crate::utils::Error; +use crate::{ + presentation_exchange::{ + Constraints, ConstraintsField, InputDescriptor, + PresentationDefinition, + }, + utils::NonEmptyVec, +}; +use serde_json::{json, Value}; +use isomdl::definitions::helpers::NonEmptyMap; +use ssi::jwk::JWK; +use std::collections::BTreeMap; +use serde::{Deserialize, Serialize}; +use x509_cert::der::Decode; +use x509_cert::der::referenced::OwnedToRef; + +use crate::{ + mdl_request::{self}, +}; + +#[derive(Clone, Debug, Serialize, Deserialize,)] +pub struct RequestObject { + // pub iss: String, omitting iss is okay since the client_id is already in the request object + pub aud: String, + pub response_type: String, + pub client_id: String, + pub client_id_scheme: Option, + pub redirect_uri: Option, + pub scope: Option, + pub state: String, + pub presentation_definition: Option, + pub presentation_definition_uri: Option, + pub client_metadata: mdl_request::ClientMetadata, + pub client_metadata_uri: Option, + pub response_mode: Option, + pub nonce: Option, + pub supported_algorithm: String, +} + +#[derive(Clone, Debug, Serialize, Deserialize,)] +pub struct ClientMetadata { + pub authorization_encrypted_response_alg: String, + pub authorization_encrypted_response_enc: String, + pub jwks: Value, + pub vp_formats: String, + pub client_id_scheme: Option, +} + +pub fn prepare_mdl_request_object(jwk: JWK, requested_fields: NonEmptyMap< String, NonEmptyMap, Option>> , client_id: String, redirect_uri: String, presentation_id: String) -> Result{ + let presentation_definition = mdl_presentation_definition(requested_fields, presentation_id)?; + let client_metadata = ClientMetadata { + authorization_encrypted_response_alg: "ES256".to_string(), + authorization_encrypted_response_enc: "A128GCM".to_string(), + vp_formats: "mso_mdoc".to_string(), // TODO fix + client_id_scheme: Some("ISO_X509".to_string()), + jwks: json!(jwk), + }; + + Ok( RequestObject{ + aud: "https://self-issued.me/v2".to_string(), // per openid4vp chapter 5.6 + response_type: "vp_token".to_string(), + client_id: client_id.clone(), + client_id_scheme: Some("ISO_X509".to_string()), + redirect_uri: Some(redirect_uri), + scope: Some("openid".to_string()), // I think it could also be None + state:"".to_string(), + presentation_definition: Some(presentation_definition), + presentation_definition_uri: None, + client_metadata, + client_metadata_uri: None, + response_mode: Some("direct_post.jwt".to_string()), + nonce: Some(client_id), + supported_algorithm: "ES256".to_string() + }) +} + +fn mdl_presentation_definition( + namespaces: NonEmptyMap< String, NonEmptyMap, Option>>, + presentation_id: String +) -> Result { + let input_descriptors = build_input_descriptors(namespaces); + Ok(PresentationDefinition{ + id: presentation_id, + input_descriptors: input_descriptors, + name: None, + purpose: None, + format: None, + }) +} + +//TODO: allow for specifying the algorithm +fn build_input_descriptors(namespaces: NonEmptyMap< String, NonEmptyMap, Option>>) -> Vec{ + let path_base = "$.mdoc."; + + let doc_type_filter = json!({ + "type": "string", + "const": "org.iso.18013.5.1.mDL" + }); + + let input_descriptors: Vec = namespaces.iter().map(|namespace| { + let namespace_filter = json!({ + "type": "string", + "const": namespace.0 + }); + + let format = json!({ + "mso_mdoc": { + "alg": [ + "EdDSA", + "ES256" + //TODO add all supported algorithms + ] + }}); + let mut namespace_fields = BTreeMap::from(namespace.1.to_owned()); + namespace_fields.retain(|k, _v| k.is_some()); + + let mut fields: Vec = namespace_fields.iter().map(|field| { + ConstraintsField { + //safe unwrap since none values are removed above + path: NonEmptyVec::new(format!("{}{}", path_base, field.0.as_ref().unwrap().to_owned())), + id: None, + purpose:None, + name:None, + filter: None, + optional: None, + intent_to_retain: *field.1 + + } + }).collect(); + + fields.push(ConstraintsField { + path: NonEmptyVec::new(format!("{}{}", path_base, "doc_type")), + id: None, + purpose: None, + name: None, + filter: Some(doc_type_filter.clone()), + optional: None, + intent_to_retain: None, + }); + + fields.push(ConstraintsField { + path: NonEmptyVec::new(format!("{}{}", path_base, "namespace")), + id: None, + purpose: None, + name: None, + filter: Some(namespace_filter), + optional: None, + intent_to_retain: None, + }); + + let constraints = Constraints{ + fields: Some(fields), + limit_disclosure: None, + }; + + InputDescriptor{ + id: "mDL".to_string(), + name: None, + purpose: None, + format: Some(format), + constraints: Some(constraints), + schema: None } + }).collect(); + + input_descriptors + +} + +pub fn x509_public_key(der: Vec) -> Result { + x509_cert::Certificate::from_der(&der) + .map_err(|e| format!("could not parse certificate from DER: {e}"))? + .tbs_certificate + .subject_public_key_info + .owned_to_ref() + .try_into() + .map_err(|e| format!("could not parse p256 public key from pkcs8 spki: {e}")) +} + +fn _minimal_mdl_request_isomdl() -> BTreeMap { + BTreeMap::from([ + ("org.iso.18013.5.1.family_name".to_string(), false), + ("org.iso.18013.5.1.given_name".to_string(), false), + ("org.iso.18013.5.1.birth_date".to_string(), false), + ("org.iso.18013.5.1.issue_date".to_string(), false), + ("org.iso.18013.5.1.expiry_date".to_string(), false), + ("org.iso.18013.5.1.issuing_country".to_string(), false), + ("org.iso.18013.5.1.issuing_authority".to_string(), false), + ("org.iso.18013.5.1.document_number".to_string(), false), + ("org.iso.18013.5.1.portrait".to_string(), false), + ("org.iso.18013.5.1.driving_privileges".to_string(), false), + ("org.iso.18013.5.1.un_distinguishing_sign".to_string(), false), + ("org.iso.18013.5.1.administrative_number".to_string(), false), + ("org.iso.18013.5.1.sex".to_string(), false), + ("org.iso.18013.5.1.height".to_string(), false), + ("org.iso.18013.5.1.weight".to_string(), false), + ("org.iso.18013.5.1.eye_colour".to_string(), false), + ("org.iso.18013.5.1.hair_colour".to_string(), false), + ("org.iso.18013.5.1.birth_place".to_string(), false), + ("org.iso.18013.5.1.resident_address".to_string(), false), + ("org.iso.18013.5.1.portrait_capture_date".to_string(), false), + ("org.iso.18013.5.1.age_in_years".to_string(), false), + ("org.iso.18013.5.1.age_birth_year".to_string(), false), + ("org.iso.18013.5.1.age_over_18".to_string(), true,), + ("org.iso.18013.5.1.age_over_21".to_string(), true,), + ("org.iso.18013.5.1.issuing_jurisdiction".to_string(), false), + ("org.iso.18013.5.1.nationality".to_string(), false), + ("org.iso.18013.5.1.resident_city".to_string(), false), + ("org.iso.18013.5.1.resident_state".to_string(), false), + ("org.iso.18013.5.1.resident_postal_code".to_string(), false), + ("org.iso.18013.5.1.resident_country".to_string(), false), + + ]) +} + +fn _aamva_isomdl_data() -> BTreeMap { + BTreeMap::from([ + ("domestic_driving_privileges".to_string(), false), + ("name_suffix".to_string(), false), + ("organ_donor".to_string(), false), + ("veteran".to_string(), false), + ("family_name_truncation".to_string(), false), + ("given_name_truncation".to_string(), false), + ("aka_family_name.v2".to_string(), false), + ("aka_given_name.v2".to_string(), false), + ("weight_range".to_string(), false), + ("race_ethnicity".to_string(), false), + ("EDL_credential".to_string(), false), + ("DHS_compliance".to_string(), false), + ("sex".to_string(), false), + ("resident_county".to_string(), false), + ("hazmat_endorsement_expiration_date".to_string(), false), + ("CDL_indicator".to_string(), false), + ("DHS_compliance_text".to_string(), false), + ("DHS_temporary_lawful_status".to_string(), false), + ]) +} + +pub fn minimal_mdl_request() -> BTreeMap, Option> { + BTreeMap::from([ + (Some("org.iso.18013.5.1.family_name".to_string()),Some( true)), + (Some("org.iso.18013.5.1.given_name".to_string()),Some( true)), + (Some("org.iso.18013.5.1.birth_date".to_string()), Some(false)), + (Some("org.iso.18013.5.1.issue_date".to_string()), Some(false)), + (Some("org.iso.18013.5.1.expiry_date".to_string()), Some(false)), + (Some("org.iso.18013.5.1.issuing_country".to_string()), Some(false)), + (Some("org.iso.18013.5.1.issuing_authority".to_string()), Some(false)), + (Some("org.iso.18013.5.1.document_number".to_string()), Some(false)), + (Some("org.iso.18013.5.1.portrait".to_string()), Some(false)), + (Some("org.iso.18013.5.1.driving_privileges".to_string()), Some(false)), + (Some("org.iso.18013.5.1.un_distinguishing_sign".to_string()), Some(false)), + (Some("org.iso.18013.5.1.administrative_number".to_string()), Some(false)), + (Some("org.iso.18013.5.1.sex".to_string()), Some(false)), + (Some("org.iso.18013.5.1.height".to_string()), Some(false)), + (Some("org.iso.18013.5.1.weight".to_string()), Some(false)), + (Some("org.iso.18013.5.1.eye_colour".to_string()), Some(false)), + (Some("org.iso.18013.5.1.hair_colour".to_string()), Some(false)), + (Some("org.iso.18013.5.1.birth_place".to_string()), Some(false)), + (Some("org.iso.18013.5.1.resident_address".to_string()), Some(false)), + (Some("org.iso.18013.5.1.portrait_capture_date".to_string()), Some(false)), + (Some("org.iso.18013.5.1.age_in_years".to_string()), Some(false)), + (Some("org.iso.18013.5.1.age_birth_year".to_string()), Some(false)), + (Some("org.iso.18013.5.1.age_over_18".to_string()), Some(true,)), + (Some("org.iso.18013.5.1.age_over_21".to_string()), Some(false,)), + (Some("org.iso.18013.5.1.issuing_jurisdiction".to_string()), Some(false)), + (Some("org.iso.18013.5.1.nationality".to_string()), Some(false)), + (Some("org.iso.18013.5.1.resident_city".to_string()), Some(false)), + (Some("org.iso.18013.5.1.resident_state".to_string()), Some(false)), + (Some("org.iso.18013.5.1.resident_postal_code".to_string()), Some(false)), + (Some("org.iso.18013.5.1.resident_country".to_string()), Some(false)), + + ]) +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + + #[test] + fn request_example() { + const DID_JWK: &str = r#"{"kty":"EC","crv":"secp256k1","x":"nrVtymZmqiSu9lU8DmVnB6W7XayJUj4uN7hC3uujZ9s","y":"XZA56MU96ne2c2K-ldbZxrAmLOsneJL1lE4PFnkyQnA","d":"mojL_WMJuMp1vmHNLUkc4es6IeAfcDB7qyZqTeKCEqE"}"#; + let minimal_mdl_request = NonEmptyMap::try_from(minimal_mdl_request()).unwrap(); + let namespaces = NonEmptyMap::new("org.iso.18013.5.1".to_string(), minimal_mdl_request); + let client_id = "nonce".to_string(); + let redirect_uri = "localhost::3000".to_string(); + let presentation_id = "test minimal mdl request".to_string(); + + let jwk: JWK = serde_json::from_str(DID_JWK).unwrap(); + let request_object = prepare_mdl_request_object(jwk, namespaces, client_id, redirect_uri, presentation_id).unwrap(); + + println!("request object: {:?}", request_object); + + } +} \ No newline at end of file diff --git a/src/mdl_response.rs b/src/mdl_response.rs new file mode 100644 index 0000000..1fb16e4 --- /dev/null +++ b/src/mdl_response.rs @@ -0,0 +1,152 @@ +use std::collections::BTreeMap; +use crate::presentation_exchange::PresentationSubmission; +use crate::presentation_exchange::{InputDescriptor}; +use crate::utils::NonEmptyVec; +use isomdl; +use serde::{Serialize, Deserialize}; +use isomdl::definitions::device_request::ItemsRequest; +use isomdl::definitions::helpers::NonEmptyMap; +use crate::utils::Error; + +#[derive(Clone, Debug, Serialize, Deserialize,)] +pub struct Jarm { + pub vp_token: String, + pub presentation_submission: PresentationSubmission +} + +fn match_path_to_mdl_field(paths: NonEmptyVec, mdl_field_paths: Vec, namespace_name: String) -> Option { + let mut matched_mdl_paths: Vec> = paths.iter().map(|suggested_path| { + let suggested_field_name = suggested_path.strip_prefix("$.mdoc.")?; + let mut matches: Vec> = mdl_field_paths.iter().map(|known_path| { + let known_path_field_name = known_path.strip_prefix(&format!("{}{}", &namespace_name, ".")); + if let Some(path) = known_path_field_name { + if path == suggested_field_name { + Some(path.to_owned()) + } + else { None + } + } + else { + None + } + + }).collect(); + matches.retain(|item| item.is_some()); + //TODO: if constraints limit = required and there are no matched paths for a certain field, throw an Error, if not then ignore. + if matches.len() > 0 { + matches.first()?.to_owned() + } else { + None + } + }).collect(); + + matched_mdl_paths.retain(|path| path.is_some()); + if matched_mdl_paths.len() > 0 { + matched_mdl_paths.first()?.to_owned() // always return the first match as defined in Presentation Exchange + } else { + None + } +} + +fn mdl_field_paths() -> Vec { + vec![ + "org.iso.18013.5.1.family_name".to_string(), + "org.iso.18013.5.1.given_name".to_string(), + "org.iso.18013.5.1.birth_date".to_string(), + "org.iso.18013.5.1.issue_date".to_string(), + "org.iso.18013.5.1.expiry_date".to_string(), + "org.iso.18013.5.1.issuing_country".to_string(), + "org.iso.18013.5.1.issuing_authority".to_string(), + "org.iso.18013.5.1.document_number".to_string(), + "org.iso.18013.5.1.portrait".to_string(), + "org.iso.18013.5.1.driving_privileges".to_string(), + "org.iso.18013.5.1.un_distinguishing_sign".to_string(), + "org.iso.18013.5.1.administrative_number".to_string(), + "org.iso.18013.5.1.sex".to_string(), + "org.iso.18013.5.1.height".to_string(), + "org.iso.18013.5.1.weight".to_string(), + "org.iso.18013.5.1.eye_colour".to_string(), + "org.iso.18013.5.1.hair_colour".to_string(), + "org.iso.18013.5.1.birth_place".to_string(), + "org.iso.18013.5.1.resident_address".to_string(), + "org.iso.18013.5.1.portrait_capture_date".to_string(), + "org.iso.18013.5.1.age_in_years".to_string(), + "org.iso.18013.5.1.age_birth_year".to_string(), + "org.iso.18013.5.1.age_over_18".to_string(), + "org.iso.18013.5.1.age_over_21".to_string(), + "org.iso.18013.5.1.issuing_jurisdiction".to_string(), + "org.iso.18013.5.1.nationality".to_string(), + "org.iso.18013.5.1.resident_city".to_string(), + "org.iso.18013.5.1.resident_state".to_string(), + "org.iso.18013.5.1.resident_postal_code".to_string(), + "org.iso.18013.5.1.resident_country".to_string(), + "org.iso.18013.5.1.aamva.domestic_driving_privileges".to_string(), + "org.iso.18013.5.1.aamva.name_suffix".to_string(), + "org.iso.18013.5.1.aamva.organ_donor".to_string(), + "org.iso.18013.5.1.aamva.veteran".to_string(), + "org.iso.18013.5.1.aamva.family_name_truncation".to_string(), + "org.iso.18013.5.1.aamva.given_name_truncation".to_string(), + "org.iso.18013.5.1.aamva.aka_family_name.v2".to_string(), + "org.iso.18013.5.1.aamva.aka_given_name.v2".to_string(), + "org.iso.18013.5.1.aamva.weight_range".to_string(), + "org.iso.18013.5.1.aamva.race_ethnicity".to_string(), + "org.iso.18013.5.1.aamva.EDL_credential".to_string(), + "org.iso.18013.5.1.aamva.DHS_compliance".to_string(), + "org.iso.18013.5.1.aamva.sex".to_string(), + "org.iso.18013.5.1.aamva.resident_county".to_string(), + "org.iso.18013.5.1.aamva.hazmat_endorsement_expiration_date".to_string(), + "org.iso.18013.5.1.aamva.CDL_indicator".to_string(), + "org.iso.18013.5.1.aamva.DHS_compliance_text".to_string(), + "org.iso.18013.5.1.aamva.DHS_temporary_lawful_status".to_string(), + ] +} + +impl TryFrom for ItemsRequest { + type Error = Error; + fn try_from(input_descriptor: InputDescriptor) -> Result { + if let Some(constraints) = input_descriptor.constraints{ + let doc_type = "org.iso.18013.5.1.mDL".to_string(); + let namespace_name = "org.iso.18013.5.1".to_string(); + let constraints_fields = constraints.fields; + + if let Some(cf) = constraints_fields { + let mut fields: BTreeMap, Option> = cf.iter().map(|constraints_field| { + let path = match_path_to_mdl_field(constraints_field.path.clone(), mdl_field_paths(), namespace_name.clone()); + if let Some(p) = path { + (Some(p), constraints_field.intent_to_retain) + } else { + (None, None) + } + + }).collect(); + + fields.retain(|k, _v| k.is_some()); + let x: BTreeMap, Option> = fields.iter().map(|(k,v)| { + if v.is_none() { + (k.to_owned(), Some(false)) + } else { + (k.to_owned(), v.to_owned()) + } + }).collect(); + // safe unwraps + let requested_fields: BTreeMap = x.iter().map(|(k, v)| (k.clone().unwrap(), v.unwrap())).collect(); + + let namespace: NonEmptyMap = NonEmptyMap::try_from(requested_fields)?; + let namespaces = NonEmptyMap::new(namespace_name, namespace); + + Ok(ItemsRequest { + namespaces, + doc_type, + request_info: None + }) + + } else { + Err(Error::Empty) + } + } else { + Err(Error::Empty) + } + + + } +} \ No newline at end of file diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 83e106a..01942ad 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -1,13 +1,17 @@ -use serde::{Deserialize, Serialize}; -use serde_json::Map; - use crate::utils::NonEmptyVec; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; // TODO does openidconnect have a Request type? #[derive(Debug, Deserialize)] pub struct ResponseRequest { - id_token: serde_json::Value, // IdTokenSIOP, // CoreIdTokenClaims, - vp_token: VpToken, + _id_token: serde_json::Value, // IdTokenSIOP, // CoreIdTokenClaims, + _vp_token: VpToken, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct MdlVpToken { + pub presentation_submission: PresentationSubmission } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -20,6 +24,16 @@ pub struct VpToken { pub presentation_definition: PresentationDefinition, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct RequestObject{ + pub presentation_definition: PresentationDefinition, + pub presentation_definition_uri: Option, + pub client_id_scheme: Option, + pub client_metadata: Option, + pub client_metadata_uri: Option, + +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct PresentationDefinition { pub id: String, // Uuid, @@ -69,6 +83,32 @@ pub struct ConstraintsField { pub filter: Option, // TODO JSONSchema validation at deserialization time #[serde(skip_serializing_if = "Option::is_none")] pub optional: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub intent_to_retain: Option, +} + +pub type ConstraintsFields = Vec; + +impl ConstraintsField { + pub fn new( + path: NonEmptyVec, + id: Option, + purpose: Option, + name: Option, + filter: Option, + optional: Option, + intent_to_retain: Option, + ) -> ConstraintsField { + ConstraintsField { + path, + id, + purpose, + name, + filter, + optional, + intent_to_retain, + } + } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -80,25 +120,25 @@ pub enum ConstraintsLimitDisclosure { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct PresentationSubmission { - id: String, - definition_id: String, - descriptor_map: Vec, + pub id: String, + pub definition_id: String, + pub descriptor_map: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct DescriptorMap { - id: String, - format: String, // TODO should be enum of supported formats - path: String, - path_nested: Option>, + pub id: String, + pub format: String, // TODO should be enum of supported formats + pub path: String, + pub path_nested: Option>, } #[derive(Deserialize)] pub struct SubmissionRequirementBaseBase { - name: Option, - purpose: Option, + _name: Option, + _purpose: Option, #[serde(flatten)] - property_set: Option>, + _property_set: Option>, } #[derive(Deserialize)] @@ -126,10 +166,10 @@ pub enum SubmissionRequirement { #[derive(Deserialize)] pub struct SubmissionRequirementPick { #[serde(flatten)] - submission_requirement: SubmissionRequirementBase, - count: Option, - min: Option, - max: Option, + _submission_requirement: SubmissionRequirementBase, + _count: Option, + _min: Option, + _max: Option, } #[cfg(test)] @@ -187,7 +227,7 @@ pub(crate) mod tests { #[derive(Deserialize)] pub struct PresentationDefinitionTest { - presentation_definition: PresentationDefinition, + _presentation_definition: PresentationDefinition, } #[test] @@ -217,7 +257,7 @@ pub(crate) mod tests { // TODO use VP type? #[derive(Deserialize)] pub struct PresentationSubmissionTest { - presentation_submission: PresentationSubmission, + _presentation_submission: PresentationSubmission, } #[test] @@ -249,7 +289,7 @@ pub(crate) mod tests { #[derive(Deserialize)] pub struct SubmissionRequirementsTest { - submission_requirements: Vec, + _submission_requirements: Vec, } #[test] diff --git a/src/presentment.rs b/src/presentment.rs new file mode 100644 index 0000000..5941884 --- /dev/null +++ b/src/presentment.rs @@ -0,0 +1,27 @@ +use crate::{utils::Error, mdl_request::RequestObject}; +use serde_json::Value; +use isomdl::definitions::helpers::NonEmptyMap; +use std::collections::BTreeMap; +use crate::mdl_request::ClientMetadata; +use isomdl; +use isomdl::presentation::device::PreparedDeviceResponse; +use isomdl::definitions::oid4vp::DeviceResponse; +use isomdl::presentation::reader::oid4vp::SessionManager; + + +pub trait Verify { + fn mdl_request(&self, requested_fields: NonEmptyMap< String, NonEmptyMap, Option>> , client_id: String, redirect_uri: String, presentation_id: String, response_mode: String, client_metadata: ClientMetadata) -> Result; + + fn validate_mdl_response(&self, response: &[u8]) -> Result, Error> { + let device_response: DeviceResponse = serde_cbor::from_slice(&response)?; + let mut session_manager = SessionManager::new(device_response)?; + Ok(session_manager.handle_response()?) + } + + //fn vc_request(&self) {} + //fn validate_vc_response(&self){} +} + +pub trait Present { + fn prepare_mdl_response(&self, request: RequestObject) -> Result; +} \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs index 179b436..8aba048 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,6 +1,10 @@ -use jsonpath_rust::JsonPathInst; use serde::{Deserialize, Serialize}; use std::ops::Deref; +use ssi::jws::Error as JwsError; +use serde_cbor::Error as CborError; +use isomdl::presentation::reader::oid4vp::Error as IsomdlError; +use isomdl::definitions::helpers::non_empty_map::Error as NonEmptyMapError; +use anyhow; // #[derive(Clone)] // pub struct JsonPath(JsonPathInst); @@ -13,6 +17,14 @@ pub struct NonEmptyVec(Vec); pub enum Error { #[error("cannot construct a non-empty vec from an empty vec")] Empty, + #[error("field requested that cannot be mapped to an ISO18013-5 mDL field")] + UnrecognizedField, + #[error("could not deserialize cbor")] + CborError, + #[error("could not instantiate session manager")] + OID4VPError, + #[error("could not instantiate session manager")] + IsomdlError, } impl NonEmptyVec { @@ -63,3 +75,34 @@ impl Deref for NonEmptyVec { &self.0 } } + +impl From for Error { + fn from(_value: JwsError) -> Self { + Error::UnrecognizedField + } + +} + +impl From for Error { + fn from(_value: CborError) -> Self { + Error::CborError + } +} + +impl From for Error { + fn from(_value: IsomdlError) -> Self { + Error::IsomdlError + } +} + +impl From for Error { + fn from(_value: NonEmptyMapError) -> Self { + Error::Empty + } +} + +impl From for Error { + fn from(_value: anyhow::Error) -> Self { + Error::Empty + } +} \ No newline at end of file From c1f6f7a5941e19c15d1169aacd97a9a33b446a40 Mon Sep 17 00:00:00 2001 From: Arjen van Veen Date: Fri, 11 Aug 2023 18:55:24 +0200 Subject: [PATCH 02/23] various fixes and updates --- Cargo.toml | 7 +- src/lib.rs | 2 +- src/mdl_request.rs | 373 +++++++++++++++++++++-------------- src/mdl_response.rs | 141 +++++++------ src/presentation_exchange.rs | 5 +- src/presentment.rs | 37 ++-- src/utils.rs | 128 +++++++++--- 7 files changed, 446 insertions(+), 247 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7d9b3b8..1a1f88b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,13 +17,18 @@ thiserror = "1.0.37" lazy_static = "1.4.0" url = "2.3.1" rand = "0.8.5" -isomdl = { git = "ssh://git@github.com/spruceid/isomdl.git", rev = "8f87768"} +isomdl = { git = "ssh://git@github.com/spruceid/isomdl.git", rev = "b2324b7"} +# isomdl = {path = "../isomdl"} time = "0.3.21" p256 = "0.13.2" base64 = "0.13.1" x509-cert = "0.2.3" serde_cbor = "0.11.2" anyhow = "1.0.71" +async-trait = "0.1.72" +reqwest = "0.11.18" +josekit = "0.8.3" +openssl = "0.10.53" [dev-dependencies] serde_path_to_error = "0.1.8" diff --git a/src/lib.rs b/src/lib.rs index 26a3acd..9b2d4ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ pub mod mdl_request; pub mod mdl_response; pub mod presentation_exchange; -pub mod utils; pub mod presentment; +pub mod utils; diff --git a/src/mdl_request.rs b/src/mdl_request.rs index 4503acc..f3e4fe5 100644 --- a/src/mdl_request.rs +++ b/src/mdl_request.rs @@ -1,85 +1,108 @@ -use crate::utils::Error; +use crate::utils::Openid4vpError; use crate::{ presentation_exchange::{ - Constraints, ConstraintsField, InputDescriptor, - PresentationDefinition, + Constraints, ConstraintsField, InputDescriptor, PresentationDefinition, }, utils::NonEmptyVec, }; -use serde_json::{json, Value}; use isomdl::definitions::helpers::NonEmptyMap; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; use ssi::jwk::JWK; use std::collections::BTreeMap; -use serde::{Deserialize, Serialize}; -use x509_cert::der::Decode; use x509_cert::der::referenced::OwnedToRef; +use x509_cert::der::Decode; -use crate::{ - mdl_request::{self}, -}; - -#[derive(Clone, Debug, Serialize, Deserialize,)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct RequestObject { - // pub iss: String, omitting iss is okay since the client_id is already in the request object + // Omitting iss is okay since the client_id is already in the request object + // pub iss: String, pub aud: String, pub response_type: String, pub client_id: String, pub client_id_scheme: Option, - pub redirect_uri: Option, + pub response_uri: Option, pub scope: Option, - pub state: String, - pub presentation_definition: Option, - pub presentation_definition_uri: Option, - pub client_metadata: mdl_request::ClientMetadata, - pub client_metadata_uri: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub state: Option, + #[serde(flatten)] + pub presentation_definition: PresDef, + #[serde(flatten)] + pub client_metadata: MetaData, pub response_mode: Option, pub nonce: Option, - pub supported_algorithm: String, } -#[derive(Clone, Debug, Serialize, Deserialize,)] +#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] +#[serde(untagged)] +pub enum MetaData { + ClientMetadata { client_metadata: ClientMetadata }, + ClientMetadataUri { client_metadata_uri: String }, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] +#[serde(untagged)] +pub enum PresDef { + PresentationDefinition { + presentation_definition: PresentationDefinition, + }, + PresentationDefintionUri { + presentation_definition_uri: String, + }, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct ClientMetadata { pub authorization_encrypted_response_alg: String, pub authorization_encrypted_response_enc: String, + pub require_signed_request_object: bool, pub jwks: Value, - pub vp_formats: String, - pub client_id_scheme: Option, + pub vp_formats: Value, } -pub fn prepare_mdl_request_object(jwk: JWK, requested_fields: NonEmptyMap< String, NonEmptyMap, Option>> , client_id: String, redirect_uri: String, presentation_id: String) -> Result{ +pub fn prepare_mdl_request_object( + jwk: JWK, + requested_fields: NonEmptyMap, Option>>, + client_id: String, + response_uri: String, + presentation_id: String, +) -> Result { let presentation_definition = mdl_presentation_definition(requested_fields, presentation_id)?; let client_metadata = ClientMetadata { authorization_encrypted_response_alg: "ES256".to_string(), authorization_encrypted_response_enc: "A128GCM".to_string(), - vp_formats: "mso_mdoc".to_string(), // TODO fix - client_id_scheme: Some("ISO_X509".to_string()), + vp_formats: json!({"mso_mdoc": { + "alg": [ + "ES256" + ] + }}), jwks: json!(jwk), + require_signed_request_object: false, }; - Ok( RequestObject{ - aud: "https://self-issued.me/v2".to_string(), // per openid4vp chapter 5.6 + Ok(RequestObject { + aud: "https://self-issued.me/v2".to_string(), // per openid4vp chapter 5.6 response_type: "vp_token".to_string(), client_id: client_id.clone(), - client_id_scheme: Some("ISO_X509".to_string()), - redirect_uri: Some(redirect_uri), + client_id_scheme: Some("x509_san_uri".to_string()), + response_uri: Some(response_uri), scope: Some("openid".to_string()), // I think it could also be None - state:"".to_string(), - presentation_definition: Some(presentation_definition), - presentation_definition_uri: None, - client_metadata, - client_metadata_uri: None, + state: None, + presentation_definition: PresDef::PresentationDefinition { + presentation_definition: presentation_definition, + }, + client_metadata: MetaData::ClientMetadata { client_metadata }, response_mode: Some("direct_post.jwt".to_string()), - nonce: Some(client_id), - supported_algorithm: "ES256".to_string() + nonce: Some(client_id), //TODO: should be some nonce }) } fn mdl_presentation_definition( - namespaces: NonEmptyMap< String, NonEmptyMap, Option>>, - presentation_id: String -) -> Result { + namespaces: NonEmptyMap, Option>>, + presentation_id: String, +) -> Result { let input_descriptors = build_input_descriptors(namespaces); - Ok(PresentationDefinition{ + Ok(PresentationDefinition { id: presentation_id, input_descriptors: input_descriptors, name: None, @@ -89,81 +112,68 @@ fn mdl_presentation_definition( } //TODO: allow for specifying the algorithm -fn build_input_descriptors(namespaces: NonEmptyMap< String, NonEmptyMap, Option>>) -> Vec{ - let path_base = "$.mdoc."; +fn build_input_descriptors( + namespaces: NonEmptyMap, Option>>, +) -> Vec { + let path_base = "$['org.iso.18013.5.1']"; - let doc_type_filter = json!({ - "type": "string", - "const": "org.iso.18013.5.1.mDL" - }); + // let doc_type_filter = json!({ + // "type": "string", + // "const": "org.iso.18013.5.1.mDL" + // }); - let input_descriptors: Vec = namespaces.iter().map(|namespace| { - let namespace_filter = json!({ - "type": "string", - "const": namespace.0 - }); - - let format = json!({ + let input_descriptors: Vec = namespaces + .iter() + .map(|namespace| { + let format = json!({ "mso_mdoc": { "alg": [ - "EdDSA", "ES256" - //TODO add all supported algorithms + //TODO: add all supported algorithms ] }}); - let mut namespace_fields = BTreeMap::from(namespace.1.to_owned()); - namespace_fields.retain(|k, _v| k.is_some()); - - let mut fields: Vec = namespace_fields.iter().map(|field| { - ConstraintsField { - //safe unwrap since none values are removed above - path: NonEmptyVec::new(format!("{}{}", path_base, field.0.as_ref().unwrap().to_owned())), - id: None, - purpose:None, - name:None, - filter: None, - optional: None, - intent_to_retain: *field.1 - - } - }).collect(); + let mut namespace_fields = BTreeMap::from(namespace.1.to_owned()); + namespace_fields.retain(|k, _v| k.is_some()); - fields.push(ConstraintsField { - path: NonEmptyVec::new(format!("{}{}", path_base, "doc_type")), - id: None, - purpose: None, - name: None, - filter: Some(doc_type_filter.clone()), - optional: None, - intent_to_retain: None, - }); - - fields.push(ConstraintsField { - path: NonEmptyVec::new(format!("{}{}", path_base, "namespace")), - id: None, - purpose: None, - name: None, - filter: Some(namespace_filter), - optional: None, - intent_to_retain: None, - }); + let fields: Vec = namespace_fields + .iter() + .map(|field| { + ConstraintsField { + //safe unwrap since none values are removed above + path: NonEmptyVec::new(format!( + "{}['{}']", + path_base, + field.0.as_ref().unwrap().to_owned() + )), + id: None, + purpose: None, + name: None, + filter: None, + optional: None, + intent_to_retain: *field.1, + } + }) + .collect(); - let constraints = Constraints{ - fields: Some(fields), - limit_disclosure: None, - }; + let constraints = Constraints { + fields: Some(fields), + limit_disclosure: Some( + crate::presentation_exchange::ConstraintsLimitDisclosure::Required, + ), + }; - InputDescriptor{ - id: "mDL".to_string(), - name: None, - purpose: None, - format: Some(format), - constraints: Some(constraints), - schema: None } - }).collect(); + InputDescriptor { + id: "org.iso.18013.5.1.mDL ".to_string(), + name: None, + purpose: None, + format: Some(format), + constraints: Some(constraints), + schema: None, + } + }) + .collect(); input_descriptors - } pub fn x509_public_key(der: Vec) -> Result { @@ -188,7 +198,10 @@ fn _minimal_mdl_request_isomdl() -> BTreeMap { ("org.iso.18013.5.1.document_number".to_string(), false), ("org.iso.18013.5.1.portrait".to_string(), false), ("org.iso.18013.5.1.driving_privileges".to_string(), false), - ("org.iso.18013.5.1.un_distinguishing_sign".to_string(), false), + ( + "org.iso.18013.5.1.un_distinguishing_sign".to_string(), + false, + ), ("org.iso.18013.5.1.administrative_number".to_string(), false), ("org.iso.18013.5.1.sex".to_string(), false), ("org.iso.18013.5.1.height".to_string(), false), @@ -200,16 +213,15 @@ fn _minimal_mdl_request_isomdl() -> BTreeMap { ("org.iso.18013.5.1.portrait_capture_date".to_string(), false), ("org.iso.18013.5.1.age_in_years".to_string(), false), ("org.iso.18013.5.1.age_birth_year".to_string(), false), - ("org.iso.18013.5.1.age_over_18".to_string(), true,), - ("org.iso.18013.5.1.age_over_21".to_string(), true,), + ("org.iso.18013.5.1.age_over_18".to_string(), true), + ("org.iso.18013.5.1.age_over_21".to_string(), true), ("org.iso.18013.5.1.issuing_jurisdiction".to_string(), false), ("org.iso.18013.5.1.nationality".to_string(), false), ("org.iso.18013.5.1.resident_city".to_string(), false), ("org.iso.18013.5.1.resident_state".to_string(), false), ("org.iso.18013.5.1.resident_postal_code".to_string(), false), ("org.iso.18013.5.1.resident_country".to_string(), false), - - ]) + ]) } fn _aamva_isomdl_data() -> BTreeMap { @@ -232,43 +244,117 @@ fn _aamva_isomdl_data() -> BTreeMap { ("CDL_indicator".to_string(), false), ("DHS_compliance_text".to_string(), false), ("DHS_temporary_lawful_status".to_string(), false), - ]) + ]) } pub fn minimal_mdl_request() -> BTreeMap, Option> { BTreeMap::from([ - (Some("org.iso.18013.5.1.family_name".to_string()),Some( true)), - (Some("org.iso.18013.5.1.given_name".to_string()),Some( true)), - (Some("org.iso.18013.5.1.birth_date".to_string()), Some(false)), - (Some("org.iso.18013.5.1.issue_date".to_string()), Some(false)), - (Some("org.iso.18013.5.1.expiry_date".to_string()), Some(false)), - (Some("org.iso.18013.5.1.issuing_country".to_string()), Some(false)), - (Some("org.iso.18013.5.1.issuing_authority".to_string()), Some(false)), - (Some("org.iso.18013.5.1.document_number".to_string()), Some(false)), + ( + Some("org.iso.18013.5.1.family_name".to_string()), + Some(true), + ), + (Some("org.iso.18013.5.1.given_name".to_string()), Some(true)), + ( + Some("org.iso.18013.5.1.birth_date".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.issue_date".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.expiry_date".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.issuing_country".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.issuing_authority".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.document_number".to_string()), + Some(false), + ), (Some("org.iso.18013.5.1.portrait".to_string()), Some(false)), - (Some("org.iso.18013.5.1.driving_privileges".to_string()), Some(false)), - (Some("org.iso.18013.5.1.un_distinguishing_sign".to_string()), Some(false)), - (Some("org.iso.18013.5.1.administrative_number".to_string()), Some(false)), + ( + Some("org.iso.18013.5.1.driving_privileges".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.un_distinguishing_sign".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.administrative_number".to_string()), + Some(false), + ), (Some("org.iso.18013.5.1.sex".to_string()), Some(false)), (Some("org.iso.18013.5.1.height".to_string()), Some(false)), (Some("org.iso.18013.5.1.weight".to_string()), Some(false)), - (Some("org.iso.18013.5.1.eye_colour".to_string()), Some(false)), - (Some("org.iso.18013.5.1.hair_colour".to_string()), Some(false)), - (Some("org.iso.18013.5.1.birth_place".to_string()), Some(false)), - (Some("org.iso.18013.5.1.resident_address".to_string()), Some(false)), - (Some("org.iso.18013.5.1.portrait_capture_date".to_string()), Some(false)), - (Some("org.iso.18013.5.1.age_in_years".to_string()), Some(false)), - (Some("org.iso.18013.5.1.age_birth_year".to_string()), Some(false)), - (Some("org.iso.18013.5.1.age_over_18".to_string()), Some(true,)), - (Some("org.iso.18013.5.1.age_over_21".to_string()), Some(false,)), - (Some("org.iso.18013.5.1.issuing_jurisdiction".to_string()), Some(false)), - (Some("org.iso.18013.5.1.nationality".to_string()), Some(false)), - (Some("org.iso.18013.5.1.resident_city".to_string()), Some(false)), - (Some("org.iso.18013.5.1.resident_state".to_string()), Some(false)), - (Some("org.iso.18013.5.1.resident_postal_code".to_string()), Some(false)), - (Some("org.iso.18013.5.1.resident_country".to_string()), Some(false)), - - ]) + ( + Some("org.iso.18013.5.1.eye_colour".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.hair_colour".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.birth_place".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.resident_address".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.portrait_capture_date".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.age_in_years".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.age_birth_year".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.age_over_18".to_string()), + Some(true), + ), + ( + Some("org.iso.18013.5.1.age_over_21".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.issuing_jurisdiction".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.nationality".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.resident_city".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.resident_state".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.resident_postal_code".to_string()), + Some(false), + ), + ( + Some("org.iso.18013.5.1.resident_country".to_string()), + Some(false), + ), + ]) } #[cfg(test)] @@ -282,12 +368,11 @@ pub(crate) mod tests { let namespaces = NonEmptyMap::new("org.iso.18013.5.1".to_string(), minimal_mdl_request); let client_id = "nonce".to_string(); let redirect_uri = "localhost::3000".to_string(); - let presentation_id = "test minimal mdl request".to_string(); - - let jwk: JWK = serde_json::from_str(DID_JWK).unwrap(); - let request_object = prepare_mdl_request_object(jwk, namespaces, client_id, redirect_uri, presentation_id).unwrap(); - - println!("request object: {:?}", request_object); + let presentation_id = "mDL".to_string(); + let jwk: JWK = serde_json::from_str(DID_JWK).unwrap(); + let _request_object = + prepare_mdl_request_object(jwk, namespaces, client_id, redirect_uri, presentation_id) + .unwrap(); } -} \ No newline at end of file +} diff --git a/src/mdl_response.rs b/src/mdl_response.rs index 1fb16e4..804465b 100644 --- a/src/mdl_response.rs +++ b/src/mdl_response.rs @@ -1,44 +1,54 @@ -use std::collections::BTreeMap; +use crate::presentation_exchange::InputDescriptor; use crate::presentation_exchange::PresentationSubmission; -use crate::presentation_exchange::{InputDescriptor}; use crate::utils::NonEmptyVec; +use crate::utils::Openid4vpError; use isomdl; -use serde::{Serialize, Deserialize}; -use isomdl::definitions::device_request::ItemsRequest; +pub use isomdl::definitions::device_request::ItemsRequest; use isomdl::definitions::helpers::NonEmptyMap; -use crate::utils::Error; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; -#[derive(Clone, Debug, Serialize, Deserialize,)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Jarm { pub vp_token: String, - pub presentation_submission: PresentationSubmission + pub presentation_submission: PresentationSubmission, } -fn match_path_to_mdl_field(paths: NonEmptyVec, mdl_field_paths: Vec, namespace_name: String) -> Option { - let mut matched_mdl_paths: Vec> = paths.iter().map(|suggested_path| { - let suggested_field_name = suggested_path.strip_prefix("$.mdoc.")?; - let mut matches: Vec> = mdl_field_paths.iter().map(|known_path| { - let known_path_field_name = known_path.strip_prefix(&format!("{}{}", &namespace_name, ".")); - if let Some(path) = known_path_field_name { - if path == suggested_field_name { - Some(path.to_owned()) - } - else { None - } - } - else { +fn match_path_to_mdl_field( + paths: NonEmptyVec, + mdl_field_paths: Vec, + namespace_name: String, +) -> Option { + let mut matched_mdl_paths: Vec> = paths + .iter() + .map(|suggested_path| { + let suggested_field_name = suggested_path.strip_prefix("$['org.iso.18013.5.1']")?; + let suggested_field_name = suggested_field_name.replace(&['[', ']', '\''], ""); + let mut matches: Vec> = mdl_field_paths + .iter() + .map(|known_path| { + let known_path_field_name = + known_path.strip_prefix(&format!("{}{}", &namespace_name, ".")); + if let Some(path) = known_path_field_name { + if path.to_string() == suggested_field_name { + Some(path.to_owned()) + } else { + None + } + } else { + None + } + }) + .collect(); + matches.retain(|item| item.is_some()); + //TODO: if constraints limit = required and there are no matched paths for a certain field, throw an Error, if not then ignore. + if matches.len() > 0 { + matches.first()?.to_owned() + } else { None } - - }).collect(); - matches.retain(|item| item.is_some()); - //TODO: if constraints limit = required and there are no matched paths for a certain field, throw an Error, if not then ignore. - if matches.len() > 0 { - matches.first()?.to_owned() - } else { - None - } - }).collect(); + }) + .collect(); matched_mdl_paths.retain(|path| path.is_some()); if matched_mdl_paths.len() > 0 { @@ -98,55 +108,64 @@ fn mdl_field_paths() -> Vec { "org.iso.18013.5.1.aamva.CDL_indicator".to_string(), "org.iso.18013.5.1.aamva.DHS_compliance_text".to_string(), "org.iso.18013.5.1.aamva.DHS_temporary_lawful_status".to_string(), - ] + ] } impl TryFrom for ItemsRequest { - type Error = Error; - fn try_from(input_descriptor: InputDescriptor) -> Result { - if let Some(constraints) = input_descriptor.constraints{ + type Error = Openid4vpError; + fn try_from(input_descriptor: InputDescriptor) -> Result { + if let Some(constraints) = input_descriptor.constraints { let doc_type = "org.iso.18013.5.1.mDL".to_string(); let namespace_name = "org.iso.18013.5.1".to_string(); let constraints_fields = constraints.fields; if let Some(cf) = constraints_fields { - let mut fields: BTreeMap, Option> = cf.iter().map(|constraints_field| { - let path = match_path_to_mdl_field(constraints_field.path.clone(), mdl_field_paths(), namespace_name.clone()); - if let Some(p) = path { - (Some(p), constraints_field.intent_to_retain) - } else { - (None, None) - } + let mut fields: BTreeMap, Option> = cf + .iter() + .map(|constraints_field| { + let path = match_path_to_mdl_field( + constraints_field.path.clone(), + mdl_field_paths(), + namespace_name.clone(), + ); + if let Some(p) = path { + (Some(p), constraints_field.intent_to_retain) + } else { + (None, None) + } + }) + .collect(); - }).collect(); - fields.retain(|k, _v| k.is_some()); - let x: BTreeMap, Option> = fields.iter().map(|(k,v)| { - if v.is_none() { - (k.to_owned(), Some(false)) - } else { - (k.to_owned(), v.to_owned()) - } - }).collect(); + let x: BTreeMap, Option> = fields + .iter() + .map(|(k, v)| { + if v.is_none() { + (k.to_owned(), Some(false)) + } else { + (k.to_owned(), v.to_owned()) + } + }) + .collect(); // safe unwraps - let requested_fields: BTreeMap = x.iter().map(|(k, v)| (k.clone().unwrap(), v.unwrap())).collect(); + let requested_fields: BTreeMap = x + .iter() + .map(|(k, v)| (k.clone().unwrap(), v.unwrap())) + .collect(); let namespace: NonEmptyMap = NonEmptyMap::try_from(requested_fields)?; let namespaces = NonEmptyMap::new(namespace_name, namespace); - + Ok(ItemsRequest { - namespaces, - doc_type, - request_info: None + namespaces, + doc_type, + request_info: None, }) - } else { - Err(Error::Empty) + Err(Openid4vpError::Empty) } } else { - Err(Error::Empty) + Err(Openid4vpError::Empty) } - - } -} \ No newline at end of file +} diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 01942ad..7986731 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -11,7 +11,7 @@ pub struct ResponseRequest { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct MdlVpToken { - pub presentation_submission: PresentationSubmission + pub presentation_submission: PresentationSubmission, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -25,13 +25,12 @@ pub struct VpToken { } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct RequestObject{ +pub struct RequestObject { pub presentation_definition: PresentationDefinition, pub presentation_definition_uri: Option, pub client_id_scheme: Option, pub client_metadata: Option, pub client_metadata_uri: Option, - } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] diff --git a/src/presentment.rs b/src/presentment.rs index 5941884..a9c74bf 100644 --- a/src/presentment.rs +++ b/src/presentment.rs @@ -1,18 +1,29 @@ -use crate::{utils::Error, mdl_request::RequestObject}; -use serde_json::Value; -use isomdl::definitions::helpers::NonEmptyMap; -use std::collections::BTreeMap; use crate::mdl_request::ClientMetadata; -use isomdl; -use isomdl::presentation::device::PreparedDeviceResponse; +use crate::{mdl_request::RequestObject, utils::Openid4vpError}; +use async_trait::async_trait; +use isomdl::definitions::helpers::non_empty_map::NonEmptyMap; use isomdl::definitions::oid4vp::DeviceResponse; +use isomdl::presentation::device::PreparedDeviceResponse; use isomdl::presentation::reader::oid4vp::SessionManager; - +use serde_json::Value; +use std::collections::BTreeMap; pub trait Verify { - fn mdl_request(&self, requested_fields: NonEmptyMap< String, NonEmptyMap, Option>> , client_id: String, redirect_uri: String, presentation_id: String, response_mode: String, client_metadata: ClientMetadata) -> Result; + fn mdl_request( + &self, + requested_fields: NonEmptyMap, Option>>, + client_id: String, + redirect_uri: String, + presentation_id: String, + response_mode: String, + client_metadata: ClientMetadata, + e_reader_key_bytes: String, + ) -> Result; - fn validate_mdl_response(&self, response: &[u8]) -> Result, Error> { + fn validate_mdl_response( + &self, + response: &[u8], + ) -> Result, Openid4vpError> { let device_response: DeviceResponse = serde_cbor::from_slice(&response)?; let mut session_manager = SessionManager::new(device_response)?; Ok(session_manager.handle_response()?) @@ -22,6 +33,10 @@ pub trait Verify { //fn validate_vc_response(&self){} } +#[async_trait] pub trait Present { - fn prepare_mdl_response(&self, request: RequestObject) -> Result; -} \ No newline at end of file + async fn prepare_mdl_response( + &self, + request: RequestObject, + ) -> Result; +} diff --git a/src/utils.rs b/src/utils.rs index 8aba048..f7173dc 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,10 +1,11 @@ +use anyhow; +use isomdl::definitions::helpers::non_empty_map::Error as NonEmptyMapError; +use isomdl::presentation::reader::oid4vp::Error as IsomdlError; +use reqwest::Error as ReqwestError; use serde::{Deserialize, Serialize}; -use std::ops::Deref; -use ssi::jws::Error as JwsError; use serde_cbor::Error as CborError; -use isomdl::presentation::reader::oid4vp::Error as IsomdlError; -use isomdl::definitions::helpers::non_empty_map::Error as NonEmptyMapError; -use anyhow; +use ssi::jws::Error as JwsError; +use std::ops::Deref; // #[derive(Clone)] // pub struct JsonPath(JsonPathInst); @@ -14,17 +15,63 @@ use anyhow; pub struct NonEmptyVec(Vec); #[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("cannot construct a non-empty vec from an empty vec")] +pub enum Openid4vpError { + #[error( + "The request is missing a required parameter, includes an + invalid parameter value, includes a parameter more than + once, or is otherwise malformed." + )] + InvalidRequest, + #[error( + "The client is not authorized to request an authorization + code using this method" + )] + UnauthorizedClient, + #[error( + "The resource owner or authorization server denied the + request." + )] + AccessDenied, + #[error( + "The authorization server does not support obtaining an + authorization code using this method." + )] + UnnsupportedResponseType, + #[error("Requested scope value is invalid, unknown, or malformed.")] + InvalidScope, + #[error( + "The server encountered an unexpected + condition that prevented it from fulfilling the request." + )] + ServerError, + #[error( + "The server is currently unable to handle + the request due to a temporary overloading or maintenance + of the server." + )] + TemporarilyUnavailable, + #[error("Verifier's pre-registered metadata has been found based on the Client Identifier, but client_metadata parameter is also present.")] + InvalidClient, + #[error("The Wallet does not support any of the formats requested by the Verifier")] + VpFormatsNotSupported, + #[error("The Presentation Definition URL cannot be reached.")] + InvalidPresentationDefinitionUri, + #[error("The Presentation Definition URL can be reached, but the specified presentation_definition cannot be found at the URL.")] + InvalidPresentationDefinitionReference, + #[error("Cannot construct a non-empty vec from an empty vec")] Empty, - #[error("field requested that cannot be mapped to an ISO18013-5 mDL field")] + #[error("Field requested that cannot be mapped to an ISO18013-5 mDL field")] UnrecognizedField, - #[error("could not deserialize cbor")] + #[error("Could not encode or decode cbor")] CborError, - #[error("could not instantiate session manager")] + #[error("Could not instantiate session manager")] OID4VPError, - #[error("could not instantiate session manager")] + #[error("Isomdl error")] IsomdlError, + #[error("The requested encryption algorithm is not supported.")] + UnsupportedEncryptionAlgorithm, + #[error("The requested encryption encoding is not supported.")] + UnsupportedEncryptionEncoding, } impl NonEmptyVec { @@ -46,11 +93,11 @@ impl NonEmptyVec { } impl TryFrom> for NonEmptyVec { - type Error = Error; + type Error = Openid4vpError; - fn try_from(v: Vec) -> Result, Error> { + fn try_from(v: Vec) -> Result, Openid4vpError> { if v.is_empty() { - return Err(Error::Empty); + return Err(Openid4vpError::Empty); } Ok(NonEmptyVec(v)) } @@ -76,33 +123,62 @@ impl Deref for NonEmptyVec { } } -impl From for Error { +impl From for Openid4vpError { fn from(_value: JwsError) -> Self { - Error::UnrecognizedField + Openid4vpError::UnrecognizedField } - } -impl From for Error { +impl From for Openid4vpError { fn from(_value: CborError) -> Self { - Error::CborError + Openid4vpError::CborError } } -impl From for Error { +impl From for Openid4vpError { fn from(_value: IsomdlError) -> Self { - Error::IsomdlError + Openid4vpError::IsomdlError } } -impl From for Error { +impl From for Openid4vpError { fn from(_value: NonEmptyMapError) -> Self { - Error::Empty + Openid4vpError::Empty } } -impl From for Error { +impl From for Openid4vpError { fn from(_value: anyhow::Error) -> Self { - Error::Empty + Openid4vpError::Empty + } +} + +impl From for Openid4vpError { + fn from(_value: reqwest::Error) -> Self { + Openid4vpError::InvalidRequest } -} \ No newline at end of file +} + +impl From for Openid4vpError { + fn from(_value: serde_json::Error) -> Self { + Openid4vpError::Empty + } +} + +impl From for Openid4vpError { + fn from(_value: x509_cert::der::Error) -> Self { + Openid4vpError::InvalidRequest + } +} + +impl From for Openid4vpError { + fn from(_value: openssl::error::ErrorStack) -> Self { + Openid4vpError::InvalidRequest + } +} + +impl From for Openid4vpError { + fn from(_value: ssi::jwk::Error) -> Self { + Openid4vpError::InvalidRequest + } +} From ca3f112fc43cd6eec1e9ec5d63e8a2dc6aa29b24 Mon Sep 17 00:00:00 2001 From: Arjen van Veen Date: Fri, 11 Aug 2023 18:57:24 +0200 Subject: [PATCH 03/23] clippy fix --- src/mdl_request.rs | 4 ++-- src/mdl_response.rs | 8 ++++---- src/presentment.rs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/mdl_request.rs b/src/mdl_request.rs index f3e4fe5..73a217a 100644 --- a/src/mdl_request.rs +++ b/src/mdl_request.rs @@ -89,7 +89,7 @@ pub fn prepare_mdl_request_object( scope: Some("openid".to_string()), // I think it could also be None state: None, presentation_definition: PresDef::PresentationDefinition { - presentation_definition: presentation_definition, + presentation_definition, }, client_metadata: MetaData::ClientMetadata { client_metadata }, response_mode: Some("direct_post.jwt".to_string()), @@ -104,7 +104,7 @@ fn mdl_presentation_definition( let input_descriptors = build_input_descriptors(namespaces); Ok(PresentationDefinition { id: presentation_id, - input_descriptors: input_descriptors, + input_descriptors, name: None, purpose: None, format: None, diff --git a/src/mdl_response.rs b/src/mdl_response.rs index 804465b..ab1951a 100644 --- a/src/mdl_response.rs +++ b/src/mdl_response.rs @@ -23,14 +23,14 @@ fn match_path_to_mdl_field( .iter() .map(|suggested_path| { let suggested_field_name = suggested_path.strip_prefix("$['org.iso.18013.5.1']")?; - let suggested_field_name = suggested_field_name.replace(&['[', ']', '\''], ""); + let suggested_field_name = suggested_field_name.replace(['[', ']', '\''], ""); let mut matches: Vec> = mdl_field_paths .iter() .map(|known_path| { let known_path_field_name = known_path.strip_prefix(&format!("{}{}", &namespace_name, ".")); if let Some(path) = known_path_field_name { - if path.to_string() == suggested_field_name { + if *path == suggested_field_name { Some(path.to_owned()) } else { None @@ -42,7 +42,7 @@ fn match_path_to_mdl_field( .collect(); matches.retain(|item| item.is_some()); //TODO: if constraints limit = required and there are no matched paths for a certain field, throw an Error, if not then ignore. - if matches.len() > 0 { + if !matches.is_empty() { matches.first()?.to_owned() } else { None @@ -51,7 +51,7 @@ fn match_path_to_mdl_field( .collect(); matched_mdl_paths.retain(|path| path.is_some()); - if matched_mdl_paths.len() > 0 { + if !matched_mdl_paths.is_empty() { matched_mdl_paths.first()?.to_owned() // always return the first match as defined in Presentation Exchange } else { None diff --git a/src/presentment.rs b/src/presentment.rs index a9c74bf..1955894 100644 --- a/src/presentment.rs +++ b/src/presentment.rs @@ -24,7 +24,7 @@ pub trait Verify { &self, response: &[u8], ) -> Result, Openid4vpError> { - let device_response: DeviceResponse = serde_cbor::from_slice(&response)?; + let device_response: DeviceResponse = serde_cbor::from_slice(response)?; let mut session_manager = SessionManager::new(device_response)?; Ok(session_manager.handle_response()?) } From e11b07a97afbb58d297b4177248a6418f4d3f3a0 Mon Sep 17 00:00:00 2001 From: Arjen van Veen Date: Mon, 14 Aug 2023 12:32:19 +0200 Subject: [PATCH 04/23] small fixes --- Cargo.toml | 1 + src/mdl_request.rs | 1 + src/utils.rs | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 1a1f88b..96213af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ async-trait = "0.1.72" reqwest = "0.11.18" josekit = "0.8.3" openssl = "0.10.53" +base64url = "0.1.0" [dev-dependencies] serde_path_to_error = "0.1.8" diff --git a/src/mdl_request.rs b/src/mdl_request.rs index 73a217a..43d6d28 100644 --- a/src/mdl_request.rs +++ b/src/mdl_request.rs @@ -22,6 +22,7 @@ pub struct RequestObject { pub client_id: String, pub client_id_scheme: Option, pub response_uri: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub scope: Option, #[serde(skip_serializing_if = "Option::is_none")] pub state: Option, diff --git a/src/utils.rs b/src/utils.rs index f7173dc..2d1337d 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; use serde_cbor::Error as CborError; use ssi::jws::Error as JwsError; use std::ops::Deref; +use josekit::JoseError; // #[derive(Clone)] // pub struct JsonPath(JsonPathInst); @@ -72,6 +73,9 @@ pub enum Openid4vpError { UnsupportedEncryptionAlgorithm, #[error("The requested encryption encoding is not supported.")] UnsupportedEncryptionEncoding, + #[error("There is an error in the base64 encoding.")] + DecodingError, + } impl NonEmptyVec { @@ -153,6 +157,12 @@ impl From for Openid4vpError { } } +impl From for Openid4vpError { + fn from(_value: JoseError) -> Self { + Openid4vpError::ServerError + } +} + impl From for Openid4vpError { fn from(_value: reqwest::Error) -> Self { Openid4vpError::InvalidRequest @@ -182,3 +192,12 @@ impl From for Openid4vpError { Openid4vpError::InvalidRequest } } + +impl From for Openid4vpError { + fn from(_value: base64::DecodeError) -> Self { + Openid4vpError::DecodingError + } +} + +// impl From for Openid4vpError { +// } \ No newline at end of file From 6f59896de3fa5fbebb0a2b3b4576e3f1d02c191f Mon Sep 17 00:00:00 2001 From: Arjen van Veen Date: Mon, 14 Aug 2023 12:35:04 +0200 Subject: [PATCH 05/23] clippy fix --- src/presentment.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/presentment.rs b/src/presentment.rs index 1955894..95ee5e7 100644 --- a/src/presentment.rs +++ b/src/presentment.rs @@ -8,6 +8,7 @@ use isomdl::presentation::reader::oid4vp::SessionManager; use serde_json::Value; use std::collections::BTreeMap; +#[allow(clippy::too_many_arguments)] pub trait Verify { fn mdl_request( &self, From 0b58d3c826806fea9e6801ed0b957dc8a53383da Mon Sep 17 00:00:00 2001 From: Arjen van Veen Date: Mon, 14 Aug 2023 16:17:29 +0200 Subject: [PATCH 06/23] remove openssl --- Cargo.toml | 1 - src/utils.rs | 11 +---------- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 96213af..b4755a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,6 @@ anyhow = "1.0.71" async-trait = "0.1.72" reqwest = "0.11.18" josekit = "0.8.3" -openssl = "0.10.53" base64url = "0.1.0" [dev-dependencies] diff --git a/src/utils.rs b/src/utils.rs index 2d1337d..eee9cdd 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -181,12 +181,6 @@ impl From for Openid4vpError { } } -impl From for Openid4vpError { - fn from(_value: openssl::error::ErrorStack) -> Self { - Openid4vpError::InvalidRequest - } -} - impl From for Openid4vpError { fn from(_value: ssi::jwk::Error) -> Self { Openid4vpError::InvalidRequest @@ -197,7 +191,4 @@ impl From for Openid4vpError { fn from(_value: base64::DecodeError) -> Self { Openid4vpError::DecodingError } -} - -// impl From for Openid4vpError { -// } \ No newline at end of file +} \ No newline at end of file From 55c16b72288dc53938f29f09273ad03a44aacc61 Mon Sep 17 00:00:00 2001 From: Arjen van Veen Date: Tue, 15 Aug 2023 17:18:38 +0200 Subject: [PATCH 07/23] clean up --- src/mdl_request.rs | 195 ++++----------------------------------------- 1 file changed, 17 insertions(+), 178 deletions(-) diff --git a/src/mdl_request.rs b/src/mdl_request.rs index 43d6d28..02edebf 100644 --- a/src/mdl_request.rs +++ b/src/mdl_request.rs @@ -68,6 +68,7 @@ pub fn prepare_mdl_request_object( response_uri: String, presentation_id: String, ) -> Result { + let jwks = json!({"keys": vec![jwk]}); let presentation_definition = mdl_presentation_definition(requested_fields, presentation_id)?; let client_metadata = ClientMetadata { authorization_encrypted_response_alg: "ES256".to_string(), @@ -77,7 +78,7 @@ pub fn prepare_mdl_request_object( "ES256" ] }}), - jwks: json!(jwk), + jwks: jwks, require_signed_request_object: false, }; @@ -118,11 +119,6 @@ fn build_input_descriptors( ) -> Vec { let path_base = "$['org.iso.18013.5.1']"; - // let doc_type_filter = json!({ - // "type": "string", - // "const": "org.iso.18013.5.1.mDL" - // }); - let input_descriptors: Vec = namespaces .iter() .map(|namespace| { @@ -187,185 +183,28 @@ pub fn x509_public_key(der: Vec) -> Result { .map_err(|e| format!("could not parse p256 public key from pkcs8 spki: {e}")) } -fn _minimal_mdl_request_isomdl() -> BTreeMap { - BTreeMap::from([ - ("org.iso.18013.5.1.family_name".to_string(), false), - ("org.iso.18013.5.1.given_name".to_string(), false), - ("org.iso.18013.5.1.birth_date".to_string(), false), - ("org.iso.18013.5.1.issue_date".to_string(), false), - ("org.iso.18013.5.1.expiry_date".to_string(), false), - ("org.iso.18013.5.1.issuing_country".to_string(), false), - ("org.iso.18013.5.1.issuing_authority".to_string(), false), - ("org.iso.18013.5.1.document_number".to_string(), false), - ("org.iso.18013.5.1.portrait".to_string(), false), - ("org.iso.18013.5.1.driving_privileges".to_string(), false), - ( - "org.iso.18013.5.1.un_distinguishing_sign".to_string(), - false, - ), - ("org.iso.18013.5.1.administrative_number".to_string(), false), - ("org.iso.18013.5.1.sex".to_string(), false), - ("org.iso.18013.5.1.height".to_string(), false), - ("org.iso.18013.5.1.weight".to_string(), false), - ("org.iso.18013.5.1.eye_colour".to_string(), false), - ("org.iso.18013.5.1.hair_colour".to_string(), false), - ("org.iso.18013.5.1.birth_place".to_string(), false), - ("org.iso.18013.5.1.resident_address".to_string(), false), - ("org.iso.18013.5.1.portrait_capture_date".to_string(), false), - ("org.iso.18013.5.1.age_in_years".to_string(), false), - ("org.iso.18013.5.1.age_birth_year".to_string(), false), - ("org.iso.18013.5.1.age_over_18".to_string(), true), - ("org.iso.18013.5.1.age_over_21".to_string(), true), - ("org.iso.18013.5.1.issuing_jurisdiction".to_string(), false), - ("org.iso.18013.5.1.nationality".to_string(), false), - ("org.iso.18013.5.1.resident_city".to_string(), false), - ("org.iso.18013.5.1.resident_state".to_string(), false), - ("org.iso.18013.5.1.resident_postal_code".to_string(), false), - ("org.iso.18013.5.1.resident_country".to_string(), false), - ]) -} - -fn _aamva_isomdl_data() -> BTreeMap { - BTreeMap::from([ - ("domestic_driving_privileges".to_string(), false), - ("name_suffix".to_string(), false), - ("organ_donor".to_string(), false), - ("veteran".to_string(), false), - ("family_name_truncation".to_string(), false), - ("given_name_truncation".to_string(), false), - ("aka_family_name.v2".to_string(), false), - ("aka_given_name.v2".to_string(), false), - ("weight_range".to_string(), false), - ("race_ethnicity".to_string(), false), - ("EDL_credential".to_string(), false), - ("DHS_compliance".to_string(), false), - ("sex".to_string(), false), - ("resident_county".to_string(), false), - ("hazmat_endorsement_expiration_date".to_string(), false), - ("CDL_indicator".to_string(), false), - ("DHS_compliance_text".to_string(), false), - ("DHS_temporary_lawful_status".to_string(), false), - ]) -} - -pub fn minimal_mdl_request() -> BTreeMap, Option> { - BTreeMap::from([ - ( - Some("org.iso.18013.5.1.family_name".to_string()), - Some(true), - ), - (Some("org.iso.18013.5.1.given_name".to_string()), Some(true)), - ( - Some("org.iso.18013.5.1.birth_date".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.issue_date".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.expiry_date".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.issuing_country".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.issuing_authority".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.document_number".to_string()), - Some(false), - ), - (Some("org.iso.18013.5.1.portrait".to_string()), Some(false)), - ( - Some("org.iso.18013.5.1.driving_privileges".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.un_distinguishing_sign".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.administrative_number".to_string()), - Some(false), - ), - (Some("org.iso.18013.5.1.sex".to_string()), Some(false)), - (Some("org.iso.18013.5.1.height".to_string()), Some(false)), - (Some("org.iso.18013.5.1.weight".to_string()), Some(false)), - ( - Some("org.iso.18013.5.1.eye_colour".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.hair_colour".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.birth_place".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.resident_address".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.portrait_capture_date".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.age_in_years".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.age_birth_year".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.age_over_18".to_string()), - Some(true), - ), - ( - Some("org.iso.18013.5.1.age_over_21".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.issuing_jurisdiction".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.nationality".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.resident_city".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.resident_state".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.resident_postal_code".to_string()), - Some(false), - ), - ( - Some("org.iso.18013.5.1.resident_country".to_string()), - Some(false), - ), - ]) -} - #[cfg(test)] pub(crate) mod tests { use super::*; + fn prove_name_request() -> BTreeMap, Option> { + BTreeMap::from([ + ( + Some("org.iso.18013.5.1.family_name".to_string()), + Some(true), + ), + (Some("org.iso.18013.5.1.given_name".to_string()), Some(true)), + ( + Some("org.iso.18013.5.1.birth_date".to_string()), + Some(false), + ), + ]) + } + #[test] fn request_example() { const DID_JWK: &str = r#"{"kty":"EC","crv":"secp256k1","x":"nrVtymZmqiSu9lU8DmVnB6W7XayJUj4uN7hC3uujZ9s","y":"XZA56MU96ne2c2K-ldbZxrAmLOsneJL1lE4PFnkyQnA","d":"mojL_WMJuMp1vmHNLUkc4es6IeAfcDB7qyZqTeKCEqE"}"#; - let minimal_mdl_request = NonEmptyMap::try_from(minimal_mdl_request()).unwrap(); + let minimal_mdl_request = NonEmptyMap::try_from(prove_name_request()).unwrap(); let namespaces = NonEmptyMap::new("org.iso.18013.5.1".to_string(), minimal_mdl_request); let client_id = "nonce".to_string(); let redirect_uri = "localhost::3000".to_string(); From 17013ce616626732cd79ac14a538e722a7536651 Mon Sep 17 00:00:00 2001 From: Arjen van Veen Date: Tue, 15 Aug 2023 17:22:54 +0200 Subject: [PATCH 08/23] clippy fix --- src/mdl_request.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mdl_request.rs b/src/mdl_request.rs index 02edebf..d3b6574 100644 --- a/src/mdl_request.rs +++ b/src/mdl_request.rs @@ -78,7 +78,7 @@ pub fn prepare_mdl_request_object( "ES256" ] }}), - jwks: jwks, + jwks, require_signed_request_object: false, }; From 0182b9e483ba215e44aa69bf0bddffa7bb169082 Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 15 Aug 2023 16:48:00 +0100 Subject: [PATCH 09/23] Use josekit fork --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b4755a5..1894d24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ thiserror = "1.0.37" lazy_static = "1.4.0" url = "2.3.1" rand = "0.8.5" -isomdl = { git = "ssh://git@github.com/spruceid/isomdl.git", rev = "b2324b7"} +isomdl = { git = "ssh://git@github.com/spruceid/isomdl.git", rev = "b2324b7" } # isomdl = {path = "../isomdl"} time = "0.3.21" p256 = "0.13.2" @@ -27,7 +27,7 @@ serde_cbor = "0.11.2" anyhow = "1.0.71" async-trait = "0.1.72" reqwest = "0.11.18" -josekit = "0.8.3" +josekit = { git = "https://github.com/cobward/josekit-rs", rev = "cee96f0" } base64url = "0.1.0" [dev-dependencies] From f5d74841be83ed95837b4e8f56dc1211ac1d7150 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 16 Aug 2023 12:10:42 +0100 Subject: [PATCH 10/23] Update josekit dep --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1894d24..63bc7e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ thiserror = "1.0.37" lazy_static = "1.4.0" url = "2.3.1" rand = "0.8.5" -isomdl = { git = "ssh://git@github.com/spruceid/isomdl.git", rev = "b2324b7" } +isomdl = { git = "ssh://git@github.com/spruceid/isomdl.git", rev = "635c8a7" } # isomdl = {path = "../isomdl"} time = "0.3.21" p256 = "0.13.2" From 903a9704b12bca570f04f159b70b33caa5a3e830 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 16 Aug 2023 12:12:07 +0100 Subject: [PATCH 11/23] Fix dependencies --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 63bc7e8..ac143f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ thiserror = "1.0.37" lazy_static = "1.4.0" url = "2.3.1" rand = "0.8.5" -isomdl = { git = "ssh://git@github.com/spruceid/isomdl.git", rev = "635c8a7" } +isomdl = { git = "ssh://git@github.com/spruceid/isomdl.git", rev = "b2324b7" } # isomdl = {path = "../isomdl"} time = "0.3.21" p256 = "0.13.2" @@ -27,7 +27,7 @@ serde_cbor = "0.11.2" anyhow = "1.0.71" async-trait = "0.1.72" reqwest = "0.11.18" -josekit = { git = "https://github.com/cobward/josekit-rs", rev = "cee96f0" } +josekit = { git = "https://github.com/cobward/josekit-rs", rev = "635c8a7" } base64url = "0.1.0" [dev-dependencies] From 02b5ee35d0d4accb56521e7790936149981a929d Mon Sep 17 00:00:00 2001 From: Arjen van Veen Date: Wed, 16 Aug 2023 13:44:21 +0200 Subject: [PATCH 12/23] Send impl for async trait --- src/presentment.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/presentment.rs b/src/presentment.rs index 95ee5e7..8585ab6 100644 --- a/src/presentment.rs +++ b/src/presentment.rs @@ -34,7 +34,7 @@ pub trait Verify { //fn validate_vc_response(&self){} } -#[async_trait] +#[async_trait(?Send)] pub trait Present { async fn prepare_mdl_response( &self, From a41687054a7bd7957c2e6993067024e0c23171e5 Mon Sep 17 00:00:00 2001 From: Arjen van Veen Date: Wed, 16 Aug 2023 16:56:44 +0200 Subject: [PATCH 13/23] fix unwraps --- src/mdl_request.rs | 2 +- src/utils.rs | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/mdl_request.rs b/src/mdl_request.rs index d3b6574..ea9753c 100644 --- a/src/mdl_request.rs +++ b/src/mdl_request.rs @@ -68,7 +68,7 @@ pub fn prepare_mdl_request_object( response_uri: String, presentation_id: String, ) -> Result { - let jwks = json!({"keys": vec![jwk]}); + let jwks = json!({ "keys": vec![jwk] }); let presentation_definition = mdl_presentation_definition(requested_fields, presentation_id)?; let client_metadata = ClientMetadata { authorization_encrypted_response_alg: "ES256".to_string(), diff --git a/src/utils.rs b/src/utils.rs index eee9cdd..cb2b290 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,12 +1,12 @@ use anyhow; use isomdl::definitions::helpers::non_empty_map::Error as NonEmptyMapError; use isomdl::presentation::reader::oid4vp::Error as IsomdlError; +use josekit::JoseError; use reqwest::Error as ReqwestError; use serde::{Deserialize, Serialize}; use serde_cbor::Error as CborError; use ssi::jws::Error as JwsError; use std::ops::Deref; -use josekit::JoseError; // #[derive(Clone)] // pub struct JsonPath(JsonPathInst); @@ -75,7 +75,6 @@ pub enum Openid4vpError { UnsupportedEncryptionEncoding, #[error("There is an error in the base64 encoding.")] DecodingError, - } impl NonEmptyVec { @@ -191,4 +190,10 @@ impl From for Openid4vpError { fn from(_value: base64::DecodeError) -> Self { Openid4vpError::DecodingError } -} \ No newline at end of file +} + +impl From for Openid4vpError { + fn from(_value: String) -> Self { + Openid4vpError::OID4VPError + } +} From ef2aa2f85c4cdcba66c07020007544dc3d345208 Mon Sep 17 00:00:00 2001 From: Arjen van Veen Date: Fri, 18 Aug 2023 11:51:27 +0200 Subject: [PATCH 14/23] propagate error message for empty error --- src/mdl_response.rs | 16 ++++++---------- src/utils.rs | 22 +++++++++++++--------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/mdl_response.rs b/src/mdl_response.rs index ab1951a..51ee6ad 100644 --- a/src/mdl_response.rs +++ b/src/mdl_response.rs @@ -1,19 +1,11 @@ use crate::presentation_exchange::InputDescriptor; -use crate::presentation_exchange::PresentationSubmission; use crate::utils::NonEmptyVec; use crate::utils::Openid4vpError; use isomdl; pub use isomdl::definitions::device_request::ItemsRequest; use isomdl::definitions::helpers::NonEmptyMap; -use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Jarm { - pub vp_token: String, - pub presentation_submission: PresentationSubmission, -} - fn match_path_to_mdl_field( paths: NonEmptyVec, mdl_field_paths: Vec, @@ -162,10 +154,14 @@ impl TryFrom for ItemsRequest { request_info: None, }) } else { - Err(Openid4vpError::Empty) + Err(Openid4vpError::Empty( + "Missing constraints_fields".to_string(), + )) } } else { - Err(Openid4vpError::Empty) + Err(Openid4vpError::Empty( + "Missing inputdescriptors".to_string(), + )) } } } diff --git a/src/utils.rs b/src/utils.rs index cb2b290..904f280 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -59,8 +59,8 @@ pub enum Openid4vpError { InvalidPresentationDefinitionUri, #[error("The Presentation Definition URL can be reached, but the specified presentation_definition cannot be found at the URL.")] InvalidPresentationDefinitionReference, - #[error("Cannot construct a non-empty vec from an empty vec")] - Empty, + #[error("{0}")] + Empty(String), #[error("Field requested that cannot be mapped to an ISO18013-5 mDL field")] UnrecognizedField, #[error("Could not encode or decode cbor")] @@ -75,6 +75,8 @@ pub enum Openid4vpError { UnsupportedEncryptionEncoding, #[error("There is an error in the base64 encoding.")] DecodingError, + #[error("JoseError {0}")] + JoseError(String), } impl NonEmptyVec { @@ -100,7 +102,9 @@ impl TryFrom> for NonEmptyVec { fn try_from(v: Vec) -> Result, Openid4vpError> { if v.is_empty() { - return Err(Openid4vpError::Empty); + return Err(Openid4vpError::Empty( + "Can not create a NonEmptyVec from an empty Vec".to_string(), + )); } Ok(NonEmptyVec(v)) } @@ -145,14 +149,14 @@ impl From for Openid4vpError { } impl From for Openid4vpError { - fn from(_value: NonEmptyMapError) -> Self { - Openid4vpError::Empty + fn from(value: NonEmptyMapError) -> Self { + Openid4vpError::Empty(value.to_string()) } } impl From for Openid4vpError { - fn from(_value: anyhow::Error) -> Self { - Openid4vpError::Empty + fn from(value: anyhow::Error) -> Self { + Openid4vpError::Empty(value.to_string()) } } @@ -169,8 +173,8 @@ impl From for Openid4vpError { } impl From for Openid4vpError { - fn from(_value: serde_json::Error) -> Self { - Openid4vpError::Empty + fn from(value: serde_json::Error) -> Self { + Openid4vpError::Empty(value.to_string()) } } From 78036c7d83360fbdff42f5f5a9f779581b7b85ef Mon Sep 17 00:00:00 2001 From: Arjen van Veen Date: Tue, 5 Sep 2023 15:25:51 +0200 Subject: [PATCH 15/23] fix presentation_submission --- Cargo.toml | 2 +- src/presentation_exchange.rs | 2 +- src/utils.rs | 40 ++++++++++++++++++++++++++++++------ 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ac143f2..ba56c77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ lazy_static = "1.4.0" url = "2.3.1" rand = "0.8.5" isomdl = { git = "ssh://git@github.com/spruceid/isomdl.git", rev = "b2324b7" } -# isomdl = {path = "../isomdl"} +#isomdl = {path = "../isomdl"} time = "0.3.21" p256 = "0.13.2" base64 = "0.13.1" diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 7986731..143b135 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -129,7 +129,7 @@ pub struct DescriptorMap { pub id: String, pub format: String, // TODO should be enum of supported formats pub path: String, - pub path_nested: Option>, + //pub path_nested: Option>, } #[derive(Deserialize)] diff --git a/src/utils.rs b/src/utils.rs index 904f280..0db4379 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,11 +1,13 @@ use anyhow; use isomdl::definitions::helpers::non_empty_map::Error as NonEmptyMapError; use isomdl::presentation::reader::oid4vp::Error as IsomdlError; +use isomdl::presentation::reader::Error as IsomdlReaderErrror; use josekit::JoseError; use reqwest::Error as ReqwestError; use serde::{Deserialize, Serialize}; use serde_cbor::Error as CborError; use ssi::jws::Error as JwsError; +use isomdl::definitions::Error as IsomdlDefinitionError; use std::ops::Deref; // #[derive(Clone)] @@ -67,8 +69,8 @@ pub enum Openid4vpError { CborError, #[error("Could not instantiate session manager")] OID4VPError, - #[error("Isomdl error")] - IsomdlError, + #[error("Isomdl error {0}")] + IsomdlError(String), #[error("The requested encryption algorithm is not supported.")] UnsupportedEncryptionAlgorithm, #[error("The requested encryption encoding is not supported.")] @@ -77,6 +79,8 @@ pub enum Openid4vpError { DecodingError, #[error("JoseError {0}")] JoseError(String), + #[error("ResponseError {0}")] + ResponseError(String), } impl NonEmptyVec { @@ -143,8 +147,20 @@ impl From for Openid4vpError { } impl From for Openid4vpError { - fn from(_value: IsomdlError) -> Self { - Openid4vpError::IsomdlError + fn from(value: IsomdlError) -> Self { + Openid4vpError::IsomdlError(value.to_string()) + } +} + +impl From for Openid4vpError { + fn from(value: IsomdlReaderErrror) -> Self { + Openid4vpError::ResponseError(value.to_string()) + } +} + +impl From for Openid4vpError { + fn from(value: IsomdlDefinitionError) -> Self { + Openid4vpError::Empty(value.to_string()) } } @@ -161,8 +177,8 @@ impl From for Openid4vpError { } impl From for Openid4vpError { - fn from(_value: JoseError) -> Self { - Openid4vpError::ServerError + fn from(value: JoseError) -> Self { + Openid4vpError::JoseError(value.to_string()) } } @@ -201,3 +217,15 @@ impl From for Openid4vpError { Openid4vpError::OID4VPError } } + +impl From for Openid4vpError { + fn from(value: p256::ecdsa::Error) -> Self { + Openid4vpError::JoseError(value.to_string()) + } +} + +impl From for Openid4vpError { + fn from(value: x509_cert::spki::Error) -> Self { + Openid4vpError::ResponseError(value.to_string()) + } +} From f9557eb8e779a7aab07b2682a4a518f3aeda9e3c Mon Sep 17 00:00:00 2001 From: Arjen van Veen Date: Tue, 5 Sep 2023 15:27:27 +0200 Subject: [PATCH 16/23] fmt --- src/utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.rs b/src/utils.rs index 0db4379..4f30afb 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,6 @@ use anyhow; use isomdl::definitions::helpers::non_empty_map::Error as NonEmptyMapError; +use isomdl::definitions::Error as IsomdlDefinitionError; use isomdl::presentation::reader::oid4vp::Error as IsomdlError; use isomdl::presentation::reader::Error as IsomdlReaderErrror; use josekit::JoseError; @@ -7,7 +8,6 @@ use reqwest::Error as ReqwestError; use serde::{Deserialize, Serialize}; use serde_cbor::Error as CborError; use ssi::jws::Error as JwsError; -use isomdl::definitions::Error as IsomdlDefinitionError; use std::ops::Deref; // #[derive(Clone)] From 4acc8126041dc0d27b38ce8d3d7eda110011b611 Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 9 Oct 2023 14:40:32 +0100 Subject: [PATCH 17/23] Refactor in core. --- Cargo.toml | 35 +- src/core/authorization_request/mod.rs | 306 +++++++++++++ src/core/authorization_request/parameters.rs | 422 ++++++++++++++++++ .../authorization_request/verification/did.rs | 106 +++++ .../authorization_request/verification/mod.rs | 111 +++++ .../verification/x509_san_dns.rs | 141 ++++++ .../verification/x509_san_uri.rs | 155 +++++++ src/core/credential_format/mod.rs | 17 + src/core/metadata/mod.rs | 89 ++++ src/core/metadata/parameters/mod.rs | 4 + src/core/metadata/parameters/verifier.rs | 112 +++++ src/core/metadata/parameters/wallet.rs | 300 +++++++++++++ src/core/mod.rs | 9 + src/core/object/mod.rs | 105 +++++ src/core/profile/mod.rs | 98 ++++ src/core/response/mod.rs | 37 ++ src/core/response/parameters.rs | 69 +++ src/core/util/mod.rs | 8 + src/core/verifier/builder/by_reference.rs | 10 + src/core/verifier/builder/client.rs | 89 ++++ src/core/verifier/builder/mod.rs | 257 +++++++++++ src/core/verifier/mod.rs | 30 ++ src/core/verifier/request_signer.rs | 42 ++ src/core/wallet/mod.rs | 1 + src/lib.rs | 1 + src/mdl_request.rs | 2 +- src/utils.rs | 6 +- 27 files changed, 2540 insertions(+), 22 deletions(-) create mode 100644 src/core/authorization_request/mod.rs create mode 100644 src/core/authorization_request/parameters.rs create mode 100644 src/core/authorization_request/verification/did.rs create mode 100644 src/core/authorization_request/verification/mod.rs create mode 100644 src/core/authorization_request/verification/x509_san_dns.rs create mode 100644 src/core/authorization_request/verification/x509_san_uri.rs create mode 100644 src/core/credential_format/mod.rs create mode 100644 src/core/metadata/mod.rs create mode 100644 src/core/metadata/parameters/mod.rs create mode 100644 src/core/metadata/parameters/verifier.rs create mode 100644 src/core/metadata/parameters/wallet.rs create mode 100644 src/core/mod.rs create mode 100644 src/core/object/mod.rs create mode 100644 src/core/profile/mod.rs create mode 100644 src/core/response/mod.rs create mode 100644 src/core/response/parameters.rs create mode 100644 src/core/util/mod.rs create mode 100644 src/core/verifier/builder/by_reference.rs create mode 100644 src/core/verifier/builder/client.rs create mode 100644 src/core/verifier/builder/mod.rs create mode 100644 src/core/verifier/mod.rs create mode 100644 src/core/verifier/request_signer.rs create mode 100644 src/core/wallet/mod.rs diff --git a/Cargo.toml b/Cargo.toml index ba56c77..118c00d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,29 +9,28 @@ repository = "https://github.com/spruceid/oidc4vp-rs/" documentation = "https://docs.rs/oidc4vp/" [dependencies] -jsonpath-rust = "0.2.0" -serde = { version = "1.0.147", features = ["derive"] } -serde_json = "1.0.87" -ssi = { version = "0.6.0", default-features = false } -thiserror = "1.0.37" -lazy_static = "1.4.0" -url = "2.3.1" -rand = "0.8.5" +anyhow = "1.0.75" +async-trait = "0.1.73" +base64 = "0.21.4" +did-web = "0.2.2" +didkit = "0.6.0" isomdl = { git = "ssh://git@github.com/spruceid/isomdl.git", rev = "b2324b7" } -#isomdl = {path = "../isomdl"} -time = "0.3.21" -p256 = "0.13.2" -base64 = "0.13.1" -x509-cert = "0.2.3" -serde_cbor = "0.11.2" -anyhow = "1.0.71" -async-trait = "0.1.72" -reqwest = "0.11.18" josekit = { git = "https://github.com/cobward/josekit-rs", rev = "635c8a7" } -base64url = "0.1.0" +p256 = { version = "0.13.2", features = ["jwk"] } +reqwest = "0.11.20" +serde = "1.0.188" +serde_cbor = "0.11.2" +serde_json = "1.0.107" +serde_qs = "0.12.0" +serde_urlencoded = "0.7.1" +thiserror = "1.0.49" +tracing = "0.1.37" +url = { version = "2.4.1", features = ["serde"] } +x509-cert = "0.2.4" [dev-dependencies] serde_path_to_error = "0.1.8" +tokio = { version = "1.32.0", features = ["macros"] } [target.'cfg(target_arch = "wasm32")'.dependencies] uuid = { version = "1.2", features = ["v4", "serde", "js"] } diff --git a/src/core/authorization_request/mod.rs b/src/core/authorization_request/mod.rs new file mode 100644 index 0000000..4ebd5fc --- /dev/null +++ b/src/core/authorization_request/mod.rs @@ -0,0 +1,306 @@ +use std::ops::{Deref, DerefMut}; + +use anyhow::{anyhow, bail, Context, Error, Result}; +use serde::{Deserialize, Serialize}; +use serde_json::Value as Json; +use url::Url; + +use self::{ + parameters::{ + ClientId, ClientIdScheme, PresentationDefinition, PresentationDefinitionUri, RedirectUri, + ResponseMode, ResponseType, ResponseUri, + }, + verification::verify_request, +}; + +use super::{ + object::{ParsingErrorContext, UntypedObject}, + profile::WalletProfile, + util::http_client, +}; + +pub mod parameters; +pub mod verification; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "UntypedObject", into = "UntypedObject")] +pub struct AuthorizationRequestObject( + UntypedObject, + ClientId, + ClientIdScheme, + ResponseMode, + ResponseType, + PresentationDefinitionIndirection, + Url, +); + +/// An Authorization Request. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthorizationRequest { + pub client_id: String, + #[serde(flatten)] + pub request_indirection: RequestIndirection, +} + +/// A RequestObject, passed by value or by reference. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum RequestIndirection { + #[serde(rename = "request")] + ByValue(String), + #[serde(rename = "request_uri")] + ByReference(Url), +} + +/// A PresentationDefinition, passed by value or by reference +#[derive(Debug, Clone)] +pub enum PresentationDefinitionIndirection { + ByValue(PresentationDefinition), + ByReference(Url), +} + +impl AuthorizationRequest { + /// Validate the [AuthorizationRequest] according to the client_id scheme and return the parsed + /// [RequestObject]. + /// + /// Custom wallet metadata can be provided, otherwise the default metadata for this profile is used. + pub async fn validate( + self, + wallet_profile: &WP, + ) -> Result { + let jwt = match self.request_indirection { + RequestIndirection::ByValue(jwt) => jwt, + RequestIndirection::ByReference(url) => http_client()? + .get(url.clone()) + .header("Prefer", "OID4VP-0.0.20") + .send() + .await + .context(format!("failed to GET {url}"))? + .text() + .await + .context(format!("failed to parse data from {url}"))?, + }; + let aro = verify_request(wallet_profile, jwt) + .await + .context("unable to validate Authorization Request")?; + if self.client_id.as_str() != aro.client_id().0.as_str() { + bail!( + "Authorization Request and Request Object have different client ids: '{}' vs. '{}'", + self.client_id, + aro.client_id().0 + ); + } + Ok(aro) + } + + /// Encode as [Url], using the `authorization_endpoint` as a base. + /// ``` + /// # use verifier_api::mock::authorization_request::AuthorizationRequest; + /// # use verifier_api::mock::authorization_request::RequestIndirection; + /// # use verifier_api::mock::client_id::ClientId; + /// # use url::Url; + /// let authorization_endpoint: Url = "example://".parse().unwrap(); + /// let authorization_request = AuthorizationRequest { + /// client_id: ClientId("xyz".to_string()), + /// request_indirection: RequestIndirection::ByValue("test".to_string()), + /// }; + /// + /// let authorization_request_url = authorization_request.to_url(authorization_endpoint).unwrap(); + /// + /// assert_eq!(authorization_request_url.as_str(), "example://?client_id=xyz&request=test"); + /// ``` + pub fn to_url(self, mut authorization_endpoint: Url) -> Result { + let query = serde_urlencoded::to_string(self)?; + authorization_endpoint.set_query(Some(&query)); + Ok(authorization_endpoint) + } + + /// Parse from [Url], validating the authorization_endpoint. + /// ``` + /// # use verifier_api::mock::authorization_request::AuthorizationRequest; + /// # use verifier_api::mock::authorization_request::RequestIndirection; + /// # use url::Url; + /// let url: Url = "example://?client_id=xyz&request=test".parse().unwrap(); + /// let authorization_endpoint: Url = "example://".parse().unwrap(); + /// + /// let authorization_request = AuthorizationRequest::from_url(url, &authorization_endpoint).unwrap(); + /// + /// assert_eq!(authorization_request.client_id.0, "xyz"); + /// + /// let RequestIndirection::ByValue(request_object) = authorization_request.request_indirection + /// else { panic!("expected request-by-value") }; + /// assert_eq!(request_object, "test"); + /// ``` + pub fn from_url(mut url: Url, authorization_endpoint: &Url) -> Result { + let query = url + .query() + .ok_or(anyhow!("missing query params in Authorization Request uri"))? + .to_string(); + url.set_query(None); + if &url != authorization_endpoint { + bail!("unexpected authorization_endpoint, expected '{authorization_endpoint}', received '{url}'") + } + Self::from_query_params(&query) + } + + /// Parse from urlencoded query parameters. + /// ``` + /// # use verifier_api::mock::authorization_request::AuthorizationRequest; + /// # use verifier_api::mock::authorization_request::RequestIndirection; + /// let query = "client_id=xyz&request=test"; + /// + /// let authorization_request = AuthorizationRequest::from_query_params(query).unwrap(); + /// + /// assert_eq!(authorization_request.client_id.0, "xyz"); + /// + /// let RequestIndirection::ByValue(request_object) = authorization_request.request_indirection + /// else { panic!("expected request-by-value") }; + /// assert_eq!(request_object, "test"); + /// ``` + pub fn from_query_params(query_params: &str) -> Result { + serde_urlencoded::from_str(query_params) + .context("unable to parse Authorization Request from query params") + } +} + +impl AuthorizationRequestObject { + pub fn client_id(&self) -> &ClientId { + &self.1 + } + + pub fn client_id_scheme(&self) -> &ClientIdScheme { + &self.2 + } + + pub async fn presentation_definition(&self) -> Result { + match &self.5 { + PresentationDefinitionIndirection::ByValue(by_value) => Ok(by_value.clone()), + PresentationDefinitionIndirection::ByReference(by_reference) => { + let value: Json = http_client()? + .get(by_reference.clone()) + .send() + .await + .context(format!( + "failed to GET Presentation Definition from '{by_reference}'" + ))? + .json() + .await + .context(format!( + "response received from '{by_reference}' was not JSON" + ))?; + value.try_into() + } + } + } + + pub fn is_id_token_requested(&self) -> Option { + match self.4 { + ResponseType::VpToken => Some(false), + ResponseType::VpTokenIdToken => Some(true), + ResponseType::Unsupported(_) => None, + } + } + + pub fn response_mode(&self) -> &ResponseMode { + &self.3 + } + + pub fn response_type(&self) -> &ResponseType { + &self.4 + } + + /// Uri to submit the response at. + /// + /// AKA [ResponseUri] or [RedirectUri] depending on [ResponseMode]. + pub fn return_uri(&self) -> &Url { + &self.6 + } +} + +impl From for UntypedObject { + fn from(value: AuthorizationRequestObject) -> Self { + let mut inner = value.0; + inner.insert(value.1); + inner.insert(value.2); + inner + } +} + +impl TryFrom for AuthorizationRequestObject { + type Error = Error; + + fn try_from(value: UntypedObject) -> std::result::Result { + let client_id = value.get().parsing_error()?; + let client_id_scheme = value + .get() + .parsing_error() + .context("this library cannot handle requests that omit client_id_scheme")?; + + let redirect_uri = value.get::(); + let response_uri = value.get::(); + + let (return_uri, response_mode) = match ( + redirect_uri, + response_uri, + value.get_or_default::().parsing_error()?, + ) { + (_, _, ResponseMode::Unsupported(m)) => { + bail!("this 'response_mode' ({m}) is not currently supported") + } + (Some(_), Some(_), _) => { + bail!("'response_uri' and 'redirect_uri' are mutually exclusive") + } + (_, None, response_mode @ ResponseMode::DirectPost) + | (_, None, response_mode @ ResponseMode::DirectPostJwt) => { + bail!("'response_uri' is required for this 'response_mode' ({response_mode})") + } + (_, Some(uri), response_mode @ ResponseMode::DirectPost) + | (_, Some(uri), response_mode @ ResponseMode::DirectPostJwt) => { + (uri.parsing_error()?.0, response_mode) + } + }; + + let response_type: ResponseType = value.get().parsing_error()?; + + let pd_indirection = match ( + value.get::(), + value.get::(), + ) { + (None, None) => bail!( + "one of 'presentation_definition' and 'presentation_definition_uri' are required" + ), + (Some(_), Some(_)) => { + bail!("'presentation_definition' and 'presentation_definition_uri' are mutually exclusive") + } + (Some(by_value), None) => { + PresentationDefinitionIndirection::ByValue(by_value.parsing_error()?) + } + (None, Some(by_reference)) => { + PresentationDefinitionIndirection::ByReference(by_reference.parsing_error()?.0) + } + }; + + Ok(Self( + value, + client_id, + client_id_scheme, + response_mode, + response_type, + pd_indirection, + return_uri, + )) + } +} + +impl Deref for AuthorizationRequestObject { + type Target = UntypedObject; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for AuthorizationRequestObject { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/src/core/authorization_request/parameters.rs b/src/core/authorization_request/parameters.rs new file mode 100644 index 0000000..ffb10f6 --- /dev/null +++ b/src/core/authorization_request/parameters.rs @@ -0,0 +1,422 @@ +use std::fmt; + +use crate::core::object::{TypedParameter, UntypedObject}; +use anyhow::Error; +use serde::{Deserialize, Serialize}; +use serde_json::Value as Json; +use url::Url; + +const DID: &str = "did"; +const ENTITY_ID: &str = "entity_id"; +const PREREGISTERED: &str = "pre-registered"; +const REDIRECT_URI: &str = "redirect_uri"; +const X509_SAN_DNS: &str = "x509_san_dns"; +const X509_SAN_URI: &str = "x509_san_uri"; + +#[derive(Debug, Clone)] +pub struct ClientId(pub String); + +#[derive(Debug, Clone, PartialEq)] +pub enum ClientIdScheme { + Did, + EntityId, + PreRegistered, + RedirectUri, + X509SanDns, + X509SanUri, + Other(String), +} + +impl TypedParameter for ClientId { + const KEY: &'static str = "client_id"; +} + +impl TryFrom for ClientId { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(Self(serde_json::from_value(value)?)) + } +} + +impl From for Json { + fn from(value: ClientId) -> Self { + Json::String(value.0) + } +} + +impl TypedParameter for ClientIdScheme { + const KEY: &'static str = "client_id_scheme"; +} + +impl From for ClientIdScheme { + fn from(s: String) -> Self { + match s.as_str() { + DID => ClientIdScheme::Did, + ENTITY_ID => ClientIdScheme::EntityId, + PREREGISTERED => ClientIdScheme::PreRegistered, + REDIRECT_URI => ClientIdScheme::RedirectUri, + X509_SAN_DNS => ClientIdScheme::X509SanDns, + X509_SAN_URI => ClientIdScheme::X509SanUri, + _ => ClientIdScheme::Other(s), + } + } +} + +impl From for String { + fn from(cis: ClientIdScheme) -> Self { + match cis { + ClientIdScheme::Did => DID.into(), + ClientIdScheme::EntityId => ENTITY_ID.into(), + ClientIdScheme::PreRegistered => PREREGISTERED.into(), + ClientIdScheme::RedirectUri => REDIRECT_URI.into(), + ClientIdScheme::X509SanDns => X509_SAN_DNS.into(), + ClientIdScheme::X509SanUri => X509_SAN_URI.into(), + ClientIdScheme::Other(u) => u, + } + } +} + +impl TryFrom for ClientIdScheme { + type Error = Error; + + fn try_from(value: Json) -> Result { + serde_json::from_value(value) + .map(String::into) + .map_err(Error::from) + } +} + +impl From for Json { + fn from(value: ClientIdScheme) -> Self { + Json::String(value.into()) + } +} + +impl fmt::Display for ClientIdScheme { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ClientIdScheme::Did => DID, + ClientIdScheme::EntityId => ENTITY_ID, + ClientIdScheme::PreRegistered => PREREGISTERED, + ClientIdScheme::RedirectUri => REDIRECT_URI, + ClientIdScheme::X509SanDns => X509_SAN_DNS, + ClientIdScheme::X509SanUri => X509_SAN_URI, + ClientIdScheme::Other(o) => &o, + } + .fmt(f) + } +} + +/// `client_metadata` field in the Authorization Request. +#[derive(Debug, Clone)] +pub struct ClientMetadata(UntypedObject); + +impl TypedParameter for ClientMetadata { + const KEY: &'static str = "client_metadata"; +} + +impl From for Json { + fn from(cm: ClientMetadata) -> Self { + cm.0 .0.into() + } +} + +impl TryFrom for ClientMetadata { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(serde_json::from_value(value).map(ClientMetadata)?) + } +} + +/// `client_metadata_uri` field in the Authorization Request. +#[derive(Debug, Clone)] +pub struct ClientMetadataUri(pub Url); + +impl TypedParameter for ClientMetadataUri { + const KEY: &'static str = "client_metadata_uri"; +} + +impl From for Json { + fn from(cmu: ClientMetadataUri) -> Self { + cmu.0.to_string().into() + } +} + +impl TryFrom for ClientMetadataUri { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(serde_json::from_value(value).map(ClientMetadataUri)?) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Nonce(pub String); + +impl TypedParameter for Nonce { + const KEY: &'static str = "nonce"; +} + +impl TryFrom for Nonce { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(Self(serde_json::from_value(value)?)) + } +} + +impl From for Json { + fn from(value: Nonce) -> Self { + Json::String(value.0) + } +} + +#[derive(Debug, Clone)] +pub struct Audience(pub String); + +impl TypedParameter for Audience { + const KEY: &'static str = "aud"; +} + +impl TryFrom for Audience { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(Self(serde_json::from_value(value)?)) + } +} + +impl From for Json { + fn from(value: Audience) -> Json { + Json::String(value.0) + } +} + +/// `redirect_uri` field in the Authorization Request. +#[derive(Debug, Clone)] +pub struct RedirectUri(pub Url); + +impl TypedParameter for RedirectUri { + const KEY: &'static str = "redirect_uri"; +} + +impl From for Json { + fn from(cmu: RedirectUri) -> Self { + cmu.0.to_string().into() + } +} + +impl TryFrom for RedirectUri { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(serde_json::from_value(value).map(RedirectUri)?) + } +} + +/// `response_uri` field in the Authorization Request. +#[derive(Debug, Clone)] +pub struct ResponseUri(pub Url); + +impl TypedParameter for ResponseUri { + const KEY: &'static str = "response_uri"; +} + +impl From for Json { + fn from(cmu: ResponseUri) -> Self { + cmu.0.to_string().into() + } +} + +impl TryFrom for ResponseUri { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(serde_json::from_value(value).map(ResponseUri)?) + } +} + +const DIRECT_POST: &str = "direct_post"; +const DIRECT_POST_JWT: &str = "direct_post.jwt"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(into = "String", from = "String")] +pub enum ResponseMode { + /// The `direct_post` response mode as defined in OID4VP. + DirectPost, + /// The `direct_post.jwt` response mode as defined in OID4VP. + DirectPostJwt, + /// A ResponseMode that is unsupported by this library. + Unsupported(String), +} + +impl TypedParameter for ResponseMode { + const KEY: &'static str = "response_mode"; +} + +impl From for ResponseMode { + fn from(s: String) -> Self { + match s.as_str() { + DIRECT_POST => ResponseMode::DirectPost, + DIRECT_POST_JWT => ResponseMode::DirectPostJwt, + _ => ResponseMode::Unsupported(s), + } + } +} + +impl From for String { + fn from(s: ResponseMode) -> Self { + match s { + ResponseMode::DirectPost => DIRECT_POST.into(), + ResponseMode::DirectPostJwt => DIRECT_POST_JWT.into(), + ResponseMode::Unsupported(u) => u, + } + } +} + +impl TryFrom for ResponseMode { + type Error = Error; + + fn try_from(value: Json) -> Result { + let s: String = serde_json::from_value(value)?; + Ok(s.into()) + } +} + +impl From for Json { + fn from(rm: ResponseMode) -> Self { + String::from(rm).into() + } +} + +impl fmt::Display for ResponseMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ResponseMode::DirectPost => DIRECT_POST, + ResponseMode::DirectPostJwt => DIRECT_POST_JWT, + ResponseMode::Unsupported(u) => u, + } + .fmt(f) + } +} + +impl Default for ResponseMode { + fn default() -> Self { + Self::Unsupported("fragment".into()) + } +} + +const VP_TOKEN: &str = "vp_token"; +const VP_TOKEN_ID_TOKEN: &str = "vp_token id_token"; + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[serde(into = "String", from = "String")] +pub enum ResponseType { + VpToken, + VpTokenIdToken, + Unsupported(String), +} + +impl From for String { + fn from(rt: ResponseType) -> Self { + match rt { + ResponseType::VpToken => VP_TOKEN.into(), + ResponseType::VpTokenIdToken => VP_TOKEN_ID_TOKEN.into(), + ResponseType::Unsupported(s) => s, + } + } +} + +impl From for ResponseType { + fn from(s: String) -> Self { + match s.as_str() { + VP_TOKEN => ResponseType::VpToken, + VP_TOKEN_ID_TOKEN => ResponseType::VpTokenIdToken, + _ => ResponseType::Unsupported(s), + } + } +} + +impl TypedParameter for ResponseType { + const KEY: &'static str = "response_type"; +} + +impl TryFrom for ResponseType { + type Error = Error; + + fn try_from(value: Json) -> Result { + let s: String = serde_json::from_value(value)?; + Ok(s.into()) + } +} + +impl From for Json { + fn from(rt: ResponseType) -> Self { + Json::String(rt.into()) + } +} + +#[derive(Debug, Clone)] +pub struct State(pub String); + +impl TypedParameter for State { + const KEY: &'static str = "state"; +} + +impl TryFrom for State { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(Self(serde_json::from_value(value)?)) + } +} + +impl From for Json { + fn from(value: State) -> Self { + Json::String(value.0) + } +} + +#[derive(Debug, Clone)] +pub struct PresentationDefinition(pub Json); + +impl TypedParameter for PresentationDefinition { + const KEY: &'static str = "presentation_definition"; +} + +impl TryFrom for PresentationDefinition { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(value).map(Self) + } +} + +impl From for Json { + fn from(value: PresentationDefinition) -> Self { + value.0 + } +} + +#[derive(Debug, Clone)] +pub struct PresentationDefinitionUri(pub Url); + +impl TypedParameter for PresentationDefinitionUri { + const KEY: &'static str = "presentation_definition_uri"; +} + +impl TryFrom for PresentationDefinitionUri { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(serde_json::from_value(value).map(Self)?) + } +} + +impl From for Json { + fn from(value: PresentationDefinitionUri) -> Self { + value.0.to_string().into() + } +} diff --git a/src/core/authorization_request/verification/did.rs b/src/core/authorization_request/verification/did.rs new file mode 100644 index 0000000..1d69eeb --- /dev/null +++ b/src/core/authorization_request/verification/did.rs @@ -0,0 +1,106 @@ +use crate::core::{ + authorization_request::AuthorizationRequestObject, + metadata::{parameters::wallet::RequestObjectSigningAlgValuesSupported, WalletMetadata}, + object::ParsingErrorContext, +}; +use anyhow::{bail, Context, Result}; +use base64::prelude::*; +use didkit::{resolve_key, DIDResolver}; +use serde_json::{Map, Value as Json}; + +/// Default implementation of request verification for `client_id_scheme` `did`. +/// +/// Uses the default didkit [DIDResolver]. +pub async fn verify( + wallet_metadata: &WalletMetadata, + request_object: &AuthorizationRequestObject, + request_jwt: String, + trusted_dids: Option<&[String]>, +) -> Result<()> { + verify_with_resolver( + wallet_metadata, + request_object, + request_jwt, + trusted_dids, + didkit::DID_METHODS.to_resolver(), + ) + .await +} + +/// Default implementation of request validation for `client_id_scheme` `did`. +pub async fn verify_with_resolver( + wallet_metadata: &WalletMetadata, + request_object: &AuthorizationRequestObject, + request_jwt: String, + trusted_dids: Option<&[String]>, + resolver: &dyn DIDResolver, +) -> Result<()> { + let (headers_b64, _, _) = didkit::ssi::jws::split_jws(&request_jwt)?; + + let headers_json_bytes = BASE64_URL_SAFE_NO_PAD + .decode(headers_b64) + .context("jwt headers were not valid base64url")?; + + let mut headers = serde_json::from_slice::>(&headers_json_bytes) + .context("jwt headers were not valid json")?; + + let Json::String(alg) = headers + .remove("alg") + .context("'alg' was missing from jwt headers")? + else { + bail!("'alg' header was not a string") + }; + + let supported_algs: RequestObjectSigningAlgValuesSupported = + wallet_metadata.get().parsing_error()?; + + if !supported_algs.0.contains(&alg) { + bail!("request was signed with unsupported algorithm: {alg}") + } + + let Json::String(kid) = headers + .remove("kid") + .context("'kid' was missing from jwt headers")? + else { + bail!("'kid' header was not a string") + }; + + let client_id = request_object.client_id(); + let (did, _f) = kid.split_once('#').context(format!( + "expected a DID verification method in 'kid' header, received '{kid}'" + ))?; + + if &client_id.0 != did { + bail!( + "DIDs from 'kid' ({did}) and 'client_id' ({}) do not match", + client_id.0 + ) + } + + if let Some(dids) = trusted_dids { + if !dids.iter().any(|trusted_did| trusted_did == did) { + bail!("'client_id' ({did}) is not in the list of trusted dids") + } + } + + println!( + "{}", + serde_json::to_string_pretty( + &didkit::dereference(resolver, did, &Default::default()) + .await + .1 + ) + .unwrap() + ); + + println!("{kid:?}"); + + let jwk = resolve_key(&kid, resolver) + .await + .context("unable to resolve verification method from 'kid' header")?; + + let _: Json = didkit::ssi::jwt::decode_verify(&request_jwt, &jwk) + .context("request signature could not be verified")?; + + Ok(()) +} diff --git a/src/core/authorization_request/verification/mod.rs b/src/core/authorization_request/verification/mod.rs new file mode 100644 index 0000000..459f98e --- /dev/null +++ b/src/core/authorization_request/verification/mod.rs @@ -0,0 +1,111 @@ +use crate::core::{object::UntypedObject, profile::WalletProfile}; +use anyhow::{bail, Context, Error, Result}; +use async_trait::async_trait; + +use super::{parameters::ClientIdScheme, AuthorizationRequestObject}; + +pub mod did; +pub mod x509_san_dns; +pub mod x509_san_uri; + +/// Verifies Authorization Request Objects. +#[async_trait] +pub trait RequestVerification { + /// Performs verification on Authorization Request Objects when `client_id_scheme` is `did`. + /// + /// See default implementation [did]. + async fn did( + &self, + decoded_request: &AuthorizationRequestObject, + request_jwt: String, + ) -> Result<(), Error>; + + /// Performs verification on Authorization Request Objects when `client_id_scheme` is `entity_id`. + async fn entity_id( + &self, + decoded_request: &AuthorizationRequestObject, + request_jwt: String, + ) -> Result<(), Error>; + + /// Performs verification on Authorization Request Objects when `client_id_scheme` is `pre-registered`. + async fn preregistered( + &self, + decoded_request: &AuthorizationRequestObject, + request_jwt: String, + ) -> Result<(), Error>; + + /// Performs verification on Authorization Request Objects when `client_id_scheme` is `redirect_uri`. + /// + /// See default implementation [redirect_uri]. + async fn redirect_uri( + &self, + decoded_request: &AuthorizationRequestObject, + request_jwt: String, + ) -> Result<(), Error>; + + /// Performs verification on Authorization Request Objects when `client_id_scheme` is `x509_san_dns`. + /// + /// See default implementation [x509_san_uri]. + async fn x509_san_dns( + &self, + decoded_request: &AuthorizationRequestObject, + request_jwt: String, + ) -> Result<(), Error>; + + /// Performs verification on Authorization Request Objects when `client_id_scheme` is `x509_san_uri`. + /// + /// See default implementation [x509_san_uri]. + async fn x509_san_uri( + &self, + decoded_request: &AuthorizationRequestObject, + request_jwt: String, + ) -> Result<(), Error>; + + /// Performs verification on Authorization Request Objects when `client_id_scheme` is any other value. + async fn other( + &self, + client_id_scheme: &str, + decoded_request: &AuthorizationRequestObject, + request_jwt: String, + ) -> Result<(), Error>; +} + +pub(crate) async fn verify_request( + profile: &WP, + jwt: String, +) -> Result { + let request: AuthorizationRequestObject = + didkit::ssi::jwt::decode_unverified::(&jwt) + .context("unable to decode Authorization Request Object JWT")? + .try_into()?; + + let client_id_scheme = request.client_id_scheme(); + + if !profile + .wallet_metadata() + .client_id_schemes_supported() + .0 + .contains(client_id_scheme) + { + bail!( + "wallet does not support client_id_scheme '{}'", + client_id_scheme + ) + } + + profile + .validate_request(&request) + .context("unable to validate request according to profile-specific checks:")?; + + match client_id_scheme { + ClientIdScheme::Did => profile.did(&request, jwt).await?, + ClientIdScheme::EntityId => profile.entity_id(&request, jwt).await?, + ClientIdScheme::PreRegistered => profile.preregistered(&request, jwt).await?, + ClientIdScheme::RedirectUri => profile.redirect_uri(&request, jwt).await?, + ClientIdScheme::X509SanDns => profile.x509_san_dns(&request, jwt).await?, + ClientIdScheme::X509SanUri => profile.x509_san_uri(&request, jwt).await?, + ClientIdScheme::Other(scheme) => profile.other(&scheme, &request, jwt).await?, + }; + + Ok(request) +} diff --git a/src/core/authorization_request/verification/x509_san_dns.rs b/src/core/authorization_request/verification/x509_san_dns.rs new file mode 100644 index 0000000..5e0d923 --- /dev/null +++ b/src/core/authorization_request/verification/x509_san_dns.rs @@ -0,0 +1,141 @@ +use anyhow::{bail, Context, Error, Result}; +use base64::prelude::*; +use p256::ecdsa::signature::Verifier as _; +use serde_json::{Map, Value as Json}; +use tracing::debug; +use x509_cert::{ + der::{referenced::OwnedToRef, Decode}, + ext::pkix::{name::GeneralName, SubjectAltName}, + spki::SubjectPublicKeyInfoRef, + Certificate, +}; + +use crate::core::{ + authorization_request::AuthorizationRequestObject, + metadata::{parameters::wallet::RequestObjectSigningAlgValuesSupported, WalletMetadata}, + object::ParsingErrorContext, +}; + +/// Default implementation of request validation for `client_id_scheme` `x509_san_dns`. +pub fn validate( + wallet_metadata: &WalletMetadata, + request_object: &AuthorizationRequestObject, + request_jwt: String, + trusted_roots: Option<&[Certificate]>, +) -> Result<()> { + let client_id = request_object.client_id().0.as_str(); + let (headers_b64, body_b64, sig_b64) = didkit::ssi::jws::split_jws(&request_jwt)?; + + let headers_json_bytes = BASE64_URL_SAFE_NO_PAD + .decode(headers_b64) + .context("jwt headers were not valid base64url")?; + + let mut headers = serde_json::from_slice::>(&headers_json_bytes) + .context("jwt headers were not valid json")?; + + let Json::String(alg) = headers + .remove("alg") + .context("'alg' was missing from jwt headers")? + else { + bail!("'alg' header was not a string") + }; + + let supported_algs: RequestObjectSigningAlgValuesSupported = + wallet_metadata.get().parsing_error()?; + + if !supported_algs.0.contains(&alg) { + bail!("request was signed with unsupported algorithm: {alg}") + } + + let Json::Array(x5chain) = headers + .remove("x5c") + .context("'x5c' was missing from jwt headers")? + else { + bail!("'x5c' header was not an array") + }; + + let Json::String(b64_x509) = x5chain.get(0).context("'x5c' was an empty array")? else { + bail!("'x5c' header was not an array of strings"); + }; + + let leaf_cert_der = BASE64_STANDARD_NO_PAD + .decode(b64_x509.trim_end_matches('=')) + .context("leaf certificate in 'x5c' was not valid base64")?; + + let leaf_cert = Certificate::from_der(&leaf_cert_der) + .context("leaf certificate in 'x5c' was not valid DER")?; + + if !leaf_cert + .tbs_certificate + .filter::() + .filter_map(|r| match r { + Ok((_crit, san)) => Some(san.0.into_iter()), + Err(e) => { + debug!("unable to parse SubjectAlternativeName from DER: {e}"); + None + } + }) + .flatten() + .filter_map(|gn| match gn { + GeneralName::DnsName(dns) => Some(dns.to_string()), + _ => { + debug!("found non-URI SAN: {gn:?}"); + None + } + }) + .any(|uri| uri == client_id) + { + bail!("client_id does not match any Subject Alternative Name") + } + + if let Some(_trusted_roots) = trusted_roots { + // TODO: Verify chain to root. + } + + let verifier = V::from_spki( + leaf_cert + .tbs_certificate + .subject_public_key_info + .owned_to_ref(), + alg, + ) + .context("unable to parse SPKI")?; + + let payload = [headers_b64.as_bytes(), b".", body_b64.as_bytes()].concat(); + let signature = BASE64_URL_SAFE_NO_PAD + .decode(sig_b64) + .context("could not decode base64url encoded jwt signature")?; + + verifier + .verify(&payload, &signature) + .context("request signature could not be verified")?; + + Ok(()) +} + +pub trait Verifier: Sized { + /// Construct a [Verifier] from [SubjectPublicKeyInfoRef]. + /// + /// ## Params + /// * `spki` - the public key information necessary to construct a [Verifier]. + /// * `algorithm` - the value taken from the `alg` header of the request, to hint at what curve should be used by the [Verifier]. + fn from_spki<'a>(spki: SubjectPublicKeyInfoRef<'a>, algorithm: String) -> Result; + fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()>; +} + +#[derive(Debug, Clone)] +pub struct P256Verifier(p256::ecdsa::VerifyingKey); + +impl Verifier for P256Verifier { + fn from_spki<'a>(spki: SubjectPublicKeyInfoRef<'a>, algorithm: String) -> Result { + if algorithm != "ES256" { + bail!("P256Verifier cannot verify requests signed with '{algorithm}'") + } + spki.try_into().map(Self).map_err(Error::from) + } + + fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> { + let signature = p256::ecdsa::Signature::from_slice(signature)?; + self.0.verify(payload, &signature).map_err(Error::from) + } +} diff --git a/src/core/authorization_request/verification/x509_san_uri.rs b/src/core/authorization_request/verification/x509_san_uri.rs new file mode 100644 index 0000000..30e6489 --- /dev/null +++ b/src/core/authorization_request/verification/x509_san_uri.rs @@ -0,0 +1,155 @@ +use anyhow::{bail, Context, Error, Result}; +use base64::prelude::*; +use p256::ecdsa::signature::Verifier as _; +use serde_json::{Map, Value as Json}; +use tracing::{debug, warn}; +use x509_cert::{ + der::{referenced::OwnedToRef, Decode}, + ext::pkix::{name::GeneralName, SubjectAltName}, + spki::SubjectPublicKeyInfoRef, + Certificate, +}; + +use crate::core::{ + authorization_request::AuthorizationRequestObject, + metadata::{parameters::wallet::RequestObjectSigningAlgValuesSupported, WalletMetadata}, + object::ParsingErrorContext, +}; + +/// Default implementation of request validation for `client_id_scheme` `x509_san_uri`. +pub fn validate( + wallet_metadata: &WalletMetadata, + request_object: &AuthorizationRequestObject, + request_jwt: String, + trusted_roots: Option<&[Certificate]>, +) -> Result<()> { + let client_id = request_object.client_id().0.as_str(); + let (headers_b64, body_b64, sig_b64) = didkit::ssi::jws::split_jws(&request_jwt)?; + + let headers_json_bytes = BASE64_URL_SAFE_NO_PAD + .decode(headers_b64) + .context("jwt headers were not valid base64url")?; + + let mut headers = serde_json::from_slice::>(&headers_json_bytes) + .context("jwt headers were not valid json")?; + + let Json::String(alg) = headers + .remove("alg") + .context("'alg' was missing from jwt headers")? + else { + bail!("'alg' header was not a string") + }; + + let supported_algs: RequestObjectSigningAlgValuesSupported = + wallet_metadata.get().parsing_error()?; + + if !supported_algs.0.contains(&alg) { + bail!("request was signed with unsupported algorithm: {alg}") + } + + let Json::Array(x5chain) = headers + .remove("x5c") + .context("'x5c' was missing from jwt headers")? + else { + bail!("'x5c' header was not an array") + }; + + let Json::String(b64_x509) = x5chain.get(0).context("'x5c' was an empty array")? else { + bail!("'x5c' header was not an array of strings"); + }; + + let leaf_cert_der = BASE64_STANDARD_NO_PAD + .decode(b64_x509.trim_end_matches('=')) + .context("leaf certificate in 'x5c' was not valid base64")?; + + let leaf_cert = Certificate::from_der(&leaf_cert_der) + .context("leaf certificate in 'x5c' was not valid DER")?; + + // NOTE: Fallback to common name is removed in latest drafts of OID4VP. + if leaf_cert.tbs_certificate.get::() == Ok(None) { + warn!("x509 certificate does not contain Subject Alternative Name, falling back to Common Name"); + if !leaf_cert + .tbs_certificate + .subject + .0 + .iter() + .flat_map(|n| n.0.iter()) + .filter_map(|n| n.to_string().strip_prefix("CN=").map(ToOwned::to_owned)) + .any(|cn| cn == client_id) + { + bail!("client_id does not match Common Name and x509 certificate does not contain Subject Alternative Name") + } + } else if !leaf_cert + .tbs_certificate + .filter::() + .filter_map(|r| match r { + Ok((_crit, san)) => Some(san.0.into_iter()), + Err(e) => { + debug!("unable to parse SubjectAlternativeName from DER: {e}"); + None + } + }) + .flatten() + .filter_map(|gn| match gn { + GeneralName::UniformResourceIdentifier(uri) => Some(uri.to_string()), + _ => { + debug!("found non-URI SAN: {gn:?}"); + None + } + }) + .any(|uri| uri == client_id) + { + bail!("client_id does not match any Subject Alternative Name") + } + + if let Some(_trusted_roots) = trusted_roots { + // TODO: Verify chain to root. + } + + let verifier = V::from_spki( + leaf_cert + .tbs_certificate + .subject_public_key_info + .owned_to_ref(), + alg, + ) + .context("unable to parse SPKI")?; + + let payload = [headers_b64.as_bytes(), b".", body_b64.as_bytes()].concat(); + let signature = BASE64_URL_SAFE_NO_PAD + .decode(sig_b64) + .context("could not decode base64url encoded jwt signature")?; + + verifier + .verify(&payload, &signature) + .context("request signature could not be verified")?; + + Ok(()) +} + +pub trait Verifier: Sized { + /// Construct a [Verifier] from [SubjectPublicKeyInfoRef]. + /// + /// ## Params + /// * `spki` - the public key information necessary to construct a [Verifier]. + /// * `algorithm` - the value taken from the `alg` header of the request, to hint at what curve should be used by the [Verifier]. + fn from_spki<'a>(spki: SubjectPublicKeyInfoRef<'a>, algorithm: String) -> Result; + fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()>; +} + +#[derive(Debug, Clone)] +pub struct P256Verifier(p256::ecdsa::VerifyingKey); + +impl Verifier for P256Verifier { + fn from_spki<'a>(spki: SubjectPublicKeyInfoRef<'a>, algorithm: String) -> Result { + if algorithm != "ES256" { + bail!("P256Verifier cannot verify requests signed with '{algorithm}'") + } + spki.try_into().map(Self).map_err(Error::from) + } + + fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()> { + let signature = p256::ecdsa::Signature::from_slice(signature)?; + self.0.verify(payload, &signature).map_err(Error::from) + } +} diff --git a/src/core/credential_format/mod.rs b/src/core/credential_format/mod.rs new file mode 100644 index 0000000..863258e --- /dev/null +++ b/src/core/credential_format/mod.rs @@ -0,0 +1,17 @@ +/// A credential format that can be transmitted using OID4VP. +pub trait CredentialFormat { + /// The ID of the credential format. + const ID: &'static str; +} + +pub struct MsoMdoc; + +impl CredentialFormat for MsoMdoc { + const ID: &'static str = "mso_mdoc"; +} + +pub struct JwtVc; + +impl CredentialFormat for JwtVc { + const ID: &'static str = "jwt_vc"; +} diff --git a/src/core/metadata/mod.rs b/src/core/metadata/mod.rs new file mode 100644 index 0000000..fc63b6b --- /dev/null +++ b/src/core/metadata/mod.rs @@ -0,0 +1,89 @@ +use std::ops::{Deref, DerefMut}; + +use anyhow::Error; +use serde::{Deserialize, Serialize}; + +use self::parameters::wallet::{ + AuthorizationEndpoint, ClientIdSchemesSupported, VpFormatsSupported, +}; + +use super::object::{ParsingErrorContext, UntypedObject}; + +pub mod parameters; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "UntypedObject", into = "UntypedObject")] +pub struct WalletMetadata( + UntypedObject, + AuthorizationEndpoint, + VpFormatsSupported, + ClientIdSchemesSupported, +); + +impl WalletMetadata { + pub fn new( + authorization_endpoint: AuthorizationEndpoint, + vp_formats_supported: VpFormatsSupported, + client_id_schemes_supported: Option, + other: Option, + ) -> Self { + Self( + other.unwrap_or_default(), + authorization_endpoint, + vp_formats_supported, + client_id_schemes_supported.unwrap_or_default(), + ) + } + + pub fn authorization_endpoint(&self) -> &AuthorizationEndpoint { + &self.1 + } + + pub fn vp_formats_supported(&self) -> &VpFormatsSupported { + &self.2 + } + + pub fn client_id_schemes_supported(&self) -> &ClientIdSchemesSupported { + &self.3 + } +} + +impl From for UntypedObject { + fn from(value: WalletMetadata) -> Self { + let mut inner = value.0; + inner.insert(value.1); + inner.insert(value.2); + inner.insert(value.3); + inner + } +} + +impl TryFrom for WalletMetadata { + type Error = Error; + + fn try_from(value: UntypedObject) -> Result { + let authorization_endpoint = value.get().parsing_error()?; + let vp_formats_supported = value.get().parsing_error()?; + let client_id_schemes_supported = value.get_or_default().parsing_error()?; + Ok(Self( + value, + authorization_endpoint, + vp_formats_supported, + client_id_schemes_supported, + )) + } +} + +impl Deref for WalletMetadata { + type Target = UntypedObject; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for WalletMetadata { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} diff --git a/src/core/metadata/parameters/mod.rs b/src/core/metadata/parameters/mod.rs new file mode 100644 index 0000000..8aeb810 --- /dev/null +++ b/src/core/metadata/parameters/mod.rs @@ -0,0 +1,4 @@ +/// Metadata supplied by the verifier, also known as the Client Metadata. +pub mod verifier; +/// Metadata supplied by the wallet, also known as the Authorization Server Metadata. +pub mod wallet; diff --git a/src/core/metadata/parameters/verifier.rs b/src/core/metadata/parameters/verifier.rs new file mode 100644 index 0000000..365d0f5 --- /dev/null +++ b/src/core/metadata/parameters/verifier.rs @@ -0,0 +1,112 @@ +use anyhow::Error; +use serde::Deserialize; +use serde_json::{Map, Value as Json}; + +use crate::core::object::TypedParameter; + +#[derive(Debug, Clone, Deserialize)] +pub struct JWKs { + pub keys: Vec>, +} + +impl TypedParameter for JWKs { + const KEY: &'static str = "jwks"; +} + +impl TryFrom for JWKs { + type Error = Error; + + fn try_from(value: Json) -> Result { + serde_json::from_value(value).map_err(Into::into) + } +} + +impl From for Json { + fn from(value: JWKs) -> Json { + let keys = value.keys.into_iter().map(Json::Object).collect(); + let mut obj = Map::default(); + obj.insert("keys".into(), Json::Array(keys)); + obj.into() + } +} + +#[derive(Debug, Clone)] +pub struct RequireSignedRequestObject(pub bool); + +impl TypedParameter for RequireSignedRequestObject { + const KEY: &'static str = "require_signed_request_object"; +} + +impl TryFrom for RequireSignedRequestObject { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(Self(serde_json::from_value(value)?)) + } +} + +impl From for Json { + fn from(value: RequireSignedRequestObject) -> Json { + Json::Bool(value.0) + } +} + +#[cfg(test)] +mod test { + use serde_json::json; + + use crate::core::object::UntypedObject; + + use super::*; + + fn metadata() -> UntypedObject { + serde_json::from_value(json!( + { + "jwks":{ + "keys":[ + { + "kty":"EC", + "crv":"P-256", + "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", + "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", + "use":"enc", + "kid":"1" + } + ] + }, + "authorization_encrypted_response_alg":"ECDH-ES", + "authorization_encrypted_response_enc":"A256GCM", + "require_signed_request_object":true, + "vp_formats":{ "mso_mdoc":{} } + } + )) + .unwrap() + } + + #[test] + fn jwks() { + let JWKs { keys } = metadata().get().unwrap().unwrap(); + assert_eq!(keys.len(), 1); + + let jwk = &keys[0]; + assert_eq!(jwk.get("kty").unwrap(), "EC"); + assert_eq!(jwk.get("crv").unwrap(), "P-256"); + assert_eq!( + jwk.get("x").unwrap(), + "MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4" + ); + assert_eq!( + jwk.get("y").unwrap(), + "4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM" + ); + assert_eq!(jwk.get("use").unwrap(), "enc"); + assert_eq!(jwk.get("kid").unwrap(), "1"); + } + + #[test] + fn require_signed_request_object() { + let exp = true; + let RequireSignedRequestObject(b) = metadata().get().unwrap().unwrap(); + assert_eq!(b, exp); + } +} diff --git a/src/core/metadata/parameters/wallet.rs b/src/core/metadata/parameters/wallet.rs new file mode 100644 index 0000000..27ef948 --- /dev/null +++ b/src/core/metadata/parameters/wallet.rs @@ -0,0 +1,300 @@ +use crate::core::{ + authorization_request::parameters::{ClientIdScheme, ResponseType}, + object::TypedParameter, +}; +use anyhow::{bail, Error, Result}; +use serde_json::{Map, Value as Json}; +use url::Url; + +#[derive(Debug, Clone)] +pub struct Issuer(pub String); + +impl TypedParameter for Issuer { + const KEY: &'static str = "issuer"; +} + +impl TryFrom for Issuer { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(Self(serde_json::from_value(value)?)) + } +} + +impl From for Json { + fn from(value: Issuer) -> Json { + Json::String(value.0) + } +} + +#[derive(Debug, Clone)] +pub struct AuthorizationEndpoint(pub Url); + +impl TypedParameter for AuthorizationEndpoint { + const KEY: &'static str = "authorization_endpoint"; +} + +impl TryFrom for AuthorizationEndpoint { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(Self(serde_json::from_value(value)?)) + } +} + +impl From for Json { + fn from(value: AuthorizationEndpoint) -> Json { + Json::String(value.0.to_string()) + } +} + +#[derive(Debug, Clone)] +pub struct ResponseTypesSupported(pub Vec); + +impl TypedParameter for ResponseTypesSupported { + const KEY: &'static str = "response_types_supported"; +} + +impl TryFrom for ResponseTypesSupported { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(Self(serde_json::from_value(value)?)) + } +} + +impl From for Json { + fn from(value: ResponseTypesSupported) -> Json { + Json::Array( + value + .0 + .iter() + .cloned() + .map(String::from) + .map(Json::from) + .collect(), + ) + } +} + +// TODO: Client ID scheme types? +#[derive(Debug, Clone)] +pub struct ClientIdSchemesSupported(pub Vec); + +impl TypedParameter for ClientIdSchemesSupported { + const KEY: &'static str = "client_id_schemes_supported"; +} + +impl TryFrom for ClientIdSchemesSupported { + type Error = Error; + + fn try_from(value: Json) -> Result { + let Json::Array(xs) = value else { + bail!("expected JSON array") + }; + xs.into_iter() + .map(Json::try_into) + .collect::>>() + .map(Self) + } +} + +impl From for Json { + fn from(value: ClientIdSchemesSupported) -> Json { + Json::Array(value.0.into_iter().map(Json::from).collect()) + } +} + +impl Default for ClientIdSchemesSupported { + fn default() -> Self { + Self(vec![ClientIdScheme::PreRegistered]) + } +} + +#[derive(Debug, Clone)] +pub struct RequestObjectSigningAlgValuesSupported(pub Vec); + +impl TypedParameter for RequestObjectSigningAlgValuesSupported { + const KEY: &'static str = "request_object_signing_alg_values_supported"; +} + +impl TryFrom for RequestObjectSigningAlgValuesSupported { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(Self(serde_json::from_value(value)?)) + } +} + +impl From for Json { + fn from(value: RequestObjectSigningAlgValuesSupported) -> Json { + Json::Array(value.0.into_iter().map(Json::from).collect()) + } +} + +// TODO: Better types +#[derive(Debug, Clone)] +pub struct VpFormatsSupported(pub Map); + +impl TypedParameter for VpFormatsSupported { + const KEY: &'static str = "vp_formats_supported"; +} + +impl TryFrom for VpFormatsSupported { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(Self(serde_json::from_value(value)?)) + } +} + +impl From for Json { + fn from(value: VpFormatsSupported) -> Json { + Json::Object(value.0) + } +} + +#[derive(Debug, Clone)] +pub struct AuthorizationEncryptionAlgValuesSupported(pub Vec); + +impl TypedParameter for AuthorizationEncryptionAlgValuesSupported { + const KEY: &'static str = "authorization_encryption_alg_values_supported"; +} + +impl TryFrom for AuthorizationEncryptionAlgValuesSupported { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(Self(serde_json::from_value(value)?)) + } +} + +impl From for Json { + fn from(value: AuthorizationEncryptionAlgValuesSupported) -> Json { + Json::Array(value.0.into_iter().map(Json::from).collect()) + } +} + +#[derive(Debug, Clone)] +pub struct AuthorizationEncryptionEncValuesSupported(pub Vec); + +impl TypedParameter for AuthorizationEncryptionEncValuesSupported { + const KEY: &'static str = "authorization_encryption_enc_values_supported"; +} + +impl TryFrom for AuthorizationEncryptionEncValuesSupported { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(Self(serde_json::from_value(value)?)) + } +} + +impl From for Json { + fn from(value: AuthorizationEncryptionEncValuesSupported) -> Json { + Json::Array(value.0.into_iter().map(Json::from).collect()) + } +} + +#[cfg(test)] +mod test { + use serde_json::json; + + use crate::core::object::UntypedObject; + + use super::*; + + fn metadata() -> UntypedObject { + serde_json::from_value(json!({ + "issuer": "https://self-issued.me/v2", + "authorization_endpoint": "mdoc-openid4vp://", + "response_types_supported": [ + "vp_token" + ], + "vp_formats_supported": + { + "mso_mdoc": { + } + }, + "client_id_schemes_supported": [ + "redirect_uri", + "x509_san_uri" + ], + "request_object_signing_alg_values_supported": [ + "ES256" + ], + "authorization_encryption_alg_values_supported": [ + "ECDH-ES" + ], + "authorization_encryption_enc_values_supported": [ + "A256GCM" + ] + } + )) + .unwrap() + } + + #[test] + fn issuer() { + let exp = "https://self-issued.me/v2"; + let Issuer(s) = metadata().get().unwrap().unwrap(); + assert_eq!(s, exp); + } + + #[test] + fn authorization_endpoint() { + let exp = "mdoc-openid4vp://".parse().unwrap(); + let AuthorizationEndpoint(s) = metadata().get().unwrap().unwrap(); + assert_eq!(s, exp); + } + + #[test] + fn response_types_supported() { + let exp = vec![ResponseType::VpToken]; + let ResponseTypesSupported(v) = metadata().get().unwrap().unwrap(); + assert!(exp.iter().all(|x| v.contains(x))); + assert!(v.iter().all(|x| exp.contains(x))); + } + + #[test] + fn client_id_schemes_supported() { + let exp = vec![ClientIdScheme::RedirectUri, ClientIdScheme::X509SanUri]; + let ClientIdSchemesSupported(v) = metadata().get().unwrap().unwrap(); + assert!(exp.iter().all(|x| v.contains(x))); + assert!(v.iter().all(|x| exp.contains(x))); + } + + #[test] + fn request_object_signing_alg_values_supported() { + let exp = vec!["ES256".to_string()]; + let RequestObjectSigningAlgValuesSupported(v) = metadata().get().unwrap().unwrap(); + assert!(exp.iter().all(|x| v.contains(x))); + assert!(v.iter().all(|x| exp.contains(x))); + } + + #[test] + fn vp_formats_supported() { + let VpFormatsSupported(mut m) = metadata().get().unwrap().unwrap(); + assert_eq!(m.len(), 1); + assert_eq!( + m.remove("mso_mdoc").unwrap(), + Json::Object(Default::default()) + ); + } + + #[test] + fn authorization_encryption_alg_values_supported() { + let exp = vec!["ECDH-ES".to_string()]; + let AuthorizationEncryptionAlgValuesSupported(v) = metadata().get().unwrap().unwrap(); + assert!(exp.iter().all(|x| v.contains(x))); + assert!(v.iter().all(|x| exp.contains(x))); + } + + #[test] + fn authorization_encryption_enc_values_supported() { + let exp = vec!["A256GCM".to_string()]; + let AuthorizationEncryptionEncValuesSupported(v) = metadata().get().unwrap().unwrap(); + assert!(exp.iter().all(|x| v.contains(x))); + assert!(v.iter().all(|x| exp.contains(x))); + } +} diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..5b5ccda --- /dev/null +++ b/src/core/mod.rs @@ -0,0 +1,9 @@ +pub mod authorization_request; +pub mod credential_format; +pub mod metadata; +pub mod object; +pub mod profile; +pub mod response; +pub(crate) mod util; +pub mod verifier; +pub mod wallet; diff --git a/src/core/object/mod.rs b/src/core/object/mod.rs new file mode 100644 index 0000000..0d7de33 --- /dev/null +++ b/src/core/object/mod.rs @@ -0,0 +1,105 @@ +use std::collections::HashMap; + +use anyhow::{Context, Error, Result}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value as Json}; + +/// An untyped (JSON) Object from which [TypedParameters](TypedParameter) can be parsed. +/// +/// Can represent metadata or request objects. +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct UntypedObject(pub(crate) Map); + +// TODO: Replace anyhow error type. +/// A strongly typed parameter that can represent metadata entries or request parameters. +pub trait TypedParameter: + TryFrom + Into + Clone + std::fmt::Debug +{ + const KEY: &'static str; +} + +impl UntypedObject { + /// Get a [TypedParameter] from the Object or return the default value. + /// + /// Note that this method clones the underlying data. + pub fn get_or_default(&self) -> Result { + Ok(self + .0 + .get(T::KEY) + .cloned() + .map(TryInto::try_into) + .transpose()? + .unwrap_or_default()) + } + + /// Get a [TypedParameter] from the Object. + /// + /// Note that this method clones the underlying data. + pub fn get(&self) -> Option> { + Some(self.0.get(T::KEY)?.clone().try_into().map_err(Into::into)) + } + + /// Remove a [TypedParameter] from the Object. + pub fn remove(&mut self) -> Option> { + Some(self.0.remove(T::KEY)?.try_into().map_err(Into::into)) + } + + /// Insert a [TypedParameter]. + /// + /// Returns the existing [TypedParameter] if one already exists. + /// + /// # Errors + /// Returns an error if there was already an entry in the Object, but it could not be parsed from JSON. + pub fn insert(&mut self, t: T) -> Option> { + Some( + self.0 + .insert(T::KEY.to_owned(), t.into())? + .try_into() + .map_err(Into::into), + ) + } + + /// Flatten the structure for posting as a form. + pub(crate) fn flatten_for_form(self) -> Result> { + self.0 + .into_iter() + .map(|(k, v)| { + if let Json::String(s) = v { + return Ok((k, s)); + } + serde_json::to_string(&v) + .map(|v| (k, v)) + .map_err(Error::from) + }) + .collect() + } +} + +impl From for Json { + fn from(value: UntypedObject) -> Self { + value.0.into() + } +} + +pub trait ParsingErrorContext { + type T: TypedParameter; + + fn parsing_error(self) -> Result; +} + +impl ParsingErrorContext for Option> { + type T = T; + + fn parsing_error(self) -> Result { + self.context(format!("'{}' is missing", T::KEY))? + .context(format!("'{}' could not be parsed", T::KEY)) + } +} + +impl ParsingErrorContext for Result { + type T = T; + + fn parsing_error(self) -> Result { + self.context(format!("'{}' could not be parsed", T::KEY)) + } +} diff --git a/src/core/profile/mod.rs b/src/core/profile/mod.rs new file mode 100644 index 0000000..66a3560 --- /dev/null +++ b/src/core/profile/mod.rs @@ -0,0 +1,98 @@ +use anyhow::{bail, Context, Error}; +use async_trait::async_trait; +use tracing::warn; +use url::Url; + +use super::{ + authorization_request::{ + parameters::{PresentationDefinition, ResponseMode}, + verification::RequestVerification, + AuthorizationRequest, AuthorizationRequestObject, + }, + credential_format::CredentialFormat, + metadata::WalletMetadata, + response::{AuthorizationResponse, PostRedirection}, + util::http_client, +}; + +/// A specific profile of OID4VP. +pub trait Profile { + /// Credential Format used in this profile. + type CredentialFormat: CredentialFormat; + + /// Builder for profile-specific [PresentationDefinition]. + type PresentationBuilder: PresentationBuilder; + + /// Perform additional profile-specific checks on outbound and inbound requests. + fn validate_request(&self, request_object: &AuthorizationRequestObject) -> Result<(), Error>; +} + +pub trait PresentationBuilder: Default { + fn build(self) -> Result; +} + +#[async_trait] +pub trait WalletProfile: Profile + RequestVerification + Sync { + type PresentationHandler: PresentationHandler; + + fn wallet_metadata(&self) -> &WalletMetadata; + + async fn to_handler( + &self, + request_object: &AuthorizationRequestObject, + ) -> Result; + + async fn handle_request(&self, url: Url) -> Result { + let ar = + AuthorizationRequest::from_url(url, &self.wallet_metadata().authorization_endpoint().0) + .context("unable to parse authorization request")?; + let aro = ar + .validate(self) + .await + .context("unable to validate authorization request")?; + self.to_handler(&aro).await + } + + async fn submit_response( + &self, + handler: Self::PresentationHandler, + ) -> Result, Error> { + let aro = handler.request().clone(); + let response_object = handler.to_response()?; + let return_uri = aro.return_uri(); + match aro.response_mode() { + ResponseMode::DirectPost => { + let body = response_object + .serializable() + .flatten_for_form() + .context("unable to flatten authorization response")?; + let response = http_client()? + .post(return_uri.clone()) + .form(&body) + .header("Prefer", "OID4VP-0.0.20") + .send() + .await + .context("failed to post authorization response")?; + + let status = response.status(); + let text = response.text().await.context("text")?; + + if !status.is_success() { + bail!("error submitting authorization response ({status}): {text}") + } + + Ok(serde_json::from_str(&text) + .map_err(|e| warn!("response did not contain a redirect: {e}")) + .ok() + .map(|PostRedirection { redirect_uri }| redirect_uri)) + } + ResponseMode::DirectPostJwt => todo!(), + ResponseMode::Unsupported(rm) => bail!("unsupported response_mode {rm}"), + } + } +} + +pub trait PresentationHandler: Send { + fn request(&self) -> &AuthorizationRequestObject; + fn to_response(self) -> Result; +} diff --git a/src/core/response/mod.rs b/src/core/response/mod.rs new file mode 100644 index 0000000..1e25eda --- /dev/null +++ b/src/core/response/mod.rs @@ -0,0 +1,37 @@ +use anyhow::Error; +use serde::{Deserialize, Serialize}; +use url::Url; + +use self::parameters::{PresentationSubmission, VpToken}; + +use super::object::{ParsingErrorContext, UntypedObject}; + +pub mod parameters; + +#[derive(Debug, Clone)] +pub struct AuthorizationResponse(UntypedObject, VpToken, PresentationSubmission); + +impl AuthorizationResponse { + pub fn as_query(self) -> Result { + Ok(serde_urlencoded::to_string(self.0)?) + } + + pub fn serializable(self) -> UntypedObject { + self.0 + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PostRedirection { + pub redirect_uri: Url, +} + +impl TryFrom for AuthorizationResponse { + type Error = Error; + + fn try_from(value: UntypedObject) -> Result { + let vp_token = value.get().parsing_error()?; + let presentation_submission = value.get().parsing_error()?; + Ok(Self(value, vp_token, presentation_submission)) + } +} diff --git a/src/core/response/parameters.rs b/src/core/response/parameters.rs new file mode 100644 index 0000000..3a0411c --- /dev/null +++ b/src/core/response/parameters.rs @@ -0,0 +1,69 @@ +use anyhow::Error; +use serde_json::Value as Json; + +pub use crate::core::authorization_request::parameters::State; +use crate::core::object::TypedParameter; + +#[derive(Debug, Clone)] +pub struct IdToken(pub String); + +impl TypedParameter for IdToken { + const KEY: &'static str = "id_token"; +} + +impl TryFrom for IdToken { + type Error = Error; + + fn try_from(value: Json) -> Result { + serde_json::from_value(value).map(Self).map_err(Into::into) + } +} + +impl From for Json { + fn from(value: IdToken) -> Self { + value.0.into() + } +} + +#[derive(Debug, Clone)] +pub struct VpToken(pub String); + +impl TypedParameter for VpToken { + const KEY: &'static str = "vp_token"; +} + +impl TryFrom for VpToken { + type Error = Error; + + fn try_from(value: Json) -> Result { + serde_json::from_value(value).map(Self).map_err(Into::into) + } +} + +impl From for Json { + fn from(value: VpToken) -> Self { + value.0.into() + } +} + +// TODO: Better type. +#[derive(Debug, Clone)] +pub struct PresentationSubmission(pub Json); + +impl TypedParameter for PresentationSubmission { + const KEY: &'static str = "presentation_submission"; +} + +impl TryFrom for PresentationSubmission { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(Self(value)) + } +} + +impl From for Json { + fn from(value: PresentationSubmission) -> Self { + value.0 + } +} diff --git a/src/core/util/mod.rs b/src/core/util/mod.rs new file mode 100644 index 0000000..70b2ca4 --- /dev/null +++ b/src/core/util/mod.rs @@ -0,0 +1,8 @@ +use anyhow::{Context, Error}; + +pub fn http_client() -> Result { + reqwest::Client::builder() + .use_rustls_tls() + .build() + .context("unable to build http_client") +} diff --git a/src/core/verifier/builder/by_reference.rs b/src/core/verifier/builder/by_reference.rs new file mode 100644 index 0000000..9131408 --- /dev/null +++ b/src/core/verifier/builder/by_reference.rs @@ -0,0 +1,10 @@ +use url::Url; + +#[derive(Debug, Clone, Default)] +pub enum ByReference { + #[default] + False, + True { + at: Url, + }, +} diff --git a/src/core/verifier/builder/client.rs b/src/core/verifier/builder/client.rs new file mode 100644 index 0000000..562f4ea --- /dev/null +++ b/src/core/verifier/builder/client.rs @@ -0,0 +1,89 @@ +use anyhow::Result; +use base64::prelude::*; +use serde_json::{json, Value as Json}; +use x509_cert::{der::Encode, Certificate}; + +use crate::core::{ + authorization_request::{ + parameters::{ClientId, ClientIdScheme}, + AuthorizationRequestObject, + }, + verifier::request_signer::RequestSigner, +}; + +#[derive(Debug, Clone)] +pub(crate) enum Client { + Did { + id: ClientId, + vm: String, + signer: S, + }, + X509SanUri { + id: ClientId, + x5c: Vec, + signer: S, + }, +} + +impl Client { + pub fn id(&self) -> &ClientId { + match self { + Client::Did { id, .. } => id, + Client::X509SanUri { id, .. } => id, + } + } + + pub fn scheme(&self) -> &ClientIdScheme { + match self { + Client::Did { .. } => &ClientIdScheme::Did, + Client::X509SanUri { .. } => &ClientIdScheme::X509SanUri, + } + } + + pub async fn generate_request_object_jwt( + &self, + body: &AuthorizationRequestObject, + ) -> Result { + match self { + Client::Did { + vm: kid, signer, .. + } => { + let algorithm = signer.alg(); + let header = json!({ + "alg": algorithm, + "kid": kid, + "typ": "JWT" + }); + make_jwt(header, body, signer).await + } + Client::X509SanUri { x5c, signer, .. } => { + let algorithm = signer.alg(); + let x5c: Vec = x5c + .iter() + .map(|x509| x509.to_der()) + .map(|der| Ok(BASE64_STANDARD.encode(der?))) + .collect::>()?; + let header = json!({ + "alg": algorithm, + "x5c": x5c, + "typ": "JWT" + }); + make_jwt(header, body, signer).await + } + } + } +} + +async fn make_jwt( + header: Json, + body: &AuthorizationRequestObject, + signer: &S, +) -> Result { + let header_b64: String = + serde_json::to_vec(&header).map(|b| BASE64_URL_SAFE_NO_PAD.encode(b))?; + let body_b64 = serde_json::to_vec(body).map(|b| BASE64_URL_SAFE_NO_PAD.encode(b))?; + let payload = [header_b64.as_bytes(), b".", body_b64.as_bytes()].concat(); + let signature = signer.sign(&payload).await; + let signature_b64 = BASE64_URL_SAFE_NO_PAD.encode(signature); + Ok(format!("{header_b64}.{body_b64}.{signature_b64}")) +} diff --git a/src/core/verifier/builder/mod.rs b/src/core/verifier/builder/mod.rs new file mode 100644 index 0000000..2e7b55f --- /dev/null +++ b/src/core/verifier/builder/mod.rs @@ -0,0 +1,257 @@ +use anyhow::{bail, Context, Result}; +use didkit::{DIDResolver, DID_METHODS}; +use tracing::{debug, warn}; +use url::Url; +use x509_cert::{ + ext::pkix::{name::GeneralName, SubjectAltName}, + Certificate, +}; + +use crate::core::{ + authorization_request::{ + parameters::ClientId, AuthorizationRequest, AuthorizationRequestObject, RequestIndirection, + }, + metadata::{parameters::wallet::AuthorizationEndpoint, WalletMetadata}, + object::{ParsingErrorContext, TypedParameter, UntypedObject}, + profile::Profile, +}; + +use self::{by_reference::ByReference, client::Client}; + +use super::{ + request_signer::{P256Signer, RequestSigner}, + Session, +}; + +mod by_reference; +mod client; + +#[derive(Debug, Clone)] +pub struct SessionBuilder { + wallet_metadata: WalletMetadata, + client: Option>, + pass_by_reference: ByReference, + request_params: UntypedObject, +} + +impl SessionBuilder { + pub fn new(wallet_metadata: WalletMetadata) -> Self { + Self { + wallet_metadata, + client: None, + pass_by_reference: ByReference::False, + request_params: UntypedObject::default(), + } + } + + pub async fn build(self, p: P) -> Result { + let Self { + wallet_metadata, + client, + pass_by_reference, + mut request_params, + } = self; + + let authorization_endpoint = wallet_metadata + .get::() + .parsing_error()? + .0; + + let Some(client) = client else { + bail!("client is required, see `with_X_client_id` functions") + }; + + let client_id = client.id(); + let client_id_scheme = client.scheme(); + if !wallet_metadata + .client_id_schemes_supported() + .0 + .contains(&client_id_scheme) + { + bail!("wallet does not support client_id_scheme '{client_id_scheme}'") + } + + let _ = request_params.insert(client_id.clone()); + let _ = request_params.insert(client_id_scheme.clone()); + + let request_object: AuthorizationRequestObject = request_params + .try_into() + .context("unable to construct Authorization Request Object from provided parameters")?; + + p.validate_request(&request_object)?; + + let request_object_jwt = client.generate_request_object_jwt(&request_object).await?; + + let request_indirection = match pass_by_reference { + ByReference::False => RequestIndirection::ByValue(request_object_jwt.clone()), + ByReference::True { at } => RequestIndirection::ByReference(at), + }; + + let authorization_request = AuthorizationRequest { + client_id: client_id.0.clone(), + request_indirection, + } + .to_url(authorization_endpoint)?; + + Ok(Session { + authorization_request, + request_object, + request_object_jwt, + }) + } + + /// Encode the Authorization Request directly in the `request` parameter. + pub fn by_value(mut self) -> Self { + self.pass_by_reference = ByReference::False; + self + } + + /// Pass the Authorization Request by reference in the `request_uri` parameter. + pub fn by_reference(mut self, at: Url) -> Self { + self.pass_by_reference = ByReference::True { at }; + self + } + + pub fn with_request_parameter(mut self, p: P) -> Self { + self.request_params.insert(p); + self + } + + /// Configure the [ClientId] and set the [ClientIdScheme] to `did`. + /// + /// Uses the default didkit [DIDResolver]. + pub async fn with_did_client_id( + self, + vm: String, + signer: T, + ) -> Result> { + self.with_did_client_id_and_resolver(vm, signer, DID_METHODS.to_resolver()) + .await + } + + /// Configure the [ClientId] and set the [ClientIdScheme] to `did`. + pub async fn with_did_client_id_and_resolver( + self, + vm: String, + signer: T, + resolver: &dyn DIDResolver, + ) -> Result> { + let (id, _f) = vm.rsplit_once('#').context(format!( + "expected a DID verification method, received '{vm}'" + ))?; + + let key = didkit::resolve_key(&vm, resolver) + .await + .context("unable to resolve key from verification method")?; + + if &key != signer.jwk() { + bail!( + "verification method resolved from DID document did not match public key of signer" + ) + } + + let SessionBuilder { + wallet_metadata, + pass_by_reference, + request_params, + .. + } = self; + + let client = Some(Client::Did { + id: ClientId(id.to_string()), + vm, + signer, + }); + + Ok(SessionBuilder { + wallet_metadata, + client, + pass_by_reference, + request_params, + }) + } + + /// Configure the [ClientId] and set the [ClientIdScheme] to `x509_san_dns`. + pub fn with_x509_san_dns_client_id(mut self, x5c: Vec, signer: S) -> Result { + // TODO: Check certificate chain. + let leaf = &x5c[0]; + let id = if let Some(san) = leaf + .tbs_certificate + .filter::() + .filter_map(|r| match r { + Ok((_crit, san)) => Some(san.0.into_iter()), + Err(e) => { + debug!("unable to parse SubjectAlternativeName from DER: {e}"); + None + } + }) + .flatten() + .filter_map(|gn| match gn { + GeneralName::DnsName(uri) => Some(uri.to_string()), + _ => { + debug!("found non-DNS SAN: {gn:?}"); + None + } + }) + .next() + { + san + } else { + bail!("x509 certificate does not contain Subject Alternative Name"); + }; + self.client = Some(Client::X509SanUri { + id: ClientId(id), + x5c, + signer, + }); + Ok(self) + } + + /// Configure the [ClientId] and set the [ClientIdScheme] to `x509_san_uri`. + pub fn with_x509_san_uri_client_id(mut self, x5c: Vec, signer: S) -> Result { + // TODO: Check certificate chain. + let leaf = &x5c[0]; + let id = if let Some(san) = leaf + .tbs_certificate + .filter::() + .filter_map(|r| match r { + Ok((_crit, san)) => Some(san.0.into_iter()), + Err(e) => { + debug!("unable to parse SubjectAlternativeName from DER: {e}"); + None + } + }) + .flatten() + .filter_map(|gn| match gn { + GeneralName::UniformResourceIdentifier(uri) => Some(uri.to_string()), + _ => { + debug!("found non-URI SAN: {gn:?}"); + None + } + }) + .next() + { + san + } else { + let Some(cn) = leaf + .tbs_certificate + .subject + .0 + .iter() + .flat_map(|n| n.0.iter()) + .filter_map(|n| n.to_string().strip_prefix("CN=").map(ToOwned::to_owned)) + .next() + else { + bail!("x509 certificate does not contain Subject Alternative Name or Common Name"); + }; + warn!("x509 certificate does not contain Subject Alternative Name, falling back to Common Name for client_id"); + cn + }; + self.client = Some(Client::X509SanUri { + id: ClientId(id), + x5c, + signer, + }); + Ok(self) + } +} diff --git a/src/core/verifier/mod.rs b/src/core/verifier/mod.rs new file mode 100644 index 0000000..5fa424b --- /dev/null +++ b/src/core/verifier/mod.rs @@ -0,0 +1,30 @@ +use serde::{Deserialize, Serialize}; +use url::Url; + +use self::builder::SessionBuilder; + +use super::{authorization_request::AuthorizationRequestObject, metadata::WalletMetadata}; + +pub mod builder; +pub mod request_signer; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Session { + authorization_request: Url, + request_object: AuthorizationRequestObject, + request_object_jwt: String, +} + +impl Session { + pub fn builder(wallet_metadata: WalletMetadata) -> SessionBuilder { + SessionBuilder::new(wallet_metadata) + } + + pub fn authorization_request(&self) -> &Url { + &self.authorization_request + } + + pub fn request_object_jwt(&self) -> &str { + &self.request_object_jwt + } +} diff --git a/src/core/verifier/request_signer.rs b/src/core/verifier/request_signer.rs new file mode 100644 index 0000000..e4265a5 --- /dev/null +++ b/src/core/verifier/request_signer.rs @@ -0,0 +1,42 @@ +use anyhow::Result; +use async_trait::async_trait; +use didkit::JWK; +use p256::ecdsa::{signature::Signer, Signature, SigningKey}; + +#[async_trait] +pub trait RequestSigner { + /// The algorithm that will be used to sign. + fn alg(&self) -> &str; + /// The public JWK of the signer. + fn jwk(&self) -> &JWK; + async fn sign(&self, payload: &[u8]) -> Vec; +} + +pub struct P256Signer { + key: SigningKey, + jwk: JWK, +} + +impl P256Signer { + pub fn new(key: SigningKey) -> Result { + let pk: p256::PublicKey = key.verifying_key().into(); + let jwk = serde_json::from_str(&pk.to_jwk_string())?; + Ok(Self { key, jwk }) + } +} + +#[async_trait] +impl RequestSigner for P256Signer { + fn alg(&self) -> &str { + "ES256" + } + + fn jwk(&self) -> &JWK { + &self.jwk + } + + async fn sign(&self, payload: &[u8]) -> Vec { + let sig: Signature = self.key.sign(payload); + sig.to_vec() + } +} diff --git a/src/core/wallet/mod.rs b/src/core/wallet/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/core/wallet/mod.rs @@ -0,0 +1 @@ + diff --git a/src/lib.rs b/src/lib.rs index 9b2d4ea..653d391 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod core; pub mod mdl_request; pub mod mdl_response; pub mod presentation_exchange; diff --git a/src/mdl_request.rs b/src/mdl_request.rs index ea9753c..5a4cac2 100644 --- a/src/mdl_request.rs +++ b/src/mdl_request.rs @@ -5,10 +5,10 @@ use crate::{ }, utils::NonEmptyVec, }; +use didkit::ssi::jwk::JWK; use isomdl::definitions::helpers::NonEmptyMap; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use ssi::jwk::JWK; use std::collections::BTreeMap; use x509_cert::der::referenced::OwnedToRef; use x509_cert::der::Decode; diff --git a/src/utils.rs b/src/utils.rs index 4f30afb..c884d74 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,5 @@ use anyhow; +use didkit::ssi::jws::Error as JwsError; use isomdl::definitions::helpers::non_empty_map::Error as NonEmptyMapError; use isomdl::definitions::Error as IsomdlDefinitionError; use isomdl::presentation::reader::oid4vp::Error as IsomdlError; @@ -7,7 +8,6 @@ use josekit::JoseError; use reqwest::Error as ReqwestError; use serde::{Deserialize, Serialize}; use serde_cbor::Error as CborError; -use ssi::jws::Error as JwsError; use std::ops::Deref; // #[derive(Clone)] @@ -200,8 +200,8 @@ impl From for Openid4vpError { } } -impl From for Openid4vpError { - fn from(_value: ssi::jwk::Error) -> Self { +impl From for Openid4vpError { + fn from(_value: didkit::ssi::jwk::Error) -> Self { Openid4vpError::InvalidRequest } } From 7b29a6bf29a18718110b118e8329f0f687c50dd9 Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 9 Oct 2023 14:43:57 +0100 Subject: [PATCH 18/23] Update isomdl dep --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 118c00d..8dae754 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ async-trait = "0.1.73" base64 = "0.21.4" did-web = "0.2.2" didkit = "0.6.0" -isomdl = { git = "ssh://git@github.com/spruceid/isomdl.git", rev = "b2324b7" } +isomdl = { git = "https://git@github.com/spruceid/isomdl", rev = "b2324b7" } josekit = { git = "https://github.com/cobward/josekit-rs", rev = "635c8a7" } p256 = { version = "0.13.2", features = ["jwk"] } reqwest = "0.11.20" From 7bef5370a568219b0e2208111da777f868bf025b Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 9 Oct 2023 14:45:51 +0100 Subject: [PATCH 19/23] Clippy --- src/core/authorization_request/parameters.rs | 2 +- src/core/authorization_request/verification/did.rs | 14 +------------- src/core/authorization_request/verification/mod.rs | 2 +- .../verification/x509_san_dns.rs | 4 ++-- .../verification/x509_san_uri.rs | 4 ++-- src/core/verifier/builder/mod.rs | 2 +- src/presentation_exchange.rs | 10 ++++------ 7 files changed, 12 insertions(+), 26 deletions(-) diff --git a/src/core/authorization_request/parameters.rs b/src/core/authorization_request/parameters.rs index ffb10f6..ae2807e 100644 --- a/src/core/authorization_request/parameters.rs +++ b/src/core/authorization_request/parameters.rs @@ -102,7 +102,7 @@ impl fmt::Display for ClientIdScheme { ClientIdScheme::RedirectUri => REDIRECT_URI, ClientIdScheme::X509SanDns => X509_SAN_DNS, ClientIdScheme::X509SanUri => X509_SAN_URI, - ClientIdScheme::Other(o) => &o, + ClientIdScheme::Other(o) => o, } .fmt(f) } diff --git a/src/core/authorization_request/verification/did.rs b/src/core/authorization_request/verification/did.rs index 1d69eeb..68c6639 100644 --- a/src/core/authorization_request/verification/did.rs +++ b/src/core/authorization_request/verification/did.rs @@ -70,7 +70,7 @@ pub async fn verify_with_resolver( "expected a DID verification method in 'kid' header, received '{kid}'" ))?; - if &client_id.0 != did { + if client_id.0 != did { bail!( "DIDs from 'kid' ({did}) and 'client_id' ({}) do not match", client_id.0 @@ -83,18 +83,6 @@ pub async fn verify_with_resolver( } } - println!( - "{}", - serde_json::to_string_pretty( - &didkit::dereference(resolver, did, &Default::default()) - .await - .1 - ) - .unwrap() - ); - - println!("{kid:?}"); - let jwk = resolve_key(&kid, resolver) .await .context("unable to resolve verification method from 'kid' header")?; diff --git a/src/core/authorization_request/verification/mod.rs b/src/core/authorization_request/verification/mod.rs index 459f98e..43fac7b 100644 --- a/src/core/authorization_request/verification/mod.rs +++ b/src/core/authorization_request/verification/mod.rs @@ -104,7 +104,7 @@ pub(crate) async fn verify_request( ClientIdScheme::RedirectUri => profile.redirect_uri(&request, jwt).await?, ClientIdScheme::X509SanDns => profile.x509_san_dns(&request, jwt).await?, ClientIdScheme::X509SanUri => profile.x509_san_uri(&request, jwt).await?, - ClientIdScheme::Other(scheme) => profile.other(&scheme, &request, jwt).await?, + ClientIdScheme::Other(scheme) => profile.other(scheme, &request, jwt).await?, }; Ok(request) diff --git a/src/core/authorization_request/verification/x509_san_dns.rs b/src/core/authorization_request/verification/x509_san_dns.rs index 5e0d923..615a356 100644 --- a/src/core/authorization_request/verification/x509_san_dns.rs +++ b/src/core/authorization_request/verification/x509_san_dns.rs @@ -119,7 +119,7 @@ pub trait Verifier: Sized { /// ## Params /// * `spki` - the public key information necessary to construct a [Verifier]. /// * `algorithm` - the value taken from the `alg` header of the request, to hint at what curve should be used by the [Verifier]. - fn from_spki<'a>(spki: SubjectPublicKeyInfoRef<'a>, algorithm: String) -> Result; + fn from_spki(spki: SubjectPublicKeyInfoRef<'_>, algorithm: String) -> Result; fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()>; } @@ -127,7 +127,7 @@ pub trait Verifier: Sized { pub struct P256Verifier(p256::ecdsa::VerifyingKey); impl Verifier for P256Verifier { - fn from_spki<'a>(spki: SubjectPublicKeyInfoRef<'a>, algorithm: String) -> Result { + fn from_spki(spki: SubjectPublicKeyInfoRef<'_>, algorithm: String) -> Result { if algorithm != "ES256" { bail!("P256Verifier cannot verify requests signed with '{algorithm}'") } diff --git a/src/core/authorization_request/verification/x509_san_uri.rs b/src/core/authorization_request/verification/x509_san_uri.rs index 30e6489..16e09ff 100644 --- a/src/core/authorization_request/verification/x509_san_uri.rs +++ b/src/core/authorization_request/verification/x509_san_uri.rs @@ -133,7 +133,7 @@ pub trait Verifier: Sized { /// ## Params /// * `spki` - the public key information necessary to construct a [Verifier]. /// * `algorithm` - the value taken from the `alg` header of the request, to hint at what curve should be used by the [Verifier]. - fn from_spki<'a>(spki: SubjectPublicKeyInfoRef<'a>, algorithm: String) -> Result; + fn from_spki(spki: SubjectPublicKeyInfoRef<'_>, algorithm: String) -> Result; fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()>; } @@ -141,7 +141,7 @@ pub trait Verifier: Sized { pub struct P256Verifier(p256::ecdsa::VerifyingKey); impl Verifier for P256Verifier { - fn from_spki<'a>(spki: SubjectPublicKeyInfoRef<'a>, algorithm: String) -> Result { + fn from_spki(spki: SubjectPublicKeyInfoRef<'_>, algorithm: String) -> Result { if algorithm != "ES256" { bail!("P256Verifier cannot verify requests signed with '{algorithm}'") } diff --git a/src/core/verifier/builder/mod.rs b/src/core/verifier/builder/mod.rs index 2e7b55f..6b5f6bb 100644 --- a/src/core/verifier/builder/mod.rs +++ b/src/core/verifier/builder/mod.rs @@ -66,7 +66,7 @@ impl SessionBuilder { if !wallet_metadata .client_id_schemes_supported() .0 - .contains(&client_id_scheme) + .contains(client_id_scheme) { bail!("wallet does not support client_id_scheme '{client_id_scheme}'") } diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 143b135..3e6f172 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -237,7 +237,7 @@ pub(crate) mod tests { let path = path.unwrap().path(); if let Some(ext) = path.extension() { if ext != OsStr::new("json") - || vec!["VC_expiration_example.json", "VC_revocation_example.json"] // TODO bad format + || ["VC_expiration_example.json", "VC_revocation_example.json"] // TODO bad format .contains(&path.file_name().unwrap().to_str().unwrap()) { continue; @@ -267,10 +267,8 @@ pub(crate) mod tests { let path = path.unwrap().path(); if let Some(ext) = path.extension() { if ext != OsStr::new("json") - || vec![ - "appendix_DIDComm_example.json", - "appendix_CHAPI_example.json", - ] + || ["appendix_DIDComm_example.json", + "appendix_CHAPI_example.json"] .contains(&path.file_name().unwrap().to_str().unwrap()) { continue; @@ -299,7 +297,7 @@ pub(crate) mod tests { let path = path.unwrap().path(); if let Some(ext) = path.extension() { if ext != OsStr::new("json") - || vec!["schema.json"].contains(&path.file_name().unwrap().to_str().unwrap()) + || ["schema.json"].contains(&path.file_name().unwrap().to_str().unwrap()) { continue; } From 4d3f29ec84ebffe5ef18f711b0491aeb61c3405e Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 9 Oct 2023 15:48:31 +0100 Subject: [PATCH 20/23] Make client_metadata inner public --- src/core/authorization_request/parameters.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/authorization_request/parameters.rs b/src/core/authorization_request/parameters.rs index ae2807e..a745cba 100644 --- a/src/core/authorization_request/parameters.rs +++ b/src/core/authorization_request/parameters.rs @@ -110,7 +110,7 @@ impl fmt::Display for ClientIdScheme { /// `client_metadata` field in the Authorization Request. #[derive(Debug, Clone)] -pub struct ClientMetadata(UntypedObject); +pub struct ClientMetadata(pub UntypedObject); impl TypedParameter for ClientMetadata { const KEY: &'static str = "client_metadata"; From da43e65d8e2bec69fb48c15d52656776e1c50316 Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 13 Oct 2023 12:15:54 +0100 Subject: [PATCH 21/23] Remove didkit, rename crate, add missing metadata. --- Cargo.toml | 6 +- .../authorization_request/verification/did.rs | 25 +-------- .../authorization_request/verification/mod.rs | 7 +-- .../verification/x509_san_dns.rs | 2 +- .../verification/x509_san_uri.rs | 2 +- src/core/metadata/parameters/verifier.rs | 56 +++++++++++++++++++ src/core/verifier/builder/mod.rs | 16 +----- src/core/verifier/request_signer.rs | 2 +- src/mdl_request.rs | 2 +- src/utils.rs | 6 +- 10 files changed, 74 insertions(+), 50 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8dae754..b213e0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,18 @@ [package] -name = "oidc4vp" +name = "oid4vp" version = "0.1.0" edition = "2021" authors = ["Spruce Systems, Inc."] license = "MIT OR Apache-2.0" description = "OpenID Connect for Verifiable Presentations" repository = "https://github.com/spruceid/oidc4vp-rs/" -documentation = "https://docs.rs/oidc4vp/" +documentation = "https://docs.rs/oid4vp/" [dependencies] anyhow = "1.0.75" async-trait = "0.1.73" base64 = "0.21.4" did-web = "0.2.2" -didkit = "0.6.0" isomdl = { git = "https://git@github.com/spruceid/isomdl", rev = "b2324b7" } josekit = { git = "https://github.com/cobward/josekit-rs", rev = "635c8a7" } p256 = { version = "0.13.2", features = ["jwk"] } @@ -23,6 +22,7 @@ serde_cbor = "0.11.2" serde_json = "1.0.107" serde_qs = "0.12.0" serde_urlencoded = "0.7.1" +ssi = "0.7.0" thiserror = "1.0.49" tracing = "0.1.37" url = { version = "2.4.1", features = ["serde"] } diff --git a/src/core/authorization_request/verification/did.rs b/src/core/authorization_request/verification/did.rs index 68c6639..8b14607 100644 --- a/src/core/authorization_request/verification/did.rs +++ b/src/core/authorization_request/verification/did.rs @@ -5,27 +5,8 @@ use crate::core::{ }; use anyhow::{bail, Context, Result}; use base64::prelude::*; -use didkit::{resolve_key, DIDResolver}; use serde_json::{Map, Value as Json}; - -/// Default implementation of request verification for `client_id_scheme` `did`. -/// -/// Uses the default didkit [DIDResolver]. -pub async fn verify( - wallet_metadata: &WalletMetadata, - request_object: &AuthorizationRequestObject, - request_jwt: String, - trusted_dids: Option<&[String]>, -) -> Result<()> { - verify_with_resolver( - wallet_metadata, - request_object, - request_jwt, - trusted_dids, - didkit::DID_METHODS.to_resolver(), - ) - .await -} +use ssi::did_resolve::{resolve_key, DIDResolver}; /// Default implementation of request validation for `client_id_scheme` `did`. pub async fn verify_with_resolver( @@ -35,7 +16,7 @@ pub async fn verify_with_resolver( trusted_dids: Option<&[String]>, resolver: &dyn DIDResolver, ) -> Result<()> { - let (headers_b64, _, _) = didkit::ssi::jws::split_jws(&request_jwt)?; + let (headers_b64, _, _) = ssi::jws::split_jws(&request_jwt)?; let headers_json_bytes = BASE64_URL_SAFE_NO_PAD .decode(headers_b64) @@ -87,7 +68,7 @@ pub async fn verify_with_resolver( .await .context("unable to resolve verification method from 'kid' header")?; - let _: Json = didkit::ssi::jwt::decode_verify(&request_jwt, &jwk) + let _: Json = ssi::jwt::decode_verify(&request_jwt, &jwk) .context("request signature could not be verified")?; Ok(()) diff --git a/src/core/authorization_request/verification/mod.rs b/src/core/authorization_request/verification/mod.rs index 43fac7b..6c725c2 100644 --- a/src/core/authorization_request/verification/mod.rs +++ b/src/core/authorization_request/verification/mod.rs @@ -74,10 +74,9 @@ pub(crate) async fn verify_request( profile: &WP, jwt: String, ) -> Result { - let request: AuthorizationRequestObject = - didkit::ssi::jwt::decode_unverified::(&jwt) - .context("unable to decode Authorization Request Object JWT")? - .try_into()?; + let request: AuthorizationRequestObject = ssi::jwt::decode_unverified::(&jwt) + .context("unable to decode Authorization Request Object JWT")? + .try_into()?; let client_id_scheme = request.client_id_scheme(); diff --git a/src/core/authorization_request/verification/x509_san_dns.rs b/src/core/authorization_request/verification/x509_san_dns.rs index 615a356..90432db 100644 --- a/src/core/authorization_request/verification/x509_san_dns.rs +++ b/src/core/authorization_request/verification/x509_san_dns.rs @@ -24,7 +24,7 @@ pub fn validate( trusted_roots: Option<&[Certificate]>, ) -> Result<()> { let client_id = request_object.client_id().0.as_str(); - let (headers_b64, body_b64, sig_b64) = didkit::ssi::jws::split_jws(&request_jwt)?; + let (headers_b64, body_b64, sig_b64) = ssi::jws::split_jws(&request_jwt)?; let headers_json_bytes = BASE64_URL_SAFE_NO_PAD .decode(headers_b64) diff --git a/src/core/authorization_request/verification/x509_san_uri.rs b/src/core/authorization_request/verification/x509_san_uri.rs index 16e09ff..1c61833 100644 --- a/src/core/authorization_request/verification/x509_san_uri.rs +++ b/src/core/authorization_request/verification/x509_san_uri.rs @@ -24,7 +24,7 @@ pub fn validate( trusted_roots: Option<&[Certificate]>, ) -> Result<()> { let client_id = request_object.client_id().0.as_str(); - let (headers_b64, body_b64, sig_b64) = didkit::ssi::jws::split_jws(&request_jwt)?; + let (headers_b64, body_b64, sig_b64) = ssi::jws::split_jws(&request_jwt)?; let headers_json_bytes = BASE64_URL_SAFE_NO_PAD .decode(headers_b64) diff --git a/src/core/metadata/parameters/verifier.rs b/src/core/metadata/parameters/verifier.rs index 365d0f5..5d09a13 100644 --- a/src/core/metadata/parameters/verifier.rs +++ b/src/core/metadata/parameters/verifier.rs @@ -51,6 +51,48 @@ impl From for Json { } } +#[derive(Debug, Clone)] +pub struct AuthorizationEncryptedResponseAlg(pub String); + +impl TypedParameter for AuthorizationEncryptedResponseAlg { + const KEY: &'static str = "authorization_encrypted_response_alg"; +} + +impl TryFrom for AuthorizationEncryptedResponseAlg { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(Self(serde_json::from_value(value)?)) + } +} + +impl From for Json { + fn from(value: AuthorizationEncryptedResponseAlg) -> Json { + Json::String(value.0) + } +} + +#[derive(Debug, Clone)] +pub struct AuthorizationEncryptedResponseEnc(pub String); + +impl TypedParameter for AuthorizationEncryptedResponseEnc { + const KEY: &'static str = "authorization_encrypted_response_enc"; +} + +impl TryFrom for AuthorizationEncryptedResponseEnc { + type Error = Error; + + fn try_from(value: Json) -> Result { + Ok(Self(serde_json::from_value(value)?)) + } +} + +impl From for Json { + fn from(value: AuthorizationEncryptedResponseEnc) -> Json { + Json::String(value.0) + } +} + #[cfg(test)] mod test { use serde_json::json; @@ -109,4 +151,18 @@ mod test { let RequireSignedRequestObject(b) = metadata().get().unwrap().unwrap(); assert_eq!(b, exp); } + + #[test] + fn authorization_encrypted_response_alg() { + let exp = "ECDH-ES"; + let AuthorizationEncryptedResponseAlg(s) = metadata().get().unwrap().unwrap(); + assert_eq!(s, exp); + } + + #[test] + fn authorization_encrypted_response_enc() { + let exp = "A256GCM"; + let AuthorizationEncryptedResponseEnc(s) = metadata().get().unwrap().unwrap(); + assert_eq!(s, exp); + } } diff --git a/src/core/verifier/builder/mod.rs b/src/core/verifier/builder/mod.rs index 6b5f6bb..d80ee5b 100644 --- a/src/core/verifier/builder/mod.rs +++ b/src/core/verifier/builder/mod.rs @@ -1,5 +1,5 @@ use anyhow::{bail, Context, Result}; -use didkit::{DIDResolver, DID_METHODS}; +use ssi::did_resolve::DIDResolver; use tracing::{debug, warn}; use url::Url; use x509_cert::{ @@ -117,18 +117,6 @@ impl SessionBuilder { self } - /// Configure the [ClientId] and set the [ClientIdScheme] to `did`. - /// - /// Uses the default didkit [DIDResolver]. - pub async fn with_did_client_id( - self, - vm: String, - signer: T, - ) -> Result> { - self.with_did_client_id_and_resolver(vm, signer, DID_METHODS.to_resolver()) - .await - } - /// Configure the [ClientId] and set the [ClientIdScheme] to `did`. pub async fn with_did_client_id_and_resolver( self, @@ -140,7 +128,7 @@ impl SessionBuilder { "expected a DID verification method, received '{vm}'" ))?; - let key = didkit::resolve_key(&vm, resolver) + let key = ssi::did_resolve::resolve_key(&vm, resolver) .await .context("unable to resolve key from verification method")?; diff --git a/src/core/verifier/request_signer.rs b/src/core/verifier/request_signer.rs index e4265a5..0684da7 100644 --- a/src/core/verifier/request_signer.rs +++ b/src/core/verifier/request_signer.rs @@ -1,7 +1,7 @@ use anyhow::Result; use async_trait::async_trait; -use didkit::JWK; use p256::ecdsa::{signature::Signer, Signature, SigningKey}; +use ssi::jwk::JWK; #[async_trait] pub trait RequestSigner { diff --git a/src/mdl_request.rs b/src/mdl_request.rs index 5a4cac2..ea9753c 100644 --- a/src/mdl_request.rs +++ b/src/mdl_request.rs @@ -5,10 +5,10 @@ use crate::{ }, utils::NonEmptyVec, }; -use didkit::ssi::jwk::JWK; use isomdl::definitions::helpers::NonEmptyMap; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; +use ssi::jwk::JWK; use std::collections::BTreeMap; use x509_cert::der::referenced::OwnedToRef; use x509_cert::der::Decode; diff --git a/src/utils.rs b/src/utils.rs index c884d74..4f30afb 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,4 @@ use anyhow; -use didkit::ssi::jws::Error as JwsError; use isomdl::definitions::helpers::non_empty_map::Error as NonEmptyMapError; use isomdl::definitions::Error as IsomdlDefinitionError; use isomdl::presentation::reader::oid4vp::Error as IsomdlError; @@ -8,6 +7,7 @@ use josekit::JoseError; use reqwest::Error as ReqwestError; use serde::{Deserialize, Serialize}; use serde_cbor::Error as CborError; +use ssi::jws::Error as JwsError; use std::ops::Deref; // #[derive(Clone)] @@ -200,8 +200,8 @@ impl From for Openid4vpError { } } -impl From for Openid4vpError { - fn from(_value: didkit::ssi::jwk::Error) -> Self { +impl From for Openid4vpError { + fn from(_value: ssi::jwk::Error) -> Self { Openid4vpError::InvalidRequest } } From cd0f80db83bdb8e855558b1311c4018773ebbeb4 Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 23 Oct 2023 14:43:48 +0100 Subject: [PATCH 22/23] Various updates - rename crate - make http client to be configurable - refactor traits and request builder - add client metadata resolution --- .github/workflows/ci.yml | 5 +- Cargo.toml | 1 - src/core/authorization_request/mod.rs | 103 ++++++--- src/core/authorization_request/parameters.rs | 96 +++++++- .../authorization_request/verification/mod.rs | 106 +++++++-- src/core/profile/mod.rs | 45 +++- src/core/util/mod.rs | 12 +- src/core/verifier/builder/mod.rs | 28 ++- src/core/verifier/mod.rs | 13 +- src/lib.rs | 3 - src/mdl_request.rs | 218 ------------------ src/mdl_response.rs | 167 -------------- src/presentation_exchange.rs | 6 +- src/presentment.rs | 43 ---- src/utils.rs | 28 --- 15 files changed, 335 insertions(+), 539 deletions(-) delete mode 100644 src/mdl_request.rs delete mode 100644 src/mdl_response.rs delete mode 100644 src/presentment.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61bb994..b0bb227 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: [push, pull_request] env: CARGO_TERM_COLOR: always + RUSTFLAGS: "-Dwarnings" jobs: build: @@ -18,5 +19,5 @@ jobs: run: cargo test - name: Fmt run: cargo fmt -- --check - # - name: Clippy - # run: RUSTFLAGS="-Dwarnings" cargo clippy + - name: Clippy + run: cargo clippy diff --git a/Cargo.toml b/Cargo.toml index b213e0a..f8bcbff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ anyhow = "1.0.75" async-trait = "0.1.73" base64 = "0.21.4" did-web = "0.2.2" -isomdl = { git = "https://git@github.com/spruceid/isomdl", rev = "b2324b7" } josekit = { git = "https://github.com/cobward/josekit-rs", rev = "635c8a7" } p256 = { version = "0.13.2", features = ["jwk"] } reqwest = "0.11.20" diff --git a/src/core/authorization_request/mod.rs b/src/core/authorization_request/mod.rs index 4ebd5fc..8c8c77a 100644 --- a/src/core/authorization_request/mod.rs +++ b/src/core/authorization_request/mod.rs @@ -7,16 +7,16 @@ use url::Url; use self::{ parameters::{ - ClientId, ClientIdScheme, PresentationDefinition, PresentationDefinitionUri, RedirectUri, - ResponseMode, ResponseType, ResponseUri, + ClientId, ClientIdScheme, Nonce, PresentationDefinition, PresentationDefinitionUri, + RedirectUri, ResponseMode, ResponseType, ResponseUri, }, verification::verify_request, }; use super::{ object::{ParsingErrorContext, UntypedObject}, - profile::WalletProfile, - util::http_client, + profile::Wallet, + util::default_http_client, }; pub mod parameters; @@ -32,6 +32,7 @@ pub struct AuthorizationRequestObject( ResponseType, PresentationDefinitionIndirection, Url, + Nonce, ); /// An Authorization Request. @@ -63,23 +64,25 @@ impl AuthorizationRequest { /// [RequestObject]. /// /// Custom wallet metadata can be provided, otherwise the default metadata for this profile is used. - pub async fn validate( + pub async fn validate_with_http_client( self, wallet_profile: &WP, + http_client: &reqwest::Client, ) -> Result { let jwt = match self.request_indirection { RequestIndirection::ByValue(jwt) => jwt, - RequestIndirection::ByReference(url) => http_client()? + RequestIndirection::ByReference(url) => http_client .get(url.clone()) - .header("Prefer", "OID4VP-0.0.20") .send() .await .context(format!("failed to GET {url}"))? + .error_for_status() + .context(format!("failed to GET {url}"))? .text() .await .context(format!("failed to parse data from {url}"))?, }; - let aro = verify_request(wallet_profile, jwt) + let aro = verify_request(wallet_profile, jwt, http_client) .await .context("unable to validate Authorization Request")?; if self.client_id.as_str() != aro.client_id().0.as_str() { @@ -92,15 +95,28 @@ impl AuthorizationRequest { Ok(aro) } + /// Validate the [AuthorizationRequest] according to the client_id scheme and return the parsed + /// [RequestObject]. + /// + /// Custom wallet metadata can be provided, otherwise the default metadata for this profile is used. + /// + /// This method uses the library default http client to fetch the request object if it is passed by reference. + pub async fn validate( + self, + wallet_profile: &WP, + ) -> Result { + self.validate_with_http_client(wallet_profile, &default_http_client()?) + .await + } + /// Encode as [Url], using the `authorization_endpoint` as a base. /// ``` - /// # use verifier_api::mock::authorization_request::AuthorizationRequest; - /// # use verifier_api::mock::authorization_request::RequestIndirection; - /// # use verifier_api::mock::client_id::ClientId; + /// # use oid4vp::core::authorization_request::AuthorizationRequest; + /// # use oid4vp::core::authorization_request::RequestIndirection; /// # use url::Url; /// let authorization_endpoint: Url = "example://".parse().unwrap(); /// let authorization_request = AuthorizationRequest { - /// client_id: ClientId("xyz".to_string()), + /// client_id: "xyz".to_string(), /// request_indirection: RequestIndirection::ByValue("test".to_string()), /// }; /// @@ -116,41 +132,54 @@ impl AuthorizationRequest { /// Parse from [Url], validating the authorization_endpoint. /// ``` - /// # use verifier_api::mock::authorization_request::AuthorizationRequest; - /// # use verifier_api::mock::authorization_request::RequestIndirection; + /// # use oid4vp::core::authorization_request::AuthorizationRequest; + /// # use oid4vp::core::authorization_request::RequestIndirection; /// # use url::Url; /// let url: Url = "example://?client_id=xyz&request=test".parse().unwrap(); /// let authorization_endpoint: Url = "example://".parse().unwrap(); /// - /// let authorization_request = AuthorizationRequest::from_url(url, &authorization_endpoint).unwrap(); + /// let authorization_request = AuthorizationRequest::from_url( + /// url, + /// &authorization_endpoint + /// ).unwrap(); /// - /// assert_eq!(authorization_request.client_id.0, "xyz"); + /// assert_eq!(authorization_request.client_id, "xyz"); + /// + /// let RequestIndirection::ByValue(request_object) = + /// authorization_request.request_indirection + /// else { + /// panic!("expected request-by-value") + /// }; /// - /// let RequestIndirection::ByValue(request_object) = authorization_request.request_indirection - /// else { panic!("expected request-by-value") }; /// assert_eq!(request_object, "test"); /// ``` - pub fn from_url(mut url: Url, authorization_endpoint: &Url) -> Result { + pub fn from_url(url: Url, authorization_endpoint: &Url) -> Result { let query = url .query() .ok_or(anyhow!("missing query params in Authorization Request uri"))? .to_string(); - url.set_query(None); - if &url != authorization_endpoint { - bail!("unexpected authorization_endpoint, expected '{authorization_endpoint}', received '{url}'") + let fnd = url.authority(); + let exp = authorization_endpoint.authority(); + if fnd != exp { + bail!("unexpected authorization_endpoint authority, expected '{exp}', received '{fnd}'") + } + let fnd = url.path(); + let exp = authorization_endpoint.path(); + if fnd != exp { + bail!("unexpected authorization_endpoint path, expected '{exp}', received '{fnd}'") } Self::from_query_params(&query) } /// Parse from urlencoded query parameters. /// ``` - /// # use verifier_api::mock::authorization_request::AuthorizationRequest; - /// # use verifier_api::mock::authorization_request::RequestIndirection; + /// # use oid4vp::core::authorization_request::AuthorizationRequest; + /// # use oid4vp::core::authorization_request::RequestIndirection; /// let query = "client_id=xyz&request=test"; /// /// let authorization_request = AuthorizationRequest::from_query_params(query).unwrap(); /// - /// assert_eq!(authorization_request.client_id.0, "xyz"); + /// assert_eq!(authorization_request.client_id, "xyz"); /// /// let RequestIndirection::ByValue(request_object) = authorization_request.request_indirection /// else { panic!("expected request-by-value") }; @@ -171,17 +200,24 @@ impl AuthorizationRequestObject { &self.2 } - pub async fn presentation_definition(&self) -> Result { + pub async fn resolve_presentation_definition_with_http_client( + &self, + http_client: reqwest::Client, + ) -> Result { match &self.5 { PresentationDefinitionIndirection::ByValue(by_value) => Ok(by_value.clone()), PresentationDefinitionIndirection::ByReference(by_reference) => { - let value: Json = http_client()? + let value: Json = http_client .get(by_reference.clone()) .send() .await .context(format!( "failed to GET Presentation Definition from '{by_reference}'" ))? + .error_for_status() + .context(format!( + "failed to GET Presentation Definition from '{by_reference}'" + ))? .json() .await .context(format!( @@ -192,6 +228,12 @@ impl AuthorizationRequestObject { } } + /// Uses the default library http client. + pub async fn resolve_presentation_definition(&self) -> Result { + self.resolve_presentation_definition_with_http_client(default_http_client()?) + .await + } + pub fn is_id_token_requested(&self) -> Option { match self.4 { ResponseType::VpToken => Some(false), @@ -214,6 +256,10 @@ impl AuthorizationRequestObject { pub fn return_uri(&self) -> &Url { &self.6 } + + pub fn nonce(&self) -> &Nonce { + &self.7 + } } impl From for UntypedObject { @@ -279,6 +325,8 @@ impl TryFrom for AuthorizationRequestObject { } }; + let nonce = value.get().parsing_error()?; + Ok(Self( value, client_id, @@ -287,6 +335,7 @@ impl TryFrom for AuthorizationRequestObject { response_type, pd_indirection, return_uri, + nonce, )) } } diff --git a/src/core/authorization_request/parameters.rs b/src/core/authorization_request/parameters.rs index a745cba..6e4968c 100644 --- a/src/core/authorization_request/parameters.rs +++ b/src/core/authorization_request/parameters.rs @@ -1,11 +1,16 @@ use std::fmt; -use crate::core::object::{TypedParameter, UntypedObject}; -use anyhow::Error; +use crate::core::{ + object::{ParsingErrorContext, TypedParameter, UntypedObject}, + util::default_http_client, +}; +use anyhow::{bail, Context, Error, Ok}; use serde::{Deserialize, Serialize}; use serde_json::Value as Json; use url::Url; +use super::AuthorizationRequestObject; + const DID: &str = "did"; const ENTITY_ID: &str = "entity_id"; const PREREGISTERED: &str = "pre-registered"; @@ -130,6 +135,51 @@ impl TryFrom for ClientMetadata { } } +impl ClientMetadata { + /// Resolves the client metadata from the Authorization Request Object. + /// + /// If the client metadata is not passed by reference or value if the Authorization Request Object, + /// then this function will return an error. + /// + /// Uses the library's default http client. + pub async fn resolve(request: &AuthorizationRequestObject) -> Result { + Self::resolve_with_http_client(request, &default_http_client()?).await + } + + /// Resolves the client metadata from the Authorization Request Object. + /// + /// If the client metadata is not passed by reference or value if the Authorization Request Object, + /// then this function will return an error. + pub async fn resolve_with_http_client( + request: &AuthorizationRequestObject, + http_client: &reqwest::Client, + ) -> Result { + if let Some(metadata) = request.get() { + return metadata; + } + + if let Some(metadata_uri) = request.get::() { + let uri = metadata_uri.parsing_error()?; + return http_client + .get(uri.0.clone()) + .send() + .await + .context(format!("failed to GET {}", uri.0))? + .error_for_status() + .context(format!("failed to GET {}", uri.0))? + .json() + .await + .map(ClientMetadata) + .context(format!( + "could not parse response from GET '{}' as JSON", + uri.0 + )); + } + + bail!("") + } +} + /// `client_metadata_uri` field in the Authorization Request. #[derive(Debug, Clone)] pub struct ClientMetadataUri(pub Url); @@ -308,6 +358,16 @@ impl Default for ResponseMode { } } +impl ResponseMode { + pub fn is_jarm(&self) -> Result { + match self { + ResponseMode::DirectPost => Ok(false), + ResponseMode::DirectPostJwt => Ok(true), + ResponseMode::Unsupported(rm) => bail!("unsupported response_mode: {rm}"), + } + } +} + const VP_TOKEN: &str = "vp_token"; const VP_TOKEN_ID_TOKEN: &str = "vp_token id_token"; @@ -379,8 +439,33 @@ impl From for Json { } } +// TODO: Revisit the inner parsed type. #[derive(Debug, Clone)] -pub struct PresentationDefinition(pub Json); +pub struct PresentationDefinition { + raw: Json, + parsed: crate::presentation_exchange::PresentationDefinition, +} + +impl PresentationDefinition { + pub fn into_parsed(self) -> crate::presentation_exchange::PresentationDefinition { + self.parsed + } + + pub fn parsed(&self) -> &crate::presentation_exchange::PresentationDefinition { + &self.parsed + } +} + +impl TryFrom for PresentationDefinition { + type Error = Error; + + fn try_from( + parsed: crate::presentation_exchange::PresentationDefinition, + ) -> Result { + let raw = serde_json::to_value(parsed.clone())?; + Ok(Self { raw, parsed }) + } +} impl TypedParameter for PresentationDefinition { const KEY: &'static str = "presentation_definition"; @@ -390,13 +475,14 @@ impl TryFrom for PresentationDefinition { type Error = Error; fn try_from(value: Json) -> Result { - Ok(value).map(Self) + let parsed = serde_json::from_value(value.clone())?; + Ok(Self { raw: value, parsed }) } } impl From for Json { fn from(value: PresentationDefinition) -> Self { - value.0 + value.raw } } diff --git a/src/core/authorization_request/verification/mod.rs b/src/core/authorization_request/verification/mod.rs index 6c725c2..5aeb4fd 100644 --- a/src/core/authorization_request/verification/mod.rs +++ b/src/core/authorization_request/verification/mod.rs @@ -1,8 +1,24 @@ -use crate::core::{object::UntypedObject, profile::WalletProfile}; +use crate::core::{ + metadata::{ + parameters::{ + verifier::{AuthorizationEncryptedResponseAlg, AuthorizationEncryptedResponseEnc}, + wallet::{ + AuthorizationEncryptionAlgValuesSupported, + AuthorizationEncryptionEncValuesSupported, + }, + }, + WalletMetadata, + }, + object::{ParsingErrorContext, TypedParameter, UntypedObject}, + profile::{Profile, Wallet}, +}; use anyhow::{bail, Context, Error, Result}; use async_trait::async_trait; -use super::{parameters::ClientIdScheme, AuthorizationRequestObject}; +use super::{ + parameters::{ClientIdScheme, ClientMetadata, ResponseMode}, + AuthorizationRequestObject, +}; pub mod did; pub mod x509_san_dns; @@ -70,18 +86,46 @@ pub trait RequestVerification { ) -> Result<(), Error>; } -pub(crate) async fn verify_request( +pub(crate) async fn verify_request( profile: &WP, jwt: String, + http_client: &reqwest::Client, ) -> Result { let request: AuthorizationRequestObject = ssi::jwt::decode_unverified::(&jwt) .context("unable to decode Authorization Request Object JWT")? .try_into()?; + validate_request_against_metadata::( + profile, + &request, + profile.wallet_metadata(), + http_client, + ) + .await?; + let client_id_scheme = request.client_id_scheme(); - if !profile - .wallet_metadata() + match client_id_scheme { + ClientIdScheme::Did => profile.did(&request, jwt).await?, + ClientIdScheme::EntityId => profile.entity_id(&request, jwt).await?, + ClientIdScheme::PreRegistered => profile.preregistered(&request, jwt).await?, + ClientIdScheme::RedirectUri => profile.redirect_uri(&request, jwt).await?, + ClientIdScheme::X509SanDns => profile.x509_san_dns(&request, jwt).await?, + ClientIdScheme::X509SanUri => profile.x509_san_uri(&request, jwt).await?, + ClientIdScheme::Other(scheme) => profile.other(scheme, &request, jwt).await?, + }; + + Ok(request) +} + +pub(crate) async fn validate_request_against_metadata( + profile: &P, + request: &AuthorizationRequestObject, + wallet_metadata: &WalletMetadata, + http_client: &reqwest::Client, +) -> Result<(), Error> { + let client_id_scheme = request.client_id_scheme(); + if !wallet_metadata .client_id_schemes_supported() .0 .contains(client_id_scheme) @@ -92,19 +136,45 @@ pub(crate) async fn verify_request( ) } - profile - .validate_request(&request) - .context("unable to validate request according to profile-specific checks:")?; + let client_metadata = ClientMetadata::resolve_with_http_client(request, http_client) + .await? + .0; - match client_id_scheme { - ClientIdScheme::Did => profile.did(&request, jwt).await?, - ClientIdScheme::EntityId => profile.entity_id(&request, jwt).await?, - ClientIdScheme::PreRegistered => profile.preregistered(&request, jwt).await?, - ClientIdScheme::RedirectUri => profile.redirect_uri(&request, jwt).await?, - ClientIdScheme::X509SanDns => profile.x509_san_dns(&request, jwt).await?, - ClientIdScheme::X509SanUri => profile.x509_san_uri(&request, jwt).await?, - ClientIdScheme::Other(scheme) => profile.other(scheme, &request, jwt).await?, - }; + let response_mode = request.get::().parsing_error()?; - Ok(request) + if response_mode.is_jarm()? { + let alg = client_metadata + .get::() + .parsing_error()?; + let enc = client_metadata + .get::() + .parsing_error()?; + + if let Some(supported_algs) = + wallet_metadata.get::() + { + if !supported_algs?.0.contains(&alg.0) { + bail!( + "unsupported {} '{}'", + AuthorizationEncryptedResponseAlg::KEY, + alg.0 + ) + } + } + if let Some(supported_encs) = + wallet_metadata.get::() + { + if !supported_encs?.0.contains(&enc.0) { + bail!( + "unsupported {} '{}'", + AuthorizationEncryptedResponseEnc::KEY, + enc.0 + ) + } + } + } + + profile + .validate_request(wallet_metadata, request) + .context("unable to validate request according to profile-specific checks:") } diff --git a/src/core/profile/mod.rs b/src/core/profile/mod.rs index 66a3560..e471786 100644 --- a/src/core/profile/mod.rs +++ b/src/core/profile/mod.rs @@ -12,7 +12,7 @@ use super::{ credential_format::CredentialFormat, metadata::WalletMetadata, response::{AuthorizationResponse, PostRedirection}, - util::http_client, + util::default_http_client, }; /// A specific profile of OID4VP. @@ -20,11 +20,17 @@ pub trait Profile { /// Credential Format used in this profile. type CredentialFormat: CredentialFormat; + /// Perform additional profile-specific checks on outbound and inbound requests. + fn validate_request( + &self, + wallet_metadata: &WalletMetadata, + request_object: &AuthorizationRequestObject, + ) -> Result<(), Error>; +} + +pub trait Verifier: Profile { /// Builder for profile-specific [PresentationDefinition]. type PresentationBuilder: PresentationBuilder; - - /// Perform additional profile-specific checks on outbound and inbound requests. - fn validate_request(&self, request_object: &AuthorizationRequestObject) -> Result<(), Error>; } pub trait PresentationBuilder: Default { @@ -32,7 +38,7 @@ pub trait PresentationBuilder: Default { } #[async_trait] -pub trait WalletProfile: Profile + RequestVerification + Sync { +pub trait Wallet: Profile + RequestVerification + Sync { type PresentationHandler: PresentationHandler; fn wallet_metadata(&self) -> &WalletMetadata; @@ -42,20 +48,31 @@ pub trait WalletProfile: Profile + RequestVerification + Sync { request_object: &AuthorizationRequestObject, ) -> Result; - async fn handle_request(&self, url: Url) -> Result { + async fn handle_request_with_http_client( + &self, + url: Url, + http_client: &reqwest::Client, + ) -> Result { let ar = AuthorizationRequest::from_url(url, &self.wallet_metadata().authorization_endpoint().0) .context("unable to parse authorization request")?; let aro = ar - .validate(self) + .validate_with_http_client(self, http_client) .await .context("unable to validate authorization request")?; self.to_handler(&aro).await } - async fn submit_response( + /// Uses library default http client. + async fn handle_request(&self, url: Url) -> Result { + self.handle_request_with_http_client(url, &default_http_client()?) + .await + } + + async fn submit_response_with_http_client( &self, handler: Self::PresentationHandler, + http_client: reqwest::Client, ) -> Result, Error> { let aro = handler.request().clone(); let response_object = handler.to_response()?; @@ -66,10 +83,9 @@ pub trait WalletProfile: Profile + RequestVerification + Sync { .serializable() .flatten_for_form() .context("unable to flatten authorization response")?; - let response = http_client()? + let response = http_client .post(return_uri.clone()) .form(&body) - .header("Prefer", "OID4VP-0.0.20") .send() .await .context("failed to post authorization response")?; @@ -90,6 +106,15 @@ pub trait WalletProfile: Profile + RequestVerification + Sync { ResponseMode::Unsupported(rm) => bail!("unsupported response_mode {rm}"), } } + + /// Uses library default http client. + async fn submit_response( + &self, + handler: Self::PresentationHandler, + ) -> Result, Error> { + self.submit_response_with_http_client(handler, default_http_client()?) + .await + } } pub trait PresentationHandler: Send { diff --git a/src/core/util/mod.rs b/src/core/util/mod.rs index 70b2ca4..78968f3 100644 --- a/src/core/util/mod.rs +++ b/src/core/util/mod.rs @@ -1,7 +1,17 @@ use anyhow::{Context, Error}; +use reqwest::header::HeaderMap; + +pub fn default_http_client() -> Result { + let mut headers: HeaderMap = Default::default(); + headers.insert( + "Prefer", + "OID4VP-0.0.20" + .parse() + .context("unable to parse Prefer header value")?, + ); -pub fn http_client() -> Result { reqwest::Client::builder() + .default_headers(headers) .use_rustls_tls() .build() .context("unable to build http_client") diff --git a/src/core/verifier/builder/mod.rs b/src/core/verifier/builder/mod.rs index d80ee5b..3c209f7 100644 --- a/src/core/verifier/builder/mod.rs +++ b/src/core/verifier/builder/mod.rs @@ -13,7 +13,7 @@ use crate::core::{ }, metadata::{parameters::wallet::AuthorizationEndpoint, WalletMetadata}, object::{ParsingErrorContext, TypedParameter, UntypedObject}, - profile::Profile, + profile::Verifier, }; use self::{by_reference::ByReference, client::Client}; @@ -27,29 +27,32 @@ mod by_reference; mod client; #[derive(Debug, Clone)] -pub struct SessionBuilder { +pub struct SessionBuilder { wallet_metadata: WalletMetadata, client: Option>, pass_by_reference: ByReference, request_params: UntypedObject, + profile: P, } -impl SessionBuilder { - pub fn new(wallet_metadata: WalletMetadata) -> Self { +impl SessionBuilder { + pub fn new(profile: P, wallet_metadata: WalletMetadata) -> Self { Self { wallet_metadata, client: None, pass_by_reference: ByReference::False, request_params: UntypedObject::default(), + profile, } } - pub async fn build(self, p: P) -> Result { + pub async fn build(self) -> Result> { let Self { wallet_metadata, client, pass_by_reference, mut request_params, + profile, } = self; let authorization_endpoint = wallet_metadata @@ -78,7 +81,7 @@ impl SessionBuilder { .try_into() .context("unable to construct Authorization Request Object from provided parameters")?; - p.validate_request(&request_object)?; + profile.validate_request(&wallet_metadata, &request_object)?; let request_object_jwt = client.generate_request_object_jwt(&request_object).await?; @@ -94,6 +97,7 @@ impl SessionBuilder { .to_url(authorization_endpoint)?; Ok(Session { + profile, authorization_request, request_object, request_object_jwt, @@ -112,8 +116,12 @@ impl SessionBuilder { self } - pub fn with_request_parameter(mut self, p: P) -> Self { - self.request_params.insert(p); + pub fn presentation_builder() -> P::PresentationBuilder { + P::PresentationBuilder::default() + } + + pub fn with_request_parameter(mut self, t: T) -> Self { + self.request_params.insert(t); self } @@ -123,7 +131,7 @@ impl SessionBuilder { vm: String, signer: T, resolver: &dyn DIDResolver, - ) -> Result> { + ) -> Result> { let (id, _f) = vm.rsplit_once('#').context(format!( "expected a DID verification method, received '{vm}'" ))?; @@ -142,6 +150,7 @@ impl SessionBuilder { wallet_metadata, pass_by_reference, request_params, + profile, .. } = self; @@ -156,6 +165,7 @@ impl SessionBuilder { client, pass_by_reference, request_params, + profile, }) } diff --git a/src/core/verifier/mod.rs b/src/core/verifier/mod.rs index 5fa424b..146a107 100644 --- a/src/core/verifier/mod.rs +++ b/src/core/verifier/mod.rs @@ -3,21 +3,24 @@ use url::Url; use self::builder::SessionBuilder; -use super::{authorization_request::AuthorizationRequestObject, metadata::WalletMetadata}; +use crate::core::{ + authorization_request::AuthorizationRequestObject, metadata::WalletMetadata, profile::Verifier, +}; pub mod builder; pub mod request_signer; #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Session { +pub struct Session { + profile: P, authorization_request: Url, request_object: AuthorizationRequestObject, request_object_jwt: String, } -impl Session { - pub fn builder(wallet_metadata: WalletMetadata) -> SessionBuilder { - SessionBuilder::new(wallet_metadata) +impl Session

{ + pub fn builder(profile: P, wallet_metadata: WalletMetadata) -> SessionBuilder

{ + SessionBuilder::new(profile, wallet_metadata) } pub fn authorization_request(&self) -> &Url { diff --git a/src/lib.rs b/src/lib.rs index 653d391..1628086 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,3 @@ pub mod core; -pub mod mdl_request; -pub mod mdl_response; pub mod presentation_exchange; -pub mod presentment; pub mod utils; diff --git a/src/mdl_request.rs b/src/mdl_request.rs deleted file mode 100644 index ea9753c..0000000 --- a/src/mdl_request.rs +++ /dev/null @@ -1,218 +0,0 @@ -use crate::utils::Openid4vpError; -use crate::{ - presentation_exchange::{ - Constraints, ConstraintsField, InputDescriptor, PresentationDefinition, - }, - utils::NonEmptyVec, -}; -use isomdl::definitions::helpers::NonEmptyMap; -use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; -use ssi::jwk::JWK; -use std::collections::BTreeMap; -use x509_cert::der::referenced::OwnedToRef; -use x509_cert::der::Decode; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RequestObject { - // Omitting iss is okay since the client_id is already in the request object - // pub iss: String, - pub aud: String, - pub response_type: String, - pub client_id: String, - pub client_id_scheme: Option, - pub response_uri: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub scope: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub state: Option, - #[serde(flatten)] - pub presentation_definition: PresDef, - #[serde(flatten)] - pub client_metadata: MetaData, - pub response_mode: Option, - pub nonce: Option, -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] -#[serde(untagged)] -pub enum MetaData { - ClientMetadata { client_metadata: ClientMetadata }, - ClientMetadataUri { client_metadata_uri: String }, -} - -#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] -#[serde(untagged)] -pub enum PresDef { - PresentationDefinition { - presentation_definition: PresentationDefinition, - }, - PresentationDefintionUri { - presentation_definition_uri: String, - }, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct ClientMetadata { - pub authorization_encrypted_response_alg: String, - pub authorization_encrypted_response_enc: String, - pub require_signed_request_object: bool, - pub jwks: Value, - pub vp_formats: Value, -} - -pub fn prepare_mdl_request_object( - jwk: JWK, - requested_fields: NonEmptyMap, Option>>, - client_id: String, - response_uri: String, - presentation_id: String, -) -> Result { - let jwks = json!({ "keys": vec![jwk] }); - let presentation_definition = mdl_presentation_definition(requested_fields, presentation_id)?; - let client_metadata = ClientMetadata { - authorization_encrypted_response_alg: "ES256".to_string(), - authorization_encrypted_response_enc: "A128GCM".to_string(), - vp_formats: json!({"mso_mdoc": { - "alg": [ - "ES256" - ] - }}), - jwks, - require_signed_request_object: false, - }; - - Ok(RequestObject { - aud: "https://self-issued.me/v2".to_string(), // per openid4vp chapter 5.6 - response_type: "vp_token".to_string(), - client_id: client_id.clone(), - client_id_scheme: Some("x509_san_uri".to_string()), - response_uri: Some(response_uri), - scope: Some("openid".to_string()), // I think it could also be None - state: None, - presentation_definition: PresDef::PresentationDefinition { - presentation_definition, - }, - client_metadata: MetaData::ClientMetadata { client_metadata }, - response_mode: Some("direct_post.jwt".to_string()), - nonce: Some(client_id), //TODO: should be some nonce - }) -} - -fn mdl_presentation_definition( - namespaces: NonEmptyMap, Option>>, - presentation_id: String, -) -> Result { - let input_descriptors = build_input_descriptors(namespaces); - Ok(PresentationDefinition { - id: presentation_id, - input_descriptors, - name: None, - purpose: None, - format: None, - }) -} - -//TODO: allow for specifying the algorithm -fn build_input_descriptors( - namespaces: NonEmptyMap, Option>>, -) -> Vec { - let path_base = "$['org.iso.18013.5.1']"; - - let input_descriptors: Vec = namespaces - .iter() - .map(|namespace| { - let format = json!({ - "mso_mdoc": { - "alg": [ - "ES256" - //TODO: add all supported algorithms - ] - }}); - let mut namespace_fields = BTreeMap::from(namespace.1.to_owned()); - namespace_fields.retain(|k, _v| k.is_some()); - - let fields: Vec = namespace_fields - .iter() - .map(|field| { - ConstraintsField { - //safe unwrap since none values are removed above - path: NonEmptyVec::new(format!( - "{}['{}']", - path_base, - field.0.as_ref().unwrap().to_owned() - )), - id: None, - purpose: None, - name: None, - filter: None, - optional: None, - intent_to_retain: *field.1, - } - }) - .collect(); - - let constraints = Constraints { - fields: Some(fields), - limit_disclosure: Some( - crate::presentation_exchange::ConstraintsLimitDisclosure::Required, - ), - }; - - InputDescriptor { - id: "org.iso.18013.5.1.mDL ".to_string(), - name: None, - purpose: None, - format: Some(format), - constraints: Some(constraints), - schema: None, - } - }) - .collect(); - - input_descriptors -} - -pub fn x509_public_key(der: Vec) -> Result { - x509_cert::Certificate::from_der(&der) - .map_err(|e| format!("could not parse certificate from DER: {e}"))? - .tbs_certificate - .subject_public_key_info - .owned_to_ref() - .try_into() - .map_err(|e| format!("could not parse p256 public key from pkcs8 spki: {e}")) -} - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - - fn prove_name_request() -> BTreeMap, Option> { - BTreeMap::from([ - ( - Some("org.iso.18013.5.1.family_name".to_string()), - Some(true), - ), - (Some("org.iso.18013.5.1.given_name".to_string()), Some(true)), - ( - Some("org.iso.18013.5.1.birth_date".to_string()), - Some(false), - ), - ]) - } - - #[test] - fn request_example() { - const DID_JWK: &str = r#"{"kty":"EC","crv":"secp256k1","x":"nrVtymZmqiSu9lU8DmVnB6W7XayJUj4uN7hC3uujZ9s","y":"XZA56MU96ne2c2K-ldbZxrAmLOsneJL1lE4PFnkyQnA","d":"mojL_WMJuMp1vmHNLUkc4es6IeAfcDB7qyZqTeKCEqE"}"#; - let minimal_mdl_request = NonEmptyMap::try_from(prove_name_request()).unwrap(); - let namespaces = NonEmptyMap::new("org.iso.18013.5.1".to_string(), minimal_mdl_request); - let client_id = "nonce".to_string(); - let redirect_uri = "localhost::3000".to_string(); - let presentation_id = "mDL".to_string(); - - let jwk: JWK = serde_json::from_str(DID_JWK).unwrap(); - let _request_object = - prepare_mdl_request_object(jwk, namespaces, client_id, redirect_uri, presentation_id) - .unwrap(); - } -} diff --git a/src/mdl_response.rs b/src/mdl_response.rs deleted file mode 100644 index 51ee6ad..0000000 --- a/src/mdl_response.rs +++ /dev/null @@ -1,167 +0,0 @@ -use crate::presentation_exchange::InputDescriptor; -use crate::utils::NonEmptyVec; -use crate::utils::Openid4vpError; -use isomdl; -pub use isomdl::definitions::device_request::ItemsRequest; -use isomdl::definitions::helpers::NonEmptyMap; -use std::collections::BTreeMap; - -fn match_path_to_mdl_field( - paths: NonEmptyVec, - mdl_field_paths: Vec, - namespace_name: String, -) -> Option { - let mut matched_mdl_paths: Vec> = paths - .iter() - .map(|suggested_path| { - let suggested_field_name = suggested_path.strip_prefix("$['org.iso.18013.5.1']")?; - let suggested_field_name = suggested_field_name.replace(['[', ']', '\''], ""); - let mut matches: Vec> = mdl_field_paths - .iter() - .map(|known_path| { - let known_path_field_name = - known_path.strip_prefix(&format!("{}{}", &namespace_name, ".")); - if let Some(path) = known_path_field_name { - if *path == suggested_field_name { - Some(path.to_owned()) - } else { - None - } - } else { - None - } - }) - .collect(); - matches.retain(|item| item.is_some()); - //TODO: if constraints limit = required and there are no matched paths for a certain field, throw an Error, if not then ignore. - if !matches.is_empty() { - matches.first()?.to_owned() - } else { - None - } - }) - .collect(); - - matched_mdl_paths.retain(|path| path.is_some()); - if !matched_mdl_paths.is_empty() { - matched_mdl_paths.first()?.to_owned() // always return the first match as defined in Presentation Exchange - } else { - None - } -} - -fn mdl_field_paths() -> Vec { - vec![ - "org.iso.18013.5.1.family_name".to_string(), - "org.iso.18013.5.1.given_name".to_string(), - "org.iso.18013.5.1.birth_date".to_string(), - "org.iso.18013.5.1.issue_date".to_string(), - "org.iso.18013.5.1.expiry_date".to_string(), - "org.iso.18013.5.1.issuing_country".to_string(), - "org.iso.18013.5.1.issuing_authority".to_string(), - "org.iso.18013.5.1.document_number".to_string(), - "org.iso.18013.5.1.portrait".to_string(), - "org.iso.18013.5.1.driving_privileges".to_string(), - "org.iso.18013.5.1.un_distinguishing_sign".to_string(), - "org.iso.18013.5.1.administrative_number".to_string(), - "org.iso.18013.5.1.sex".to_string(), - "org.iso.18013.5.1.height".to_string(), - "org.iso.18013.5.1.weight".to_string(), - "org.iso.18013.5.1.eye_colour".to_string(), - "org.iso.18013.5.1.hair_colour".to_string(), - "org.iso.18013.5.1.birth_place".to_string(), - "org.iso.18013.5.1.resident_address".to_string(), - "org.iso.18013.5.1.portrait_capture_date".to_string(), - "org.iso.18013.5.1.age_in_years".to_string(), - "org.iso.18013.5.1.age_birth_year".to_string(), - "org.iso.18013.5.1.age_over_18".to_string(), - "org.iso.18013.5.1.age_over_21".to_string(), - "org.iso.18013.5.1.issuing_jurisdiction".to_string(), - "org.iso.18013.5.1.nationality".to_string(), - "org.iso.18013.5.1.resident_city".to_string(), - "org.iso.18013.5.1.resident_state".to_string(), - "org.iso.18013.5.1.resident_postal_code".to_string(), - "org.iso.18013.5.1.resident_country".to_string(), - "org.iso.18013.5.1.aamva.domestic_driving_privileges".to_string(), - "org.iso.18013.5.1.aamva.name_suffix".to_string(), - "org.iso.18013.5.1.aamva.organ_donor".to_string(), - "org.iso.18013.5.1.aamva.veteran".to_string(), - "org.iso.18013.5.1.aamva.family_name_truncation".to_string(), - "org.iso.18013.5.1.aamva.given_name_truncation".to_string(), - "org.iso.18013.5.1.aamva.aka_family_name.v2".to_string(), - "org.iso.18013.5.1.aamva.aka_given_name.v2".to_string(), - "org.iso.18013.5.1.aamva.weight_range".to_string(), - "org.iso.18013.5.1.aamva.race_ethnicity".to_string(), - "org.iso.18013.5.1.aamva.EDL_credential".to_string(), - "org.iso.18013.5.1.aamva.DHS_compliance".to_string(), - "org.iso.18013.5.1.aamva.sex".to_string(), - "org.iso.18013.5.1.aamva.resident_county".to_string(), - "org.iso.18013.5.1.aamva.hazmat_endorsement_expiration_date".to_string(), - "org.iso.18013.5.1.aamva.CDL_indicator".to_string(), - "org.iso.18013.5.1.aamva.DHS_compliance_text".to_string(), - "org.iso.18013.5.1.aamva.DHS_temporary_lawful_status".to_string(), - ] -} - -impl TryFrom for ItemsRequest { - type Error = Openid4vpError; - fn try_from(input_descriptor: InputDescriptor) -> Result { - if let Some(constraints) = input_descriptor.constraints { - let doc_type = "org.iso.18013.5.1.mDL".to_string(); - let namespace_name = "org.iso.18013.5.1".to_string(); - let constraints_fields = constraints.fields; - - if let Some(cf) = constraints_fields { - let mut fields: BTreeMap, Option> = cf - .iter() - .map(|constraints_field| { - let path = match_path_to_mdl_field( - constraints_field.path.clone(), - mdl_field_paths(), - namespace_name.clone(), - ); - if let Some(p) = path { - (Some(p), constraints_field.intent_to_retain) - } else { - (None, None) - } - }) - .collect(); - - fields.retain(|k, _v| k.is_some()); - let x: BTreeMap, Option> = fields - .iter() - .map(|(k, v)| { - if v.is_none() { - (k.to_owned(), Some(false)) - } else { - (k.to_owned(), v.to_owned()) - } - }) - .collect(); - // safe unwraps - let requested_fields: BTreeMap = x - .iter() - .map(|(k, v)| (k.clone().unwrap(), v.unwrap())) - .collect(); - - let namespace: NonEmptyMap = NonEmptyMap::try_from(requested_fields)?; - let namespaces = NonEmptyMap::new(namespace_name, namespace); - - Ok(ItemsRequest { - namespaces, - doc_type, - request_info: None, - }) - } else { - Err(Openid4vpError::Empty( - "Missing constraints_fields".to_string(), - )) - } - } else { - Err(Openid4vpError::Empty( - "Missing inputdescriptors".to_string(), - )) - } - } -} diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 3e6f172..7eced07 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -267,8 +267,10 @@ pub(crate) mod tests { let path = path.unwrap().path(); if let Some(ext) = path.extension() { if ext != OsStr::new("json") - || ["appendix_DIDComm_example.json", - "appendix_CHAPI_example.json"] + || [ + "appendix_DIDComm_example.json", + "appendix_CHAPI_example.json", + ] .contains(&path.file_name().unwrap().to_str().unwrap()) { continue; diff --git a/src/presentment.rs b/src/presentment.rs deleted file mode 100644 index 8585ab6..0000000 --- a/src/presentment.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::mdl_request::ClientMetadata; -use crate::{mdl_request::RequestObject, utils::Openid4vpError}; -use async_trait::async_trait; -use isomdl::definitions::helpers::non_empty_map::NonEmptyMap; -use isomdl::definitions::oid4vp::DeviceResponse; -use isomdl::presentation::device::PreparedDeviceResponse; -use isomdl::presentation::reader::oid4vp::SessionManager; -use serde_json::Value; -use std::collections::BTreeMap; - -#[allow(clippy::too_many_arguments)] -pub trait Verify { - fn mdl_request( - &self, - requested_fields: NonEmptyMap, Option>>, - client_id: String, - redirect_uri: String, - presentation_id: String, - response_mode: String, - client_metadata: ClientMetadata, - e_reader_key_bytes: String, - ) -> Result; - - fn validate_mdl_response( - &self, - response: &[u8], - ) -> Result, Openid4vpError> { - let device_response: DeviceResponse = serde_cbor::from_slice(response)?; - let mut session_manager = SessionManager::new(device_response)?; - Ok(session_manager.handle_response()?) - } - - //fn vc_request(&self) {} - //fn validate_vc_response(&self){} -} - -#[async_trait(?Send)] -pub trait Present { - async fn prepare_mdl_response( - &self, - request: RequestObject, - ) -> Result; -} diff --git a/src/utils.rs b/src/utils.rs index 4f30afb..fe6e4f6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,8 +1,4 @@ use anyhow; -use isomdl::definitions::helpers::non_empty_map::Error as NonEmptyMapError; -use isomdl::definitions::Error as IsomdlDefinitionError; -use isomdl::presentation::reader::oid4vp::Error as IsomdlError; -use isomdl::presentation::reader::Error as IsomdlReaderErrror; use josekit::JoseError; use reqwest::Error as ReqwestError; use serde::{Deserialize, Serialize}; @@ -146,30 +142,6 @@ impl From for Openid4vpError { } } -impl From for Openid4vpError { - fn from(value: IsomdlError) -> Self { - Openid4vpError::IsomdlError(value.to_string()) - } -} - -impl From for Openid4vpError { - fn from(value: IsomdlReaderErrror) -> Self { - Openid4vpError::ResponseError(value.to_string()) - } -} - -impl From for Openid4vpError { - fn from(value: IsomdlDefinitionError) -> Self { - Openid4vpError::Empty(value.to_string()) - } -} - -impl From for Openid4vpError { - fn from(value: NonEmptyMapError) -> Self { - Openid4vpError::Empty(value.to_string()) - } -} - impl From for Openid4vpError { fn from(value: anyhow::Error) -> Self { Openid4vpError::Empty(value.to_string()) From 779756cf2922a729ba0e383f0bd814d772ef40eb Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 30 Oct 2023 16:29:44 +0000 Subject: [PATCH 23/23] Add VpFormats metadata param --- src/core/metadata/parameters/verifier.rs | 28 ++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/core/metadata/parameters/verifier.rs b/src/core/metadata/parameters/verifier.rs index 5d09a13..3681521 100644 --- a/src/core/metadata/parameters/verifier.rs +++ b/src/core/metadata/parameters/verifier.rs @@ -4,6 +4,27 @@ use serde_json::{Map, Value as Json}; use crate::core::object::TypedParameter; +#[derive(Debug, Clone, Deserialize)] +pub struct VpFormats(pub Map); + +impl TypedParameter for VpFormats { + const KEY: &'static str = "vp_formats"; +} + +impl TryFrom for VpFormats { + type Error = Error; + + fn try_from(value: Json) -> Result { + serde_json::from_value(value).map(Self).map_err(Into::into) + } +} + +impl From for Json { + fn from(value: VpFormats) -> Json { + value.0.into() + } +} + #[derive(Debug, Clone, Deserialize)] pub struct JWKs { pub keys: Vec>, @@ -125,6 +146,13 @@ mod test { .unwrap() } + #[test] + fn vp_formats() { + let VpFormats(fnd) = metadata().get().unwrap().unwrap(); + let exp = json!({"mso_mdoc": {}}).as_object().unwrap().clone(); + assert_eq!(fnd, exp) + } + #[test] fn jwks() { let JWKs { keys } = metadata().get().unwrap().unwrap();