From cd0f80db83bdb8e855558b1311c4018773ebbeb4 Mon Sep 17 00:00:00 2001 From: Jacob Date: Mon, 23 Oct 2023 14:43:48 +0100 Subject: [PATCH] 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())