From 13c4733ef9210eb5d8dd205ef7f74cdd8c1cd8c0 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 16 Sep 2024 08:04:39 -0700 Subject: [PATCH] Fix the presentation submission in the oid4vp-rs e2e test. (#8) * wip: use ssi 0.8.1, debugging e2e test using did resolver Signed-off-by: Ryan Tate * wip: use VerificationMethodDIDResolver for DIDClient constructor Signed-off-by: Ryan Tate * remove unused imports Signed-off-by: Ryan Tate * wip: debugging did resolver jwk not found in e2e flow Signed-off-by: Ryan Tate * feat/improve-presentation-exchange-support-in-oid4vp-rs add implementation methods for Presentation Definition. WIP: Need to continue work for Presentation Submission and the rest of the structs used in the presentation exchange flow. Signed-off-by: Ryan Tate Co-authored-by: Todd Showalter * add getter methods for presentation definition member fields Signed-off-by: Ryan Tate * fix broken links in documentation Signed-off-by: Ryan Tate * update presentation submission implementation Signed-off-by: Ryan Tate * fix test cases. todo: update test cases to use newly created interface for presentation exchange Signed-off-by: Ryan Tate * update json schema validator to use anyhow result type Signed-off-by: Ryan Tate * wip: use latest implementation changes, update tests Signed-off-by: Ryan Tate * fix verification method did resolver tests Signed-off-by: Ryan Tate * wip: remove unused imports Signed-off-by: Ryan Tate * add ClaimFormat type Signed-off-by: Ryan Tate * add regex support for string pattern matching Signed-off-by: Ryan Tate * revert uuid presentation definition id type to string Signed-off-by: Ryan Tate * fix: ensure negation of regex pattern match for error Signed-off-by: Ryan Tate * add 'other' variante to claim format type Signed-off-by: Ryan Tate * remove commented out code Signed-off-by: Ryan Tate * use ssi sub-crates instead of main ssi dependency Signed-off-by: Ryan Tate * add jwt_vc_json and jwt_vp_json claim formats Signed-off-by: Ryan Tate * wip: construct verifiable presentation for e2e test Signed-off-by: Ryan Tate * wip: perform validation on presentation submission Signed-off-by: Ryan Tate * ensure range exclusive values are checked; fix inclusive range values Signed-off-by: Ryan Tate * ensure enum values are parsed in alpha descending order This is a fix for a bug where ClaimFormat::JwtVc was being parsed when ClaimFormat::JwtVp should have been instead. The fix is to order the enum fields in alphabetical descending order, such that VP comes BEFORE VC, and so on, for the other formats. Signed-off-by: Ryan Tate * wip: verify authorized response presentation submission Signed-off-by: Ryan Tate * ensure json schema validator adheres to the specification Signed-off-by: Ryan Tate * ensure json schema validator adheres to the specification Signed-off-by: Ryan Tate * add unit tests for schema validator Signed-off-by: Ryan Tate * use serde default value for constraints field if not found during deserialization Signed-off-by: Ryan Tate * remove unused imports Signed-off-by: Ryan Tate * remove unsed imports in test files Signed-off-by: Ryan Tate * update vp token Signed-off-by: Ryan Tate * rebase with main Signed-off-by: Ryan Tate * remove unused dependencies Signed-off-by: Ryan Tate * add rand crate and provide random nonce method using Rng trait Signed-off-by: Ryan Tate * remove todos and update comments, use JWKResolver instead of VerificationMethodDIDResolver Signed-off-by: Ryan Tate * fix clippy warnings Signed-off-by: Ryan Tate * verify jwt in validate_authorization_response presentation definition method Signed-off-by: Ryan Tate * Update tests/e2e.rs Co-authored-by: Jacob * update descriptor map nested path in e2e example Signed-off-by: Ryan Tate * remove dependency patches Signed-off-by: Ryan Tate * add paths to example for input descriptor constraints field Signed-off-by: Ryan Tate * use top level json path for jwt_vp_json Signed-off-by: Ryan Tate * Update src/verifier/client.rs Co-authored-by: Jacob * rebase Signed-off-by: Ryan Tate * revert validation function async signature to use boxed pin future Signed-off-by: Ryan Tate * add helper methods Signed-off-by: Ryan Tate * debug: jwt claim signing does not include public key Signed-off-by: Ryan Tate * make request signer methods return a result Signed-off-by: Ryan Tate * refactor presentation exchange file into smaller modules Signed-off-by: Ryan Tate * fix clippy warnings Signed-off-by: Ryan Tate * fix outcome error cause Signed-off-by: Ryan Tate * add credential format and add requested fields helper method to input descriptor Signed-off-by: Ryan Tate * Update src/core/presentation_definition.rs Co-authored-by: Jacob * Update src/core/presentation_definition.rs Co-authored-by: Jacob * wip: add notes on required fields parsing Signed-off-by: Ryan Tate * update vp token base64 encoding and check for multiple vp payloads Signed-off-by: Ryan Tate * move validation to auth response impl instead of presentation definition impl Signed-off-by: Ryan Tate * remove feature gated non-optional deps Signed-off-by: Ryan Tate * remove cfg features Signed-off-by: Ryan Tate * wip: handle groups in presentation definition, input descriptor tests Signed-off-by: Ryan Tate * remove cfg feature tags Signed-off-by: Ryan Tate * remove extraneous metadata helper methods; use UntypedObject for dereferencing Signed-off-by: Ryan Tate * add submission requirement check for presentation validation Signed-off-by: Ryan Tate * add validate method to vp token; ensure submission requirement all rule is enforced. Signed-off-by: Ryan Tate * add vp token validate unencoded method. fix minor todos. Signed-off-by: Ryan Tate * Update Cargo.toml Co-authored-by: Jacob * fix other claim format serde Signed-off-by: Ryan Tate * use Vec::is_empty versus Option::is_none for various serialization fields This commit also removes validation logic from vp token response struct. Signed-off-by: Ryan Tate * revert to use of ClaimFormatMap to pass presentation defintion test suite Signed-off-by: Ryan Tate --------- Signed-off-by: Ryan Tate Co-authored-by: Todd Showalter Co-authored-by: Jacob --- Cargo.toml | 21 +- src/core/authorization_request/parameters.rs | 52 +- .../authorization_request/verification/did.rs | 14 +- .../authorization_request/verification/mod.rs | 7 +- .../verification/verifier.rs | 4 - .../verification/x509_san.rs | 2 +- src/core/credential_format/mod.rs | 308 +++++- src/core/input_descriptor.rs | 598 ++++++++++++ src/core/metadata/mod.rs | 32 +- src/core/metadata/parameters/verifier.rs | 39 +- src/core/metadata/parameters/wallet.rs | 34 +- src/core/mod.rs | 3 + src/core/object/mod.rs | 17 +- src/core/presentation_definition.rs | 373 ++++++++ src/core/presentation_submission.rs | 148 +++ src/core/response/mod.rs | 14 +- src/core/response/parameters.rs | 29 +- src/core/util/mod.rs | 5 +- src/holder/mod.rs | 1 + src/holder/verifiable_presentation_builder.rs | 164 ++++ src/lib.rs | 4 +- src/presentation_exchange.rs | 904 ------------------ src/tests.rs | 220 +++++ src/utils.rs | 2 +- src/verifier/client.rs | 28 +- src/verifier/request_builder.rs | 2 +- src/verifier/request_signer.rs | 59 +- src/verifier/session.rs | 6 +- tests/e2e.rs | 144 ++- tests/examples/vp.jwt | 1 + tests/jwt_vc.rs | 17 +- tests/jwt_vp.rs | 48 + .../presentation-submission/definition_1.json | 28 + .../presentation-submission/definition_2.json | 50 + .../presentation-submission/definition_3.json | 42 + .../presentation-submission/submission_1.json | 16 + .../presentation-submission/submission_2.json | 21 + .../presentation-submission/submission_3.json | 21 + tests/presentation-submission/vp_1.json | 26 + tests/presentation-submission/vp_2.json | 36 + tests/presentation-submission/vp_3.json | 42 + 41 files changed, 2500 insertions(+), 1082 deletions(-) create mode 100644 src/core/input_descriptor.rs create mode 100644 src/core/presentation_definition.rs create mode 100644 src/core/presentation_submission.rs create mode 100644 src/holder/mod.rs create mode 100644 src/holder/verifiable_presentation_builder.rs delete mode 100644 src/presentation_exchange.rs create mode 100644 src/tests.rs create mode 100644 tests/examples/vp.jwt create mode 100644 tests/jwt_vp.rs create mode 100644 tests/presentation-submission/definition_1.json create mode 100644 tests/presentation-submission/definition_2.json create mode 100644 tests/presentation-submission/definition_3.json create mode 100644 tests/presentation-submission/submission_1.json create mode 100644 tests/presentation-submission/submission_2.json create mode 100644 tests/presentation-submission/submission_3.json create mode 100644 tests/presentation-submission/vp_1.json create mode 100644 tests/presentation-submission/vp_2.json create mode 100644 tests/presentation-submission/vp_3.json diff --git a/Cargo.toml b/Cargo.toml index cc6926f..bec8d49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,28 +9,28 @@ repository = "https://github.com/spruceid/oidc4vp-rs/" documentation = "https://docs.rs/oid4vp/" [features] -reqwest = ["dep:reqwest"] -p256 = ["dep:p256"] +default = [] [dependencies] anyhow = "1.0.75" async-trait = "0.1.73" base64 = "0.21.4" -did-web = "0.2.2" http = "1.1.0" +# NOTE: ssi-jwk uses syntax_json, but does not use the `serde_json` feature for serialization/deserialization. +json-syntax = { version = "0.12.5", features = ["serde_json"] } jsonpath_lib = "0.3.0" jsonschema = "0.18.0" oid4vp-frontend = { version = "0.1.0", path = "oid4vp-frontend" } -p256 = { version = "0.13.2", features = ["jwk"], optional = true } -regex = "1.10.6" -reqwest = { version = "0.12.5", features = ["rustls-tls"], optional = true } +p256 = { version = "0.13.2", features = ["jwk"] } +rand = { version = "0.8.5" } +reqwest = { version = "0.12.5", features = ["rustls-tls"] } serde = "1.0.188" -serde_cbor = "0.11.2" serde_json = "1.0.107" -serde_qs = "0.12.0" serde_urlencoded = "0.7.1" -ssi = "0.7" -thiserror = "1.0.49" +ssi-claims = "0.1.0" +ssi-dids = "0.2.0" +ssi-jwk = { version = "0.2.1", features = ["secp256r1"] } +ssi-verification-methods = "0.1.1" tokio = "1.32.0" tracing = "0.1.37" url = { version = "2.4.1", features = ["serde"] } @@ -40,7 +40,6 @@ x509-cert = "0.2.4" serde_path_to_error = "0.1.8" tokio = { version = "1.32.0", features = ["macros"] } did-method-key = "0.2" -oid4vp = { path = ".", features = ["p256"] } [target.'cfg(target_arch = "wasm32")'.dependencies] uuid = { version = "1.2", features = ["v4", "serde", "js"] } diff --git a/src/core/authorization_request/parameters.rs b/src/core/authorization_request/parameters.rs index 517ab54..58429c2 100644 --- a/src/core/authorization_request/parameters.rs +++ b/src/core/authorization_request/parameters.rs @@ -1,7 +1,8 @@ -use std::fmt; +use std::{fmt, ops::Deref}; use crate::core::{ object::{ParsingErrorContext, TypedParameter, UntypedObject}, + presentation_definition::PresentationDefinition as PresentationDefinitionParsed, util::{base_request, AsyncHttpClient}, }; use anyhow::{bail, Context, Error, Ok}; @@ -193,7 +194,42 @@ impl TryFrom for ClientMetadataUri { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Nonce(pub String); +pub struct Nonce(String); + +impl From for Nonce { + fn from(value: String) -> Self { + Self(value) + } +} + +impl From<&str> for Nonce { + fn from(value: &str) -> Self { + Self(value.to_string()) + } +} + +impl Deref for Nonce { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::fmt::Display for Nonce { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Nonce { + /// Crate a new `Nonce` with a random value of the given length. + pub fn random(rng: &mut impl rand::Rng, length: usize) -> Self { + use rand::distributions::{Alphanumeric, DistString}; + + Self(Alphanumeric.sample_string(rng, length)) + } +} impl TypedParameter for Nonce { const KEY: &'static str = "nonce"; @@ -432,25 +468,23 @@ impl From for Json { #[derive(Debug, Clone)] pub struct PresentationDefinition { raw: Json, - parsed: crate::presentation_exchange::PresentationDefinition, + parsed: PresentationDefinitionParsed, } impl PresentationDefinition { - pub fn into_parsed(self) -> crate::presentation_exchange::PresentationDefinition { + pub fn into_parsed(self) -> PresentationDefinitionParsed { self.parsed } - pub fn parsed(&self) -> &crate::presentation_exchange::PresentationDefinition { + pub fn parsed(&self) -> &PresentationDefinitionParsed { &self.parsed } } -impl TryFrom for PresentationDefinition { +impl TryFrom for PresentationDefinition { type Error = Error; - fn try_from( - parsed: crate::presentation_exchange::PresentationDefinition, - ) -> Result { + fn try_from(parsed: PresentationDefinitionParsed) -> Result { let raw = serde_json::to_value(parsed.clone())?; Ok(Self { raw, parsed }) } diff --git a/src/core/authorization_request/verification/did.rs b/src/core/authorization_request/verification/did.rs index 8b14607..5817881 100644 --- a/src/core/authorization_request/verification/did.rs +++ b/src/core/authorization_request/verification/did.rs @@ -6,7 +6,8 @@ use crate::core::{ use anyhow::{bail, Context, Result}; use base64::prelude::*; use serde_json::{Map, Value as Json}; -use ssi::did_resolve::{resolve_key, DIDResolver}; + +use ssi_jwk::JWKResolver; /// Default implementation of request validation for `client_id_scheme` `did`. pub async fn verify_with_resolver( @@ -14,9 +15,9 @@ pub async fn verify_with_resolver( request_object: &AuthorizationRequestObject, request_jwt: String, trusted_dids: Option<&[String]>, - resolver: &dyn DIDResolver, + resolver: impl JWKResolver, ) -> Result<()> { - let (headers_b64, _, _) = ssi::jws::split_jws(&request_jwt)?; + let (headers_b64, _, _) = ssi_claims::jws::split_jws(&request_jwt)?; let headers_json_bytes = BASE64_URL_SAFE_NO_PAD .decode(headers_b64) @@ -64,11 +65,12 @@ pub async fn verify_with_resolver( } } - let jwk = resolve_key(&kid, resolver) + let jwk = resolver + .fetch_public_jwk(Some(&kid)) .await - .context("unable to resolve verification method from 'kid' header")?; + .context("unable to resolve key from verification method")?; - let _: Json = ssi::jwt::decode_verify(&request_jwt, &jwk) + let _: Json = ssi_claims::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 01faab7..8773b25 100644 --- a/src/core/authorization_request/verification/mod.rs +++ b/src/core/authorization_request/verification/mod.rs @@ -105,9 +105,10 @@ pub(crate) async fn verify_request( wallet: &W, jwt: String, ) -> Result { - let request: AuthorizationRequestObject = ssi::jwt::decode_unverified::(&jwt) - .context("unable to decode Authorization Request Object JWT")? - .try_into()?; + let request: AuthorizationRequestObject = + ssi_claims::jwt::decode_unverified::(&jwt) + .context("unable to decode Authorization Request Object JWT")? + .try_into()?; validate_request_against_metadata(wallet, &request).await?; diff --git a/src/core/authorization_request/verification/verifier.rs b/src/core/authorization_request/verification/verifier.rs index 7c5698c..5b0d5e9 100644 --- a/src/core/authorization_request/verification/verifier.rs +++ b/src/core/authorization_request/verification/verifier.rs @@ -1,7 +1,5 @@ use anyhow::Result; -#[cfg(feature = "p256")] use anyhow::{bail, Error}; -#[cfg(feature = "p256")] use p256::ecdsa::signature::Verifier as _; use x509_cert::spki::SubjectPublicKeyInfoRef; @@ -15,11 +13,9 @@ pub trait Verifier: Sized { fn verify(&self, payload: &[u8], signature: &[u8]) -> Result<()>; } -#[cfg(feature = "p256")] #[derive(Debug, Clone)] pub struct P256Verifier(p256::ecdsa::VerifyingKey); -#[cfg(feature = "p256")] impl Verifier for P256Verifier { fn from_spki(spki: SubjectPublicKeyInfoRef<'_>, algorithm: String) -> Result { if algorithm != "ES256" { diff --git a/src/core/authorization_request/verification/x509_san.rs b/src/core/authorization_request/verification/x509_san.rs index ffff434..27cd005 100644 --- a/src/core/authorization_request/verification/x509_san.rs +++ b/src/core/authorization_request/verification/x509_san.rs @@ -28,7 +28,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) = ssi::jws::split_jws(&request_jwt)?; + let (headers_b64, body_b64, sig_b64) = ssi_claims::jws::split_jws(&request_jwt)?; let headers_json_bytes = BASE64_URL_SAFE_NO_PAD .decode(headers_b64) diff --git a/src/core/credential_format/mod.rs b/src/core/credential_format/mod.rs index 863258e..d3239a7 100644 --- a/src/core/credential_format/mod.rs +++ b/src/core/credential_format/mod.rs @@ -1,17 +1,305 @@ -/// A credential format that can be transmitted using OID4VP. -pub trait CredentialFormat { - /// The ID of the credential format. - const ID: &'static str; +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +/// A Json object of claim formats. +pub type ClaimFormatMap = HashMap; + +/// The credential type that may be requested in a presentation request. +// NOTE: Credential types can be presented in a number of formats and therefore +// is an alias of a String is used. In the future, there may be a case to create +// a new type with associative methods, e.g., to parse various credential types, etc. +pub type CredentialType = String; + +/// The Presentation Definition MAY include a format property. The value MUST be an object with one or +/// more properties matching the registered [ClaimFormatDesignation] (e.g., jwt, jwt_vc, jwt_vp, etc.). +/// The properties inform the Holder of the Claim format configurations the Verifier can process. +/// The value for each claim format property MUST be an object composed as follows: +/// +/// The object MUST include a format-specific property (i.e., alg, proof_type) that expresses which +/// algorithms the Verifier supports for the format. Its value MUST be an array of one or more +/// format-specific algorithmic identifier references, as noted in the [ClaimFormatDesignation]. +/// +/// See [https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition](https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition) +/// for an example schema. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum ClaimFormat { + #[serde(rename = "jwt")] + Jwt { + /// The algorithm used to sign the JWT. + alg: Vec, + }, + #[serde(rename = "jwt_vc")] + JwtVc { + /// The algorithm used to sign the JWT verifiable credential. + alg: Vec, + }, + #[serde(rename = "jwt_vp")] + JwtVp { + /// The algorithm used to sign the JWT verifiable presentation. + alg: Vec, + }, + #[serde(rename = "jwt_vc_json")] + JwtVcJson { + /// Used in the OID4VP specification for wallet methods supported. + alg_values_supported: Vec, + }, + #[serde(rename = "jwt_vp_json")] + JwtVpJson { + /// Used in the OID4VP specification for wallet methods supported. + alg_values_supported: Vec, + }, + #[serde(rename = "ldp")] + Ldp { + /// The proof type used to sign the linked data proof. + /// e.g., "JsonWebSignature2020", "Ed25519Signature2018", "EcdsaSecp256k1Signature2019", "RsaSignature2018" + proof_type: Vec, + }, + #[serde(rename = "ldp_vc")] + LdpVc { + /// The proof type used to sign the linked data proof verifiable credential. + proof_type: Vec, + }, + #[serde(rename = "ldp_vp")] + LdpVp { + /// The proof type used to sign the linked data proof verifiable presentation. + proof_type: Vec, + }, + #[serde(rename = "ac_vc")] + AcVc { + /// The proof type used to sign the anoncreds verifiable credential. + proof_type: Vec, + }, + #[serde(rename = "ac_vp")] + AcVp { + /// The proof type used to sign the anoncreds verifiable presentation. + proof_type: Vec, + }, + #[serde(rename = "mso_mdoc")] + MsoMDoc(serde_json::Value), + /// Support for non-standard claim formats. + // NOTE: a `format` property will be included within the serialized + // type. This will help for identifying the claim format designation type. + #[serde(untagged)] + Other(serde_json::Value), +} + +impl ClaimFormat { + /// Returns the designated format of the claim. + /// + /// e.g., jwt, jwt_vc, jwt_vp, ldp, ldp_vc, ldp_vp, ac_vc, ac_vp, mso_mdoc + pub fn designation(&self) -> ClaimFormatDesignation { + match self { + ClaimFormat::Jwt { .. } => ClaimFormatDesignation::Jwt, + ClaimFormat::JwtVc { .. } => ClaimFormatDesignation::JwtVc, + ClaimFormat::JwtVcJson { .. } => ClaimFormatDesignation::JwtVcJson, + ClaimFormat::JwtVp { .. } => ClaimFormatDesignation::JwtVp, + ClaimFormat::JwtVpJson { .. } => ClaimFormatDesignation::JwtVpJson, + ClaimFormat::Ldp { .. } => ClaimFormatDesignation::Ldp, + ClaimFormat::LdpVc { .. } => ClaimFormatDesignation::LdpVc, + ClaimFormat::LdpVp { .. } => ClaimFormatDesignation::LdpVp, + ClaimFormat::AcVc { .. } => ClaimFormatDesignation::AcVc, + ClaimFormat::AcVp { .. } => ClaimFormatDesignation::AcVp, + ClaimFormat::MsoMDoc(_) => ClaimFormatDesignation::MsoMDoc, + ClaimFormat::Other(value) => { + // Parse the format from the first key found in the value map. + let format = value + .as_object() + .and_then(|map| map.keys().next()) + .map(ToOwned::to_owned) + .unwrap_or("other".into()); + + ClaimFormatDesignation::Other(format) + } + } + } +} + +/// Claim format payload +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub enum ClaimFormatPayload { + #[serde(rename = "alg")] + Alg(Vec), + /// This variant is primarily used for `jwt_vc_json` and `jwt_vp_json` + /// claim presentation algorithm types supported by a wallet. + #[serde(rename = "alg_values_supported")] + AlgValuesSupported(Vec), + #[serde(rename = "proof_type")] + ProofType(Vec), + #[serde(untagged)] + Json(serde_json::Value), } -pub struct MsoMdoc; +impl ClaimFormatPayload { + /// Adds an algorithm value to the list of supported algorithms. + /// + /// This method is a no-op if self is not of type `AlgValuesSupported` or `Alg`. + pub fn add_alg(&mut self, alg: String) { + if let Self::Alg(algs) | Self::AlgValuesSupported(algs) = self { + algs.push(alg); + } + } -impl CredentialFormat for MsoMdoc { - const ID: &'static str = "mso_mdoc"; + /// Adds a proof type to the list of supported proof types. + /// + /// This method is a no-op if self is not of type `ProofType`. + pub fn add_proof_type(&mut self, proof_type: String) { + if let Self::ProofType(proof_types) = self { + proof_types.push(proof_type); + } + } } -pub struct JwtVc; +/// The claim format designation type is used in the input description object to specify the format of the claim. +/// +/// Registry of claim format type: https://identity.foundation/claim-format-registry/#registry +/// +/// Documentation based on the [DIF Presentation Exchange Specification v2.0](https://identity.foundation/presentation-exchange/spec/v2.0.0/#claim-format-designations) +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub enum ClaimFormatDesignation { + /// The format is a JSON Web Token (JWT) as defined by [RFC7519](https://identity.foundation/claim-format-registry/#ref:RFC7519) + /// that will be submitted in the form of a JWT encoded string. Expression of + /// supported algorithms in relation to this format MUST be conveyed using an `alg` + /// property paired with values that are identifiers from the JSON Web Algorithms + /// registry [RFC7518](https://identity.foundation/claim-format-registry/#ref:RFC7518). + #[serde(rename = "jwt")] + Jwt, + /// These formats are JSON Web Tokens (JWTs) [RFC7519](https://identity.foundation/claim-format-registry/#ref:RFC7519) + /// that will be submitted in the form of a JWT-encoded string, with a payload extractable from it defined according to the + /// JSON Web Token (JWT) [section] of the W3C [VC-DATA-MODEL](https://identity.foundation/claim-format-registry/#term:vc-data-model) + /// specification. Expression of supported algorithms in relation to these formats MUST be conveyed using an JWT alg + /// property paired with values that are identifiers from the JSON Web Algorithms registry in + /// [RFC7518](https://identity.foundation/claim-format-registry/#ref:RFC7518) Section 3. + #[serde(rename = "jwt_vc")] + JwtVc, + /// See [JwtVc](JwtVc) for more information. + #[serde(rename = "jwt_vp")] + JwtVp, + #[serde(rename = "jwt_vc_json")] + JwtVcJson, + #[serde(rename = "jwt_vp_json")] + JwtVpJson, + /// The format is a Linked-Data Proof that will be submitted as an object. + /// Expression of supported algorithms in relation to these formats MUST be + /// conveyed using a proof_type property with values that are identifiers from + /// the Linked Data Cryptographic Suite Registry [LDP-Registry](https://identity.foundation/claim-format-registry/#term:ldp-registry). + #[serde(rename = "ldp")] + Ldp, + /// Verifiable Credentials or Verifiable Presentations signed with Linked Data Proof formats. + /// These are descriptions of formats normatively defined in the W3C Verifiable Credentials + /// specification [VC-DATA-MODEL](https://identity.foundation/claim-format-registry/#term:vc-data-model), + /// and will be submitted in the form of a JSON object. Expression of supported algorithms in relation to + /// these formats MUST be conveyed using a proof_type property paired with values that are identifiers from the + /// Linked Data Cryptographic Suite Registry (LDP-Registry). + #[serde(rename = "ldp_vc")] + LdpVc, + /// See [LdpVc](LdpVc) for more information. + #[serde(rename = "ldp_vp")] + LdpVp, + /// This format is for Verifiable Credentials using AnonCreds. + /// AnonCreds is a VC format that adds important + /// privacy-protecting ZKP (zero-knowledge proof) capabilities + /// to the core VC assurances. + #[serde(rename = "ac_vc")] + AcVc, + /// This format is for Verifiable Presentations using AnonCreds. + /// AnonCreds is a VC format that adds important privacy-protecting ZKP + /// (zero-knowledge proof) capabilities to the core VC assurances. + #[serde(rename = "ac_vp")] + AcVp, + /// The format is defined by ISO/IEC 18013-5:2021 [ISO.18013-5](https://identity.foundation/claim-format-registry/#term:iso.18013-5) + /// which defines a mobile driving license (mDL) Credential in the mobile document (mdoc) format. + /// Although ISO/IEC 18013-5:2021 ISO.18013-5 is specific to mobile driving licenses (mDLs), + /// the Credential format can be utilized with any type of Credential (or mdoc document types). + #[serde(rename = "mso_mdoc")] + MsoMDoc, + /// Other claim format designations not covered by the above. + /// + /// The value of this variant is the name of the claim format designation. + #[serde(untagged)] + Other(String), +} + +impl From<&str> for ClaimFormatDesignation { + fn from(s: &str) -> Self { + match s { + "jwt" => Self::Jwt, + "jwt_vc" => Self::JwtVc, + "jwt_vp" => Self::JwtVp, + "jwt_vc_json" => Self::JwtVcJson, + "jwt_vp_json" => Self::JwtVpJson, + "ldp" => Self::Ldp, + "ldp_vc" => Self::LdpVc, + "ldp_vp" => Self::LdpVp, + "ac_vc" => Self::AcVc, + "ac_vp" => Self::AcVp, + "mso_mdoc" => Self::MsoMDoc, + s => Self::Other(s.to_string()), + } + } +} + +impl From for String { + fn from(format: ClaimFormatDesignation) -> Self { + match format { + ClaimFormatDesignation::AcVc => "ac_vc".to_string(), + ClaimFormatDesignation::AcVp => "ac_vp".to_string(), + ClaimFormatDesignation::Jwt => "jwt".to_string(), + ClaimFormatDesignation::JwtVc => "jwt_vc".to_string(), + ClaimFormatDesignation::JwtVp => "jwt_vp".to_string(), + ClaimFormatDesignation::JwtVcJson => "jwt_vc_json".to_string(), + ClaimFormatDesignation::JwtVpJson => "jwt_vp_json".to_string(), + ClaimFormatDesignation::Ldp => "ldp".to_string(), + ClaimFormatDesignation::LdpVc => "ldp_vc".to_string(), + ClaimFormatDesignation::LdpVp => "ldp_vp".to_string(), + ClaimFormatDesignation::MsoMDoc => "mso_mdoc".to_string(), + ClaimFormatDesignation::Other(s) => s, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use serde_json::json; + + #[test] + fn test_credential_format_serialization() { + let value = json!({ + "claim_formats_supported": { + "jwt_vc": { + "alg": ["ES256", "EdDSA"], + "proof_type": ["JsonWebSignature2020"] + }, + "ldp_vc": { + "proof_type": ["Ed25519Signature2018", "EcdsaSecp256k1Signature2019"] + }, + "sd_jwt_vc": { + "alg": ["ES256", "ES384"], + "kb_jwt_alg": ["ES256"] + }, + "com.example.custom_vc": { + "version": "1.0", + "encryption": ["AES-GCM"], + "signature": ["ED25519"] + } + } + }); + + let claim_format_map: ClaimFormatMap = + serde_json::from_value(value["claim_formats_supported"].clone()) + .expect("Failed to parse claim format map"); -impl CredentialFormat for JwtVc { - const ID: &'static str = "jwt_vc"; + assert!(claim_format_map.contains_key(&ClaimFormatDesignation::JwtVc)); + assert!(claim_format_map.contains_key(&ClaimFormatDesignation::LdpVc)); + assert!( + claim_format_map.contains_key(&ClaimFormatDesignation::Other("sd_jwt_vc".to_string())) + ); + assert!( + claim_format_map.contains_key(&ClaimFormatDesignation::Other( + "com.example.custom_vc".to_string() + )) + ); + } } diff --git a/src/core/input_descriptor.rs b/src/core/input_descriptor.rs new file mode 100644 index 0000000..7a8a870 --- /dev/null +++ b/src/core/input_descriptor.rs @@ -0,0 +1,598 @@ +use super::{credential_format::*, presentation_submission::*}; +use crate::utils::NonEmptyVec; + +use anyhow::{bail, Context, Result}; +use jsonschema::{JSONSchema, ValidationError}; +use serde::{Deserialize, Serialize}; +use ssi_claims::jwt::VerifiablePresentation; +use ssi_dids::ssi_json_ld::syntax::from_value; + +/// A GroupId represents a unique identifier for a group of Input Descriptors. +/// +/// This type is also used by the submission requirements to group input descriptors. +pub type GroupId = String; + +/// A JSONPath is a string that represents a path to a specific value within a JSON object. +/// +/// For syntax details, see [https://identity.foundation/presentation-exchange/spec/v2.0.0/#jsonpath-syntax-definition](https://identity.foundation/presentation-exchange/spec/v2.0.0/#jsonpath-syntax-definition) +pub type JsonPath = String; + +/// The predicate Feature introduces properties enabling Verifier to request that Holder apply a predicate and return the result. +/// +/// The predicate Feature extends the Input Descriptor Object `constraints.fields` object to add a predicate property. +/// +/// The value of predicate **MUST** be one of the following strings: `required` or `preferred`. +/// +/// If the predicate property is not present, a Conformant Consumer **MUST NOT** return derived predicate values. +/// +/// See: [https://identity.foundation/presentation-exchange/#predicate-feature](https://identity.foundation/presentation-exchange/#predicate-feature) +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +pub enum Predicate { + /// required - This indicates that the returned value **MUST** be the boolean result of + /// applying the value of the filter property to the result of evaluating the path property. + #[serde(rename = "required")] + Required, + /// preferred - This indicates that the returned value **SHOULD** be the boolean result of + /// applying the value of the filter property to the result of evaluating the path property. + #[serde(rename = "preferred")] + Preferred, +} + +/// Input Descriptors are objects used to describe the information a +/// [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) requires of a +/// [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder). +/// +/// All Input Descriptors MUST be satisfied, unless otherwise specified by a +/// [Feature](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:feature). +/// +/// See: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object](https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object) +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct InputDescriptor { + id: String, + #[serde(default)] + constraints: Constraints, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + purpose: Option, + #[serde(default, skip_serializing_if = "ClaimFormatMap::is_empty")] + format: ClaimFormatMap, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + group: Vec, +} + +impl InputDescriptor { + /// Create a new instance of the input descriptor with the given id and constraints. + /// + /// The Input Descriptor Object MUST contain an id property. The value of the id + /// property MUST be a string that does not conflict with the id of another + /// Input Descriptor Object in the same Presentation Definition. + /// + /// + /// The Input Descriptor Object MUST contain a constraints property. + /// + /// See: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object](https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object) + pub fn new(id: String, constraints: Constraints) -> Self { + Self { + id, + constraints, + ..Default::default() + } + } + + /// Return the id of the input descriptor. + pub fn id(&self) -> &str { + self.id.as_str() + } + + /// Return the constraints of the input descriptor. + pub fn constraints(&self) -> &Constraints { + &self.constraints + } + + /// Set the name of the input descriptor. + pub fn set_name(mut self, name: String) -> Self { + self.name = Some(name); + self + } + + /// Return the name of the input descriptor. + pub fn name(&self) -> Option<&String> { + self.name.as_ref() + } + + /// Set the purpose of the input descriptor. + /// + /// The purpose of the input descriptor is an optional field. + /// + /// If present, the purpose MUST be a string that describes the purpose for which the + /// [Claim](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:claim)'s + /// data is being requested. + pub fn set_purpose(mut self, purpose: String) -> Self { + self.purpose = Some(purpose); + self + } + + /// Return the purpose of the input descriptor. + /// + /// If present, the purpose MUST be a string that describes the purpose for which the + /// [Claim](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:claim)'s + /// data is being requested. + pub fn purpose(&self) -> Option<&String> { + self.purpose.as_ref() + } + + /// Set the format of the input descriptor. + /// + /// The Input Descriptor Object MAY contain a format property. If present, + /// its value MUST be an object with one or more properties matching the registered + /// Claim Format Designations (e.g., jwt, jwt_vc, jwt_vp, etc.). + /// + /// This format property is identical in value signature to the top-level format object, + /// but can be used to specifically constrain submission of a single input to a subset of formats or algorithms. + pub fn set_format(mut self, format: ClaimFormatMap) -> Self { + self.format = format; + self + } + + /// Set the group of the constraints field. + pub fn set_group(mut self, group: Vec) -> Self { + self.group = group; + self + } + + /// Return the group of the constraints field. + pub fn groups(&self) -> &Vec { + self.group.as_ref() + } + + /// Return a mutable reference to the group of the constraints field. + pub fn add_to_group(mut self, member: GroupId) -> Self { + self.group.push(member); + + self + } + + /// Validate the input descriptor against the verifiable presentation and the descriptor map. + pub fn validate_verifiable_presentation( + &self, + verifiable_presentation: &VerifiablePresentation, + descriptor_map: &DescriptorMap, + ) -> Result<()> { + // The descriptor map must match the input descriptor. + if descriptor_map.id() != self.id() { + bail!("Input Descriptor ID does not match the Descriptor Map ID.") + } + + let vp = &verifiable_presentation.0; + + let vp_json: serde_json::Value = + from_value(vp.clone()).context("failed to parse value into json type")?; + + if let Some(ConstraintsLimitDisclosure::Required) = self.constraints.limit_disclosure { + if self.constraints.fields().is_empty() { + bail!("Required limit disclosure must have fields.") + } + }; + + for constraint_field in self.constraints.fields.iter() { + // Check if the filter exists if the predicate is present + // and set to required. + if let Some(Predicate::Required) = constraint_field.predicate() { + if constraint_field.filter().is_none() { + bail!("Required predicate must have a filter.") + } + } + + let mut selector = jsonpath_lib::selector(&vp_json); + + // The root element is relative to the descriptor map path returned. + let Ok(root_element) = selector(descriptor_map.path()) else { + bail!("Failed to select root element from verifiable presentation.") + }; + + let root_element = root_element + .first() + .ok_or(anyhow::anyhow!("Root element not found."))?; + + let mut map_selector = jsonpath_lib::selector(root_element); + + let validator = constraint_field.validator(); + + let mut found_elements = false; + + for field_path in constraint_field.path.iter() { + let field_elements = map_selector(field_path) + .context("Failed to select field elements from verifiable presentation.")?; + + // Check if the field matches are empty. + if field_elements.is_empty() { + // According the specification, found here: + // [https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-evaluation](https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-evaluation) + // > If the result returned no JSONPath match, skip to the next path array element. + continue; + } + + found_elements = true; + + // If a filter is available with a valid schema, handle the field validation. + if let Some(Ok(schema_validator)) = validator.as_ref() { + let validated_fields = field_elements.iter().find(|element| { + match schema_validator.validate(element) { + Err(errors) => { + for error in errors { + tracing::debug!( + "Field did not pass filter validation: {error}", + ); + } + false + } + Ok(_) => true, + } + }); + + if validated_fields.is_none() { + if let Some(Predicate::Required) = constraint_field.predicate() { + bail!("Field did not pass filter validation, required by predicate."); + } else if constraint_field.is_required() { + bail!("Field did not pass filter validation, and is not an optional field."); + } + } + } + } + + // If no elements are found, and limit disclosure is required, return an error. + if !found_elements { + if let Some(ConstraintsLimitDisclosure::Required) = + self.constraints.limit_disclosure + { + bail!("Field elements are empty while limit disclosure is required.") + } + } + } + + Ok(()) + } + + /// Return the format of the input descriptor. + /// + /// The Input Descriptor Object MAY contain a format property. If present, + /// its value MUST be an object with one or more properties matching the registered + /// Claim Format Designations (e.g., jwt, jwt_vc, jwt_vp, etc.). + /// + /// This format property is identical in value signature to the top-level format object, + /// but can be used to specifically constrain submission of a single input to a subset of formats or algorithms. + pub fn format(&self) -> &ClaimFormatMap { + &self.format + } +} + +/// Constraints are objects used to describe the constraints that a [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder) must satisfy to fulfill an Input Descriptor. +/// +/// A constraint object MAY be empty, or it may include a `fields` and/or `limit_disclosure` property. +/// +/// For more information, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object](https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object) +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct Constraints { + #[serde(skip_serializing_if = "Vec::is_empty")] + fields: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + limit_disclosure: Option, +} + +impl Constraints { + /// Returns an empty Constraints object. + pub fn new() -> Self { + Self::default() + } + + /// Add a new field constraint to the constraints list. + pub fn add_constraint(mut self, field: ConstraintsField) -> Self { + self.fields.push(field); + self + } + + /// Returns the fields of the constraints object. + pub fn fields(&self) -> &Vec { + self.fields.as_ref() + } + + /// Set the limit disclosure value. + /// + /// For all [Claims](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:claims) submitted in relation to [InputDescriptor] Objects that include a `constraints` + /// object with a `limit_disclosure` property set to the string value `required`, + /// ensure that the data submitted is limited to the entries specified in the `fields` property of the `constraints` object. + /// If the `fields` property IS NOT present, or contains zero field objects, the submission SHOULD NOT include any data from the Claim. + /// + /// For example, a [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) may simply want to know whether a [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder) has a valid, signed [Claim](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:claim) of a particular type, + /// without disclosing any of the data it contains. + /// + /// For more information: see [https://identity.foundation/presentation-exchange/spec/v2.0.0/#limited-disclosure-submissions](https://identity.foundation/presentation-exchange/spec/v2.0.0/#limited-disclosure-submissions) + pub fn set_limit_disclosure(mut self, limit_disclosure: ConstraintsLimitDisclosure) -> Self { + self.limit_disclosure = Some(limit_disclosure); + self + } + + /// Returns the limit disclosure value. + pub fn limit_disclosure(&self) -> Option<&ConstraintsLimitDisclosure> { + self.limit_disclosure.as_ref() + } + + /// Returns if the constraints fields contain non-optional + /// fields that must be satisfied. + pub fn is_required(&self) -> bool { + self.fields.iter().any(|field| field.is_required()) + } +} + +/// ConstraintsField objects are used to describe the constraints that a +/// [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder) +/// must satisfy to fulfill an Input Descriptor. +/// +/// For more information, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object](https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object) +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +pub struct ConstraintsField { + path: NonEmptyVec, + #[serde(skip_serializing_if = "Option::is_none")] + id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + purpose: Option, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option, + // Optional predicate value + predicate: Option, + #[serde(skip_serializing_if = "Option::is_none")] + filter: Option, + #[serde(skip_serializing_if = "Option::is_none")] + optional: Option, + #[serde(default)] + intent_to_retain: bool, +} + +pub type ConstraintsFields = Vec; + +impl From> for ConstraintsField { + fn from(path: NonEmptyVec) -> Self { + Self { + path, + id: None, + purpose: None, + name: None, + filter: None, + predicate: None, + optional: None, + intent_to_retain: false, + } + } +} + +impl ConstraintsField { + /// Create a new instance of the constraints field with the given path. + /// + /// Constraint fields must have at least one JSONPath to the field for which the constraint is applied. + /// + /// Tip: Use the [ConstraintsField::From](ConstraintsField::From) trait to convert a [NonEmptyVec](NonEmptyVec) of + /// [JsonPath](JsonPath) to a [ConstraintsField](ConstraintsField) if more than one path is known. + pub fn new(path: JsonPath) -> ConstraintsField { + ConstraintsField { + path: NonEmptyVec::new(path), + ..Default::default() + } + } + + /// Add a new path to the constraints field. + pub fn add_path(mut self, path: JsonPath) -> Self { + self.path.push(path); + self + } + + /// Return the paths of the constraints field. + /// + /// `path` is a non empty list of [JsonPath](https://goessner.net/articles/JsonPath/) expressions. + /// + /// For syntax definition, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#jsonpath-syntax-definition](https://identity.foundation/presentation-exchange/spec/v2.0.0/#jsonpath-syntax-definition) + pub fn path(&self) -> &NonEmptyVec { + &self.path + } + + /// Set the id of the constraints field. + /// + /// The fields object MAY contain an id property. If present, its value MUST be a string that + /// is unique from every other field object’s id property, including those contained in other + /// Input Descriptor Objects. + pub fn set_id(mut self, id: String) -> Self { + self.id = Some(id); + self + } + + /// Return the id of the constraints field. + pub fn id(&self) -> Option<&String> { + self.id.as_ref() + } + + /// Set the purpose of the constraints field. + /// + /// If present, its value MUST be a string that describes the purpose for which the field is being requested. + pub fn set_purpose(mut self, purpose: String) -> Self { + self.purpose = Some(purpose); + self + } + + /// Return the purpose of the constraints field. + pub fn purpose(&self) -> Option<&String> { + self.purpose.as_ref() + } + + /// Set the name of the constraints field. + /// + /// If present, its value MUST be a string, and SHOULD be a human-friendly + /// name that describes what the target field represents. + /// + /// For example, the name of the constraint could be "over_18" if the field is a date of birth. + pub fn set_name(mut self, name: String) -> Self { + self.name = Some(name); + self + } + + /// Return the name of the constraints field. + pub fn name(&self) -> Option<&String> { + self.name.as_ref() + } + + /// Set the filter of the constraints field. + /// + /// If present its value MUST be a JSON Schema descriptor used to filter against + /// the values returned from evaluation of the JSONPath string expressions in the path array. + pub fn set_filter(mut self, filter: serde_json::Value) -> Self { + self.filter = Some(filter); + self + } + + /// Set the predicate of the constraints field. + /// + /// When using the [Predicate Feature](https://identity.foundation/presentation-exchange/#predicate-feature), + /// the fields object **MAY** contain a predicate property. If the predicate property is present, + /// the filter property **MUST** also be present. + /// + /// See: [https://identity.foundation/presentation-exchange/#predicate-feature](https://identity.foundation/presentation-exchange/#predicate-feature) + pub fn set_predicate(mut self, predicate: Predicate) -> Self { + self.predicate = Some(predicate); + self + } + + /// Return the predicate of the constraints field. + /// + /// When using the [Predicate Feature](https://identity.foundation/presentation-exchange/#predicate-feature), + /// the fields object **MAY** contain a predicate property. If the predicate property is present, + /// the filter property **MUST** also be present. + /// + /// See: [https://identity.foundation/presentation-exchange/#predicate-feature](https://identity.foundation/presentation-exchange/#predicate-feature) + pub fn predicate(&self) -> Option<&Predicate> { + self.predicate.as_ref() + } + + /// Return the raw filter of the constraints field. + pub fn filter(&self) -> Option<&serde_json::Value> { + self.filter.as_ref() + } + + /// Return a JSON schema validator using the internal filter. + /// + /// If no filter is provided on the constraint field, this + /// will return None. + /// + /// # Errors + /// + /// If the filter is invalid, this will return an error. + pub fn validator(&self) -> Option> { + self.filter.as_ref().map(JSONSchema::compile) + } + + /// Set the optional value of the constraints field. + /// + /// The value of this property MUST be a boolean, wherein true indicates the + /// field is optional, and false or non-presence of the property indicates the + /// field is required. Even when the optional property is present, the value + /// located at the indicated path of the field MUST validate against the + /// JSON Schema filter, if a filter is present. + /// + /// For more information, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object](https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object) + pub fn set_optional(mut self, optional: bool) -> Self { + self.optional = Some(optional); + self + } + + /// Return the optional value of the constraints field. + pub fn is_optional(&self) -> bool { + self.optional.unwrap_or(false) + } + + /// Inverse alias for `!is_optional()`. + pub fn is_required(&self) -> bool { + !self.is_optional() + } + + /// Set the intent to retain the constraints field. + /// + /// This value indicates the verifier's intent to retain the + /// field in the presentation, storing the value in the verifier's system. + pub fn set_retained(mut self, intent_to_retain: bool) -> Self { + self.intent_to_retain = intent_to_retain; + self + } + + /// Return the intent to retain the constraints field. + pub fn intent_to_retain(&self) -> bool { + self.intent_to_retain + } + + /// Return the humanly-readable requested fields of the constraints field. + /// + /// This will convert camelCase to space-separated words with capitalized first letter. + /// + /// For example, if the path is `["dateOfBirth"]`, this will return `["Date of Birth"]`. + /// + /// This will also stripe the periods from the JSON path and return the last word in the path. + /// + /// e.g., `["$.verifiableCredential.credentialSubject.dateOfBirth"]` will return `["Date of Birth"]`. + /// e.g., `["$.verifiableCredential.credentialSubject.familyName"]` will return `["Family Name"]`. + /// + pub fn requested_fields(&self) -> Vec { + self.path() + .iter() + // NOTE: It may not be a given that the last path is the field name. + // TODO: Cannot use the field path as a unique property, it may be associated to different + // credential types. + // NOTE: Include the namespace for uniqueness of the requested field type. + .filter_map(|path| path.split(&['-', '.', ':', '@'][..]).last()) + .map(|path| { + path.chars() + .fold(String::new(), |mut acc, c| { + // Convert camelCase to space-separated words with capitalized first letter. + if c.is_uppercase() { + acc.push(' '); + } + + // Check if the field is snake_case and convert to + // space-separated words with capitalized first letter. + if c == '_' { + acc.push(' '); + return acc; + } + + acc.push(c); + acc + }) + // Split the path based on empty spaces and uppercase the first letter of each word. + .split(' ') + .fold(String::new(), |desc, word| { + let word = + word.chars() + .enumerate() + .fold(String::new(), |mut acc, (i, c)| { + // Capitalize the first letter of the word. + if i == 0 { + if let Some(c) = c.to_uppercase().next() { + acc.push(c); + return acc; + } + } + acc.push(c); + acc + }); + + format!("{desc} {}", word.trim_end()) + }) + .trim_end() + .to_string() + }) + .collect() + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum ConstraintsLimitDisclosure { + Required, + Preferred, +} diff --git a/src/core/metadata/mod.rs b/src/core/metadata/mod.rs index bf2da29..5909ced 100644 --- a/src/core/metadata/mod.rs +++ b/src/core/metadata/mod.rs @@ -1,9 +1,11 @@ +use super::credential_format::*; + use std::ops::{Deref, DerefMut}; -use anyhow::Error; +use anyhow::{Error, Result}; use parameters::wallet::{RequestObjectSigningAlgValuesSupported, ResponseTypesSupported}; use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value as Json}; +use ssi_jwk::Algorithm; use self::parameters::wallet::{AuthorizationEndpoint, VpFormatsSupported}; @@ -35,10 +37,16 @@ impl WalletMetadata { &self.1 } + /// Return a reference to the vp formats supported. pub fn vp_formats_supported(&self) -> &VpFormatsSupported { &self.2 } + /// Return a mutable reference to the vp formats supported. + pub fn vp_formats_supported_mut(&mut self) -> &mut VpFormatsSupported { + &mut self.2 + } + /// The static wallet metadata bound to `openid4vp:`: /// ```json /// { @@ -65,19 +73,21 @@ impl WalletMetadata { let response_types_supported = ResponseTypesSupported(vec![ResponseType::VpToken]); - let mut format_definition = Map::new(); - format_definition.insert( - "alg_values_supported".to_owned(), - Json::Array(vec![Json::String("ES256".to_owned())]), + let alg_values_supported = vec![Algorithm::ES256.to_string()]; + + let mut vp_formats_supported = ClaimFormatMap::new(); + vp_formats_supported.insert( + ClaimFormatDesignation::JwtVpJson, + ClaimFormatPayload::AlgValuesSupported(alg_values_supported.clone()), + ); + vp_formats_supported.insert( + ClaimFormatDesignation::JwtVcJson, + ClaimFormatPayload::AlgValuesSupported(alg_values_supported.clone()), ); - let format_definition = Json::Object(format_definition); - let mut vp_formats_supported = Map::new(); - vp_formats_supported.insert("jwt_vp_json".to_owned(), format_definition.clone()); - vp_formats_supported.insert("jwt_vc_json".to_owned(), format_definition.clone()); let vp_formats_supported = VpFormatsSupported(vp_formats_supported); let request_object_signing_alg_values_supported = - RequestObjectSigningAlgValuesSupported(vec!["ES256".to_owned()]); + RequestObjectSigningAlgValuesSupported(alg_values_supported); let mut object = UntypedObject::default(); diff --git a/src/core/metadata/parameters/verifier.rs b/src/core/metadata/parameters/verifier.rs index 3681521..97afa4a 100644 --- a/src/core/metadata/parameters/verifier.rs +++ b/src/core/metadata/parameters/verifier.rs @@ -1,11 +1,12 @@ -use anyhow::Error; -use serde::Deserialize; -use serde_json::{Map, Value as Json}; - +use crate::core::credential_format::ClaimFormatMap; use crate::core::object::TypedParameter; -#[derive(Debug, Clone, Deserialize)] -pub struct VpFormats(pub Map); +use anyhow::{Context, Error}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value as Json}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VpFormats(pub ClaimFormatMap); impl TypedParameter for VpFormats { const KEY: &'static str = "vp_formats"; @@ -19,9 +20,11 @@ impl TryFrom for VpFormats { } } -impl From for Json { - fn from(value: VpFormats) -> Json { - value.0.into() +impl TryFrom for Json { + type Error = Error; + + fn try_from(value: VpFormats) -> Result { + serde_json::to_value(value.0).context("Failed to serialize VpFormats") } } @@ -118,7 +121,10 @@ impl From for Json { mod test { use serde_json::json; - use crate::core::object::UntypedObject; + use crate::core::{ + credential_format::{ClaimFormatDesignation, ClaimFormatPayload}, + object::UntypedObject, + }; use super::*; @@ -148,9 +154,16 @@ mod test { #[test] fn vp_formats() { - let VpFormats(fnd) = metadata().get().unwrap().unwrap(); - let exp = json!({"mso_mdoc": {}}).as_object().unwrap().clone(); - assert_eq!(fnd, exp) + let VpFormats(formats) = metadata().get().unwrap().unwrap(); + + let mso_doc = formats + .get(&ClaimFormatDesignation::MsoMDoc) + .expect("failed to find mso doc"); + + assert_eq!( + mso_doc, + &ClaimFormatPayload::Json(serde_json::Value::Object(Default::default())) + ) } #[test] diff --git a/src/core/metadata/parameters/wallet.rs b/src/core/metadata/parameters/wallet.rs index 3454a01..09af0cd 100644 --- a/src/core/metadata/parameters/wallet.rs +++ b/src/core/metadata/parameters/wallet.rs @@ -1,9 +1,11 @@ use crate::core::{ authorization_request::parameters::{ClientIdScheme, ResponseType}, + credential_format::{ClaimFormatDesignation, ClaimFormatMap}, object::TypedParameter, }; + use anyhow::{bail, Error, Result}; -use serde_json::{Map, Value as Json}; +use serde_json::Value as Json; use url::Url; #[derive(Debug, Clone)] @@ -132,9 +134,8 @@ impl From for Json { } } -// TODO: Better types -#[derive(Debug, Clone)] -pub struct VpFormatsSupported(pub Map); +#[derive(Debug, Clone, Default)] +pub struct VpFormatsSupported(pub ClaimFormatMap); impl TypedParameter for VpFormatsSupported { const KEY: &'static str = "vp_formats_supported"; @@ -144,13 +145,21 @@ impl TryFrom for VpFormatsSupported { type Error = Error; fn try_from(value: Json) -> Result { - Ok(Self(serde_json::from_value(value)?)) + serde_json::from_value(value).map(Self).map_err(Into::into) + } +} + +impl TryFrom for Json { + type Error = Error; + + fn try_from(value: VpFormatsSupported) -> Result { + serde_json::to_value(value.0).map_err(Into::into) } } -impl From for Json { - fn from(value: VpFormatsSupported) -> Json { - Json::Object(value.0) +impl VpFormatsSupported { + pub fn is_claim_format_supported(&self, designation: &ClaimFormatDesignation) -> bool { + self.0.contains_key(designation) } } @@ -200,7 +209,10 @@ impl From for Json { mod test { use serde_json::json; - use crate::core::object::UntypedObject; + use crate::core::{ + credential_format::{ClaimFormatDesignation, ClaimFormatPayload}, + object::UntypedObject, + }; use super::*; @@ -277,8 +289,8 @@ mod test { let VpFormatsSupported(mut m) = metadata().get().unwrap().unwrap(); assert_eq!(m.len(), 1); assert_eq!( - m.remove("mso_mdoc").unwrap(), - Json::Object(Default::default()) + m.remove(&ClaimFormatDesignation::MsoMDoc).unwrap(), + ClaimFormatPayload::Json(serde_json::Value::Object(Default::default())) ); } diff --git a/src/core/mod.rs b/src/core/mod.rs index a4979fa..abae24c 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -1,6 +1,9 @@ pub mod authorization_request; pub mod credential_format; +pub mod input_descriptor; pub mod metadata; pub mod object; +pub mod presentation_definition; +pub mod presentation_submission; pub mod response; pub mod util; diff --git a/src/core/object/mod.rs b/src/core/object/mod.rs index ac56ed7..02447c5 100644 --- a/src/core/object/mod.rs +++ b/src/core/object/mod.rs @@ -13,7 +13,7 @@ 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 + TryFrom + TryInto + Clone + std::fmt::Debug { const KEY: &'static str; } @@ -51,12 +51,15 @@ impl UntypedObject { /// # 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), - ) + match t.try_into() { + Err(_) => Some(Err(Error::msg("failed to parse typed parameter"))), + Ok(value) => Some( + self.0 + .insert(T::KEY.to_owned(), value)? + .try_into() + .map_err(Into::into), + ), + } } /// Flatten the structure for posting as a form. diff --git a/src/core/presentation_definition.rs b/src/core/presentation_definition.rs new file mode 100644 index 0000000..caa402a --- /dev/null +++ b/src/core/presentation_definition.rs @@ -0,0 +1,373 @@ +use super::credential_format::*; +use super::input_descriptor::*; +use super::presentation_submission::*; + +use std::collections::HashMap; + +use anyhow::{bail, Result}; +use serde::{Deserialize, Serialize}; +use serde_json::Map; +use ssi_claims::jwt::VerifiablePresentation; + +/// A presentation definition is a JSON object that describes the information a [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) requires of a [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder). +/// +/// > Presentation Definitions are objects that articulate what proofs a [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) requires. +/// > These help the [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) to decide how or whether to interact with a [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder). +/// +/// Presentation Definitions are composed of inputs, which describe the forms and details of the +/// proofs they require, and optional sets of selection rules, to allow [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder)s flexibility +/// in cases where different types of proofs may satisfy an input requirement. +/// +/// For more information, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition](https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition) +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +pub struct PresentationDefinition { + id: String, + input_descriptors: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + submission_requirements: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + purpose: Option, + #[serde(default, skip_serializing_if = "ClaimFormatMap::is_empty")] + format: ClaimFormatMap, +} + +impl PresentationDefinition { + /// The Presentation Definition MUST contain an id property. The value of this property MUST be a string. + /// The string SHOULD provide a unique ID for the desired context. + /// + /// The Presentation Definition MUST contain an input_descriptors property. Its value MUST be an array of Input Descriptor Objects, + /// the composition of which are found [InputDescriptor] type. + /// + pub fn new(id: String, input_descriptor: InputDescriptor) -> Self { + Self { + id, + input_descriptors: vec![input_descriptor], + ..Default::default() + } + } + + /// Return the id of the presentation definition. + pub fn id(&self) -> &String { + &self.id + } + + /// Add a new input descriptor to the presentation definition. + pub fn add_input_descriptors(mut self, input_descriptor: InputDescriptor) -> Self { + self.input_descriptors.push(input_descriptor); + self + } + + /// Return the input descriptors of the presentation definition. + pub fn input_descriptors(&self) -> &Vec { + &self.input_descriptors + } + + /// Return the input descriptors as a mapping of the input descriptor id to the input descriptor. + pub fn input_descriptors_map(&self) -> HashMap { + self.input_descriptors + .iter() + .map(|input_descriptor| (input_descriptor.id().to_string(), input_descriptor)) + .collect() + } + + /// Return a mutable reference to the input descriptors of the presentation definition. + pub fn input_descriptors_mut(&mut self) -> &mut Vec { + &mut self.input_descriptors + } + + /// Set the submission requirements of the presentation definition. + pub fn set_submission_requirements( + mut self, + submission_requirements: Vec, + ) -> Self { + self.submission_requirements = Some(submission_requirements); + self + } + + /// Return the submission requirements of the presentation definition. + pub fn submission_requirements(&self) -> Option<&Vec> { + self.submission_requirements.as_ref() + } + + /// Return a mutable reference to the submission requirements of the presentation definition. + pub fn submission_requirements_mut(&mut self) -> Option<&mut Vec> { + self.submission_requirements.as_mut() + } + + /// Add a new submission requirement to the presentation definition. + pub fn add_submission_requirement( + mut self, + submission_requirement: SubmissionRequirement, + ) -> Self { + self.submission_requirements + .get_or_insert_with(Vec::new) + .push(submission_requirement); + self + } + + /// Validate submission requirements provided an input descriptor and descriptor map. + pub fn validate_submission_requirements(&self, descriptor_map: &[DescriptorMap]) -> Result<()> { + match self.submission_requirements.as_ref() { + None => Ok(()), + Some(requirements) => { + for requirement in requirements { + requirement.validate(self.input_descriptors(), descriptor_map)?; + } + Ok(()) + } + } + } + + /// Set the name of the presentation definition. + /// + /// The [PresentationDefinition] MAY contain a name property. If present, its value SHOULD be a + /// human-friendly string intended to constitute a distinctive designation of the Presentation Definition. + pub fn set_name(mut self, name: String) -> Self { + self.name = Some(name); + self + } + + /// Return the name of the presentation definition. + pub fn name(&self) -> Option<&String> { + self.name.as_ref() + } + + /// Set the purpose of the presentation definition. + /// + /// The [PresentationDefinition] MAY contain a purpose property. If present, its value MUST be a string that + /// describes the purpose for which the Presentation Definition's inputs are being used for. + pub fn set_purpose(mut self, purpose: String) -> Self { + self.purpose = Some(purpose); + self + } + + /// Return the purpose of the presentation definition. + pub fn purpose(&self) -> Option<&String> { + self.purpose.as_ref() + } + + /// Attach a format to the presentation definition. + /// + /// The Presentation Definition MAY include a format property. If present, + /// the value MUST be an object with one or more properties matching the + /// registered Claim Format Designations (e.g., jwt, jwt_vc, jwt_vp, etc.). + /// + /// The properties inform the [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder) of the Claim format configurations the [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) can process. + /// The value for each claim format property MUST be an object composed as follows: + /// + /// The object MUST include a format-specific property (i.e., alg, proof_type) + /// that expresses which algorithms the [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) supports for the format. + /// Its value MUST be an array of one or more format-specific algorithmic identifier references, + /// as noted in the Claim Format Designations section. + /// + /// See: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition](https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition) + pub fn set_format(mut self, format: ClaimFormatMap) -> Self { + self.format = format; + self + } + + /// Add a new format to the presentation definition. + pub fn add_format(mut self, key: ClaimFormatDesignation, value: ClaimFormatPayload) -> Self { + self.format.insert(key, value); + self + } + + /// Return the format of the presentation definition. + pub fn format(&self) -> &ClaimFormatMap { + &self.format + } + + /// Return the human-readable string representation of the fields requested + /// in the presentation definition's input descriptors. + /// + /// For example, the following paths would be coverted as follows: + /// + /// `$.verifiableCredential[0].credentialSubject.id` -> Id + /// `$.credentialSubject.givenName` -> Given Name + /// `$.credentialSubject.familyName` -> Family Name + pub fn requested_fields(&self) -> Vec { + self.input_descriptors + .iter() + .flat_map(|input_descriptor| { + input_descriptor + .constraints() + .fields() + .iter() + .map(|constraint| constraint.requested_fields()) + }) + .flatten() + .collect() + } + + /// Validate a presentation submission against the presentation definition. + /// + /// This descriptor map is a map of descriptor objects, keyed by their id. + /// + /// For convenience, use [PresentationSubmission::descriptor_map_by_id] to generate this map. + /// + /// Internally, this method will call [PresentationDefinition::validate_submission_requirements]. + pub fn validate_presentation( + &self, + verifiable_presentation: VerifiablePresentation, + descriptor_map: &[DescriptorMap], + ) -> Result<()> { + // Validate the submission requirements. This will + // no-op if there are no submission requirements. + self.validate_submission_requirements(descriptor_map)?; + + let input_descript_map = self.input_descriptors_map(); + + // Validate the submission requirements + + for descriptor in descriptor_map.iter() { + match input_descript_map.get(descriptor.id()) { + None => { + bail!( + "Descriptor map ID, {}, does not match a valid input descriptor.", + descriptor.id() + ) + } + Some(input_descriptor) => { + input_descriptor + .validate_verifiable_presentation(&verifiable_presentation, descriptor)?; + } + } + } + + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SubmissionRequirementObject { + pub name: Option, + pub purpose: Option, + #[serde(flatten)] + pub property_set: Option>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum SubmissionRequirementBase { + From { + from: GroupId, + #[serde(flatten)] + submission_requirement_base: SubmissionRequirementObject, + }, + FromNested { + from_nested: Vec, + #[serde(flatten)] + submission_requirement_base: SubmissionRequirementObject, + }, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(tag = "rule", rename_all = "snake_case")] +pub enum SubmissionRequirement { + All(SubmissionRequirementBase), + Pick(SubmissionRequirementPick), +} + +impl SubmissionRequirement { + // Internal method to group the submission requirement, + // based on the `from` or recurse the `from_nested` field. + fn validate_group( + group: &GroupId, + input_descriptors: &[InputDescriptor], + decriptor_map: &[DescriptorMap], + options: Option<&SubmissionRequirementPick>, + ) -> Result<()> { + // Group all the input descriptors according to the matching groups of this submission requirement. + let grouped_input_descriptors = input_descriptors + .iter() + .filter(|input_descriptor| input_descriptor.groups().contains(group)) + .collect::>(); + + // Filter for the descriptor maps that match the grouped input descriptors. + let group_count = decriptor_map + .iter() + .filter(|descriptor| { + grouped_input_descriptors + .iter() + .any(|input_descriptor| input_descriptor.id() == descriptor.id()) + }) + .count(); + + if let Some(opts) = options { + if let Some(min_count) = opts.min { + if group_count < min_count { + bail!("Submission Requirement validation failed. Descriptor Map count {group_count} is less than the minimum count: {min_count}."); + } + } + + if let Some(max_count) = opts.max { + if group_count > max_count { + bail!("Submission Requirement validation failed. Descriptor Map count {group_count} is greater than the maximum count: {max_count}."); + } + } + + if let Some(count) = opts.count { + if group_count != count { + bail!("Submission Requirement group, {group}, validation failed. Descriptor Map count {group_count} is not equal to the count: {count}."); + } + } + } else { + // If the descriptor maps are less than the grouped input descriptors, + // then the submission requirement is not satisfied. + if group_count < grouped_input_descriptors.len() { + bail!("Submission Requirement group, {group}, validation failed. Descriptor Map count {group_count} is not equal to the count of grouped input descriptors: {}.", grouped_input_descriptors.len()); + } + } + + Ok(()) + } + + /// Validate a submission requirement against a input descriptors and descriptor maps. + pub fn validate( + &self, + input_descriptors: &[InputDescriptor], + decriptor_map: &[DescriptorMap], + ) -> Result<()> { + // Validate the submission requirement against the grouped descriptor maps. + match self { + SubmissionRequirement::All(base) => match base { + SubmissionRequirementBase::From { from, .. } => { + return Self::validate_group(from, input_descriptors, decriptor_map, None); + } + SubmissionRequirementBase::FromNested { from_nested, .. } => { + for requirement in from_nested { + requirement.validate(input_descriptors, decriptor_map)?; + } + } + }, + SubmissionRequirement::Pick(pick) => match &pick.submission_requirement { + SubmissionRequirementBase::From { from, .. } => { + return Self::validate_group( + from, + input_descriptors, + decriptor_map, + Some(pick), + ); + } + SubmissionRequirementBase::FromNested { from_nested, .. } => { + for requirement in from_nested { + requirement.validate(input_descriptors, decriptor_map)?; + } + } + }, + } + + Ok(()) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SubmissionRequirementPick { + #[serde(flatten)] + pub submission_requirement: SubmissionRequirementBase, + pub count: Option, + pub min: Option, + pub max: Option, +} diff --git a/src/core/presentation_submission.rs b/src/core/presentation_submission.rs new file mode 100644 index 0000000..dd1fb53 --- /dev/null +++ b/src/core/presentation_submission.rs @@ -0,0 +1,148 @@ +use super::{credential_format::*, input_descriptor::*}; + +use serde::{Deserialize, Serialize}; + +/// A DescriptorMapId is a unique identifier for a DescriptorMap. +pub type DescriptorMapId = String; + +/// Presentation Submissions are objects embedded within target +/// [Claim](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:claim) negotiation +/// formats that express how the inputs presented as proofs to a +/// [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) are +/// provided in accordance with the requirements specified in a [PresentationDefinition]. +/// +/// Embedded Presentation Submission objects MUST be located within target data format as +/// the value of a `presentation_submission` property. +/// +/// For more information, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission](https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission) +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct PresentationSubmission { + id: uuid::Uuid, + definition_id: DescriptorMapId, + descriptor_map: Vec, +} + +impl PresentationSubmission { + /// The presentation submission MUST contain an id property. The value of this property MUST be a unique identifier, i.e. a UUID. + /// + /// The presentation submission object MUST contain a `definition_id` property. + /// The value of this property MUST be the id value of a valid [PresentationDefinition::id()]. + /// + /// The object MUST include a `descriptor_map` property. The value of this property MUST be an array of + /// Input [DescriptorMap] Objects. + pub fn new( + id: uuid::Uuid, + definition_id: DescriptorMapId, + descriptor_map: Vec, + ) -> Self { + Self { + id, + definition_id, + descriptor_map, + } + } + + /// Return the id of the presentation submission. + pub fn id(&self) -> &uuid::Uuid { + &self.id + } + + /// Return the definition id of the presentation submission. + pub fn definition_id(&self) -> &String { + &self.definition_id + } + + /// Return the descriptor map of the presentation submission. + pub fn descriptor_map(&self) -> &Vec { + &self.descriptor_map + } + + /// Return a mutable reference to the descriptor map of the presentation submission. + pub fn descriptor_map_mut(&mut self) -> &mut Vec { + &mut self.descriptor_map + } + + /// Returns the descriptor map as a mapping of descriptor map id to descriptor map. + /// + /// The descriptor map id is expected to match the id of the input descriptor. + /// This mapping is helpful for checking if an input descriptor has an associated descriptor map, + /// using this mapping from the presentation submission. + pub fn descriptor_map_by_id( + &self, + ) -> std::collections::HashMap { + self.descriptor_map + .iter() + .map(|descriptor_map| (descriptor_map.id.clone(), descriptor_map)) + .collect() + } +} + +/// Descriptor Maps are objects used to describe the information a [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder) provides to a [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier). +/// +/// For more information, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission](https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission) +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct DescriptorMap { + id: DescriptorMapId, + format: ClaimFormatDesignation, + path: JsonPath, + path_nested: Option>, +} + +impl DescriptorMap { + /// The descriptor map MUST include an `id` property. The value of this property MUST be a string that matches the `id` property of the [InputDescriptor::id()] in the [PresentationDefinition] that this [PresentationSubmission] is related to. + /// + /// The descriptor map object MUST include a `format` property. The value of this property MUST be a string that matches one of the [ClaimFormatDesignation]. This denotes the data format of the [Claim](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:claim). + /// + /// The descriptor map object MUST include a `path` property. The value of this property MUST be a [JSONPath](https://goessner.net/articles/JsonPath/) string expression. The path property indicates the [Claim](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:claim) submitted in relation to the identified [InputDescriptor], when executed against the top-level of the object the [PresentationSubmission] is embedded within. + /// + /// For more information, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission](https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission) + pub fn new( + id: impl Into, + format: ClaimFormatDesignation, + path: JsonPath, + ) -> Self { + Self { + id: id.into(), + format, + path, + path_nested: None, + } + } + + /// Return the id of the descriptor map. + pub fn id(&self) -> &DescriptorMapId { + &self.id + } + + /// Return the format of the descriptor map. + /// + /// The value of this property MUST be a string that matches one of the + /// [ClaimFormatDesignation]. This denotes the data format of the Claim. + /// + /// See: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission](https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission) + pub fn format(&self) -> &ClaimFormatDesignation { + &self.format + } + + /// Return the path of the descriptor map. + pub fn path(&self) -> &JsonPath { + &self.path + } + + /// Set the nested path of the descriptor map. + /// + /// The format of a path_nested object mirrors that of a [DescriptorMap] property. The nesting may be any number of levels deep. + /// The `id` property MUST be the same for each level of nesting. + /// + /// > The path property inside each `path_nested` property provides a relative path within a given nested value. + /// + /// See: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#processing-of-submission-entries](https://identity.foundation/presentation-exchange/spec/v2.0.0/#processing-of-submission-entries) + pub fn set_path_nested(mut self, mut path_nested: DescriptorMap) -> Self { + // Ensure the nested path has the same id as the parent. + path_nested.id.clone_from(self.id()); + + self.path_nested = Some(Box::new(path_nested)); + + self + } +} diff --git a/src/core/response/mod.rs b/src/core/response/mod.rs index b6752ee..e448e37 100644 --- a/src/core/response/mod.rs +++ b/src/core/response/mod.rs @@ -1,3 +1,5 @@ +use super::object::{ParsingErrorContext, UntypedObject}; + use std::collections::BTreeMap; use anyhow::{Context, Error, Result}; @@ -7,8 +9,6 @@ use url::Url; use self::parameters::{PresentationSubmission, VpToken}; -use super::object::{ParsingErrorContext, UntypedObject}; - pub mod parameters; #[derive(Debug, Clone)] @@ -53,6 +53,16 @@ impl UnencodedAuthorizationResponse { serde_urlencoded::to_string(inner.flatten_for_form()?) .context("failed to encode response as 'application/x-www-form-urlencoded'") } + + /// Return the Verifiable Presentation Token. + pub fn vp_token(&self) -> &VpToken { + &self.1 + } + + /// Return the Presentation Submission. + pub fn presentation_submission(&self) -> &PresentationSubmission { + &self.2 + } } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/src/core/response/parameters.rs b/src/core/response/parameters.rs index b9d6b8b..8fd36e2 100644 --- a/src/core/response/parameters.rs +++ b/src/core/response/parameters.rs @@ -1,8 +1,9 @@ -use anyhow::Error; -use serde_json::Value as Json; - pub use crate::core::authorization_request::parameters::State; use crate::core::object::TypedParameter; +use crate::core::presentation_submission::PresentationSubmission as PresentationSubmissionParsed; + +use anyhow::Error; +use serde_json::Value as Json; #[derive(Debug, Clone)] pub struct IdToken(pub String); @@ -25,6 +26,16 @@ impl From for Json { } } +// TODO: Update this type to something like: +// +// enum VpToken { +// Single(String), +// SingleAsMap(Map), +// Many(Vec), +// } +// +// See: https://github.com/spruceid/oid4vp-rs/pull/8#discussion_r1750274969 +// #[derive(Debug, Clone)] pub struct VpToken(pub String); @@ -49,25 +60,23 @@ impl From for Json { #[derive(Debug, Clone)] pub struct PresentationSubmission { raw: Json, - parsed: crate::presentation_exchange::PresentationSubmission, + parsed: PresentationSubmissionParsed, } impl PresentationSubmission { - pub fn into_parsed(self) -> crate::presentation_exchange::PresentationSubmission { + pub fn into_parsed(self) -> PresentationSubmissionParsed { self.parsed } - pub fn parsed(&self) -> &crate::presentation_exchange::PresentationSubmission { + pub fn parsed(&self) -> &PresentationSubmissionParsed { &self.parsed } } -impl TryFrom for PresentationSubmission { +impl TryFrom for PresentationSubmission { type Error = Error; - fn try_from( - parsed: crate::presentation_exchange::PresentationSubmission, - ) -> Result { + fn try_from(parsed: PresentationSubmissionParsed) -> Result { let raw = serde_json::to_value(parsed.clone())?; Ok(Self { raw, parsed }) } diff --git a/src/core/util/mod.rs b/src/core/util/mod.rs index 0fb529d..b5f1cc6 100644 --- a/src/core/util/mod.rs +++ b/src/core/util/mod.rs @@ -1,4 +1,3 @@ -#[cfg(feature = "reqwest")] use anyhow::Context; use anyhow::Result; use async_trait::async_trait; @@ -16,10 +15,9 @@ pub(crate) fn base_request() -> http::request::Builder { Request::builder().header("Prefer", "OID4VP-0.0.20") } -#[cfg(feature = "reqwest")] +#[derive(Debug)] pub struct ReqwestClient(reqwest::Client); -#[cfg(feature = "reqwest")] impl ReqwestClient { pub fn new() -> Result { reqwest::Client::builder() @@ -30,7 +28,6 @@ impl ReqwestClient { } } -#[cfg(feature = "reqwest")] #[async_trait] impl AsyncHttpClient for ReqwestClient { async fn execute(&self, request: Request>) -> Result>> { diff --git a/src/holder/mod.rs b/src/holder/mod.rs new file mode 100644 index 0000000..307051f --- /dev/null +++ b/src/holder/mod.rs @@ -0,0 +1 @@ +pub mod verifiable_presentation_builder; diff --git a/src/holder/verifiable_presentation_builder.rs b/src/holder/verifiable_presentation_builder.rs new file mode 100644 index 0000000..e7db9d3 --- /dev/null +++ b/src/holder/verifiable_presentation_builder.rs @@ -0,0 +1,164 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use serde::{Deserialize, Serialize}; +use ssi_claims::jwt::{VerifiableCredential, VerifiablePresentation}; +use ssi_claims::vc::v2::syntax::VERIFIABLE_PRESENTATION_TYPE; +use ssi_dids::ssi_json_ld::CREDENTIALS_V1_CONTEXT; +use ssi_dids::{ + ssi_json_ld::syntax::{Object, Value}, + DIDURLBuf, +}; + +#[derive(Debug, Clone)] +pub struct VerifiablePresentationBuilderOptions { + pub issuer: DIDURLBuf, + pub subject: DIDURLBuf, + pub audience: DIDURLBuf, + pub nonce: String, + // TODO: we may wish to support an explicit + // issuance and expiration date rather than seconds from now. + /// Expiration is in seconds from `now`. + /// e.g. 3600 for 1 hour. + pub expiration_secs: u64, + pub credentials: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VerifiablePresentationBuilder(VerifiablePresentation); + +impl From for VerifiablePresentation { + fn from(builder: VerifiablePresentationBuilder) -> Self { + builder.0 + } +} + +impl Default for VerifiablePresentationBuilder { + fn default() -> Self { + Self::new() + } +} + +impl VerifiablePresentationBuilder { + /// Returns an empty verifiable presentation builder. + pub fn new() -> Self { + Self(VerifiablePresentation(Value::Object(Object::new()))) + } + + /// Returns a verifiable presentation builder from options. + /// + /// This will set the issuance date to the current time and the expiration + /// date to the expiration secs from the issuance date. + pub fn from_options(options: VerifiablePresentationBuilderOptions) -> VerifiablePresentation { + let mut verifiable_presentation = VerifiablePresentation(Value::Object(Object::new())); + + if let Some(obj) = verifiable_presentation.0.as_object_mut() { + // The issuer is the holder of the verifiable credential (subject of the verifiable credential). + obj.insert("iss".into(), Value::String(options.issuer.as_str().into())); + + // The audience is the verifier of the verifiable credential. + obj.insert( + "aud".into(), + Value::String(options.audience.as_str().into()), + ); + + if let Ok(dur) = SystemTime::now().duration_since(UNIX_EPOCH) { + // The issuance date is the current time. + obj.insert("iat".into(), Value::Number(dur.as_secs().into())); + + // The expiration date is 1 hour from the current time. + obj.insert( + "exp".into(), + Value::Number((dur.as_secs() + options.expiration_secs).into()), + ); + } + + obj.insert("nonce".into(), Value::String(options.nonce.into())); + + let mut verifiable_credential_field = Value::Object(Object::new()); + + if let Some(cred) = verifiable_credential_field.as_object_mut() { + cred.insert( + "@context".into(), + Value::String(CREDENTIALS_V1_CONTEXT.to_string().into()), + ); + + cred.insert( + "type".into(), + Value::String(VERIFIABLE_PRESENTATION_TYPE.to_string().into()), + ); + + cred.insert( + "verifiableCredential".into(), + Value::Array(options.credentials.into_iter().map(|vc| vc.0).collect()), + ); + } + + obj.insert("vp".into(), verifiable_credential_field); + } + + verifiable_presentation + } + + /// Add an issuer to the verifiable presentation. + /// + /// The issuer is the entity that issues the verifiable presentation. + /// This is typically the holder of the verifiable credential. + pub fn add_issuer(mut self, issuer: DIDURLBuf) -> Self { + if let Some(obj) = self.0 .0.as_object_mut() { + // The issuer is the holder of the verifiable credential (subject of the verifiable credential). + obj.insert("iss".into(), Value::String(issuer.as_str().into())); + }; + self + } + + /// Add a subject to the verifiable presentation. + /// + /// The subject is the entity that is the subject of the verifiable presentation. + /// This is typically the holder of the verifiable credential. + pub fn add_subject(mut self, subject: DIDURLBuf) -> Self { + if let Some(obj) = self.0 .0.as_object_mut() { + // The subject is the entity that is the subject of the verifiable presentation. + obj.insert("sub".into(), Value::String(subject.as_str().into())); + }; + self + } + + /// Add an audience to the verifiable presentation. + /// The audience is the entity to which the verifiable presentation is addressed. + /// This is typically the verifier of the verifiable presentation. + pub fn add_audience(mut self, audience: DIDURLBuf) -> Self { + if let Some(obj) = self.0 .0.as_object_mut() { + // The audience is the entity to which the verifiable presentation is addressed. + obj.insert("aud".into(), Value::String(audience.as_str().into())); + }; + self + } + + /// Set the issuance date of the verifiable presentation. + pub fn set_issuance_date(mut self, issuance_date: i64) -> Self { + if let Some(obj) = self.0 .0.as_object_mut() { + obj.insert("iat".into(), Value::Number(issuance_date.into())); + }; + self + } + + /// Set the expiration date of the verifiable presentation. + pub fn set_expiration_date(mut self, expiration_date: i64) -> Self { + if let Some(obj) = self.0 .0.as_object_mut() { + obj.insert("exp".into(), Value::Number(expiration_date.into())); + }; + self + } + + /// Set the nonce of the verifiable presentation. + pub fn set_nonce(mut self, nonce: String) -> Self { + if let Some(obj) = self.0 .0.as_object_mut() { + obj.insert("nonce".into(), Value::String(nonce.into())); + } + self + } + + pub fn build(self) -> VerifiablePresentation { + self.0 + } +} diff --git a/src/lib.rs b/src/lib.rs index 551dda8..f62a444 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ pub mod core; -pub mod presentation_exchange; +pub mod holder; +#[cfg(test)] +pub(crate) mod tests; mod utils; pub mod verifier; pub mod wallet; diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs deleted file mode 100644 index 93f5f34..0000000 --- a/src/presentation_exchange.rs +++ /dev/null @@ -1,904 +0,0 @@ -use std::collections::HashMap; - -pub use crate::utils::NonEmptyVec; - -use anyhow::{bail, Result}; -use serde::{Deserialize, Serialize}; -use serde_json::Map; - -/// A JSONPath is a string that represents a path to a specific value within a JSON object. -/// -/// For syntax details, see [https://identity.foundation/presentation-exchange/spec/v2.0.0/#jsonpath-syntax-definition](https://identity.foundation/presentation-exchange/spec/v2.0.0/#jsonpath-syntax-definition) -pub type JsonPath = String; - -/// A Json object of claim formats. -pub type ClaimFormatMap = HashMap; - -/// The Presentation Definition MAY include a format property. The value MUST be an object with one or -/// more properties matching the registered [ClaimFormatDesignation] (e.g., jwt, jwt_vc, jwt_vp, etc.). -/// The properties inform the Holder of the Claim format configurations the Verifier can process. -/// The value for each claim format property MUST be an object composed as follows: -/// -/// The object MUST include a format-specific property (i.e., alg, proof_type) that expresses which -/// algorithms the Verifier supports for the format. Its value MUST be an array of one or more -/// format-specific algorithmic identifier references, as noted in the [ClaimFormatDesignation]. -/// -/// See [https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition](https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition) -/// for an example schema. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub enum ClaimFormat { - #[serde(rename = "jwt")] - Jwt { - // The algorithm used to sign the JWT. - alg: Vec, - }, - #[serde(rename = "jwt_vc")] - JwtVc { - // The algorithm used to sign the JWT verifiable credential. - alg: Vec, - }, - #[serde(rename = "jwt_vp")] - JwtVp { - // The algorithm used to sign the JWT verifiable presentation. - alg: Vec, - }, - #[serde(rename = "ldp")] - Ldp { - // The proof type used to sign the linked data proof. - // e.g., "JsonWebSignature2020", "Ed25519Signature2018", "EcdsaSecp256k1Signature2019", "RsaSignature2018" - proof_type: Vec, - }, - #[serde(rename = "ldp_vc")] - LdpVc { - // The proof type used to sign the linked data proof verifiable credential. - proof_type: Vec, - }, - #[serde(rename = "ldp_vp")] - LdpVp { - // The proof type used to sign the linked data proof verifiable presentation. - proof_type: Vec, - }, - #[serde(rename = "ac_vc")] - AcVc { - // The proof type used to sign the anoncreds verifiable credential. - proof_type: Vec, - }, - #[serde(rename = "ac_vp")] - AcVp { - // The proof type used to sign the anoncreds verifiable presentation. - proof_type: Vec, - }, - #[serde(rename = "mso_mdoc")] - MsoMDoc(serde_json::Value), - Other(serde_json::Value), -} - -impl ClaimFormat { - /// Returns the designated format of the claim. - /// - /// e.g., jwt, jwt_vc, jwt_vp, ldp, ldp_vc, ldp_vp, ac_vc, ac_vp, mso_mdoc - pub fn designation(&self) -> ClaimFormatDesignation { - match self { - ClaimFormat::Jwt { .. } => ClaimFormatDesignation::Jwt, - ClaimFormat::JwtVc { .. } => ClaimFormatDesignation::JwtVc, - ClaimFormat::JwtVp { .. } => ClaimFormatDesignation::JwtVp, - ClaimFormat::Ldp { .. } => ClaimFormatDesignation::Ldp, - ClaimFormat::LdpVc { .. } => ClaimFormatDesignation::LdpVc, - ClaimFormat::LdpVp { .. } => ClaimFormatDesignation::LdpVp, - ClaimFormat::AcVc { .. } => ClaimFormatDesignation::AcVc, - ClaimFormat::AcVp { .. } => ClaimFormatDesignation::AcVp, - ClaimFormat::MsoMDoc(_) => ClaimFormatDesignation::MsoMDoc, - ClaimFormat::Other(_) => ClaimFormatDesignation::Other, - } - } -} - -/// Claim format payload -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub enum ClaimFormatPayload { - #[serde(rename = "alg")] - Alg(Vec), - #[serde(rename = "proof_type")] - ProofType(Vec), -} - -/// The claim format designation type is used in the input description object to specify the format of the claim. -/// -/// Registry of claim format type: https://identity.foundation/claim-format-registry/#registry -/// -/// Documentation based on the [DIF Presentation Exchange Specification v2.0](https://identity.foundation/presentation-exchange/spec/v2.0.0/#claim-format-designations) -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] -pub enum ClaimFormatDesignation { - /// The format is a JSON Web Token (JWT) as defined by [RFC7519](https://identity.foundation/claim-format-registry/#ref:RFC7519) - /// that will be submitted in the form of a JWT encoded string. Expression of - /// supported algorithms in relation to this format MUST be conveyed using an `alg` - /// property paired with values that are identifiers from the JSON Web Algorithms - /// registry [RFC7518](https://identity.foundation/claim-format-registry/#ref:RFC7518). - #[serde(rename = "jwt")] - Jwt, - /// These formats are JSON Web Tokens (JWTs) [RFC7519](https://identity.foundation/claim-format-registry/#ref:RFC7519) - /// that will be submitted in the form of a JWT-encoded string, with a payload extractable from it defined according to the - /// JSON Web Token (JWT) [section] of the W3C [VC-DATA-MODEL](https://identity.foundation/claim-format-registry/#term:vc-data-model) - /// specification. Expression of supported algorithms in relation to these formats MUST be conveyed using an JWT alg - /// property paired with values that are identifiers from the JSON Web Algorithms registry in - /// [RFC7518](https://identity.foundation/claim-format-registry/#ref:RFC7518) Section 3. - #[serde(rename = "jwt_vc")] - JwtVc, - /// See [JwtVc](JwtVc) for more information. - #[serde(rename = "jwt_vp")] - JwtVp, - #[serde(rename = "jwt_vc_json")] - JwtVcJson, - #[serde(rename = "jwt_vp_json")] - JwtVpJson, - /// The format is a Linked-Data Proof that will be submitted as an object. - /// Expression of supported algorithms in relation to these formats MUST be - /// conveyed using a proof_type property with values that are identifiers from - /// the Linked Data Cryptographic Suite Registry [LDP-Registry](https://identity.foundation/claim-format-registry/#term:ldp-registry). - #[serde(rename = "ldp")] - Ldp, - /// Verifiable Credentials or Verifiable Presentations signed with Linked Data Proof formats. - /// These are descriptions of formats normatively defined in the W3C Verifiable Credentials - /// specification [VC-DATA-MODEL](https://identity.foundation/claim-format-registry/#term:vc-data-model), - /// and will be submitted in the form of a JSON object. Expression of supported algorithms in relation to - /// these formats MUST be conveyed using a proof_type property paired with values that are identifiers from the - /// Linked Data Cryptographic Suite Registry (LDP-Registry). - #[serde(rename = "ldp_vc")] - LdpVc, - /// See [LdpVc](LdpVc) for more information. - #[serde(rename = "ldp_vp")] - LdpVp, - /// This format is for Verifiable Credentials using AnonCreds. - /// AnonCreds is a VC format that adds important - /// privacy-protecting ZKP (zero-knowledge proof) capabilities - /// to the core VC assurances. - #[serde(rename = "ac_vc")] - AcVc, - /// This format is for Verifiable Presentations using AnonCreds. - /// AnonCreds is a VC format that adds important privacy-protecting ZKP - /// (zero-knowledge proof) capabilities to the core VC assurances. - #[serde(rename = "ac_vp")] - AcVp, - /// The format is defined by ISO/IEC 18013-5:2021 [ISO.18013-5](https://identity.foundation/claim-format-registry/#term:iso.18013-5) - /// which defines a mobile driving license (mDL) Credential in the mobile document (mdoc) format. - /// Although ISO/IEC 18013-5:2021 ISO.18013-5 is specific to mobile driving licenses (mDLs), - /// the Credential format can be utilized with any type of Credential (or mdoc document types). - #[serde(rename = "mso_mdoc")] - MsoMDoc, - /// Other claim format designations not covered by the above. - /// - /// The value of this variant is the name of the claim format designation. - Other, -} - -/// A presentation definition is a JSON object that describes the information a [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) requires of a [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder). -/// -/// > Presentation Definitions are objects that articulate what proofs a [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) requires. -/// > These help the [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) to decide how or whether to interact with a [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder). -/// -/// Presentation Definitions are composed of inputs, which describe the forms and details of the -/// proofs they require, and optional sets of selection rules, to allow [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder)s flexibility -/// in cases where different types of proofs may satisfy an input requirement. -/// -/// For more information, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition](https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition) -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct PresentationDefinition { - id: String, - input_descriptors: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - purpose: Option, - #[serde(skip_serializing_if = "Option::is_none")] - format: Option, -} - -impl PresentationDefinition { - /// The Presentation Definition MUST contain an id property. The value of this property MUST be a string. - /// The string SHOULD provide a unique ID for the desired context. - /// - /// The Presentation Definition MUST contain an input_descriptors property. Its value MUST be an array of Input Descriptor Objects, - /// the composition of which are found [InputDescriptor] type. - /// - pub fn new(id: String, input_descriptor: InputDescriptor) -> Self { - Self { - id, - input_descriptors: vec![input_descriptor], - ..Default::default() - } - } - - /// Return the id of the presentation definition. - pub fn id(&self) -> &String { - &self.id - } - - /// Add a new input descriptor to the presentation definition. - pub fn add_input_descriptors(mut self, input_descriptor: InputDescriptor) -> Self { - self.input_descriptors.push(input_descriptor); - self - } - - /// Return the input descriptors of the presentation definition. - pub fn input_descriptors(&self) -> &Vec { - &self.input_descriptors - } - - /// Return a mutable reference to the input descriptors of the presentation definition. - pub fn input_descriptors_mut(&mut self) -> &mut Vec { - &mut self.input_descriptors - } - - /// Set the name of the presentation definition. - /// - /// The [PresentationDefinition] MAY contain a name property. If present, its value SHOULD be a - /// human-friendly string intended to constitute a distinctive designation of the Presentation Definition. - pub fn set_name(mut self, name: String) -> Self { - self.name = Some(name); - self - } - - /// Return the name of the presentation definition. - pub fn name(&self) -> Option<&String> { - self.name.as_ref() - } - - /// Set the purpose of the presentation definition. - /// - /// The [PresentationDefinition] MAY contain a purpose property. If present, its value MUST be a string that - /// describes the purpose for which the Presentation Definition's inputs are being used for. - pub fn set_purpose(mut self, purpose: String) -> Self { - self.purpose = Some(purpose); - self - } - - /// Return the purpose of the presentation definition. - pub fn purpose(&self) -> Option<&String> { - self.purpose.as_ref() - } - - /// Attach a format to the presentation definition. - /// - /// The Presentation Definition MAY include a format property. If present, - /// the value MUST be an object with one or more properties matching the - /// registered Claim Format Designations (e.g., jwt, jwt_vc, jwt_vp, etc.). - /// - /// The properties inform the [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder) of the Claim format configurations the [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) can process. - /// The value for each claim format property MUST be an object composed as follows: - /// - /// The object MUST include a format-specific property (i.e., alg, proof_type) - /// that expresses which algorithms the [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) supports for the format. - /// Its value MUST be an array of one or more format-specific algorithmic identifier references, - /// as noted in the Claim Format Designations section. - /// - /// See: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition](https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition) - pub fn set_format(mut self, format: ClaimFormatMap) -> Self { - self.format = Some(format); - self - } - - /// Add a new format to the presentation definition. - pub fn add_format(mut self, format: ClaimFormatDesignation, value: ClaimFormatPayload) -> Self { - self.format - .get_or_insert_with(HashMap::new) - .insert(format, value); - self - } - - /// Return the format of the presentation definition. - pub fn format(&self) -> Option<&ClaimFormatMap> { - self.format.as_ref() - } -} - -/// Input Descriptors are objects used to describe the information a -/// [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) requires of a -/// [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder). -/// -/// All Input Descriptors MUST be satisfied, unless otherwise specified by a -/// [Feature](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:feature). -/// -/// See: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object](https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object) -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] -pub struct InputDescriptor { - id: String, - #[serde(default)] - constraints: Constraints, - #[serde(skip_serializing_if = "Option::is_none")] - name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - purpose: Option, - #[serde(skip_serializing_if = "Option::is_none")] - format: Option, -} - -impl InputDescriptor { - /// Create a new instance of the input descriptor with the given id and constraints. - /// - /// The Input Descriptor Object MUST contain an id property. The value of the id - /// property MUST be a string that does not conflict with the id of another - /// Input Descriptor Object in the same Presentation Definition. - /// - /// - /// The Input Descriptor Object MUST contain a constraints property. - /// - /// See: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object](https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object) - pub fn new(id: String, constraints: Constraints) -> Self { - Self { - id, - constraints, - ..Default::default() - } - } - - /// Return the id of the input descriptor. - pub fn id(&self) -> &String { - &self.id - } - - /// Return the constraints of the input descriptor. - pub fn constraints(&self) -> &Constraints { - &self.constraints - } - - /// Set the name of the input descriptor. - pub fn set_name(mut self, name: String) -> Self { - self.name = Some(name); - self - } - - /// Return the name of the input descriptor. - pub fn name(&self) -> Option<&String> { - self.name.as_ref() - } - - /// Set the purpose of the input descriptor. - /// - /// The purpose of the input descriptor is an optional field. - /// - /// If present, the purpose MUST be a string that describes the purpose for which the - /// [Claim](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:claim)'s - /// data is being requested. - pub fn set_purpose(mut self, purpose: String) -> Self { - self.purpose = Some(purpose); - self - } - - /// Return the purpose of the input descriptor. - /// - /// If present, the purpose MUST be a string that describes the purpose for which the - /// [Claim](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:claim)'s - /// data is being requested. - pub fn purpose(&self) -> Option<&String> { - self.purpose.as_ref() - } - - /// Set the format of the input descriptor. - /// - /// The Input Descriptor Object MAY contain a format property. If present, - /// its value MUST be an object with one or more properties matching the registered - /// Claim Format Designations (e.g., jwt, jwt_vc, jwt_vp, etc.). - /// - /// This format property is identical in value signature to the top-level format object, - /// but can be used to specifically constrain submission of a single input to a subset of formats or algorithms. - pub fn set_format(mut self, format: ClaimFormatMap) -> Self { - self.format = Some(format); - self - } - - /// Return the format of the input descriptor. - /// - /// The Input Descriptor Object MAY contain a format property. If present, - /// its value MUST be an object with one or more properties matching the registered - /// Claim Format Designations (e.g., jwt, jwt_vc, jwt_vp, etc.). - /// - /// This format property is identical in value signature to the top-level format object, - /// but can be used to specifically constrain submission of a single input to a subset of formats or algorithms. - pub fn format(&self) -> Option<&ClaimFormatMap> { - self.format.as_ref() - } -} - -/// Constraints are objects used to describe the constraints that a [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder) must satisfy to fulfill an Input Descriptor. -/// -/// A constraint object MAY be empty, or it may include a `fields` and/or `limit_disclosure` property. -/// -/// For more information, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object](https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object) -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] -pub struct Constraints { - #[serde(skip_serializing_if = "Option::is_none")] - fields: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - limit_disclosure: Option, -} - -impl Constraints { - /// Returns an empty Constraints object. - pub fn new() -> Self { - Self::default() - } - - /// Add a new field constraint to the constraints list. - pub fn add_constraint(mut self, field: ConstraintsField) -> Self { - self.fields.get_or_insert_with(Vec::new).push(field); - self - } - - /// Returns the fields of the constraints object. - pub fn fields(&self) -> Option<&Vec> { - self.fields.as_ref() - } - - /// Set the limit disclosure value. - /// - /// For all [Claims](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:claims) submitted in relation to [InputDescriptor] Objects that include a `constraints` - /// object with a `limit_disclosure` property set to the string value `required`, - /// ensure that the data submitted is limited to the entries specified in the `fields` property of the `constraints` object. - /// If the `fields` property IS NOT present, or contains zero field objects, the submission SHOULD NOT include any data from the Claim. - /// - /// For example, a [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) may simply want to know whether a [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder) has a valid, signed [Claim](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:claim) of a particular type, - /// without disclosing any of the data it contains. - /// - /// For more information: see [https://identity.foundation/presentation-exchange/spec/v2.0.0/#limited-disclosure-submissions](https://identity.foundation/presentation-exchange/spec/v2.0.0/#limited-disclosure-submissions) - pub fn set_limit_disclosure(mut self, limit_disclosure: ConstraintsLimitDisclosure) -> Self { - self.limit_disclosure = Some(limit_disclosure); - self - } - - /// Returns the limit disclosure value. - pub fn limit_disclosure(&self) -> Option<&ConstraintsLimitDisclosure> { - self.limit_disclosure.as_ref() - } -} - -/// ConstraintsField objects are used to describe the constraints that a [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder) must satisfy to fulfill an Input Descriptor. -/// -/// For more information, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object](https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object) -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct ConstraintsField { - // JSON Regex path -> check regex against JSON structure to check if there is a match; - // TODO JsonPath validation at deserialization time - // Regular expression includes the path -> whether or not the JSON object contains a property. - path: NonEmptyVec, - #[serde(skip_serializing_if = "Option::is_none")] - id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - purpose: Option, - #[serde(skip_serializing_if = "Option::is_none")] - name: Option, - #[serde(skip_serializing_if = "Option::is_none")] - filter: Option, - #[serde(skip_serializing_if = "Option::is_none")] - optional: Option, - #[serde(skip_serializing_if = "Option::is_none")] - intent_to_retain: Option, -} - -pub type ConstraintsFields = Vec; - -impl From> for ConstraintsField { - fn from(path: NonEmptyVec) -> Self { - Self { - path, - id: None, - purpose: None, - name: None, - filter: None, - optional: None, - intent_to_retain: None, - } - } -} - -impl ConstraintsField { - /// Create a new instance of the constraints field with the given path. - /// - /// Tip: Use the [ConstraintsField::From](ConstraintsField::From) trait to convert a [NonEmptyVec](NonEmptyVec) of - /// [JsonPath](JsonPath) to a [ConstraintsField](ConstraintsField) if more than one path is known. - pub fn new(path: JsonPath) -> ConstraintsField { - ConstraintsField { - path: NonEmptyVec::new(path), - id: None, - purpose: None, - name: None, - filter: None, - optional: None, - intent_to_retain: None, - } - } - - /// Add a new path to the constraints field. - pub fn add_path(mut self, path: JsonPath) -> Self { - self.path.push(path); - self - } - - /// Return the paths of the constraints field. - /// - /// `path` is a non empty list of [JsonPath](https://goessner.net/articles/JsonPath/) expressions. - /// - /// For syntax definition, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#jsonpath-syntax-definition](https://identity.foundation/presentation-exchange/spec/v2.0.0/#jsonpath-syntax-definition) - pub fn path(&self) -> &NonEmptyVec { - &self.path - } - - /// Set the id of the constraints field. - /// - /// The fields object MAY contain an id property. If present, its value MUST be a string that - /// is unique from every other field object’s id property, including those contained in other - /// Input Descriptor Objects. - pub fn set_id(mut self, id: String) -> Self { - self.id = Some(id); - self - } - - /// Return the id of the constraints field. - pub fn id(&self) -> Option<&String> { - self.id.as_ref() - } - - /// Set the purpose of the constraints field. - /// - /// If present, its value MUST be a string that describes the purpose for which the field is being requested. - pub fn set_purpose(mut self, purpose: String) -> Self { - self.purpose = Some(purpose); - self - } - - /// Return the purpose of the constraints field. - pub fn purpose(&self) -> Option<&String> { - self.purpose.as_ref() - } - - /// Set the name of the constraints field. - /// - /// If present, its value MUST be a string, and SHOULD be a human-friendly - /// name that describes what the target field represents. - /// - /// For example, the name of the constraint could be "over_18" if the field is a date of birth. - pub fn set_name(mut self, name: String) -> Self { - self.name = Some(name); - self - } - - /// Return the name of the constraints field. - pub fn name(&self) -> Option<&String> { - self.name.as_ref() - } - - /// Set the filter of the constraints field. - /// - /// If present its value MUST be a JSON Schema descriptor used to filter against - /// the values returned from evaluation of the JSONPath string expressions in the path array. - pub fn set_filter(mut self, filter: serde_json::Value) -> Self { - self.filter = Some(filter); - self - } - - /// Return the filter of the constraints field. - pub fn filter(&self) -> Option<&serde_json::Value> { - self.filter.as_ref() - } - - /// Set the optional value of the constraints field. - /// - /// The value of this property MUST be a boolean, wherein true indicates the - /// field is optional, and false or non-presence of the property indicates the - /// field is required. Even when the optional property is present, the value - /// located at the indicated path of the field MUST validate against the - /// JSON Schema filter, if a filter is present. - /// - /// For more information, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object](https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object) - pub fn set_optional(mut self, optional: bool) -> Self { - self.optional = Some(optional); - self - } - - /// Return the optional value of the constraints field. - pub fn optional(&self) -> bool { - self.optional.unwrap_or(false) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum ConstraintsLimitDisclosure { - Required, - Preferred, -} - -/// Presentation Submissions are objects embedded within target -/// [Claim](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:claim) negotiation -/// formats that express how the inputs presented as proofs to a -/// [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier) are -/// provided in accordance with the requirements specified in a [PresentationDefinition]. -/// -/// Embedded Presentation Submission objects MUST be located within target data format as -/// the value of a `presentation_submission` property. -/// -/// For more information, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission](https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission) -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct PresentationSubmission { - id: uuid::Uuid, - definition_id: uuid::Uuid, - descriptor_map: Vec, -} - -impl PresentationSubmission { - /// The presentation submission MUST contain an id property. The value of this property MUST be a unique identifier, i.e. a UUID. - /// - /// The presentation submission object MUST contain a `definition_id` property. The value of this property MUST be the id value of a valid [PresentationDefinition::id()]. - pub fn new( - id: uuid::Uuid, - definition_id: uuid::Uuid, - descriptor_map: Vec, - ) -> Self { - Self { - id, - definition_id, - descriptor_map, - } - } - - /// Return the id of the presentation submission. - pub fn id(&self) -> &uuid::Uuid { - &self.id - } - - /// Return the definition id of the presentation submission. - pub fn definition_id(&self) -> &uuid::Uuid { - &self.definition_id - } - - /// Return the descriptor map of the presentation submission. - pub fn descriptor_map(&self) -> &Vec { - &self.descriptor_map - } - - /// Return a mutable reference to the descriptor map of the presentation submission. - pub fn descriptor_map_mut(&mut self) -> &mut Vec { - &mut self.descriptor_map - } -} - -/// Descriptor Maps are objects used to describe the information a [Holder](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:holder) provides to a [Verifier](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:verifier). -/// -/// For more information, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission](https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission) -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct DescriptorMap { - id: String, - format: ClaimFormatDesignation, - path: JsonPath, - path_nested: Option>, -} - -impl DescriptorMap { - /// The descriptor map MUST include an `id` property. The value of this property MUST be a string that matches the `id` property of the [InputDescriptor::id()] in the Presentation Definition that this [PresentationSubmission] is related to. - /// - /// The descriptor map object MUST include a `format` property. The value of this property MUST be a string that matches one of the [ClaimFormatDesignation]. This denotes the data format of the [Claim](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:claim). - /// - /// The descriptor map object MUST include a `path` property. The value of this property MUST be a [JSONPath](https://goessner.net/articles/JsonPath/) string expression. The path property indicates the [Claim](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:claim) submitted in relation to the identified [InputDescriptor], when executed against the top-level of the object the [PresentationSubmission] is embedded within. - /// - /// For more information, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission](https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-submission) - pub fn new(id: String, format: ClaimFormatDesignation, path: JsonPath) -> Self { - Self { - id, - format, - path, - path_nested: None, - } - } - - /// Return the id of the descriptor map. - pub fn id(&self) -> &String { - &self.id - } - - /// Return the format of the descriptor map. - pub fn format(&self) -> &ClaimFormatDesignation { - &self.format - } - - /// Return the path of the descriptor map. - pub fn path(&self) -> &JsonPath { - &self.path - } - - /// Set the nested path of the descriptor map. - /// - /// The format of a path_nested object mirrors that of a [DescriptorMap] property. The nesting may be any number of levels deep. - /// The `id` property MUST be the same for each level of nesting. - /// - /// The path property inside each `path_nested` property provides a relative path within a given nested value. - /// - /// For more information on nested paths, see: [https://identity.foundation/presentation-exchange/spec/v2.0.0/#processing-of-submission-entries](https://identity.foundation/presentation-exchange/spec/v2.0.0/#processing-of-submission-entries) - /// - /// Errors: - /// - The id of the nested path must be the same as the parent id. - pub fn set_path_nested(mut self, path_nested: DescriptorMap) -> Result { - // Check the id of the nested path is the same as the parent id. - if path_nested.id() != self.id() { - bail!("The id of the nested path must be the same as the parent id.") - } - - self.path_nested = Some(Box::new(path_nested)); - - Ok(self) - } -} - -#[derive(Deserialize)] -pub struct SubmissionRequirementBaseBase { - pub name: Option, - pub purpose: Option, - #[serde(flatten)] - pub property_set: Option>, -} - -#[derive(Deserialize)] -#[serde(untagged)] -pub enum SubmissionRequirementBase { - From { - from: String, // TODO `group` string?? - #[serde(flatten)] - submission_requirement_base: SubmissionRequirementBaseBase, - }, - FromNested { - from_nested: Vec, - #[serde(flatten)] - submission_requirement_base: SubmissionRequirementBaseBase, - }, -} - -#[derive(Deserialize)] -#[serde(tag = "rule", rename_all = "snake_case")] -pub enum SubmissionRequirement { - All(SubmissionRequirementBase), - Pick(SubmissionRequirementPick), -} - -#[derive(Deserialize)] -pub struct SubmissionRequirementPick { - #[serde(flatten)] - pub submission_requirement: SubmissionRequirementBase, - pub count: Option, - pub min: Option, - pub max: Option, -} - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use serde_json::json; - use std::{ - ffi::OsStr, - fs::{self, File}, - }; - - #[test] - fn request_example() { - let value = json!( - { - "id": "36682080-c2ed-4ba6-a4cd-37c86ef2da8c", - "input_descriptors": [ - { - "id": "d05a7f51-ac09-43af-8864-e00f0175f2c7", - "format": { - "ldp_vc": { - "proof_type": [ - "Ed25519Signature2018" - ] - } - }, - "constraints": { - "fields": [ - { - "path": [ - "$.type" - ], - "filter": { - "type": "string", - "pattern": "IDCardCredential" - } - } - ] - } - } - ] - } - ); - let _: PresentationDefinition = serde_json::from_value(value).unwrap(); - } - - #[derive(Deserialize)] - pub struct PresentationDefinitionTest { - #[serde(alias = "presentation_definition")] - _pd: PresentationDefinition, - } - - #[test] - fn presentation_definition_suite() { - let paths = - fs::read_dir("tests/presentation-exchange/test/presentation-definition").unwrap(); - for path in paths { - let path = path.unwrap().path(); - if let Some(ext) = path.extension() { - if ext != OsStr::new("json") - || ["VC_expiration_example.json", "VC_revocation_example.json"] // TODO bad format - .contains(&path.file_name().unwrap().to_str().unwrap()) - { - continue; - } - } - println!("{} -> ", path.file_name().unwrap().to_str().unwrap()); - let file = File::open(path).unwrap(); - let jd = &mut serde_json::Deserializer::from_reader(file.try_clone().unwrap()); - let _: PresentationDefinitionTest = serde_path_to_error::deserialize(jd) - .map_err(|e| e.path().to_string()) - .unwrap(); - println!("✅") - } - } - - #[derive(Deserialize)] - pub struct PresentationSubmissionTest { - #[serde(alias = "presentation_submission")] - _ps: PresentationSubmission, - } - - #[test] - fn presentation_submission_suite() { - let paths = - fs::read_dir("tests/presentation-exchange/test/presentation-submission").unwrap(); - for path in paths { - let path = path.unwrap().path(); - if let Some(ext) = path.extension() { - if ext != OsStr::new("json") - || [ - "appendix_DIDComm_example.json", - "appendix_CHAPI_example.json", - ] - .contains(&path.file_name().unwrap().to_str().unwrap()) - { - continue; - } - } - println!("{} -> ", path.file_name().unwrap().to_str().unwrap()); - let file = File::open(path).unwrap(); - let jd = &mut serde_json::Deserializer::from_reader(file.try_clone().unwrap()); - let _: PresentationSubmissionTest = serde_path_to_error::deserialize(jd) - .map_err(|e| e.path().to_string()) - .unwrap(); - println!("✅") - } - } - - #[derive(Deserialize)] - pub struct SubmissionRequirementsTest { - #[serde(alias = "submission_requirements")] - _sr: Vec, - } - - #[test] - fn submission_requirements_suite() { - let paths = - fs::read_dir("tests/presentation-exchange/test/submission-requirements").unwrap(); - for path in paths { - let path = path.unwrap().path(); - if let Some(ext) = path.extension() { - if ext != OsStr::new("json") - || ["schema.json"].contains(&path.file_name().unwrap().to_str().unwrap()) - { - continue; - } - } - print!("{} -> ", path.file_name().unwrap().to_str().unwrap()); - let file = File::open(path).unwrap(); - let jd = &mut serde_json::Deserializer::from_reader(file.try_clone().unwrap()); - let _: SubmissionRequirementsTest = serde_path_to_error::deserialize(jd) - .map_err(|e| e.path().to_string()) - .unwrap(); - println!("✅") - } - } -} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..1565cdc --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,220 @@ +use crate::core::{ + presentation_definition::{PresentationDefinition, SubmissionRequirement}, + presentation_submission::*, +}; + +use std::{ + ffi::OsStr, + fs::{self, File}, +}; + +use anyhow::Result; +use serde::Deserialize; +use serde_json::json; +use serde_json::Value; +use ssi_claims::jwt::VerifiablePresentation; + +#[test] +fn request_example() { + let value = json!( + { + "id": "36682080-c2ed-4ba6-a4cd-37c86ef2da8c", + "input_descriptors": [ + { + "id": "d05a7f51-ac09-43af-8864-e00f0175f2c7", + "format": { + "ldp_vc": { + "proof_type": [ + "Ed25519Signature2018" + ] + } + }, + "constraints": { + "fields": [ + { + "path": [ + "$.type" + ], + "filter": { + "type": "string", + "pattern": "IDCardCredential" + } + } + ] + } + } + ] + } + ); + let _: PresentationDefinition = serde_json::from_value(value).unwrap(); +} + +#[derive(Deserialize)] +pub struct PresentationDefinitionTest { + #[serde(alias = "presentation_definition")] + _pd: PresentationDefinition, +} + +#[test] +fn presentation_definition_suite() { + let paths = fs::read_dir("tests/presentation-exchange/test/presentation-definition").unwrap(); + for path in paths { + let path = path.unwrap().path(); + if let Some(ext) = path.extension() { + if ext != OsStr::new("json") + || ["VC_expiration_example.json", "VC_revocation_example.json"] // TODO bad format + .contains(&path.file_name().unwrap().to_str().unwrap()) + { + continue; + } + } + println!("{} -> ", path.file_name().unwrap().to_str().unwrap()); + let file = File::open(path).unwrap(); + let jd = &mut serde_json::Deserializer::from_reader(file.try_clone().unwrap()); + let _: PresentationDefinitionTest = serde_path_to_error::deserialize(jd) + .map_err(|e| e.path().to_string()) + .unwrap(); + println!("✅") + } +} + +#[derive(Deserialize)] +pub struct PresentationSubmissionTest { + #[serde(alias = "presentation_submission")] + _ps: PresentationSubmission, +} + +#[test] +fn presentation_submission_suite() { + let paths = fs::read_dir("tests/presentation-exchange/test/presentation-submission").unwrap(); + for path in paths { + let path = path.unwrap().path(); + if let Some(ext) = path.extension() { + if ext != OsStr::new("json") + || [ + "appendix_DIDComm_example.json", + "appendix_CHAPI_example.json", + ] + .contains(&path.file_name().unwrap().to_str().unwrap()) + { + continue; + } + } + println!("{} -> ", path.file_name().unwrap().to_str().unwrap()); + let file = File::open(path).unwrap(); + let jd = &mut serde_json::Deserializer::from_reader(file.try_clone().unwrap()); + let _: PresentationSubmissionTest = serde_path_to_error::deserialize(jd) + .map_err(|e| e.path().to_string()) + .unwrap(); + println!("✅") + } +} + +#[derive(Deserialize)] +pub struct SubmissionRequirementsTest { + #[serde(alias = "submission_requirements")] + _sr: Vec, +} + +#[test] +fn submission_requirements_suite() { + let paths = fs::read_dir("tests/presentation-exchange/test/submission-requirements").unwrap(); + for path in paths { + let path = path.unwrap().path(); + if let Some(ext) = path.extension() { + if ext != OsStr::new("json") + || ["schema.json"].contains(&path.file_name().unwrap().to_str().unwrap()) + { + continue; + } + } + print!("{} -> ", path.file_name().unwrap().to_str().unwrap()); + let file = File::open(path).unwrap(); + let jd = &mut serde_json::Deserializer::from_reader(file.try_clone().unwrap()); + let _: SubmissionRequirementsTest = serde_path_to_error::deserialize(jd) + .map_err(|e| e.path().to_string()) + .unwrap(); + println!("✅") + } +} + +#[test] +fn test_presentation_submission_validation() -> Result<()> { + // Setup the test cases + for test_case in 1..4 { + let definition: PresentationDefinition = serde_json::from_str(&fs::read_to_string( + format!("tests/presentation-submission/definition_{test_case}.json",), + )?)?; + + let submission: PresentationSubmission = serde_json::from_str(&fs::read_to_string( + format!("tests/presentation-submission/submission_{test_case}.json",), + )?)?; + + let presentation: VerifiablePresentation = serde_json::from_str(&fs::read_to_string( + format!("tests/presentation-submission/vp_{test_case}.json",), + )?)?; + + match test_case { + 1 | 2 => { + assert!(definition + .validate_presentation(presentation, submission.descriptor_map()) + .is_ok()); + } + 3 => { + // Expect this case to error because the presentation includes more descriptors + // than the submission requires. + assert!(definition + .validate_presentation(presentation, submission.descriptor_map()) + .is_err()); + } + _ => {} + } + } + + Ok(()) +} + +#[test] +fn test_input_descriptor_validation() -> Result<()> { + // Include the `input_descriptors_example.json` file in the `examples` directory. + let input_descriptors = include_str!( + "../tests/presentation-exchange/test/presentation-definition/multi_group_example.json" + ); + + let mut value: Value = serde_json::from_str(input_descriptors)?; + + let presentation_definition: PresentationDefinition = value + .as_object_mut() + .map(|obj| { + obj.remove("presentation_definition") + .map(serde_json::from_value) + }) + .flatten() + .expect("failed to parse presentation definition")?; + + let presentation_submission = include_str!( + "../tests/presentation-exchange/test/presentation-submission/appendix_VP_example.json" + ); + + let value: Value = serde_json::from_str(presentation_submission)?; + + let presentation_submission: PresentationSubmission = value + .as_object() + .map(|obj| { + obj.get("presentation_submission") + .map(|v| serde_json::from_value(v.clone())) + }) + .flatten() + .expect("failed to parse presentation submission")?; + + let descriptor_map = presentation_submission.descriptor_map(); + + let verifiable_presentation: VerifiablePresentation = serde_json::from_value(value)?; + + // Expect the example to fail here because the submission does match the definition. + assert!(presentation_definition + .validate_presentation(verifiable_presentation, &descriptor_map) + .is_err()); + + Ok(()) +} diff --git a/src/utils.rs b/src/utils.rs index 91ab96d..e60e7c6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,7 +2,7 @@ use anyhow::{bail, Error}; use serde::{Deserialize, Serialize}; use std::ops::Deref; -#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq, Eq)] #[serde(try_from = "Vec", into = "Vec")] pub struct NonEmptyVec(Vec); diff --git a/src/verifier/client.rs b/src/verifier/client.rs index 8e45186..94fcb11 100644 --- a/src/verifier/client.rs +++ b/src/verifier/client.rs @@ -4,7 +4,8 @@ use anyhow::{bail, Context as _, Result}; use async_trait::async_trait; use base64::prelude::*; use serde_json::{json, Value as Json}; -use ssi::did_resolve::DIDResolver; +use ssi_jwk::JWKResolver; + use tracing::debug; use x509_cert::{ der::Encode, @@ -36,24 +37,25 @@ pub trait Client: Debug { pub struct DIDClient { id: ClientId, vm: String, - signer: Arc, + signer: Arc + Send + Sync>, } impl DIDClient { pub async fn new( vm: String, - signer: Arc, - resolver: &dyn DIDResolver, + signer: Arc + Send + Sync>, + resolver: impl JWKResolver, ) -> Result { let (id, _f) = vm.rsplit_once('#').context(format!( "expected a DID verification method, received '{vm}'" ))?; - let key = ssi::did_resolve::resolve_key(&vm, resolver) + let jwk = resolver + .fetch_public_jwk(Some(&vm)) .await .context("unable to resolve key from verification method")?; - if &key != signer.jwk() { + if *jwk != signer.jwk().context("signer did not have a JWK")? { bail!( "verification method resolved from DID document did not match public key of signer" ) @@ -72,14 +74,14 @@ impl DIDClient { pub struct X509SanClient { id: ClientId, x5c: Vec, - signer: Arc, + signer: Arc + Send + Sync>, variant: X509SanVariant, } impl X509SanClient { pub fn new( x5c: Vec, - signer: Arc, + signer: Arc + Send + Sync>, variant: X509SanVariant, ) -> Result { let leaf = &x5c[0]; @@ -143,7 +145,10 @@ impl Client for DIDClient { &self, body: &AuthorizationRequestObject, ) -> Result { - let algorithm = self.signer.alg(); + let algorithm = self + .signer + .alg() + .context("failed to retrieve signing algorithm")?; let header = json!({ "alg": algorithm, "kid": self.vm, @@ -170,7 +175,10 @@ impl Client for X509SanClient { &self, body: &AuthorizationRequestObject, ) -> Result { - let algorithm = self.signer.alg(); + let algorithm = self + .signer + .alg() + .context("failed to retrieve signing algorithm")?; let x5c: Vec = self .x5c .iter() diff --git a/src/verifier/request_builder.rs b/src/verifier/request_builder.rs index 0e5b50f..20b42a1 100644 --- a/src/verifier/request_builder.rs +++ b/src/verifier/request_builder.rs @@ -14,8 +14,8 @@ use crate::{ WalletMetadata, }, object::{ParsingErrorContext, TypedParameter, UntypedObject}, + presentation_definition::PresentationDefinition, }, - presentation_exchange::PresentationDefinition, verifier::{by_reference::ByReference, session::Status}, }; diff --git a/src/verifier/request_signer.rs b/src/verifier/request_signer.rs index 67cb33c..7fbba83 100644 --- a/src/verifier/request_signer.rs +++ b/src/verifier/request_signer.rs @@ -1,46 +1,62 @@ -#[cfg(feature = "p256")] use anyhow::Result; use async_trait::async_trait; -#[cfg(feature = "p256")] use p256::ecdsa::{signature::Signer, Signature, SigningKey}; -use ssi::jwk::JWK; +use ssi_claims::jws::{JWSSigner, JWSSignerInfo}; +use ssi_jwk::Algorithm; + +use ssi_jwk::JWK; use std::fmt::Debug; #[async_trait] pub trait RequestSigner: Debug { + type Error: std::fmt::Display; + /// The algorithm that will be used to sign. - fn alg(&self) -> &str; + fn alg(&self) -> Result; + /// The public JWK of the signer. - fn jwk(&self) -> &JWK; + fn jwk(&self) -> Result; + + /// Sign the payload and return the signature. async fn sign(&self, payload: &[u8]) -> Vec; + + /// Attempt to sign the payload and return the signature. + async fn try_sign(&self, payload: &[u8]) -> Result, Self::Error> { + // default implementation will call sign. + // Override for custom error handling. + Ok(self.sign(payload).await) + } } -#[cfg(feature = "p256")] #[derive(Debug)] pub struct P256Signer { key: SigningKey, jwk: JWK, } -#[cfg(feature = "p256")] 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 }) } + + pub fn jwk(&self) -> &JWK { + &self.jwk + } } -#[cfg(feature = "p256")] #[async_trait] impl RequestSigner for P256Signer { - fn alg(&self) -> &str { - "ES256" + type Error = anyhow::Error; + + fn alg(&self) -> Result { + Ok(self.jwk.algorithm.unwrap_or(Algorithm::ES256).to_string()) } - fn jwk(&self) -> &JWK { - &self.jwk + fn jwk(&self) -> Result { + Ok(self.jwk.clone()) } async fn sign(&self, payload: &[u8]) -> Vec { @@ -48,3 +64,22 @@ impl RequestSigner for P256Signer { sig.to_vec() } } + +impl JWSSigner for P256Signer { + async fn fetch_info(&self) -> std::result::Result { + let algorithm = self.jwk.algorithm.unwrap_or(Algorithm::ES256); + + let key_id = self.jwk.key_id.clone(); + + Ok(JWSSignerInfo { algorithm, key_id }) + } + + async fn sign_bytes( + &self, + signing_bytes: &[u8], + ) -> std::result::Result, ssi_claims::SignatureError> { + self.try_sign(signing_bytes) + .await + .map_err(|e| ssi_claims::SignatureError::Other(format!("Failed to sign bytes: {}", e))) + } +} diff --git a/src/verifier/session.rs b/src/verifier/session.rs index 8e9a621..37e3e26 100644 --- a/src/verifier/session.rs +++ b/src/verifier/session.rs @@ -6,9 +6,9 @@ pub use oid4vp_frontend::*; use tokio::sync::Mutex; use uuid::Uuid; -use crate::{ - core::authorization_request::AuthorizationRequestObject, - presentation_exchange::PresentationDefinition, +use crate::core::{ + authorization_request::AuthorizationRequestObject, + presentation_definition::PresentationDefinition, }; #[derive(Debug, Clone)] diff --git a/tests/e2e.rs b/tests/e2e.rs index 697e7ca..b17df19 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -1,87 +1,142 @@ +use jwt_vp::create_test_verifiable_presentation; use oid4vp::{ core::{ authorization_request::parameters::{ClientMetadata, Nonce, ResponseMode, ResponseType}, + credential_format::*, + input_descriptor::*, object::UntypedObject, + presentation_definition::*, + presentation_submission::*, response::{parameters::VpToken, AuthorizationResponse, UnencodedAuthorizationResponse}, }, - presentation_exchange::{PresentationDefinition, PresentationSubmission}, verifier::session::{Outcome, Status}, wallet::Wallet, }; -use serde_json::json; +use ssi_jwk::Algorithm; mod jwt_vc; +mod jwt_vp; #[tokio::test] async fn w3c_vc_did_client_direct_post() { let (wallet, verifier) = jwt_vc::wallet_verifier().await; - let presentation_definition: PresentationDefinition = serde_json::from_value(json!({ - "id": "0b4dd017-efa6-4a05-a269-9790fa3c22c2", - "input_descriptors": [ - { - "id": "064255a8-a0fa-4108-9ded-429f83003350", - "format": { - "jwt_vc_json": { - "proof_type": [ - "JsonWebSignature2020" - ] - } - }, - "constraints": {} - } - ] - })) - .unwrap(); + let presentation_definition = PresentationDefinition::new( + "did-key-id-proof".into(), + InputDescriptor::new( + "did-key-id".into(), + Constraints::new() + .add_constraint( + // Add a constraint fields to check if the credential + // conforms to a specific path. + ConstraintsField::new("$.credentialSubject.id".into()) + // Add alternative path(s) to check multiple potential formats. + .add_path("$.vp.verifiableCredential.vc.credentialSubject.id".into()) + .add_path("$.vp.verifiableCredential[0].vc.credentialSubject.id".into()) + .set_name("Verify Identity Key".into()) + .set_purpose("Check whether your identity key has been verified.".into()) + .set_filter(serde_json::json!({ + "type": "string", + "pattern": "did:key:.*" + })) + .set_predicate(Predicate::Required), + ) + .set_limit_disclosure(ConstraintsLimitDisclosure::Required), + ) + .set_name("DID Key Identity Verification".into()) + .set_purpose("Check whether your identity key has been verified.".into()) + .set_format((|| { + let mut map = ClaimFormatMap::new(); + map.insert( + ClaimFormatDesignation::JwtVcJson, + ClaimFormatPayload::Alg(vec![Algorithm::ES256.to_string()]), + ); + map + })()), + ); let client_metadata = UntypedObject::default(); + let nonce = Nonce::from("random_nonce"); + let (id, request) = verifier .build_authorization_request() .with_presentation_definition(presentation_definition.clone()) .with_request_parameter(ResponseMode::DirectPost) .with_request_parameter(ResponseType::VpToken) - .with_request_parameter(Nonce("random123".to_owned())) + .with_request_parameter(nonce) .with_request_parameter(ClientMetadata(client_metadata)) .build(wallet.metadata().clone()) .await .unwrap(); + println!("Request: {:?}", request); + let request = wallet.validate_request(request).await.unwrap(); + let parsed_presentation_definition = request + .resolve_presentation_definition(wallet.http_client()) + .await + .unwrap(); + assert_eq!( - &presentation_definition, - request - .resolve_presentation_definition(wallet.http_client()) - .await - .unwrap() - .parsed() + presentation_definition.id(), + parsed_presentation_definition.parsed().id() ); assert_eq!(&ResponseType::VpToken, request.response_type()); assert_eq!(&ResponseMode::DirectPost, request.response_mode()); - // TODO: Response with a VP. - let presentation_submission: PresentationSubmission = serde_json::from_value(json!( - { - "id": "39881a17-e454-4d98-87ba-e3073d1014d6", - "definition_id": "0b4dd017-efa6-4a05-a269-9790fa3c22c2", - "descriptor_map": [ - { - "id": "064255a8-a0fa-4108-9ded-429f83003350", - "path": "$", - "format": "jwt_vp" - } - ] - } - - )) - .unwrap(); + let descriptor_map = parsed_presentation_definition + .parsed() + .input_descriptors() + .iter() + .map(|descriptor| { + // NOTE: the input descriptor constraint field path is relative to the path + // of the descriptor map matching the input descriptor id. + DescriptorMap::new( + descriptor.id().to_string(), + // NOTE: Since the input descriptor may support several different + // claim format types. This value should not be hardcoded in production + // code, but should be selected from available formats in the presentation definition + // input descriptor. + // + // In practice, this format will be determined by the VDC collection's credential format. + ClaimFormatDesignation::JwtVpJson, + // Starts at the top level path of the verifiable submission, which contains a `vp` key + // for verifiable presentations, which include the verifiable credentials under the `verifiableCredentials` + // field. + "$".into(), + ) + .set_path_nested(DescriptorMap::new( + // Descriptor map id must be the same as the parent descriptor map id. + descriptor.id().to_string(), + ClaimFormatDesignation::JwtVcJson, + // This nested path is relative to the resolved path of the parent descriptor map. + // In this case, the parent descriptor map resolved to the `vp` key. + // The nested path is relative to the `vp` key. + // + // See: https://identity.foundation/presentation-exchange/spec/v2.0.0/#processing-of-submission-entries + "$.vp.verifiableCredential[0]".into(), + )) + }) + .collect(); + + let presentation_submission = PresentationSubmission::new( + uuid::Uuid::new_v4(), + parsed_presentation_definition.parsed().id().clone(), + descriptor_map, + ); let response = AuthorizationResponse::Unencoded(UnencodedAuthorizationResponse( Default::default(), - VpToken(include_str!("examples/vc.jwt").to_owned()), + VpToken( + create_test_verifiable_presentation() + .await + .expect("failed to create verifiable presentation") + .to_string(), + ), presentation_submission.try_into().unwrap(), )); @@ -93,5 +148,8 @@ async fn w3c_vc_did_client_direct_post() { assert_eq!(None, redirect); let status = verifier.poll_status(id).await.unwrap(); + + println!("Status: {:?}", status); + assert!(matches!(status, Status::Complete(Outcome::Success { .. }))) } diff --git a/tests/examples/vp.jwt b/tests/examples/vp.jwt new file mode 100644 index 0000000..28eaf73 --- /dev/null +++ b/tests/examples/vp.jwt @@ -0,0 +1 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6a2V5OnpEbmFlZnFUMUJyR0dzSkVaR3dBaXVlb3VxTWg2TXFzWmhhTDFtZDVoa0hndGZ6YjIjekRuYWVmcVQxQnJHR3NKRVpHd0FpdWVvdXFNaDZNcXNaaGFMMW1kNWhrSGd0ZnpiMiIsImF1ZCI6ImRpZDprZXk6ekRuYWVhRGozWXBQUjRKWG9zMmtDQ05QUzg2aGRFTGVONVBaaDk3S0drb0Z6VXRHbiN6RG5hZWFEajNZcFBSNEpYb3Mya0NDTlBTODZoZEVMZU41UFpoOTdLR2tvRnpVdEduIiwiaWF0IjoxNzI0MTI0MDc0LCJleHAiOjE3MjQxMjc2NzQsIm5vbmNlIjoicmFuZG9tX25vbmNlIiwidnAiOnsiQGNvbnRleHQiOiJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsInR5cGUiOiJWZXJpZmlhYmxlUHJlc2VudGF0aW9uIiwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlt7ImlzcyI6ImRpZDprZXk6ekRuYWVlZXg5TUFWYmhvV2VEY2JiR1pkek0xenhxWnFwQzM4N2pXb0xoVXIxQmRTVCIsIm5iZiI6MTcwNDA2NzIwMC4wLCJqdGkiOiIyYzI4MmViYS1kMTQ3LTQyMmMtOWNlNS05OTFmMTk5ODAwYzUiLCJzdWIiOiJkaWQ6a2V5OnpEbmFlZnFUMUJyR0dzSkVaR3dBaXVlb3VxTWg2TXFzWmhhTDFtZDVoa0hndGZ6YjIiLCJ2YyI6eyJAY29udGV4dCI6Imh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaWQiOiIyYzI4MmViYS1kMTQ3LTQyMmMtOWNlNS05OTFmMTk5ODAwYzUiLCJ0eXBlIjoiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ekRuYWVmcVQxQnJHR3NKRVpHd0FpdWVvdXFNaDZNcXNaaGFMMW1kNWhrSGd0ZnpiMiJ9LCJpc3N1ZXIiOiJkaWQ6a2V5OnpEbmFlZWV4OU1BVmJob1dlRGNiYkdaZHpNMXp4cVpxcEMzODdqV29MaFVyMUJkU1QiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTAxLTAxVDAwOjAwOjAwKzAwOjAwIn19XX19.tsP3YHS6CouT-Fe-p2E16HRUY0qKLZYi79V8-pUw0tuGEhL4i5BPCZo14vigthtk37pJGb-rM2qB_NlsDyJJkQ \ No newline at end of file diff --git a/tests/jwt_vc.rs b/tests/jwt_vc.rs index a102c9e..9d60245 100644 --- a/tests/jwt_vc.rs +++ b/tests/jwt_vc.rs @@ -2,7 +2,6 @@ use std::sync::Arc; use anyhow::{Context, Result}; use async_trait::async_trait; -use did_method_key::DIDKey; use http::{Request, Response}; use oid4vp::{ core::{ @@ -22,7 +21,8 @@ use oid4vp::{ wallet::Wallet, }; use serde_json::json; -use ssi::did::DIDMethod; +use ssi_dids::{DIDKey, VerificationMethodDIDResolver}; +use ssi_verification_methods::AnyJwkMethod; pub async fn wallet_verifier() -> (JwtVcWallet, Arc) { let verifier_did = "did:key:zDnaeaDj3YpPR4JXos2kCCNPS86hdELeN5PZh97KGkoFzUtGn".to_owned(); @@ -36,11 +36,15 @@ pub async fn wallet_verifier() -> (JwtVcWallet, Arc) { ) .unwrap(), ); + + let resolver: VerificationMethodDIDResolver = + VerificationMethodDIDResolver::new(DIDKey); + let client = Arc::new( oid4vp::verifier::client::DIDClient::new( verifier_did_vm.clone(), signer.clone(), - DIDKey.to_resolver(), + &resolver, ) .await .unwrap(), @@ -125,12 +129,15 @@ impl RequestVerifier for JwtVcWallet { decoded_request: &AuthorizationRequestObject, request_jwt: String, ) -> Result<()> { + let resolver: VerificationMethodDIDResolver = + VerificationMethodDIDResolver::new(DIDKey); + did::verify_with_resolver( self.metadata(), decoded_request, request_jwt, Some(self.trusted_dids()), - DIDKey.to_resolver(), + &resolver, ) .await } @@ -153,7 +160,7 @@ impl AsyncHttpClient for MockHttpClient { AuthorizationResponse::from_x_www_form_urlencoded(body) .context("failed to parse authorization response request")?, |_, _| { - Box::pin(async { + Box::pin(async move { Outcome::Success { info: serde_json::Value::Null, } diff --git a/tests/jwt_vp.rs b/tests/jwt_vp.rs new file mode 100644 index 0000000..42253b3 --- /dev/null +++ b/tests/jwt_vp.rs @@ -0,0 +1,48 @@ +use std::str::FromStr; + +use anyhow::Result; +use base64::prelude::*; +use oid4vp::holder::verifiable_presentation_builder::{ + VerifiablePresentationBuilder, VerifiablePresentationBuilderOptions, +}; +use oid4vp::verifier::request_signer::P256Signer; +use ssi_claims::jwt; +use ssi_dids::DIDKey; +use ssi_jwk::JWK; + +pub async fn create_test_verifiable_presentation() -> Result { + let verifier = JWK::from_str(include_str!("examples/verifier.jwk"))?; + + let signer = P256Signer::new( + p256::SecretKey::from_jwk_str(include_str!("examples/subject.jwk")) + .unwrap() + .into(), + ) + .unwrap(); + + let holder_did = DIDKey::generate_url(signer.jwk())?; + let verifier_did = DIDKey::generate_url(&verifier)?; + + // Create a verifiable presentation using the `examples/vc.jwt` file + // The signer information is the holder's key, also found in the `examples/subject.jwk` file. + let verifiable_credential: jwt::VerifiableCredential = + ssi_claims::jwt::decode_unverified(include_str!("examples/vc.jwt"))?; + + let verifiable_presentation = + VerifiablePresentationBuilder::from_options(VerifiablePresentationBuilderOptions { + issuer: holder_did.clone(), + subject: holder_did.clone(), + audience: verifier_did.clone(), + expiration_secs: 3600, + credentials: vec![verifiable_credential], + nonce: "random_nonce".into(), + }); + + // Encode the verifiable presentation as base64 encoded payload. + let vp_token = verifiable_presentation.0.to_string(); + + // encode as base64. + let base64_encoded_vp = BASE64_STANDARD.encode(vp_token); + + Ok(base64_encoded_vp) +} diff --git a/tests/presentation-submission/definition_1.json b/tests/presentation-submission/definition_1.json new file mode 100644 index 0000000..df29b28 --- /dev/null +++ b/tests/presentation-submission/definition_1.json @@ -0,0 +1,28 @@ +{ + "id": "simple_example", + "input_descriptors": [ + { + "id": "name", + "group": ["personal_info"], + "schema": [{ "uri": "https://schema.org/name" }] + }, + { + "id": "email", + "group": ["personal_info"], + "schema": [{ "uri": "https://schema.org/email" }] + }, + { + "id": "age", + "group": ["personal_info"], + "schema": [{ "uri": "https://schema.org/age" }] + } + ], + "submission_requirements": [ + { + "name": "Personal Information", + "rule": "pick", + "count": 2, + "from": "personal_info" + } + ] +} diff --git a/tests/presentation-submission/definition_2.json b/tests/presentation-submission/definition_2.json new file mode 100644 index 0000000..7cbbec8 --- /dev/null +++ b/tests/presentation-submission/definition_2.json @@ -0,0 +1,50 @@ +{ + "id": "complex_example", + "input_descriptors": [ + { + "id": "given_name", + "group": ["name", "basic_info"], + "schema": [{ "uri": "https://schema.org/givenName" }] + }, + { + "id": "family_name", + "group": ["name", "basic_info"], + "schema": [{ "uri": "https://schema.org/familyName" }] + }, + { + "id": "birth_date", + "group": ["basic_info"], + "schema": [{ "uri": "https://schema.org/birthDate" }] + }, + { + "id": "passport_number", + "group": ["id_document"], + "schema": [{ "uri": "https://schema.org/identifier" }] + }, + { + "id": "drivers_license", + "group": ["id_document"], + "schema": [{ "uri": "https://schema.org/DriversLicense" }] + } + ], + "submission_requirements": [ + { + "name": "Identity Verification", + "rule": "all", + "from_nested": [ + { + "name": "Basic Information", + "rule": "pick", + "count": 2, + "from": "basic_info" + }, + { + "name": "Identification Document", + "rule": "pick", + "count": 1, + "from": "id_document" + } + ] + } + ] +} diff --git a/tests/presentation-submission/definition_3.json b/tests/presentation-submission/definition_3.json new file mode 100644 index 0000000..b149cf7 --- /dev/null +++ b/tests/presentation-submission/definition_3.json @@ -0,0 +1,42 @@ +{ + "id": "multi_requirement_example", + "input_descriptors": [ + { + "id": "university_degree", + "group": ["education"], + "schema": [ + { "uri": "https://schema.org/EducationalOccupationalCredential" } + ] + }, + { + "id": "high_school_diploma", + "group": ["education"], + "schema": [ + { "uri": "https://schema.org/EducationalOccupationalCredential" } + ] + }, + { + "id": "work_experience", + "group": ["professional"], + "schema": [{ "uri": "https://schema.org/WorkExperience" }] + }, + { + "id": "professional_certification", + "group": ["professional", "education"], + "schema": [{ "uri": "https://schema.org/Certification" }] + } + ], + "submission_requirements": [ + { + "name": "Education Requirement", + "rule": "pick", + "count": 1, + "from": "education" + }, + { + "name": "Professional Requirement", + "rule": "all", + "from": "professional" + } + ] +} diff --git a/tests/presentation-submission/submission_1.json b/tests/presentation-submission/submission_1.json new file mode 100644 index 0000000..e839018 --- /dev/null +++ b/tests/presentation-submission/submission_1.json @@ -0,0 +1,16 @@ +{ + "id": "3469f095-d6a4-44f0-8e0f-851602724b1d", + "definition_id": "simple_example", + "descriptor_map": [ + { + "id": "name", + "format": "jwt_vc", + "path": "$.verifiableCredential[0]" + }, + { + "id": "email", + "format": "jwt_vc", + "path": "$.verifiableCredential[1]" + } + ] +} diff --git a/tests/presentation-submission/submission_2.json b/tests/presentation-submission/submission_2.json new file mode 100644 index 0000000..c20b39e --- /dev/null +++ b/tests/presentation-submission/submission_2.json @@ -0,0 +1,21 @@ +{ + "id": "59a8cf09-f9ab-4b9f-9632-d5889c417271", + "definition_id": "complex_example", + "descriptor_map": [ + { + "id": "given_name", + "format": "jwt_vc", + "path": "$.verifiableCredential[0]" + }, + { + "id": "birth_date", + "format": "jwt_vc", + "path": "$.verifiableCredential[1]" + }, + { + "id": "passport_number", + "format": "jwt_vc", + "path": "$.verifiableCredential[2]" + } + ] +} diff --git a/tests/presentation-submission/submission_3.json b/tests/presentation-submission/submission_3.json new file mode 100644 index 0000000..83d4e6b --- /dev/null +++ b/tests/presentation-submission/submission_3.json @@ -0,0 +1,21 @@ +{ + "id": "9445080f-5608-4a77-9984-784fcfdf9b4b", + "definition_id": "multi_requirement_example", + "descriptor_map": [ + { + "id": "university_degree", + "format": "jwt_vc", + "path": "$.verifiableCredential[0]" + }, + { + "id": "work_experience", + "format": "jwt_vc", + "path": "$.verifiableCredential[1]" + }, + { + "id": "professional_certification", + "format": "jwt_vc", + "path": "$.verifiableCredential[2]" + } + ] +} diff --git a/tests/presentation-submission/vp_1.json b/tests/presentation-submission/vp_1.json new file mode 100644 index 0000000..4118011 --- /dev/null +++ b/tests/presentation-submission/vp_1.json @@ -0,0 +1,26 @@ +{ + "@context": ["https://www.w3.org/2018/credentials/v1"], + "type": ["VerifiablePresentation"], + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org" + ], + "type": ["VerifiableCredential"], + "credentialSubject": { + "name": "Alice Johnson" + } + }, + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org" + ], + "type": ["VerifiableCredential"], + "credentialSubject": { + "email": "alice@example.com" + } + } + ] +} diff --git a/tests/presentation-submission/vp_2.json b/tests/presentation-submission/vp_2.json new file mode 100644 index 0000000..09f0e5c --- /dev/null +++ b/tests/presentation-submission/vp_2.json @@ -0,0 +1,36 @@ +{ + "@context": ["https://www.w3.org/2018/credentials/v1"], + "type": ["VerifiablePresentation"], + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org" + ], + "type": ["VerifiableCredential"], + "credentialSubject": { + "givenName": "Alice" + } + }, + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org" + ], + "type": ["VerifiableCredential"], + "credentialSubject": { + "birthDate": "1990-01-01" + } + }, + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org" + ], + "type": ["VerifiableCredential"], + "credentialSubject": { + "identifier": "P123456789" + } + } + ] +} diff --git a/tests/presentation-submission/vp_3.json b/tests/presentation-submission/vp_3.json new file mode 100644 index 0000000..9878e2b --- /dev/null +++ b/tests/presentation-submission/vp_3.json @@ -0,0 +1,42 @@ +{ + "@context": ["https://www.w3.org/2018/credentials/v1"], + "type": ["VerifiablePresentation"], + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org" + ], + "type": ["VerifiableCredential", "EducationalOccupationalCredential"], + "credentialSubject": { + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science in Computer Science" + } + } + }, + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org" + ], + "type": ["VerifiableCredential", "WorkExperience"], + "credentialSubject": { + "jobTitle": "Software Engineer", + "startDate": "2018-01-01", + "endDate": "2023-01-01" + } + }, + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://schema.org" + ], + "type": ["VerifiableCredential", "Certification"], + "credentialSubject": { + "certificationName": "Certified Information Systems Security Professional (CISSP)", + "issuanceDate": "2022-06-01" + } + } + ] +}