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" + } + } + ] +}