From 10611a8282870a1b2a3bd8cbb2344800ba08c9ba Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Thu, 8 Aug 2024 13:41:49 -0700 Subject: [PATCH 01/71] wip: use ssi 0.8.1, debugging e2e test using did resolver Signed-off-by: Ryan Tate --- Cargo.toml | 2 +- .../authorization_request/verification/did.rs | 31 ++++++++++++++----- .../authorization_request/verification/mod.rs | 7 +++-- .../verification/x509_san.rs | 2 +- src/presentation_exchange.rs | 14 +++++++-- src/verifier/client.rs | 9 +++--- tests/jwt_vc.rs | 30 +++++++++++------- 7 files changed, 65 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e1b1a51..3646549 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ serde_cbor = "0.11.2" serde_json = "1.0.107" serde_qs = "0.12.0" serde_urlencoded = "0.7.1" -ssi = "0.7.0" +ssi = "0.8.1" thiserror = "1.0.49" tokio = "1.32.0" tracing = "0.1.37" diff --git a/src/core/authorization_request/verification/did.rs b/src/core/authorization_request/verification/did.rs index 8b14607..9a0a5ff 100644 --- a/src/core/authorization_request/verification/did.rs +++ b/src/core/authorization_request/verification/did.rs @@ -6,17 +6,29 @@ 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::{ + dids::{DIDResolver, VerificationMethodDIDResolver}, + jwk::JWKResolver, + verification_methods::{ + GenericVerificationMethod, InvalidVerificationMethod, MaybeJwkVerificationMethod, + VerificationMethodSet, + }, +}; /// Default implementation of request validation for `client_id_scheme` `did`. -pub async fn verify_with_resolver( +pub async fn verify_with_resolver( wallet_metadata: &WalletMetadata, request_object: &AuthorizationRequestObject, request_jwt: String, trusted_dids: Option<&[String]>, - resolver: &dyn DIDResolver, -) -> Result<()> { - let (headers_b64, _, _) = ssi::jws::split_jws(&request_jwt)?; + resolver: &VerificationMethodDIDResolver, +) -> Result<()> +where + M: MaybeJwkVerificationMethod + + VerificationMethodSet + + TryFrom, +{ + let (headers_b64, _, _) = ssi::claims::jws::split_jws(&request_jwt)?; let headers_json_bytes = BASE64_URL_SAFE_NO_PAD .decode(headers_b64) @@ -25,6 +37,8 @@ pub async fn verify_with_resolver( let mut headers = serde_json::from_slice::>(&headers_json_bytes) .context("jwt headers were not valid json")?; + println!("{:?}", headers); + let Json::String(alg) = headers .remove("alg") .context("'alg' was missing from jwt headers")? @@ -64,11 +78,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 fetch JWK from 'kid' header")?; - 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..9986f2f 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/x509_san.rs b/src/core/authorization_request/verification/x509_san.rs index ffff434..52fe3ec 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/presentation_exchange.rs b/src/presentation_exchange.rs index df3c7cc..c8ec335 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -84,6 +84,16 @@ pub enum ConstraintsLimitDisclosure { Preferred, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum VerifiableFormat { + #[serde(rename = "jwt_vc_json")] + JwtVcJson, + #[serde(rename = "jwt_vp_json")] + JwtVpJson, + #[serde(rename = "ldp_vc")] + LdpVc, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct PresentationSubmission { pub id: String, @@ -94,9 +104,9 @@ pub struct PresentationSubmission { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct DescriptorMap { pub id: String, - pub format: String, // TODO should be enum of supported formats + pub format: VerifiableFormat, pub path: String, - //pub path_nested: Option>, + pub path_nested: Option>, } #[derive(Deserialize)] diff --git a/src/verifier/client.rs b/src/verifier/client.rs index 8e45186..e78fcca 100644 --- a/src/verifier/client.rs +++ b/src/verifier/client.rs @@ -4,7 +4,7 @@ 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, @@ -43,17 +43,18 @@ impl DIDClient { pub async fn new( vm: String, signer: Arc, - resolver: &dyn DIDResolver, + 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 key = resolver + .fetch_public_jwk(Some(&vm)) .await .context("unable to resolve key from verification method")?; - if &key != signer.jwk() { + if &*key != signer.jwk() { bail!( "verification method resolved from DID document did not match public key of signer" ) diff --git a/tests/jwt_vc.rs b/tests/jwt_vc.rs index 0c218b7..2bcbce9 100644 --- a/tests/jwt_vc.rs +++ b/tests/jwt_vc.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{str::FromStr, sync::Arc}; use anyhow::{Context, Result}; use async_trait::async_trait; @@ -22,7 +22,11 @@ use oid4vp::{ wallet::Wallet, }; use serde_json::json; -use ssi::did::DIDMethod; +use ssi::{ + dids::{VerificationMethodDIDResolver, DIDJWK}, + prelude::AnyMethod, + JWK, +}; pub async fn wallet_verifier() -> (JwtVcWallet, Arc) { let verifier_did = "did:key:zDnaeaDj3YpPR4JXos2kCCNPS86hdELeN5PZh97KGkoFzUtGn".to_owned(); @@ -36,14 +40,16 @@ pub async fn wallet_verifier() -> (JwtVcWallet, Arc) { ) .unwrap(), ); + + let public_key = p256::PublicKey::from_jwk_str(include_str!("examples/verifier.jwk")) + .expect("Failed to parse public key from jwk"); + + let resolver = JWK::from(public_key); + let client = Arc::new( - oid4vp::verifier::client::DIDClient::new( - verifier_did_vm.clone(), - signer.clone(), - DIDKey.to_resolver(), - ) - .await - .unwrap(), + oid4vp::verifier::client::DIDClient::new(verifier_did_vm.clone(), signer.clone(), resolver) + .await + .unwrap(), ); let verifier = Arc::new( Verifier::builder() @@ -125,12 +131,14 @@ impl RequestVerifier for JwtVcWallet { decoded_request: &AuthorizationRequestObject, request_jwt: String, ) -> Result<()> { - did::verify_with_resolver( + let resolver = VerificationMethodDIDResolver::new(DIDJWK); + + did::verify_with_resolver::( self.metadata(), decoded_request, request_jwt, Some(self.trusted_dids()), - DIDKey.to_resolver(), + &resolver, ) .await } From 7ea4c5004b7cfb2daac4e627e545d440f648f768 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Thu, 8 Aug 2024 14:28:01 -0700 Subject: [PATCH 02/71] wip: use VerificationMethodDIDResolver for DIDClient constructor Signed-off-by: Ryan Tate --- .../authorization_request/verification/did.rs | 2 -- src/verifier/client.rs | 22 ++++++++++++++----- tests/jwt_vc.rs | 15 +++++++------ 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/core/authorization_request/verification/did.rs b/src/core/authorization_request/verification/did.rs index 9a0a5ff..7148056 100644 --- a/src/core/authorization_request/verification/did.rs +++ b/src/core/authorization_request/verification/did.rs @@ -37,8 +37,6 @@ where let mut headers = serde_json::from_slice::>(&headers_json_bytes) .context("jwt headers were not valid json")?; - println!("{:?}", headers); - let Json::String(alg) = headers .remove("alg") .context("'alg' was missing from jwt headers")? diff --git a/src/verifier/client.rs b/src/verifier/client.rs index e78fcca..3799c70 100644 --- a/src/verifier/client.rs +++ b/src/verifier/client.rs @@ -1,10 +1,17 @@ -use std::{fmt::Debug, sync::Arc}; +use std::{fmt::Debug, str::FromStr, sync::Arc}; use anyhow::{bail, Context as _, Result}; use async_trait::async_trait; use base64::prelude::*; use serde_json::{json, Value as Json}; -use ssi::jwk::JWKResolver; +use ssi::{ + dids::{DIDBuf, DIDResolver, VerificationMethodDIDResolver, DID}, + jwk::JWKResolver, + verification_methods::{ + GenericVerificationMethod, InvalidVerificationMethod, MaybeJwkVerificationMethod, + VerificationMethodSet, + }, +}; use tracing::debug; use x509_cert::{ der::Encode, @@ -40,11 +47,16 @@ pub struct DIDClient { } impl DIDClient { - pub async fn new( + pub async fn new( vm: String, signer: Arc, - resolver: impl JWKResolver, - ) -> Result { + resolver: &VerificationMethodDIDResolver, + ) -> Result + where + M: MaybeJwkVerificationMethod + + VerificationMethodSet + + TryFrom, + { let (id, _f) = vm.rsplit_once('#').context(format!( "expected a DID verification method, received '{vm}'" ))?; diff --git a/tests/jwt_vc.rs b/tests/jwt_vc.rs index 2bcbce9..45673b8 100644 --- a/tests/jwt_vc.rs +++ b/tests/jwt_vc.rs @@ -41,15 +41,16 @@ pub async fn wallet_verifier() -> (JwtVcWallet, Arc) { .unwrap(), ); - let public_key = p256::PublicKey::from_jwk_str(include_str!("examples/verifier.jwk")) - .expect("Failed to parse public key from jwk"); - - let resolver = JWK::from(public_key); + let resolver = VerificationMethodDIDResolver::new(DIDJWK); let client = Arc::new( - oid4vp::verifier::client::DIDClient::new(verifier_did_vm.clone(), signer.clone(), resolver) - .await - .unwrap(), + oid4vp::verifier::client::DIDClient::new::( + verifier_did_vm.clone(), + signer.clone(), + &resolver, + ) + .await + .unwrap(), ); let verifier = Arc::new( Verifier::builder() From 7b3ced03bf99e236b1dc642c933aab76dc2b2591 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Thu, 8 Aug 2024 14:32:34 -0700 Subject: [PATCH 03/71] remove unused imports Signed-off-by: Ryan Tate --- src/verifier/client.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/verifier/client.rs b/src/verifier/client.rs index 3799c70..dc100f1 100644 --- a/src/verifier/client.rs +++ b/src/verifier/client.rs @@ -1,11 +1,11 @@ -use std::{fmt::Debug, str::FromStr, sync::Arc}; +use std::{fmt::Debug, sync::Arc}; use anyhow::{bail, Context as _, Result}; use async_trait::async_trait; use base64::prelude::*; use serde_json::{json, Value as Json}; use ssi::{ - dids::{DIDBuf, DIDResolver, VerificationMethodDIDResolver, DID}, + dids::{DIDResolver, VerificationMethodDIDResolver}, jwk::JWKResolver, verification_methods::{ GenericVerificationMethod, InvalidVerificationMethod, MaybeJwkVerificationMethod, From 4a773470ae33187aee246ce98a538adb35f25015 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Fri, 9 Aug 2024 09:56:06 -0700 Subject: [PATCH 04/71] wip: debugging did resolver jwk not found in e2e flow Signed-off-by: Ryan Tate --- Cargo.toml | 3 ++ .../authorization_request/verification/did.rs | 39 ++++++++++++++----- src/verifier/client.rs | 38 ++++++++++++------ tests/e2e.rs | 3 ++ tests/jwt_vc.rs | 23 +++++------ tests/jwt_vp.rs | 0 6 files changed, 71 insertions(+), 35 deletions(-) create mode 100644 tests/jwt_vp.rs diff --git a/Cargo.toml b/Cargo.toml index 3646549..5a05759 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,9 @@ serde_json = "1.0.107" serde_qs = "0.12.0" serde_urlencoded = "0.7.1" ssi = "0.8.1" +# ssi-claims = "0.1.0" +# ssi-dids-core = "0.1.0" +# ssi-jwk = "0.2.1" thiserror = "1.0.49" tokio = "1.32.0" tracing = "0.1.37" diff --git a/src/core/authorization_request/verification/did.rs b/src/core/authorization_request/verification/did.rs index 7148056..e8640a1 100644 --- a/src/core/authorization_request/verification/did.rs +++ b/src/core/authorization_request/verification/did.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use crate::core::{ authorization_request::AuthorizationRequestObject, metadata::{parameters::wallet::RequestObjectSigningAlgValuesSupported, WalletMetadata}, @@ -7,26 +9,28 @@ use anyhow::{bail, Context, Result}; use base64::prelude::*; use serde_json::{Map, Value as Json}; use ssi::{ - dids::{DIDResolver, VerificationMethodDIDResolver}, + dids::{DIDBuf, DIDResolver, VerificationMethodDIDResolver}, jwk::JWKResolver, verification_methods::{ GenericVerificationMethod, InvalidVerificationMethod, MaybeJwkVerificationMethod, VerificationMethodSet, }, + JWK, }; /// Default implementation of request validation for `client_id_scheme` `did`. -pub async fn verify_with_resolver( +pub async fn verify_with_resolver( wallet_metadata: &WalletMetadata, request_object: &AuthorizationRequestObject, request_jwt: String, trusted_dids: Option<&[String]>, - resolver: &VerificationMethodDIDResolver, + // resolver: &VerificationMethodDIDResolver, + resolver: impl DIDResolver, ) -> Result<()> -where - M: MaybeJwkVerificationMethod - + VerificationMethodSet - + TryFrom, +// where +// M: MaybeJwkVerificationMethod +// + VerificationMethodSet +// + TryFrom, { let (headers_b64, _, _) = ssi::claims::jws::split_jws(&request_jwt)?; @@ -76,10 +80,25 @@ where } } - let jwk = resolver - .fetch_public_jwk(Some(&kid)) + let did = DIDBuf::from_str(did)?; + + let resolution_output = resolver + .resolve(did.as_did()) + // .fetch_public_jwk(Some(&vm)) .await - .context("unable to fetch JWK from 'kid' header")?; + .context("unable to resolve key from verification method")?; + + let key = resolution_output + .document + .verification_method + .iter() + .find(|method| method.type_ == "JsonWebSignature2020") + .map(|method| method.properties.get("publicKeyJwk")) + .flatten() + .context("verification method not found in DID document")?; + + let jwk: JWK = serde_json::from_value(key.clone()) + .context("unable to parse JWK from verification method")?; let _: Json = ssi::claims::jwt::decode_verify(&request_jwt, &jwk) .context("request signature could not be verified")?; diff --git a/src/verifier/client.rs b/src/verifier/client.rs index dc100f1..4bc452a 100644 --- a/src/verifier/client.rs +++ b/src/verifier/client.rs @@ -1,16 +1,17 @@ -use std::{fmt::Debug, sync::Arc}; +use std::{fmt::Debug, str::FromStr, sync::Arc}; use anyhow::{bail, Context as _, Result}; use async_trait::async_trait; use base64::prelude::*; use serde_json::{json, Value as Json}; use ssi::{ - dids::{DIDResolver, VerificationMethodDIDResolver}, + dids::{DIDBuf, DIDResolver, VerificationMethodDIDResolver}, jwk::JWKResolver, verification_methods::{ GenericVerificationMethod, InvalidVerificationMethod, MaybeJwkVerificationMethod, VerificationMethodSet, }, + JWK, }; use tracing::debug; use x509_cert::{ @@ -47,26 +48,41 @@ pub struct DIDClient { } impl DIDClient { - pub async fn new( + pub async fn new( vm: String, signer: Arc, - resolver: &VerificationMethodDIDResolver, + resolver: impl DIDResolver, // resolver: &VerificationMethodDIDResolver, ) -> Result - where - M: MaybeJwkVerificationMethod - + VerificationMethodSet - + TryFrom, +// where + // M: MaybeJwkVerificationMethod + // + VerificationMethodSet + // + TryFrom, { let (id, _f) = vm.rsplit_once('#').context(format!( "expected a DID verification method, received '{vm}'" ))?; - let key = resolver - .fetch_public_jwk(Some(&vm)) + let did = DIDBuf::from_str(id)?; + + let resolution_output = resolver + .resolve(did.as_did()) + // .fetch_public_jwk(Some(&vm)) .await .context("unable to resolve key from verification method")?; - if &*key != signer.jwk() { + let key = resolution_output + .document + .verification_method + .iter() + .find(|method| method.type_ == "JsonWebSignature2020") + .map(|method| method.properties.get("publicKeyJwk")) + .flatten() + .context("verification method not found in DID document")?; + + let jwk: JWK = serde_json::from_value(key.clone()) + .context("unable to parse JWK from verification method")?; + + if &jwk != signer.jwk() { bail!( "verification method resolved from DID document did not match public key of signer" ) diff --git a/tests/e2e.rs b/tests/e2e.rs index a9f8b22..126c32e 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -11,6 +11,7 @@ use oid4vp::{ use serde_json::json; mod jwt_vc; +mod jwt_vp; #[tokio::test] async fn w3c_vc_did_client_direct_post() { @@ -46,6 +47,8 @@ async fn w3c_vc_did_client_direct_post() { .await .unwrap(); + println!("Request: {:?}", request); + let request = wallet.validate_request(request).await.unwrap(); assert_eq!( diff --git a/tests/jwt_vc.rs b/tests/jwt_vc.rs index 45673b8..98f68d2 100644 --- a/tests/jwt_vc.rs +++ b/tests/jwt_vc.rs @@ -2,7 +2,6 @@ use std::{str::FromStr, sync::Arc}; use anyhow::{Context, Result}; use async_trait::async_trait; -use did_method_key::DIDKey; use http::{Request, Response}; use oid4vp::{ core::{ @@ -23,7 +22,7 @@ use oid4vp::{ }; use serde_json::json; use ssi::{ - dids::{VerificationMethodDIDResolver, DIDJWK}, + dids::{DIDKey, DIDResolver, VerificationMethodDIDResolver, DIDJWK}, prelude::AnyMethod, JWK, }; @@ -41,16 +40,14 @@ pub async fn wallet_verifier() -> (JwtVcWallet, Arc) { .unwrap(), ); - let resolver = VerificationMethodDIDResolver::new(DIDJWK); + // let jwk = JWK::from_str(include_str!("examples/verifier.jwk")).unwrap(); + // let did_jwk = DIDJWK::generate(&jwk); + // let resolver = VerificationMethodDIDResolver::new(did_jwk); let client = Arc::new( - oid4vp::verifier::client::DIDClient::new::( - verifier_did_vm.clone(), - signer.clone(), - &resolver, - ) - .await - .unwrap(), + oid4vp::verifier::client::DIDClient::new(verifier_did_vm.clone(), signer.clone(), DIDKey) + .await + .unwrap(), ); let verifier = Arc::new( Verifier::builder() @@ -132,14 +129,12 @@ impl RequestVerifier for JwtVcWallet { decoded_request: &AuthorizationRequestObject, request_jwt: String, ) -> Result<()> { - let resolver = VerificationMethodDIDResolver::new(DIDJWK); - - did::verify_with_resolver::( + did::verify_with_resolver( self.metadata(), decoded_request, request_jwt, Some(self.trusted_dids()), - &resolver, + DIDKey, ) .await } diff --git a/tests/jwt_vp.rs b/tests/jwt_vp.rs new file mode 100644 index 0000000..e69de29 From 48e152e4d46d6788d68b863e82db274eaa426c7e Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Fri, 9 Aug 2024 13:32:46 -0700 Subject: [PATCH 05/71] feat/improve-presentation-exchange-support-in-oid4vp-rs add implementation methods for Presentation Definition. WIP: Need to continue work for Presentation Submission and the rest of the structs used in the presentation exchange flow. Signed-off-by: Ryan Tate Co-authored-by: Todd Showalter --- Cargo.toml | 3 +- src/core/response/mod.rs | 2 +- src/json_schema_validation.rs | 203 ++++++++++++++++++++++ src/lib.rs | 1 + src/presentation_exchange.rs | 318 ++++++++++++++++++++++++++++++---- tests/e2e.rs | 11 +- 6 files changed, 502 insertions(+), 36 deletions(-) create mode 100644 src/json_schema_validation.rs diff --git a/Cargo.toml b/Cargo.toml index e1b1a51..6381df7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ async-trait = "0.1.73" base64 = "0.21.4" did-web = "0.2.2" http = "1.1.0" +jsonpath_lib = "0.3.0" p256 = { version = "0.13.2", features = ["jwk"], optional = true } reqwest = { version = "0.12.5", features = ["rustls-tls"], optional = true } serde = "1.0.188" @@ -25,7 +26,7 @@ serde_cbor = "0.11.2" serde_json = "1.0.107" serde_qs = "0.12.0" serde_urlencoded = "0.7.1" -ssi = "0.7.0" +ssi = "0.7" thiserror = "1.0.49" tokio = "1.32.0" tracing = "0.1.37" diff --git a/src/core/response/mod.rs b/src/core/response/mod.rs index 30fff9a..adc564a 100644 --- a/src/core/response/mod.rs +++ b/src/core/response/mod.rs @@ -119,7 +119,7 @@ mod test { let response = UnencodedAuthorizationResponse::try_from(object).unwrap(); assert_eq!( response.into_x_www_form_urlencoded().unwrap(), - "presentation_submission=%7B%22definition_id%22%3A%22definition_id%22%2C%22descriptor_map%22%3A%5B%5D%2C%22id%22%3A%22id%22%7D&vp_token=string", + "presentation_submission=%7B%22id%22%3A%22id%22%2C%22definition_id%22%3A%22definition_id%22%2C%22descriptor_map%22%3A%5B%5D%7D&vp_token=string", ) } } diff --git a/src/json_schema_validation.rs b/src/json_schema_validation.rs new file mode 100644 index 0000000..4110c16 --- /dev/null +++ b/src/json_schema_validation.rs @@ -0,0 +1,203 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; + +// TODO: Consider using `Value` type from `serde_json` +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum SchemaType { + String, + Number, + Integer, + Boolean, + Array, + Object, +} + +/// Schema Validator is a JSON Schema descriptor used to evaluate the return value of a JsonPath +/// expression, used by the presentation definition constraints field to ensure the property value +/// meets the expected schema. +/// +/// For more information, see the field constraints filter property: +/// +/// https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct SchemaValidator { + #[serde(rename = "type")] + schema_type: SchemaType, + #[serde(skip_serializing_if = "Option::is_none")] + min_length: Option, + #[serde(skip_serializing_if = "Option::is_none")] + max_length: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pattern: Option, + #[serde(skip_serializing_if = "Option::is_none")] + minimum: Option, + #[serde(skip_serializing_if = "Option::is_none")] + maximum: Option, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + required: Vec, + #[serde(skip_serializing_if = "HashMap::is_empty", default)] + properties: HashMap>, + #[serde(skip_serializing_if = "Option::is_none")] + items: Option>, +} + +impl PartialEq for SchemaValidator { + fn eq(&self, other: &Self) -> bool { + self.schema_type == other.schema_type + && self.min_length == other.min_length + && self.max_length == other.max_length + && self.pattern == other.pattern + && self.minimum == other.minimum + && self.maximum == other.maximum + && self.required == other.required + && self.properties == other.properties + && self.items == other.items + } +} + +impl Eq for SchemaValidator {} + +impl SchemaValidator { + pub fn validate(&self, value: &Value) -> Result<(), String> { + match self.schema_type { + SchemaType::String => self.validate_string(value), + SchemaType::Number => self.validate_number(value), + SchemaType::Integer => self.validate_integer(value), + SchemaType::Boolean => self.validate_boolean(value), + SchemaType::Array => self.validate_array(value), + SchemaType::Object => self.validate_object(value), + } + } + + pub fn validate_string(&self, value: &Value) -> Result<(), String> { + let s = value.as_str().ok_or("Expected a string")?; + + if let Some(min_length) = self.min_length { + if s.len() < min_length { + return Err(format!( + "String length {} is less than minimum {}", + s.len(), + min_length + )); + } + } + + if let Some(max_length) = self.max_length { + if s.len() > max_length { + return Err(format!( + "String length {} is greater than maximum {}", + s.len(), + max_length + )); + } + } + + if let Some(pattern) = &self.pattern { + // Note: In a real implementation, you'd use a regex library here + if !s.contains(pattern) { + return Err(format!("String does not match pattern: {}", pattern)); + } + } + + Ok(()) + } + + pub fn validate_number(&self, value: &Value) -> Result<(), String> { + let n = value.as_f64().ok_or("Expected a number")?; + + if let Some(minimum) = self.minimum { + if n < minimum { + return Err(format!("Number {} is less than minimum {}", n, minimum)); + } + } + + if let Some(maximum) = self.maximum { + if n > maximum { + return Err(format!("Number {} is greater than maximum {}", n, maximum)); + } + } + + Ok(()) + } + + pub fn validate_integer(&self, value: &Value) -> Result<(), String> { + let n = value.as_i64().ok_or("Expected an integer")?; + + if let Some(minimum) = self.minimum { + if (n as f64) < minimum { + return Err(format!("Integer {} is less than minimum {}", n, minimum)); + } + } + + if let Some(maximum) = self.maximum { + if n as f64 > maximum { + return Err(format!("Integer {} is greater than maximum {}", n, maximum)); + } + } + + Ok(()) + } + + pub fn validate_boolean(&self, value: &Value) -> Result<(), String> { + if !value.is_boolean() { + return Err("Expected a boolean".to_string()); + } + Ok(()) + } + + pub fn validate_array(&self, value: &Value) -> Result<(), String> { + let arr = value.as_array().ok_or("Expected an array")?; + + if let Some(min_length) = self.min_length { + if arr.len() < min_length { + return Err(format!( + "Array length {} is less than minimum {}", + arr.len(), + min_length + )); + } + } + + if let Some(max_length) = self.max_length { + if arr.len() > max_length { + return Err(format!( + "Array length {} is greater than maximum {}", + arr.len(), + max_length + )); + } + } + + if let Some(item_validator) = &self.items { + for (index, item) in arr.iter().enumerate() { + item_validator + .validate(item) + .map_err(|e| format!("Error in array item {}: {}", index, e))?; + } + } + + Ok(()) + } + + pub fn validate_object(&self, value: &Value) -> Result<(), String> { + let obj = value.as_object().ok_or("Expected an object")?; + + for required_prop in &self.required { + if !obj.contains_key(required_prop) { + return Err(format!("Missing required property: {}", required_prop)); + } + } + + for (prop_name, prop_validator) in &self.properties { + if let Some(prop_value) = obj.get(prop_name) { + prop_validator + .validate(prop_value) + .map_err(|e| format!("Error in property {}: {}", prop_name, e))?; + } + } + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 551dda8..0279f15 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod core; +mod json_schema_validation; pub mod presentation_exchange; mod utils; pub mod verifier; diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index df3c7cc..967f53c 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -1,34 +1,189 @@ +use crate::json_schema_validation::SchemaValidator; pub use crate::utils::NonEmptyVec; + 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. +pub type JsonPath = String; + +/// The claim format 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 [spec](https://identity.foundation/presentation-exchange/spec/v2.0.0/#claim-format-designations) #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub enum ClaimFormat { + /// 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, + /// 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) + /// whcih 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, +} + +/// A presentation definition is a JSON object that describes the information a Verifier requires of a Holder. +/// +/// > Presentation Definitions are objects that articulate what proofs a Verifier requires. +/// These help the Verifier to decide how or whether to interact with a 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 Holders 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 +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct PresentationDefinition { - pub id: String, // Uuid, - pub input_descriptors: Vec, + id: uuid::Uuid, + input_descriptors: Vec, #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, + name: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub purpose: Option, + purpose: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub format: Option, // TODO + format: Option, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +impl PresentationDefinition { + pub fn new(id: uuid::Uuid, input_descriptor: InputDescriptor) -> Self { + Self { + id, + input_descriptors: vec![input_descriptor], + ..Default::default() + } + } + + /// 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 + } + + /// Set the name of the presentation definition. + pub fn set_name(mut self, name: String) -> Self { + self.name = Some(name); + self + } + + /// Set the purpose of the presentation definition. + pub fn set_purpose(mut self, purpose: String) -> Self { + self.purpose = Some(purpose); + self + } + + /// Attach a format to the presentation definition. + pub fn set_format(mut self, format: ClaimFormat) -> Self { + self.format = Some(format); + self + } +} + +/// Input Descriptors are objects used to describe the information a Verifier requires of a Holder. +/// All Input Descriptors MUST be satisfied, unless otherwise specified by a Feature. +/// +/// See: https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct InputDescriptor { - pub id: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, + id: String, + constraints: Constraints, #[serde(skip_serializing_if = "Option::is_none")] - pub purpose: Option, + name: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub format: Option, // TODO + purpose: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub constraints: Option, // TODO shouldn't be optional + format: Option, // TODO } -// TODO must have at least one -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +impl InputDescriptor { + /// Create a new instance of the input descriptor with the given id and constraints. + pub fn new(id: String, constraints: Constraints) -> Self { + Self { + id, + constraints, + ..Default::default() + } + } + + /// Set the name of the input descriptor. + pub fn set_name(mut self, name: String) -> Self { + self.name = Some(name); + self + } + + /// 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's data is being requested. + pub fn set_purpose(mut self, purpose: String) -> Self { + self.purpose = Some(purpose); + self + } + + /// 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: serde_json::Value) -> Self { + self.format = Some(format); + self + } +} + +/// Constraints are objects used to describe the constraints that a 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 +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct Constraints { #[serde(skip_serializing_if = "Option::is_none")] pub fields: Option>, @@ -36,17 +191,46 @@ pub struct Constraints { pub 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 + } + + /// Set the limit disclosure value. + pub fn set_limit_disclosure(mut self, limit_disclosure: ConstraintsLimitDisclosure) -> Self { + self.limit_disclosure = Some(limit_disclosure); + self + } +} + +/// ConstraintsField objects are used to describe the constraints that a Holder must satisfy to fulfill an Input Descriptor. +/// +/// For more information, see: https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct ConstraintsField { - pub path: NonEmptyVec, // TODO JsonPath validation at deserialization time + // 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` 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 + pub path: NonEmptyVec, #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub purpose: Option, #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, + // TODO: JSONSchema validation at deserialization time #[serde(skip_serializing_if = "Option::is_none")] - pub filter: Option, // TODO JSONSchema validation at deserialization time + pub filter: Option, #[serde(skip_serializing_if = "Option::is_none")] pub optional: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -55,26 +239,94 @@ pub struct ConstraintsField { 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 { - pub fn new( - path: NonEmptyVec, - id: Option, - purpose: Option, - name: Option, - filter: Option, - optional: Option, - intent_to_retain: Option, - ) -> 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, - id, - purpose, - name, - filter, - optional, - intent_to_retain, + 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 + } + + /// 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 + } + + /// 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 + } + + /// 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 + } + + /// 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: SchemaValidator) -> Self { + self.filter = Some(filter); + self + } + + /// 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 + pub fn set_optional(mut self, optional: bool) -> Self { + self.optional = Some(optional); + self + } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -151,7 +403,7 @@ pub(crate) mod tests { fn request_example() { let value = json!( { - "id": "vp token example", + "id": "36682080-c2ed-4ba6-a4cd-37c86ef2da8c", "input_descriptors": [ { "id": "id card credential", diff --git a/tests/e2e.rs b/tests/e2e.rs index a9f8b22..9a4b1e1 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -16,6 +16,14 @@ mod jwt_vc; async fn w3c_vc_did_client_direct_post() { let (wallet, verifier) = jwt_vc::wallet_verifier().await; + // let + + // let presentation_definition = PresentationDefinition::new(uuid::Uuid::new_v4()) + // .with_input_descriptors(input_descriptors) + // .with_name(name) + // .with_purpose(purpose) + // .with_format(format) + let presentation_definition: PresentationDefinition = serde_json::from_value(json!({ "id": "0b4dd017-efa6-4a05-a269-9790fa3c22c2", "input_descriptors": [ @@ -27,7 +35,8 @@ async fn w3c_vc_did_client_direct_post() { "JsonWebSignature2020" ] } - } + }, + "constraints": {} } ] })) From 26e62f1e28c1d4047028bf58d858c44344d672e7 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Fri, 9 Aug 2024 15:52:17 -0700 Subject: [PATCH 06/71] add getter methods for presentation definition member fields Signed-off-by: Ryan Tate --- src/presentation_exchange.rs | 156 ++++++++++++++++++++++++++++++++--- 1 file changed, 144 insertions(+), 12 deletions(-) diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 967f53c..473c309 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -7,13 +7,13 @@ use serde_json::Map; /// A JSONPath is a string that represents a path to a specific value within a JSON object. pub type JsonPath = String; -/// The claim format type is used in the input description object to specify the format of the claim. +/// 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 [spec](https://identity.foundation/presentation-exchange/spec/v2.0.0/#claim-format-designations) +/// 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)] -pub enum ClaimFormat { +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` @@ -21,8 +21,8 @@ pub enum ClaimFormat { /// 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 + /// 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 @@ -61,7 +61,7 @@ pub enum ClaimFormat { #[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) - /// whcih defines a mobile driving license (mDL) Credential in the mobile document (mdoc) format. + /// 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")] @@ -79,17 +79,23 @@ pub enum ClaimFormat { /// > For more information, see: https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct PresentationDefinition { - id: uuid::Uuid, + id: uuid::Uuid, // TODO: The specification allows for non-uuid types, should we revert to using String type? 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, + 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: uuid::Uuid, input_descriptor: InputDescriptor) -> Self { Self { id, @@ -104,27 +110,65 @@ impl PresentationDefinition { self } + /// Return the input descriptors of the presentation definition. + pub fn input_descriptors(&self) -> &Vec { + &self.input_descriptors + } + /// Set the name 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. 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. - pub fn set_format(mut self, format: ClaimFormat) -> Self { + /// + /// 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 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 Claim Format Designations section. + /// + /// See: https://identity.foundation/presentation-exchange/spec/v2.0.0/#presentation-definition + pub fn set_format(mut self, format: serde_json::Value) -> Self { self.format = Some(format); self } + + /// Return the format of the presentation definition. + pub fn format(&self) -> Option<&serde_json::Value> { + self.format.as_ref() + } } -/// Input Descriptors are objects used to describe the information a Verifier requires of a Holder. -/// All Input Descriptors MUST be satisfied, unless otherwise specified by a Feature. +/// 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 #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] @@ -141,6 +185,14 @@ pub struct InputDescriptor { 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 pub fn new(id: String, constraints: Constraints) -> Self { Self { id, @@ -149,33 +201,72 @@ impl InputDescriptor { } } + /// 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's data is being requested. + /// 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: serde_json::Value) -> 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<&serde_json::Value> { + self.format.as_ref() + } } /// Constraints are objects used to describe the constraints that a Holder must satisfy to fulfill an Input Descriptor. @@ -203,11 +294,21 @@ impl Constraints { self } + /// Returns the fields of the constraints object. + pub fn fields(&self) -> Option<&Vec> { + self.fields.as_ref() + } + /// Set the limit disclosure value. 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 must satisfy to fulfill an Input Descriptor. @@ -218,6 +319,7 @@ 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` 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 @@ -276,6 +378,11 @@ impl ConstraintsField { self } + /// Return the paths of the constraints field. + 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 @@ -286,6 +393,11 @@ impl ConstraintsField { 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. @@ -294,6 +406,11 @@ impl ConstraintsField { 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 @@ -305,6 +422,11 @@ impl ConstraintsField { 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 @@ -314,6 +436,11 @@ impl ConstraintsField { self } + /// Return the filter of the constraints field. + pub fn filter(&self) -> Option<&SchemaValidator> { + 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 @@ -327,6 +454,11 @@ impl ConstraintsField { 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)] From 8c47808726f01c818304d7908b892b7f71a04208 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Fri, 9 Aug 2024 16:45:48 -0700 Subject: [PATCH 07/71] fix broken links in documentation Signed-off-by: Ryan Tate --- src/presentation_exchange.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 473c309..c78ba71 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -68,15 +68,15 @@ pub enum ClaimFormatDesignation { MsoMDoc, } -/// A presentation definition is a JSON object that describes the information a Verifier requires of a Holder. +/// 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 requires. -/// These help the Verifier to decide how or whether to interact with a 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 Holders flexibility +/// 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 +/// 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: uuid::Uuid, // TODO: The specification allows for non-uuid types, should we revert to using String type? @@ -143,15 +143,15 @@ impl PresentationDefinition { /// 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 of the Claim format configurations the Verifier can process. + /// 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 supports for the format. + /// 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 + /// 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: serde_json::Value) -> Self { self.format = Some(format); self @@ -170,7 +170,7 @@ impl PresentationDefinition { /// 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 +/// 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, @@ -192,7 +192,7 @@ impl InputDescriptor { /// /// The Input Descriptor Object MUST contain a constraints property. /// - /// See: https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object + /// 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, @@ -269,11 +269,11 @@ impl InputDescriptor { } } -/// Constraints are objects used to describe the constraints that a Holder must satisfy to fulfill an Input Descriptor. +/// 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 +/// 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")] @@ -311,9 +311,9 @@ impl Constraints { } } -/// ConstraintsField objects are used to describe the constraints that a Holder must satisfy to fulfill an Input Descriptor. +/// 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 +/// 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; @@ -322,7 +322,7 @@ pub struct ConstraintsField { // /// `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 + /// 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 path: NonEmptyVec, #[serde(skip_serializing_if = "Option::is_none")] pub id: Option, @@ -449,7 +449,7 @@ impl ConstraintsField { /// 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 + /// 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 From de5db7d16d0dec747d45856925c2684a824bae6d Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Sat, 10 Aug 2024 10:15:01 -0700 Subject: [PATCH 08/71] update presentation submission implementation Signed-off-by: Ryan Tate --- src/presentation_exchange.rs | 187 ++++++++++++++++++++++++++++++----- 1 file changed, 164 insertions(+), 23 deletions(-) diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index c78ba71..7741e51 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -1,10 +1,13 @@ use crate::json_schema_validation::SchemaValidator; 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; /// The claim format designation type is used in the input description object to specify the format of the claim. @@ -72,6 +75,7 @@ pub enum ClaimFormatDesignation { /// /// > 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. @@ -104,6 +108,11 @@ impl PresentationDefinition { } } + /// Return the id of the presentation definition. + pub fn id(&self) -> &uuid::Uuid { + &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); @@ -115,6 +124,11 @@ impl PresentationDefinition { &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. pub fn set_name(mut self, name: String) -> Self { self.name = Some(name); @@ -173,7 +187,7 @@ impl PresentationDefinition { /// 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, + id: uuid::Uuid, constraints: Constraints, #[serde(skip_serializing_if = "Option::is_none")] name: Option, @@ -190,10 +204,11 @@ impl InputDescriptor { /// 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 { + pub fn new(id: uuid::Uuid, constraints: Constraints) -> Self { Self { id, constraints, @@ -201,8 +216,19 @@ impl InputDescriptor { } } + /// Create a new instance of an input descriptor with a random UUID. + /// + /// The Input Descriptor Object MUST contain a constraints property. + pub fn new_random(constraints: Constraints) -> Self { + Self { + id: uuid::Uuid::new_v4(), + constraints, + ..Default::default() + } + } + /// Return the id of the input descriptor. - pub fn id(&self) -> &String { + pub fn id(&self) -> &uuid::Uuid { &self.id } @@ -277,9 +303,9 @@ impl InputDescriptor { #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct Constraints { #[serde(skip_serializing_if = "Option::is_none")] - pub fields: Option>, + fields: Option>, #[serde(skip_serializing_if = "Option::is_none")] - pub limit_disclosure: Option, + limit_disclosure: Option, } impl Constraints { @@ -300,6 +326,16 @@ impl Constraints { } /// 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 @@ -319,24 +355,20 @@ 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` 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 path: NonEmptyVec, + path: NonEmptyVec, #[serde(skip_serializing_if = "Option::is_none")] - pub id: Option, + id: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub purpose: Option, + purpose: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, + name: Option, // TODO: JSONSchema validation at deserialization time #[serde(skip_serializing_if = "Option::is_none")] - pub filter: Option, + filter: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub optional: Option, + optional: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub intent_to_retain: Option, + intent_to_retain: Option, } pub type ConstraintsFields = Vec; @@ -379,6 +411,10 @@ impl ConstraintsField { } /// 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 } @@ -468,19 +504,124 @@ pub enum ConstraintsLimitDisclosure { 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 { - pub id: String, - pub definition_id: String, - pub descriptor_map: Vec, + 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 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 { - pub id: String, - pub format: String, // TODO should be enum of supported formats - pub path: String, - //pub path_nested: Option>, + id: uuid::Uuid, + 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: uuid::Uuid, format: ClaimFormatDesignation, path: JsonPath) -> Self { + Self { + id, + format, + path, + path_nested: None, + } + } + + /// Return the id of the descriptor map. + pub fn id(&self) -> &uuid::Uuid { + &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)] From 24845c7942c7c1a7f8edc96c84d4c7f1adbe379c Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Sat, 10 Aug 2024 10:19:42 -0700 Subject: [PATCH 09/71] fix test cases. todo: update test cases to use newly created interface for presentation exchange Signed-off-by: Ryan Tate --- src/core/response/mod.rs | 6 +++--- src/presentation_exchange.rs | 2 +- tests/e2e.rs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/response/mod.rs b/src/core/response/mod.rs index adc564a..b6752ee 100644 --- a/src/core/response/mod.rs +++ b/src/core/response/mod.rs @@ -108,8 +108,8 @@ mod test { let object: UntypedObject = serde_json::from_value(json!( { "presentation_submission": { - "id": "id", - "definition_id": "definition_id", + "id": "d05a7f51-ac09-43af-8864-e00f0175f2c7", + "definition_id": "f619e64a-8f80-4b71-8373-30cf07b1e4f2", "descriptor_map": [] }, "vp_token": "string" @@ -119,7 +119,7 @@ mod test { let response = UnencodedAuthorizationResponse::try_from(object).unwrap(); assert_eq!( response.into_x_www_form_urlencoded().unwrap(), - "presentation_submission=%7B%22id%22%3A%22id%22%2C%22definition_id%22%3A%22definition_id%22%2C%22descriptor_map%22%3A%5B%5D%7D&vp_token=string", + "presentation_submission=%7B%22id%22%3A%22d05a7f51-ac09-43af-8864-e00f0175f2c7%22%2C%22definition_id%22%3A%22f619e64a-8f80-4b71-8373-30cf07b1e4f2%22%2C%22descriptor_map%22%3A%5B%5D%7D&vp_token=string", ) } } diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 7741e51..660a4e8 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -679,7 +679,7 @@ pub(crate) mod tests { "id": "36682080-c2ed-4ba6-a4cd-37c86ef2da8c", "input_descriptors": [ { - "id": "id card credential", + "id": "d05a7f51-ac09-43af-8864-e00f0175f2c7", "format": { "ldp_vc": { "proof_type": [ diff --git a/tests/e2e.rs b/tests/e2e.rs index 9a4b1e1..1d53af2 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -28,7 +28,7 @@ async fn w3c_vc_did_client_direct_post() { "id": "0b4dd017-efa6-4a05-a269-9790fa3c22c2", "input_descriptors": [ { - "id": "vc", + "id": "064255a8-a0fa-4108-9ded-429f83003350", "format": { "jwt_vc_json": { "proof_type": [ @@ -77,9 +77,9 @@ async fn w3c_vc_did_client_direct_post() { "definition_id": "0b4dd017-efa6-4a05-a269-9790fa3c22c2", "descriptor_map": [ { - "id": "vc", + "id": "064255a8-a0fa-4108-9ded-429f83003350", "path": "$", - "format": "jwt_vc_json" + "format": "jwt_vp" } ] } From 5036a690d0ca218bca591fd758c3fc61da661f81 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Sat, 10 Aug 2024 10:23:39 -0700 Subject: [PATCH 10/71] update json schema validator to use anyhow result type Signed-off-by: Ryan Tate --- src/json_schema_validation.rs | 59 ++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/src/json_schema_validation.rs b/src/json_schema_validation.rs index 4110c16..ca5fa31 100644 --- a/src/json_schema_validation.rs +++ b/src/json_schema_validation.rs @@ -1,3 +1,4 @@ +use anyhow::{bail, Context, Result}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; @@ -60,7 +61,7 @@ impl PartialEq for SchemaValidator { impl Eq for SchemaValidator {} impl SchemaValidator { - pub fn validate(&self, value: &Value) -> Result<(), String> { + pub fn validate(&self, value: &Value) -> Result<()> { match self.schema_type { SchemaType::String => self.validate_string(value), SchemaType::Number => self.validate_number(value), @@ -71,102 +72,102 @@ impl SchemaValidator { } } - pub fn validate_string(&self, value: &Value) -> Result<(), String> { - let s = value.as_str().ok_or("Expected a string")?; + pub fn validate_string(&self, value: &Value) -> Result<()> { + let s = value.as_str().context("Expected a string")?; if let Some(min_length) = self.min_length { if s.len() < min_length { - return Err(format!( + bail!( "String length {} is less than minimum {}", s.len(), min_length - )); + ); } } if let Some(max_length) = self.max_length { if s.len() > max_length { - return Err(format!( + bail!( "String length {} is greater than maximum {}", s.len(), max_length - )); + ); } } if let Some(pattern) = &self.pattern { // Note: In a real implementation, you'd use a regex library here if !s.contains(pattern) { - return Err(format!("String does not match pattern: {}", pattern)); + bail!("String does not match pattern: {}", pattern); } } Ok(()) } - pub fn validate_number(&self, value: &Value) -> Result<(), String> { - let n = value.as_f64().ok_or("Expected a number")?; + pub fn validate_number(&self, value: &Value) -> Result<()> { + let n = value.as_f64().context("Expected a number")?; if let Some(minimum) = self.minimum { if n < minimum { - return Err(format!("Number {} is less than minimum {}", n, minimum)); + bail!("Number {} is less than minimum {}", n, minimum); } } if let Some(maximum) = self.maximum { if n > maximum { - return Err(format!("Number {} is greater than maximum {}", n, maximum)); + bail!("Number {} is greater than maximum {}", n, maximum); } } Ok(()) } - pub fn validate_integer(&self, value: &Value) -> Result<(), String> { - let n = value.as_i64().ok_or("Expected an integer")?; + pub fn validate_integer(&self, value: &Value) -> Result<()> { + let n = value.as_i64().context("Expected an integer")?; if let Some(minimum) = self.minimum { if (n as f64) < minimum { - return Err(format!("Integer {} is less than minimum {}", n, minimum)); + bail!("Integer {} is less than minimum {}", n, minimum); } } if let Some(maximum) = self.maximum { if n as f64 > maximum { - return Err(format!("Integer {} is greater than maximum {}", n, maximum)); + bail!("Integer {} is greater than maximum {}", n, maximum); } } Ok(()) } - pub fn validate_boolean(&self, value: &Value) -> Result<(), String> { + pub fn validate_boolean(&self, value: &Value) -> Result<()> { if !value.is_boolean() { - return Err("Expected a boolean".to_string()); + bail!("Expected a boolean".to_string()); } Ok(()) } - pub fn validate_array(&self, value: &Value) -> Result<(), String> { - let arr = value.as_array().ok_or("Expected an array")?; + pub fn validate_array(&self, value: &Value) -> Result<()> { + let arr = value.as_array().context("Expected an array")?; if let Some(min_length) = self.min_length { if arr.len() < min_length { - return Err(format!( + bail!( "Array length {} is less than minimum {}", arr.len(), min_length - )); + ); } } if let Some(max_length) = self.max_length { if arr.len() > max_length { - return Err(format!( + bail!( "Array length {} is greater than maximum {}", arr.len(), max_length - )); + ); } } @@ -174,19 +175,19 @@ impl SchemaValidator { for (index, item) in arr.iter().enumerate() { item_validator .validate(item) - .map_err(|e| format!("Error in array item {}: {}", index, e))?; + .context(format!("Error in array item {}", index))?; } } Ok(()) } - pub fn validate_object(&self, value: &Value) -> Result<(), String> { - let obj = value.as_object().ok_or("Expected an object")?; + pub fn validate_object(&self, value: &Value) -> Result<()> { + let obj = value.as_object().context("Expected an object")?; for required_prop in &self.required { if !obj.contains_key(required_prop) { - return Err(format!("Missing required property: {}", required_prop)); + bail!("Missing required property: {}", required_prop); } } @@ -194,7 +195,7 @@ impl SchemaValidator { if let Some(prop_value) = obj.get(prop_name) { prop_validator .validate(prop_value) - .map_err(|e| format!("Error in property {}: {}", prop_name, e))?; + .context(format!("Error in property {}", prop_name))?; } } From 6fe4d46c0e9cbd13d82fecff1fec1381fd421e7b Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 12 Aug 2024 08:23:46 -0700 Subject: [PATCH 11/71] wip: use latest implementation changes, update tests Signed-off-by: Ryan Tate --- src/presentation_exchange.rs | 106 +++++++++++++++++++++++++++++------ src/verifier/client.rs | 32 ++++++----- tests/e2e.rs | 82 ++++++++++++++++++--------- tests/jwt_vc.rs | 10 +++- tests/jwt_vp.rs | 0 5 files changed, 170 insertions(+), 60 deletions(-) delete mode 100644 tests/jwt_vp.rs diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 95b31f4..6bc78ea 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -10,6 +10,84 @@ use serde_json::Map; /// 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 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)] +#[serde(untagged)] +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), +} + +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, + } + } +} + /// 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 @@ -90,7 +168,7 @@ pub struct PresentationDefinition { #[serde(skip_serializing_if = "Option::is_none")] purpose: Option, #[serde(skip_serializing_if = "Option::is_none")] - format: Option, + format: Option, } impl PresentationDefinition { @@ -130,6 +208,9 @@ impl PresentationDefinition { } /// 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 @@ -141,6 +222,9 @@ impl PresentationDefinition { } /// 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 @@ -166,13 +250,13 @@ impl PresentationDefinition { /// 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: serde_json::Value) -> Self { + pub fn set_format(mut self, format: ClaimFormat) -> Self { self.format = Some(format); self } /// Return the format of the presentation definition. - pub fn format(&self) -> Option<&serde_json::Value> { + pub fn format(&self) -> Option<&ClaimFormat> { self.format.as_ref() } } @@ -194,7 +278,7 @@ pub struct InputDescriptor { #[serde(skip_serializing_if = "Option::is_none")] purpose: Option, #[serde(skip_serializing_if = "Option::is_none")] - format: Option, // TODO + format: Option, } impl InputDescriptor { @@ -277,7 +361,7 @@ impl InputDescriptor { /// /// 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: serde_json::Value) -> Self { + pub fn set_format(mut self, format: ClaimFormat) -> Self { self.format = Some(format); self } @@ -290,7 +374,7 @@ impl InputDescriptor { /// /// 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<&serde_json::Value> { + pub fn format(&self) -> Option<&ClaimFormat> { self.format.as_ref() } } @@ -514,16 +598,6 @@ pub enum ConstraintsLimitDisclosure { /// 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 enum VerifiableFormat { - #[serde(rename = "jwt_vc_json")] - JwtVcJson, - #[serde(rename = "jwt_vp_json")] - JwtVpJson, - #[serde(rename = "ldp_vc")] - LdpVc, -} - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct PresentationSubmission { id: uuid::Uuid, diff --git a/src/verifier/client.rs b/src/verifier/client.rs index 4bc452a..48b129d 100644 --- a/src/verifier/client.rs +++ b/src/verifier/client.rs @@ -5,7 +5,7 @@ use async_trait::async_trait; use base64::prelude::*; use serde_json::{json, Value as Json}; use ssi::{ - dids::{DIDBuf, DIDResolver, VerificationMethodDIDResolver}, + dids::{DIDBuf, DIDResolver, DIDURLBuf, VerificationMethodDIDResolver, DIDURL}, jwk::JWKResolver, verification_methods::{ GenericVerificationMethod, InvalidVerificationMethod, MaybeJwkVerificationMethod, @@ -62,25 +62,29 @@ impl DIDClient { "expected a DID verification method, received '{vm}'" ))?; - let did = DIDBuf::from_str(id)?; + // let did = DIDBuf::from_str(id)?; + let Ok(did_url_buf) = DIDURLBuf::from_string(vm.clone()) else { + bail!("expected a DID verification method, received '{vm}'") + }; let resolution_output = resolver - .resolve(did.as_did()) + .dereference(did_url_buf.as_did_url()) + // .resolve(did.as_did()) // .fetch_public_jwk(Some(&vm)) .await .context("unable to resolve key from verification method")?; - let key = resolution_output - .document - .verification_method - .iter() - .find(|method| method.type_ == "JsonWebSignature2020") - .map(|method| method.properties.get("publicKeyJwk")) - .flatten() - .context("verification method not found in DID document")?; - - let jwk: JWK = serde_json::from_value(key.clone()) - .context("unable to parse JWK from verification method")?; + // let key = resolution_output + // .document + // .verification_method + // .iter() + // .find(|method| method.type_ == "JsonWebSignature2020") + // .map(|method| method.properties.get("publicKeyJwk")) + // .flatten() + // .context("verification method not found in DID document")?; + + // let jwk: JWK = serde_json::from_value(key.clone()) + // .context("unable to parse JWK from verification method")?; if &jwk != signer.jwk() { bail!( diff --git a/tests/e2e.rs b/tests/e2e.rs index 7c76581..354561e 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -4,44 +4,72 @@ use oid4vp::{ object::UntypedObject, response::{parameters::VpToken, AuthorizationResponse, UnencodedAuthorizationResponse}, }, - presentation_exchange::{PresentationDefinition, PresentationSubmission}, + presentation_exchange::{ + ClaimFormat, Constraints, ConstraintsField, InputDescriptor, JsonPath, + PresentationDefinition, PresentationSubmission, + }, verifier::session::{Outcome, Status}, wallet::Wallet, }; use serde_json::json; +use ssi::{ + claims::data_integrity::suites::JsonWebSignature2020, + crypto::{algorithm::ES256, 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 - - // let presentation_definition = PresentationDefinition::new(uuid::Uuid::new_v4()) - // .with_input_descriptors(input_descriptors) - // .with_name(name) - // .with_purpose(purpose) - // .with_format(format) - - 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( + uuid::Uuid::new_v4(), + InputDescriptor::new( + uuid::Uuid::new_v4(), + Constraints::new().add_constraint( + ConstraintsField::new( + "$.vp.verifiableCredential[0].credentialSubject.postalCode".into(), + ) + .set_name("Check Postal Code".into()) + .set_purpose(String::from( + "Check whether you live within our school district.", + )), + ), + ) + .set_name(String::from("School District Proof")) + .set_purpose(String::from( + "We need to know if you live within our school district.", + )) + .set_format(ClaimFormat::JwtVp { + alg: vec![Algorithm::ES256.to_string()], + }), + ); + + // Save the presentation definition to a `presentation-exchange/test/presentation-definition/postal-code.json` file. + std::fs::write( + "presentation-exchange/test/presentation-definition/postal-code.json", + serde_json::to_string_pretty(&presentation_definition).unwrap(), + ) + .expect("Unable to write file"); + + // 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 client_metadata = UntypedObject::default(); diff --git a/tests/jwt_vc.rs b/tests/jwt_vc.rs index 98f68d2..6705281 100644 --- a/tests/jwt_vc.rs +++ b/tests/jwt_vc.rs @@ -45,9 +45,13 @@ pub async fn wallet_verifier() -> (JwtVcWallet, Arc) { // let resolver = VerificationMethodDIDResolver::new(did_jwk); let client = Arc::new( - oid4vp::verifier::client::DIDClient::new(verifier_did_vm.clone(), signer.clone(), DIDKey) - .await - .unwrap(), + oid4vp::verifier::client::DIDClient::new( + verifier_did_vm.clone(), + signer.clone(), + DIDJWK.into_vm_resolver(), + ) + .await + .unwrap(), ); let verifier = Arc::new( Verifier::builder() diff --git a/tests/jwt_vp.rs b/tests/jwt_vp.rs deleted file mode 100644 index e69de29..0000000 From 6d29442da6db672c3b7f3360223e60341fff0e70 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 12 Aug 2024 08:36:35 -0700 Subject: [PATCH 12/71] fix verification method did resolver tests Signed-off-by: Ryan Tate --- .../authorization_request/verification/did.rs | 32 ++----- src/verifier/client.rs | 39 +++----- tests/e2e.rs | 90 +++++++++---------- tests/jwt_vc.rs | 14 +-- 4 files changed, 72 insertions(+), 103 deletions(-) diff --git a/src/core/authorization_request/verification/did.rs b/src/core/authorization_request/verification/did.rs index e8640a1..e28abe6 100644 --- a/src/core/authorization_request/verification/did.rs +++ b/src/core/authorization_request/verification/did.rs @@ -24,14 +24,13 @@ pub async fn verify_with_resolver( request_object: &AuthorizationRequestObject, request_jwt: String, trusted_dids: Option<&[String]>, - // resolver: &VerificationMethodDIDResolver, - resolver: impl DIDResolver, -) -> Result<()> -// where -// M: MaybeJwkVerificationMethod -// + VerificationMethodSet -// + TryFrom, -{ + resolver: &VerificationMethodDIDResolver< + impl DIDResolver, + impl MaybeJwkVerificationMethod + + VerificationMethodSet + + TryFrom, + >, +) -> Result<()> { let (headers_b64, _, _) = ssi::claims::jws::split_jws(&request_jwt)?; let headers_json_bytes = BASE64_URL_SAFE_NO_PAD @@ -82,24 +81,11 @@ pub async fn verify_with_resolver( let did = DIDBuf::from_str(did)?; - let resolution_output = resolver - .resolve(did.as_did()) - // .fetch_public_jwk(Some(&vm)) + let jwk = resolver + .fetch_public_jwk(Some(&kid)) .await .context("unable to resolve key from verification method")?; - let key = resolution_output - .document - .verification_method - .iter() - .find(|method| method.type_ == "JsonWebSignature2020") - .map(|method| method.properties.get("publicKeyJwk")) - .flatten() - .context("verification method not found in DID document")?; - - let jwk: JWK = serde_json::from_value(key.clone()) - .context("unable to parse JWK from verification method")?; - let _: Json = ssi::claims::jwt::decode_verify(&request_jwt, &jwk) .context("request signature could not be verified")?; diff --git a/src/verifier/client.rs b/src/verifier/client.rs index 48b129d..7cd18ec 100644 --- a/src/verifier/client.rs +++ b/src/verifier/client.rs @@ -51,42 +51,23 @@ impl DIDClient { pub async fn new( vm: String, signer: Arc, - resolver: impl DIDResolver, // resolver: &VerificationMethodDIDResolver, - ) -> Result -// where - // M: MaybeJwkVerificationMethod - // + VerificationMethodSet - // + TryFrom, - { + resolver: &VerificationMethodDIDResolver< + impl DIDResolver, + impl MaybeJwkVerificationMethod + + VerificationMethodSet + + TryFrom, + >, + ) -> Result { let (id, _f) = vm.rsplit_once('#').context(format!( "expected a DID verification method, received '{vm}'" ))?; - // let did = DIDBuf::from_str(id)?; - let Ok(did_url_buf) = DIDURLBuf::from_string(vm.clone()) else { - bail!("expected a DID verification method, received '{vm}'") - }; - - let resolution_output = resolver - .dereference(did_url_buf.as_did_url()) - // .resolve(did.as_did()) - // .fetch_public_jwk(Some(&vm)) + let jwk = resolver + .fetch_public_jwk(Some(&vm)) .await .context("unable to resolve key from verification method")?; - // let key = resolution_output - // .document - // .verification_method - // .iter() - // .find(|method| method.type_ == "JsonWebSignature2020") - // .map(|method| method.properties.get("publicKeyJwk")) - // .flatten() - // .context("verification method not found in DID document")?; - - // let jwk: JWK = serde_json::from_value(key.clone()) - // .context("unable to parse JWK from verification method")?; - - if &jwk != signer.jwk() { + if &*jwk != signer.jwk() { bail!( "verification method resolved from DID document did not match public key of signer" ) diff --git a/tests/e2e.rs b/tests/e2e.rs index 354561e..c1a35ff 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -23,53 +23,53 @@ mod jwt_vc; async fn w3c_vc_did_client_direct_post() { let (wallet, verifier) = jwt_vc::wallet_verifier().await; - let presentation_definition = PresentationDefinition::new( - uuid::Uuid::new_v4(), - InputDescriptor::new( - uuid::Uuid::new_v4(), - Constraints::new().add_constraint( - ConstraintsField::new( - "$.vp.verifiableCredential[0].credentialSubject.postalCode".into(), - ) - .set_name("Check Postal Code".into()) - .set_purpose(String::from( - "Check whether you live within our school district.", - )), - ), - ) - .set_name(String::from("School District Proof")) - .set_purpose(String::from( - "We need to know if you live within our school district.", - )) - .set_format(ClaimFormat::JwtVp { - alg: vec![Algorithm::ES256.to_string()], - }), - ); + // let presentation_definition = PresentationDefinition::new( + // uuid::Uuid::new_v4(), + // InputDescriptor::new( + // uuid::Uuid::new_v4(), + // Constraints::new().add_constraint( + // ConstraintsField::new( + // "$.vp.verifiableCredential[0].credentialSubject.postalCode".into(), + // ) + // .set_name("Check Postal Code".into()) + // .set_purpose(String::from( + // "Check whether you live within our school district.", + // )), + // ), + // ) + // .set_name(String::from("School District Proof")) + // .set_purpose(String::from( + // "We need to know if you live within our school district.", + // )) + // .set_format(ClaimFormat::JwtVp { + // alg: vec![Algorithm::ES256.to_string()], + // }), + // ); // Save the presentation definition to a `presentation-exchange/test/presentation-definition/postal-code.json` file. - std::fs::write( - "presentation-exchange/test/presentation-definition/postal-code.json", - serde_json::to_string_pretty(&presentation_definition).unwrap(), - ) - .expect("Unable to write file"); - - // 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(); + // std::fs::write( + // "presentation-exchange/test/presentation-definition/postal-code.json", + // serde_json::to_string_pretty(&presentation_definition).unwrap(), + // ) + // .expect("Unable to write file"); + + 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 client_metadata = UntypedObject::default(); diff --git a/tests/jwt_vc.rs b/tests/jwt_vc.rs index 6705281..979ac5f 100644 --- a/tests/jwt_vc.rs +++ b/tests/jwt_vc.rs @@ -23,7 +23,7 @@ use oid4vp::{ use serde_json::json; use ssi::{ dids::{DIDKey, DIDResolver, VerificationMethodDIDResolver, DIDJWK}, - prelude::AnyMethod, + prelude::{AnyJwkMethod, AnyMethod}, JWK, }; @@ -40,15 +40,14 @@ pub async fn wallet_verifier() -> (JwtVcWallet, Arc) { .unwrap(), ); - // let jwk = JWK::from_str(include_str!("examples/verifier.jwk")).unwrap(); - // let did_jwk = DIDJWK::generate(&jwk); - // let resolver = VerificationMethodDIDResolver::new(did_jwk); + let resolver: VerificationMethodDIDResolver = + VerificationMethodDIDResolver::new(DIDKey); let client = Arc::new( oid4vp::verifier::client::DIDClient::new( verifier_did_vm.clone(), signer.clone(), - DIDJWK.into_vm_resolver(), + &resolver, ) .await .unwrap(), @@ -133,12 +132,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, + &resolver, ) .await } From e885a4c07d22dc625aaf159fa50ea79f22c7886d Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 12 Aug 2024 08:40:08 -0700 Subject: [PATCH 13/71] wip: remove unused imports Signed-off-by: Ryan Tate --- src/core/authorization_request/verification/did.rs | 7 +------ src/verifier/client.rs | 5 ++--- tests/e2e.rs | 9 +-------- tests/jwt_vc.rs | 7 +++---- 4 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/core/authorization_request/verification/did.rs b/src/core/authorization_request/verification/did.rs index e28abe6..f31eb9c 100644 --- a/src/core/authorization_request/verification/did.rs +++ b/src/core/authorization_request/verification/did.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use crate::core::{ authorization_request::AuthorizationRequestObject, metadata::{parameters::wallet::RequestObjectSigningAlgValuesSupported, WalletMetadata}, @@ -9,13 +7,12 @@ use anyhow::{bail, Context, Result}; use base64::prelude::*; use serde_json::{Map, Value as Json}; use ssi::{ - dids::{DIDBuf, DIDResolver, VerificationMethodDIDResolver}, + dids::{DIDResolver, VerificationMethodDIDResolver}, jwk::JWKResolver, verification_methods::{ GenericVerificationMethod, InvalidVerificationMethod, MaybeJwkVerificationMethod, VerificationMethodSet, }, - JWK, }; /// Default implementation of request validation for `client_id_scheme` `did`. @@ -79,8 +76,6 @@ pub async fn verify_with_resolver( } } - let did = DIDBuf::from_str(did)?; - let jwk = resolver .fetch_public_jwk(Some(&kid)) .await diff --git a/src/verifier/client.rs b/src/verifier/client.rs index 7cd18ec..e08704e 100644 --- a/src/verifier/client.rs +++ b/src/verifier/client.rs @@ -1,17 +1,16 @@ -use std::{fmt::Debug, str::FromStr, sync::Arc}; +use std::{fmt::Debug, sync::Arc}; use anyhow::{bail, Context as _, Result}; use async_trait::async_trait; use base64::prelude::*; use serde_json::{json, Value as Json}; use ssi::{ - dids::{DIDBuf, DIDResolver, DIDURLBuf, VerificationMethodDIDResolver, DIDURL}, + dids::{DIDResolver, VerificationMethodDIDResolver}, jwk::JWKResolver, verification_methods::{ GenericVerificationMethod, InvalidVerificationMethod, MaybeJwkVerificationMethod, VerificationMethodSet, }, - JWK, }; use tracing::debug; use x509_cert::{ diff --git a/tests/e2e.rs b/tests/e2e.rs index c1a35ff..0902f13 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -4,18 +4,11 @@ use oid4vp::{ object::UntypedObject, response::{parameters::VpToken, AuthorizationResponse, UnencodedAuthorizationResponse}, }, - presentation_exchange::{ - ClaimFormat, Constraints, ConstraintsField, InputDescriptor, JsonPath, - PresentationDefinition, PresentationSubmission, - }, + presentation_exchange::{PresentationDefinition, PresentationSubmission}, verifier::session::{Outcome, Status}, wallet::Wallet, }; use serde_json::json; -use ssi::{ - claims::data_integrity::suites::JsonWebSignature2020, - crypto::{algorithm::ES256, Algorithm}, -}; mod jwt_vc; diff --git a/tests/jwt_vc.rs b/tests/jwt_vc.rs index 979ac5f..ab1b3c1 100644 --- a/tests/jwt_vc.rs +++ b/tests/jwt_vc.rs @@ -1,4 +1,4 @@ -use std::{str::FromStr, sync::Arc}; +use std::sync::Arc; use anyhow::{Context, Result}; use async_trait::async_trait; @@ -22,9 +22,8 @@ use oid4vp::{ }; use serde_json::json; use ssi::{ - dids::{DIDKey, DIDResolver, VerificationMethodDIDResolver, DIDJWK}, - prelude::{AnyJwkMethod, AnyMethod}, - JWK, + dids::{DIDKey, VerificationMethodDIDResolver}, + prelude::AnyJwkMethod, }; pub async fn wallet_verifier() -> (JwtVcWallet, Arc) { From 399694685d1f22ac8794dc62815290e28c896e5d Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 12 Aug 2024 09:05:00 -0700 Subject: [PATCH 14/71] add ClaimFormat type Signed-off-by: Ryan Tate --- src/presentation_exchange.rs | 96 +++++++++++++++++++++++++++++++++--- 1 file changed, 90 insertions(+), 6 deletions(-) diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 660a4e8..6bc78ea 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -10,6 +10,84 @@ use serde_json::Map; /// 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 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)] +#[serde(untagged)] +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), +} + +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, + } + } +} + /// 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 @@ -90,7 +168,7 @@ pub struct PresentationDefinition { #[serde(skip_serializing_if = "Option::is_none")] purpose: Option, #[serde(skip_serializing_if = "Option::is_none")] - format: Option, + format: Option, } impl PresentationDefinition { @@ -130,6 +208,9 @@ impl PresentationDefinition { } /// 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 @@ -141,6 +222,9 @@ impl PresentationDefinition { } /// 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 @@ -166,13 +250,13 @@ impl PresentationDefinition { /// 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: serde_json::Value) -> Self { + pub fn set_format(mut self, format: ClaimFormat) -> Self { self.format = Some(format); self } /// Return the format of the presentation definition. - pub fn format(&self) -> Option<&serde_json::Value> { + pub fn format(&self) -> Option<&ClaimFormat> { self.format.as_ref() } } @@ -194,7 +278,7 @@ pub struct InputDescriptor { #[serde(skip_serializing_if = "Option::is_none")] purpose: Option, #[serde(skip_serializing_if = "Option::is_none")] - format: Option, // TODO + format: Option, } impl InputDescriptor { @@ -277,7 +361,7 @@ impl InputDescriptor { /// /// 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: serde_json::Value) -> Self { + pub fn set_format(mut self, format: ClaimFormat) -> Self { self.format = Some(format); self } @@ -290,7 +374,7 @@ impl InputDescriptor { /// /// 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<&serde_json::Value> { + pub fn format(&self) -> Option<&ClaimFormat> { self.format.as_ref() } } From 4e841cc28b44759c9d64d505df7b46ba113fe53d Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 12 Aug 2024 09:14:24 -0700 Subject: [PATCH 15/71] add regex support for string pattern matching Signed-off-by: Ryan Tate --- Cargo.toml | 1 + src/json_schema_validation.rs | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6381df7..1769333 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ did-web = "0.2.2" http = "1.1.0" jsonpath_lib = "0.3.0" p256 = { version = "0.13.2", features = ["jwk"], optional = true } +regex = "1.10.6" reqwest = { version = "0.12.5", features = ["rustls-tls"], optional = true } serde = "1.0.188" serde_cbor = "0.11.2" diff --git a/src/json_schema_validation.rs b/src/json_schema_validation.rs index ca5fa31..e01e81b 100644 --- a/src/json_schema_validation.rs +++ b/src/json_schema_validation.rs @@ -1,9 +1,9 @@ use anyhow::{bail, Context, Result}; +use regex::Regex; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; -// TODO: Consider using `Value` type from `serde_json` #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum SchemaType { @@ -96,8 +96,9 @@ impl SchemaValidator { } if let Some(pattern) = &self.pattern { - // Note: In a real implementation, you'd use a regex library here - if !s.contains(pattern) { + let regex_pattern = Regex::new(pattern).context("Invalid regex pattern")?; + + if regex_pattern.is_match(pattern) { bail!("String does not match pattern: {}", pattern); } } From edcc8a2e5182b2af844dfe30dff4488df18bdada Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 12 Aug 2024 09:20:13 -0700 Subject: [PATCH 16/71] revert uuid presentation definition id type to string Signed-off-by: Ryan Tate --- src/presentation_exchange.rs | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 6bc78ea..c1c146a 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -161,7 +161,7 @@ pub enum ClaimFormatDesignation { /// 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: uuid::Uuid, // TODO: The specification allows for non-uuid types, should we revert to using String type? + id: String, input_descriptors: Vec, #[serde(skip_serializing_if = "Option::is_none")] name: Option, @@ -178,7 +178,7 @@ impl PresentationDefinition { /// 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: uuid::Uuid, input_descriptor: InputDescriptor) -> Self { + pub fn new(id: String, input_descriptor: InputDescriptor) -> Self { Self { id, input_descriptors: vec![input_descriptor], @@ -187,7 +187,7 @@ impl PresentationDefinition { } /// Return the id of the presentation definition. - pub fn id(&self) -> &uuid::Uuid { + pub fn id(&self) -> &String { &self.id } @@ -271,7 +271,7 @@ impl PresentationDefinition { /// 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: uuid::Uuid, + id: String, constraints: Constraints, #[serde(skip_serializing_if = "Option::is_none")] name: Option, @@ -292,7 +292,7 @@ impl InputDescriptor { /// 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: uuid::Uuid, constraints: Constraints) -> Self { + pub fn new(id: String, constraints: Constraints) -> Self { Self { id, constraints, @@ -300,19 +300,8 @@ impl InputDescriptor { } } - /// Create a new instance of an input descriptor with a random UUID. - /// - /// The Input Descriptor Object MUST contain a constraints property. - pub fn new_random(constraints: Constraints) -> Self { - Self { - id: uuid::Uuid::new_v4(), - constraints, - ..Default::default() - } - } - /// Return the id of the input descriptor. - pub fn id(&self) -> &uuid::Uuid { + pub fn id(&self) -> &String { &self.id } @@ -606,7 +595,7 @@ pub struct PresentationSubmission { } impl PresentationSubmission { - /// The presentation submission MUST contain an id property. The value of this property MUST be a UUID. + /// 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( From 23e589a93d77c706645f7bd71b0e04ecd73eea56 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 12 Aug 2024 09:22:01 -0700 Subject: [PATCH 17/71] fix: ensure negation of regex pattern match for error Signed-off-by: Ryan Tate --- src/json_schema_validation.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/json_schema_validation.rs b/src/json_schema_validation.rs index e01e81b..298474f 100644 --- a/src/json_schema_validation.rs +++ b/src/json_schema_validation.rs @@ -98,7 +98,7 @@ impl SchemaValidator { if let Some(pattern) = &self.pattern { let regex_pattern = Regex::new(pattern).context("Invalid regex pattern")?; - if regex_pattern.is_match(pattern) { + if !regex_pattern.is_match(pattern) { bail!("String does not match pattern: {}", pattern); } } From 15279335986a7e174a36dcda4813cc17417294c4 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 12 Aug 2024 09:35:20 -0700 Subject: [PATCH 18/71] add 'other' variante to claim format type Signed-off-by: Ryan Tate --- src/presentation_exchange.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index c1c146a..9361260 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -67,6 +67,11 @@ pub enum ClaimFormat { }, #[serde(rename = "mso_mdoc")] MsoMDoc(serde_json::Value), + #[serde(untagged)] + Other { + name: String, + value: serde_json::Value, + }, } impl ClaimFormat { @@ -84,6 +89,7 @@ impl ClaimFormat { ClaimFormat::AcVc { .. } => ClaimFormatDesignation::AcVc, ClaimFormat::AcVp { .. } => ClaimFormatDesignation::AcVp, ClaimFormat::MsoMDoc(_) => ClaimFormatDesignation::MsoMDoc, + ClaimFormat::Other { name, .. } => ClaimFormatDesignation::Other(name.to_owned()), } } } @@ -147,6 +153,11 @@ pub enum ClaimFormatDesignation { /// 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), } /// 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). From 8ed882879c98a5f4efb8fa8539f527ac28dcc65d Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 12 Aug 2024 09:37:30 -0700 Subject: [PATCH 19/71] remove commented out code Signed-off-by: Ryan Tate --- tests/e2e.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/tests/e2e.rs b/tests/e2e.rs index 1d53af2..af991a6 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -16,14 +16,6 @@ mod jwt_vc; async fn w3c_vc_did_client_direct_post() { let (wallet, verifier) = jwt_vc::wallet_verifier().await; - // let - - // let presentation_definition = PresentationDefinition::new(uuid::Uuid::new_v4()) - // .with_input_descriptors(input_descriptors) - // .with_name(name) - // .with_purpose(purpose) - // .with_format(format) - let presentation_definition: PresentationDefinition = serde_json::from_value(json!({ "id": "0b4dd017-efa6-4a05-a269-9790fa3c22c2", "input_descriptors": [ From dcdde5cd0247866dc2e521bbe9291ff9f3dd3e6b Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 12 Aug 2024 10:04:43 -0700 Subject: [PATCH 20/71] use ssi sub-crates instead of main ssi dependency Signed-off-by: Ryan Tate --- Cargo.toml | 9 +++++---- .../authorization_request/verification/did.rs | 17 ++++++++--------- .../authorization_request/verification/mod.rs | 2 +- .../verification/x509_san.rs | 2 +- src/verifier/client.rs | 12 +++++------- src/verifier/request_signer.rs | 2 +- tests/jwt_vc.rs | 6 ++---- 7 files changed, 23 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5d4e6d2..cd9ee35 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ p256 = ["dep:p256"] anyhow = "1.0.75" async-trait = "0.1.73" base64 = "0.21.4" +did-method-key = "0.2" did-web = "0.2.2" http = "1.1.0" jsonpath_lib = "0.3.0" @@ -27,10 +28,10 @@ serde_cbor = "0.11.2" serde_json = "1.0.107" serde_qs = "0.12.0" serde_urlencoded = "0.7.1" -ssi = "0.8.1" -# ssi-claims = "0.1.0" -# ssi-dids-core = "0.1.0" -# ssi-jwk = "0.2.1" +ssi-claims = "0.1.0" +ssi-dids = "0.2.0" +ssi-jwk = "0.2.1" +ssi-verification-methods = "0.1.1" thiserror = "1.0.49" tokio = "1.32.0" tracing = "0.1.37" diff --git a/src/core/authorization_request/verification/did.rs b/src/core/authorization_request/verification/did.rs index f31eb9c..ff0b7ac 100644 --- a/src/core/authorization_request/verification/did.rs +++ b/src/core/authorization_request/verification/did.rs @@ -6,13 +6,12 @@ use crate::core::{ use anyhow::{bail, Context, Result}; use base64::prelude::*; use serde_json::{Map, Value as Json}; -use ssi::{ - dids::{DIDResolver, VerificationMethodDIDResolver}, - jwk::JWKResolver, - verification_methods::{ - GenericVerificationMethod, InvalidVerificationMethod, MaybeJwkVerificationMethod, - VerificationMethodSet, - }, + +use ssi_dids::{DIDResolver, VerificationMethodDIDResolver}; +use ssi_jwk::JWKResolver; +use ssi_verification_methods::{ + GenericVerificationMethod, InvalidVerificationMethod, MaybeJwkVerificationMethod, + VerificationMethodSet, }; /// Default implementation of request validation for `client_id_scheme` `did`. @@ -28,7 +27,7 @@ pub async fn verify_with_resolver( + TryFrom, >, ) -> Result<()> { - let (headers_b64, _, _) = ssi::claims::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) @@ -81,7 +80,7 @@ pub async fn verify_with_resolver( .await .context("unable to resolve key from verification method")?; - let _: Json = ssi::claims::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 9986f2f..8773b25 100644 --- a/src/core/authorization_request/verification/mod.rs +++ b/src/core/authorization_request/verification/mod.rs @@ -106,7 +106,7 @@ pub(crate) async fn verify_request( jwt: String, ) -> Result { let request: AuthorizationRequestObject = - ssi::claims::jwt::decode_unverified::(&jwt) + ssi_claims::jwt::decode_unverified::(&jwt) .context("unable to decode Authorization Request Object JWT")? .try_into()?; diff --git a/src/core/authorization_request/verification/x509_san.rs b/src/core/authorization_request/verification/x509_san.rs index 52fe3ec..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::claims::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/verifier/client.rs b/src/verifier/client.rs index e08704e..8b71208 100644 --- a/src/verifier/client.rs +++ b/src/verifier/client.rs @@ -4,13 +4,11 @@ use anyhow::{bail, Context as _, Result}; use async_trait::async_trait; use base64::prelude::*; use serde_json::{json, Value as Json}; -use ssi::{ - dids::{DIDResolver, VerificationMethodDIDResolver}, - jwk::JWKResolver, - verification_methods::{ - GenericVerificationMethod, InvalidVerificationMethod, MaybeJwkVerificationMethod, - VerificationMethodSet, - }, +use ssi_dids::{DIDResolver, VerificationMethodDIDResolver}; +use ssi_jwk::JWKResolver; +use ssi_verification_methods::{ + GenericVerificationMethod, InvalidVerificationMethod, MaybeJwkVerificationMethod, + VerificationMethodSet, }; use tracing::debug; use x509_cert::{ diff --git a/src/verifier/request_signer.rs b/src/verifier/request_signer.rs index 67cb33c..3b0550d 100644 --- a/src/verifier/request_signer.rs +++ b/src/verifier/request_signer.rs @@ -3,7 +3,7 @@ use anyhow::Result; use async_trait::async_trait; #[cfg(feature = "p256")] use p256::ecdsa::{signature::Signer, Signature, SigningKey}; -use ssi::jwk::JWK; +use ssi_jwk::JWK; use std::fmt::Debug; diff --git a/tests/jwt_vc.rs b/tests/jwt_vc.rs index ab1b3c1..448f08c 100644 --- a/tests/jwt_vc.rs +++ b/tests/jwt_vc.rs @@ -21,10 +21,8 @@ use oid4vp::{ wallet::Wallet, }; use serde_json::json; -use ssi::{ - dids::{DIDKey, VerificationMethodDIDResolver}, - prelude::AnyJwkMethod, -}; +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(); From 1a2af6719c47bd8827f43504c0048480e8d273db Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 12 Aug 2024 11:12:42 -0700 Subject: [PATCH 21/71] add jwt_vc_json and jwt_vp_json claim formats Signed-off-by: Ryan Tate --- src/core/metadata/mod.rs | 33 ++++++++++++++++-------- src/core/metadata/parameters/verifier.rs | 33 ++++++++++++++++-------- src/core/metadata/parameters/wallet.rs | 31 +++++++++++++--------- src/core/object/mod.rs | 19 +++++++++----- src/presentation_exchange.rs | 20 +++++++++++++- 5 files changed, 94 insertions(+), 42 deletions(-) diff --git a/src/core/metadata/mod.rs b/src/core/metadata/mod.rs index bf2da29..e3d1318 100644 --- a/src/core/metadata/mod.rs +++ b/src/core/metadata/mod.rs @@ -1,9 +1,14 @@ -use std::ops::{Deref, DerefMut}; +use std::{ + collections::HashMap, + ops::{Deref, DerefMut}, +}; use anyhow::Error; use parameters::wallet::{RequestObjectSigningAlgValuesSupported, ResponseTypesSupported}; use serde::{Deserialize, Serialize}; -use serde_json::{Map, Value as Json}; +use ssi_jwk::Algorithm; + +use crate::presentation_exchange::{ClaimFormat, ClaimFormatDesignation}; use self::parameters::wallet::{AuthorizationEndpoint, VpFormatsSupported}; @@ -65,19 +70,25 @@ 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 = HashMap::new(); + vp_formats_supported.insert( + ClaimFormatDesignation::JwtVpJson, + ClaimFormat::JwtVpJson { + alg_values_supported: alg_values_supported.clone(), + }, + ); + vp_formats_supported.insert( + ClaimFormatDesignation::JwtVcJson, + ClaimFormat::JwtVcJson { + alg_values_supported: 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..9ddcfb8 100644 --- a/src/core/metadata/parameters/verifier.rs +++ b/src/core/metadata/parameters/verifier.rs @@ -1,11 +1,16 @@ -use anyhow::Error; -use serde::Deserialize; +use std::collections::HashMap; + +use anyhow::{Context, Error}; +use serde::{Deserialize, Serialize}; use serde_json::{Map, Value as Json}; -use crate::core::object::TypedParameter; +use crate::{ + core::object::TypedParameter, + presentation_exchange::{ClaimFormat, ClaimFormatDesignation}, +}; -#[derive(Debug, Clone, Deserialize)] -pub struct VpFormats(pub Map); +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VpFormats(pub HashMap); impl TypedParameter for VpFormats { const KEY: &'static str = "vp_formats"; @@ -19,9 +24,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 { + Ok(serde_json::to_value(value.0).context("Failed to serialize VpFormats")?) } } @@ -148,9 +155,13 @@ 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, &ClaimFormat::MsoMDoc(Json::Object(Map::new()))) } #[test] diff --git a/src/core/metadata/parameters/wallet.rs b/src/core/metadata/parameters/wallet.rs index 3454a01..23f7ca5 100644 --- a/src/core/metadata/parameters/wallet.rs +++ b/src/core/metadata/parameters/wallet.rs @@ -1,9 +1,14 @@ -use crate::core::{ - authorization_request::parameters::{ClientIdScheme, ResponseType}, - object::TypedParameter, +use std::collections::HashMap; + +use crate::{ + core::{ + authorization_request::parameters::{ClientIdScheme, ResponseType}, + object::TypedParameter, + }, + presentation_exchange::{ClaimFormat, ClaimFormatDesignation}, }; use anyhow::{bail, Error, Result}; -use serde_json::{Map, Value as Json}; +use serde_json::Value as Json; use url::Url; #[derive(Debug, Clone)] @@ -134,7 +139,7 @@ impl From for Json { // TODO: Better types #[derive(Debug, Clone)] -pub struct VpFormatsSupported(pub Map); +pub struct VpFormatsSupported(pub HashMap); impl TypedParameter for VpFormatsSupported { const KEY: &'static str = "vp_formats_supported"; @@ -144,13 +149,15 @@ 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 From for Json { - fn from(value: VpFormatsSupported) -> Json { - Json::Object(value.0) +impl TryFrom for Json { + type Error = Error; + + fn try_from(value: VpFormatsSupported) -> Result { + serde_json::to_value(value.0).map_err(Into::into) } } @@ -198,7 +205,7 @@ impl From for Json { #[cfg(test)] mod test { - use serde_json::json; + use serde_json::{json, Map}; use crate::core::object::UntypedObject; @@ -277,8 +284,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(), + ClaimFormat::MsoMDoc(Json::Object(Map::new())) ); } diff --git a/src/core/object/mod.rs b/src/core/object/mod.rs index ac56ed7..7ee2ebe 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,17 @@ 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(_) => { + return 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/presentation_exchange.rs b/src/presentation_exchange.rs index 9361260..77375da 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -34,11 +34,21 @@ pub enum ClaimFormat { // The algorithm used to sign the JWT verifiable credential. 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")] JwtVp { // The algorithm used to sign the JWT verifiable presentation. alg: 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. @@ -82,7 +92,9 @@ impl ClaimFormat { 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, @@ -99,7 +111,7 @@ impl ClaimFormat { /// 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)] +#[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 @@ -116,9 +128,15 @@ pub enum ClaimFormatDesignation { /// [RFC7518](https://identity.foundation/claim-format-registry/#ref:RFC7518) Section 3. #[serde(rename = "jwt_vc")] JwtVc, + /// JwtVcJson is used by `vp_formats_supported` in the OID4VP metadata. + #[serde(rename = "jwt_vc_json")] + JwtVcJson, /// See [JwtVc](JwtVc) for more information. #[serde(rename = "jwt_vp")] JwtVp, + /// JwtVpJson is used by `vp_formats_supported` in the OID4VP metadata. + #[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 From 61043a940297af5c51944468682a38fe7df9439d Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 13 Aug 2024 07:54:10 -0700 Subject: [PATCH 22/71] wip: construct verifiable presentation for e2e test Signed-off-by: Ryan Tate --- Cargo.toml | 2 +- src/core/authorization_request/parameters.rs | 25 +++- src/presentation_exchange.rs | 34 +++--- tests/e2e.rs | 119 ++++++++----------- tests/jwt_vp.rs | 52 ++++++++ 5 files changed, 142 insertions(+), 90 deletions(-) create mode 100644 tests/jwt_vp.rs diff --git a/Cargo.toml b/Cargo.toml index cd9ee35..0644f3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ serde_qs = "0.12.0" serde_urlencoded = "0.7.1" ssi-claims = "0.1.0" ssi-dids = "0.2.0" -ssi-jwk = "0.2.1" +ssi-jwk = { version = "0.2.1", features = ["secp256r1"] } ssi-verification-methods = "0.1.1" thiserror = "1.0.49" tokio = "1.32.0" diff --git a/src/core/authorization_request/parameters.rs b/src/core/authorization_request/parameters.rs index 517ab54..c00fed7 100644 --- a/src/core/authorization_request/parameters.rs +++ b/src/core/authorization_request/parameters.rs @@ -1,4 +1,4 @@ -use std::fmt; +use std::{fmt, ops::Deref}; use crate::core::{ object::{ParsingErrorContext, TypedParameter, UntypedObject}, @@ -193,7 +193,28 @@ 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 Deref for Nonce { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Nonce { + pub fn random() -> Self { + // NOTE: consider replacing with rng rand crate. + Self(uuid::Uuid::new_v4().to_string()) + } +} impl TypedParameter for Nonce { const KEY: &'static str = "nonce"; diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 77375da..b5784d8 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -24,11 +24,6 @@ pub type JsonPath = String; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(untagged)] 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. @@ -49,6 +44,11 @@ pub enum ClaimFormat { // Used in the OID4VP specification for wallet methods supported. alg_values_supported: Vec, }, + #[serde(rename = "jwt")] + Jwt { + // The algorithm used to sign the JWT. + alg: Vec, + }, #[serde(rename = "ldp")] Ldp { // The proof type used to sign the linked data proof. @@ -619,19 +619,19 @@ pub enum ConstraintsLimitDisclosure { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct PresentationSubmission { id: uuid::Uuid, - definition_id: uuid::Uuid, + definition_id: String, 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 { + /// 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: String, descriptor_map: Vec) -> Self { Self { id, definition_id, @@ -645,7 +645,7 @@ impl PresentationSubmission { } /// Return the definition id of the presentation submission. - pub fn definition_id(&self) -> &uuid::Uuid { + pub fn definition_id(&self) -> &String { &self.definition_id } @@ -665,21 +665,21 @@ impl PresentationSubmission { /// 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: uuid::Uuid, + 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 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: uuid::Uuid, format: ClaimFormatDesignation, path: JsonPath) -> Self { + pub fn new(id: String, format: ClaimFormatDesignation, path: JsonPath) -> Self { Self { id, format, @@ -689,7 +689,7 @@ impl DescriptorMap { } /// Return the id of the descriptor map. - pub fn id(&self) -> &uuid::Uuid { + pub fn id(&self) -> &String { &self.id } diff --git a/tests/e2e.rs b/tests/e2e.rs index 0902f13..16d5cc5 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -1,3 +1,5 @@ +use oid4vp::presentation_exchange::*; + use oid4vp::{ core::{ authorization_request::parameters::{ClientMetadata, Nonce, ResponseMode, ResponseType}, @@ -8,61 +10,31 @@ use oid4vp::{ 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::new( - // uuid::Uuid::new_v4(), - // InputDescriptor::new( - // uuid::Uuid::new_v4(), - // Constraints::new().add_constraint( - // ConstraintsField::new( - // "$.vp.verifiableCredential[0].credentialSubject.postalCode".into(), - // ) - // .set_name("Check Postal Code".into()) - // .set_purpose(String::from( - // "Check whether you live within our school district.", - // )), - // ), - // ) - // .set_name(String::from("School District Proof")) - // .set_purpose(String::from( - // "We need to know if you live within our school district.", - // )) - // .set_format(ClaimFormat::JwtVp { - // alg: vec![Algorithm::ES256.to_string()], - // }), - // ); - - // Save the presentation definition to a `presentation-exchange/test/presentation-definition/postal-code.json` file. - // std::fs::write( - // "presentation-exchange/test/presentation-definition/postal-code.json", - // serde_json::to_string_pretty(&presentation_definition).unwrap(), - // ) - // .expect("Unable to write file"); - - 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( + ConstraintsField::new("$.vp.verifiableCredential.credentialSubject.id".into()) + .set_name("Verify Identity Key".into()) + .set_purpose("Check whether your identity key has been verified.".into()), + ), + ) + .set_name("DID Key Identity Verification".into()) + .set_purpose("Check whether your identity key has been verified.".into()) + .set_format(ClaimFormat::JwtVc { + alg: vec![Algorithm::ES256.to_string()], + }), + ); let client_metadata = UntypedObject::default(); @@ -71,7 +43,7 @@ async fn w3c_vc_did_client_direct_post() { .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::random()) .with_request_parameter(ClientMetadata(client_metadata)) .build(wallet.metadata().clone()) .await @@ -81,35 +53,42 @@ async fn w3c_vc_did_client_direct_post() { 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() + parsed_presentation_definition.parsed() ); 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| { + let format = descriptor + .format() + .map(|format| format.designation()) + .to_owned() + .unwrap_or(ClaimFormatDesignation::JwtVp); + + // 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().clone(), format, "$".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(), diff --git a/tests/jwt_vp.rs b/tests/jwt_vp.rs new file mode 100644 index 0000000..39846a6 --- /dev/null +++ b/tests/jwt_vp.rs @@ -0,0 +1,52 @@ +use std::sync::Arc; + +use anyhow::Result; + +use oid4vp::verifier::request_signer::{P256Signer, RequestSigner}; +use ssi_claims::jwt::{AnyRegisteredClaim, RegisteredClaim}; +use ssi_claims::{ + jwt::{Subject, VerifiableCredential}, + vc::v1::VerifiablePresentation, + ResourceProvider, +}; +use ssi_dids::{DIDKey, DIDJWK}; +use ssi_jwk::JWK; + +#[tokio::test] +async fn test_verifiable_presentation() -> Result<()> { + // // Create a holder DID and key + // let mut holder_key = JWK::generate_p256(); + // let holder_did = DIDJWK::generate_url(&holder_key.to_public()); + + // holder_key.key_id = Some(holder_did.into()); + + let signer = Arc::new( + P256Signer::new( + p256::SecretKey::from_jwk_str(include_str!("examples/subject.jwk")) + .unwrap() + .into(), + ) + .unwrap(), + ); + + println!("Signer: {:?}", signer.jwk()); + + let holder_did = DIDKey::generate_url(&signer.jwk())?; + + // 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: VerifiableCredential = + ssi_claims::jwt::decode_unverified(include_str!("examples/vc.jwt"))?; + + let subject = Subject::extract(AnyRegisteredClaim::from(verifiable_credential.clone())); + + // assert_eq!(holder_did.as_did_url(), subject); + + println!("VC: {:?}", verifiable_credential); + + println!("Holder DID: {:?}", holder_did); + + println!("Subject: {:?}", subject); + + Ok(()) +} From 24497dd3303095f83f009d605777b8b39a333f1f Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 13 Aug 2024 14:05:26 -0700 Subject: [PATCH 23/71] wip: perform validation on presentation submission Signed-off-by: Ryan Tate --- src/core/response/mod.rs | 10 + src/holder/mod.rs | 1 + src/holder/verifiable_presentation_builder.rs | 152 +++++++++++++++ src/json_schema_validation.rs | 15 +- src/lib.rs | 1 + src/presentation_exchange.rs | 180 +++++++++++++++++- src/verifier/mod.rs | 4 +- tests/e2e.rs | 2 +- tests/examples/vp.jwt | 1 + tests/jwt_vc.rs | 11 +- tests/jwt_vp.rs | 95 +++++++-- 11 files changed, 447 insertions(+), 25 deletions(-) create mode 100644 src/holder/mod.rs create mode 100644 src/holder/verifiable_presentation_builder.rs create mode 100644 tests/examples/vp.jwt diff --git a/src/core/response/mod.rs b/src/core/response/mod.rs index b6752ee..7a752a3 100644 --- a/src/core/response/mod.rs +++ b/src/core/response/mod.rs @@ -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/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..dd31cbc --- /dev/null +++ b/src/holder/verifiable_presentation_builder.rs @@ -0,0 +1,152 @@ +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use ssi_claims::vc::{v1::VerifiableCredential, v2::syntax::VERIFIABLE_CREDENTIAL_TYPE}; +use ssi_dids::{DIDURLBuf, DIDURL}; + +pub const VERIFIABLE_PRESENTATION_CONTEXT_V1: &str = "https://www.w3.org/2018/credentials/v1"; + +// NOTE: This may make more sense to be moved to ssi_claims lib. +pub const VERIFIABLE_PRESENTATION_TYPE: &str = "VerifiablePresentation"; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct VerifiablePresentationBuilder { + /// The issuer of the presentation. + #[serde(skip_serializing_if = "Option::is_none")] + iss: Option, // TODO: Should this be a DIDURLBuf or IRI/URI type? + /// The Json Web Token ID of the presentation. + #[serde(skip_serializing_if = "Option::is_none")] + jti: Option, + /// The audience of the presentation. + #[serde(skip_serializing_if = "Option::is_none")] + aud: Option, // TODO: Should this be a DIDURLBuf? + /// The issuance date of the presentation. + #[serde(skip_serializing_if = "Option::is_none")] + iat: Option, + /// The expiration date of the presentation. + #[serde(skip_serializing_if = "Option::is_none")] + exp: Option, + /// The nonce of the presentation. + #[serde(skip_serializing_if = "Option::is_none")] + nonce: Option, + /// The verifiable presentation format. + #[serde(skip_serializing_if = "Option::is_none")] + vp: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct VerifiablePresentationCredentialBuilder { + /// The context of the presentation. + #[serde(rename = "@context")] + context: Vec, + /// The type of the presentation. + #[serde(rename = "type")] + type_: Vec, + /// The verifiable credentials list of the presentation. + verifiable_credential: Vec, +} + +impl Default for VerifiablePresentationCredentialBuilder { + fn default() -> Self { + Self { + context: vec![VERIFIABLE_PRESENTATION_CONTEXT_V1.into()], + type_: vec![VERIFIABLE_PRESENTATION_TYPE.into()], + verifiable_credential: vec![], + } + } +} + +impl VerifiablePresentationCredentialBuilder { + pub fn new() -> Self { + Self::default() + } + + /// Add a verifiable credential to the presentation. + pub fn add_verifiable_credential( + mut self, + verifiable_credential: VerifiableCredentialBuilder, + ) -> Self { + self.verifiable_credential.push(verifiable_credential); + self + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct VerifiableCredentialBuilder { + /// The context of the credential. + #[serde(rename = "@context")] + context: Vec, + /// The type of the credential. + #[serde(rename = "type")] + type_: Vec, + /// The issuer of the credential. + issuer: Option, + // TODO: Determine if we should use a DateTime type here, use chrono lib? + #[serde(rename = "issuanceDate")] + issuance_date: Option, + #[serde(rename = "credentialSubject")] + credential_subject: Option, +} + +impl Default for VerifiableCredentialBuilder { + fn default() -> Self { + Self { + context: vec![VERIFIABLE_PRESENTATION_CONTEXT_V1.into()], + type_: vec![VERIFIABLE_CREDENTIAL_TYPE.into()], + issuer: None, + issuance_date: None, + credential_subject: None, + } + } +} + +impl VerifiableCredentialBuilder { + pub fn new() -> Self { + Self::default() + } + + /// Add a credential to the credential builder, e.g. `IdentityCredential` or `mDL`. + /// + /// By default, the `VerifiableCredential` type is added to the credential. + pub fn add_type(mut self, credential_type: String) -> Self { + self.type_.push(credential_type); + self + } + + /// Set the issuer of the credential. + /// + /// The value of the issuer property MUST be either a URI or an object containing an id property. + /// It is RECOMMENDED that the URI in the issuer or its id be one which, if dereferenced, results + /// in a document containing machine-readable information about the issuer that can be used to verify + /// the information expressed in the credential. + /// + /// See: [https://www.w3.org/TR/vc-data-model-1.0/#issuer](https://www.w3.org/TR/vc-data-model-1.0/#issuer) + pub fn set_issuer(mut self, issuer: String) -> Self { + self.issuer = Some(issuer); + self + } + + /// Set the issuance date of the credential. + /// + /// A credential MUST have an issuanceDate property. + /// The value of the issuanceDate property MUST be a string value of an [RFC3339](https://www.w3.org/TR/vc-data-model-1.0/#bib-rfc3339) + /// combined date and time string representing the date and time the credential becomes valid, + /// which could be a date and time in the future. Note that this value represents the earliest + /// point in time at which the information associated with the credentialSubject property becomes valid. + /// + /// See: [https://www.w3.org/TR/vc-data-model-1.0/#issuance-date](https://www.w3.org/TR/vc-data-model-1.0/#issuance-date) + pub fn set_issuance_date(mut self, issuance_date: String) -> Self { + self.issuance_date = Some(issuance_date); + self + } + + /// Set the credential subject of the credential. + /// + /// The value of the credentialSubject property is defined as a set of objects that contain + /// one or more properties that are each related to a subject of the verifiable credential. + /// Each object MAY contain an id, as described in [Section § 4.2 Identifiers](https://www.w3.org/TR/vc-data-model-1.0/#identifiers) + /// section of the specification. + pub fn set_credential_subject(mut self, credential_subject: serde_json::Value) -> Self { + self.credential_subject = Some(credential_subject); + self + } +} diff --git a/src/json_schema_validation.rs b/src/json_schema_validation.rs index 298474f..f3a9dd4 100644 --- a/src/json_schema_validation.rs +++ b/src/json_schema_validation.rs @@ -21,21 +21,30 @@ pub enum SchemaType { /// /// For more information, see the field constraints filter property: /// -/// https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object +/// - [https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object](https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object) +/// +/// - [https://json-schema.org/understanding-json-schema](https://json-schema.org/understanding-json-schema) +/// #[derive(Debug, Clone, Deserialize, Serialize)] pub struct SchemaValidator { #[serde(rename = "type")] schema_type: SchemaType, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "minLength", skip_serializing_if = "Option::is_none")] min_length: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")] max_length: Option, #[serde(skip_serializing_if = "Option::is_none")] pattern: Option, #[serde(skip_serializing_if = "Option::is_none")] minimum: Option, + #[serde(rename = "exclusiveMinimum", skip_serializing_if = "Option::is_none")] + exclusive_minimum: Option, #[serde(skip_serializing_if = "Option::is_none")] maximum: Option, + #[serde(rename = "exclusiveMaximum", skip_serializing_if = "Option::is_none")] + exclusive_maximum: Option, + #[serde(rename = "multipleOf", skip_serializing_if = "Option::is_none")] + multiple_of: Option, #[serde(skip_serializing_if = "Vec::is_empty", default)] required: Vec, #[serde(skip_serializing_if = "HashMap::is_empty", default)] diff --git a/src/lib.rs b/src/lib.rs index 0279f15..e148bb6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod core; +// pub mod holder; mod json_schema_validation; pub mod presentation_exchange; mod utils; diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index b5784d8..de7ad54 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -1,15 +1,46 @@ -use crate::json_schema_validation::SchemaValidator; +use std::collections::HashMap; + pub use crate::utils::NonEmptyVec; +use crate::{ + core::response::{AuthorizationResponse, UnencodedAuthorizationResponse}, + json_schema_validation::SchemaValidator, +}; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use serde::{Deserialize, Serialize}; use serde_json::Map; +use ssi_claims::jwt::VerifiablePresentation; +use ssi_dids::ssi_json_ld::{ + object::value::FragmentRef, + syntax::{from_value, Value}, +}; /// 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, +} + /// 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. @@ -288,6 +319,57 @@ impl PresentationDefinition { pub fn format(&self) -> Option<&ClaimFormat> { self.format.as_ref() } + + /// Validate a presentation submission against the presentation definition. + /// + /// Checks the underlying presentation submission parsed from the authorization response, + /// against the input descriptors of the presentation definition. + pub fn validate_authorization_response( + &self, + auth_response: &AuthorizationResponse, + ) -> Result<()> { + match auth_response { + AuthorizationResponse::Jwt(jwt) => { + bail!("Authorization Response Presentation Definition Validation Not Implemented.") + } + AuthorizationResponse::Unencoded(response) => { + let presentation_submission = response.presentation_submission().parsed(); + + let verifiable_presentation: VerifiablePresentation = + ssi_claims::jwt::decode_unverified(&response.vp_token().0)?; + + // Ensure the definition id matches the submission's definition id. + if presentation_submission.definition_id() != self.id() { + bail!("Presentation Definition ID does not match the Presentation Submission.") + } + + let descriptor_map: HashMap = presentation_submission + .descriptor_map() + .iter() + .map(|descriptor_map| (descriptor_map.id().to_owned(), descriptor_map.clone())) + .collect(); + + for input_descriptor in self.input_descriptors().iter() { + match descriptor_map.get(input_descriptor.id()) { + None => { + // TODO: Determine whether input descriptor must have a corresponding descriptor map. + bail!("Input Descriptor ID not found in Descriptor Map.") + } + Some(descriptor) => { + input_descriptor + .validate_verifiable_presentation( + &verifiable_presentation, + descriptor, + ) + .context("Input Descriptor Validation Failed.")?; + } + } + } + } + } + + Ok(()) + } } /// Input Descriptors are objects used to describe the information a @@ -395,6 +477,69 @@ impl InputDescriptor { pub fn format(&self) -> Option<&ClaimFormat> { self.format.as_ref() } + + /// 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<()> { + let vp = &verifiable_presentation.0; + + let vp_json: serde_json::Value = + from_value(vp.clone()).context("failed to parse value into json type")?; + + // 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.") + } + + if let Some(field_constraints) = self.constraints().fields() { + for constraint_field in field_constraints.iter() { + 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); + + for field_path in constraint_field.path().iter() { + let Ok(field_elements) = map_selector(field_path) else { + // 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; + + // bail!("Failed to select field element from verifiable presentation."); + }; + + if let Some(filter) = constraint_field.filter() { + // TODO: possible trace a warning if a field is not valid. + // TODO: Check the predicate feature value. + let validated_fields = field_elements + .iter() + .find(|element| filter.validate(element).is_ok()); + + if validated_fields.is_none() && !constraint_field.optional.unwrap_or(false) + { + bail!("Field did not pass filter validation."); + } + } + + // TODO: Check limit disclosure of data requested. Do not provide more data + // than is necessary to satisfy the constraints. + } + } + } + + Ok(()) + } } /// 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. @@ -454,9 +599,6 @@ impl Constraints { /// 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, @@ -468,6 +610,9 @@ pub struct ConstraintsField { #[serde(skip_serializing_if = "Option::is_none")] filter: Option, #[serde(skip_serializing_if = "Option::is_none")] + // Optional predicate value + predicate: Option, + #[serde(skip_serializing_if = "Option::is_none")] optional: Option, #[serde(skip_serializing_if = "Option::is_none")] intent_to_retain: Option, @@ -483,6 +628,7 @@ impl From> for ConstraintsField { purpose: None, name: None, filter: None, + predicate: None, optional: None, intent_to_retain: None, } @@ -501,6 +647,7 @@ impl ConstraintsField { purpose: None, name: None, filter: None, + predicate: None, optional: None, intent_to_retain: None, } @@ -579,6 +726,29 @@ impl ConstraintsField { self.filter.as_ref() } + /// 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() + } + /// Set the optional value of the constraints field. /// /// The value of this property MUST be a boolean, wherein true indicates the diff --git a/src/verifier/mod.rs b/src/verifier/mod.rs index d179856..4a49db0 100644 --- a/src/verifier/mod.rs +++ b/src/verifier/mod.rs @@ -119,12 +119,12 @@ impl Verifier { validator_function: F, ) -> Result<()> where - F: FnOnce(Session, AuthorizationResponse) -> Pin>, + F: FnOnce(Session, AuthorizationResponse) -> Result>>, Fut: Future, { let session = self.session_store.get_session(reference).await?; - let outcome = validator_function(session, authorization_response).await; + let outcome = validator_function(session, authorization_response)?.await; self.session_store .update_status(reference, Status::Complete(outcome)) diff --git a/tests/e2e.rs b/tests/e2e.rs index 16d5cc5..079662a 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -92,7 +92,7 @@ async fn w3c_vc_did_client_direct_post() { let response = AuthorizationResponse::Unencoded(UnencodedAuthorizationResponse( Default::default(), - VpToken(include_str!("examples/vc.jwt").to_owned()), + VpToken(include_str!("examples/vp.jwt").to_owned()), presentation_submission.try_into().unwrap(), )); diff --git a/tests/examples/vp.jwt b/tests/examples/vp.jwt new file mode 100644 index 0000000..69b9662 --- /dev/null +++ b/tests/examples/vp.jwt @@ -0,0 +1 @@ +eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6a2V5OnpEbmFlZnFUMUJyR0dzSkVaR3dBaXVlb3VxTWg2TXFzWmhhTDFtZDVoa0hndGZ6YjIjekRuYWVmcVQxQnJHR3NKRVpHd0FpdWVvdXFNaDZNcXNaaGFMMW1kNWhrSGd0ZnpiMiIsImF1ZCI6ImRpZDprZXk6ekRuYWVhRGozWXBQUjRKWG9zMmtDQ05QUzg2aGRFTGVONVBaaDk3S0drb0Z6VXRHbiN6RG5hZWFEajNZcFBSNEpYb3Mya0NDTlBTODZoZEVMZU41UFpoOTdLR2tvRnpVdEduIiwiaWF0IjoxNzIzNTczMDg1LCJleHAiOjE3MjM1NzY2ODUsIm5vbmNlIjoiZTY4MGUzY2MtZWUzOS00YWU2LWFjZDAtNThiNTQ1MjE1YTU4IiwidnAiOnsiQGNvbnRleHQiOiJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsInR5cGUiOiJWZXJpZmlhYmxlUHJlc2VudGF0aW9uIiwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlt7ImlzcyI6ImRpZDprZXk6ekRuYWVlZXg5TUFWYmhvV2VEY2JiR1pkek0xenhxWnFwQzM4N2pXb0xoVXIxQmRTVCIsIm5iZiI6MTcwNDA2NzIwMC4wLCJqdGkiOiIyYzI4MmViYS1kMTQ3LTQyMmMtOWNlNS05OTFmMTk5ODAwYzUiLCJzdWIiOiJkaWQ6a2V5OnpEbmFlZnFUMUJyR0dzSkVaR3dBaXVlb3VxTWg2TXFzWmhhTDFtZDVoa0hndGZ6YjIiLCJ2YyI6eyJAY29udGV4dCI6Imh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaWQiOiIyYzI4MmViYS1kMTQ3LTQyMmMtOWNlNS05OTFmMTk5ODAwYzUiLCJ0eXBlIjoiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ekRuYWVmcVQxQnJHR3NKRVpHd0FpdWVvdXFNaDZNcXNaaGFMMW1kNWhrSGd0ZnpiMiJ9LCJpc3N1ZXIiOiJkaWQ6a2V5OnpEbmFlZWV4OU1BVmJob1dlRGNiYkdaZHpNMXp4cVpxcEMzODdqV29MaFVyMUJkU1QiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTAxLTAxVDAwOjAwOjAwKzAwOjAwIn19XX19.Ql_K47heJAmGpOZUkiX1NR3OajpdgiwjSUvjQOfA9wBha02okHTusAWNtblKbVtsOHN6lOSw0mNwMmUFkmCCCQ \ No newline at end of file diff --git a/tests/jwt_vc.rs b/tests/jwt_vc.rs index 448f08c..793955e 100644 --- a/tests/jwt_vc.rs +++ b/tests/jwt_vc.rs @@ -159,7 +159,16 @@ impl AsyncHttpClient for MockHttpClient { id.parse().context("failed to parse id")?, AuthorizationResponse::from_x_www_form_urlencoded(body) .context("failed to parse authorization response request")?, - |_, _| Box::pin(async { Outcome::Success }), + |session, auth_response| { + session + .presentation_definition + .validate_authorization_response(&auth_response)?; + + println!("Session: {:?}", session); + println!("Auth Response: {:?}", auth_response); + + Ok(Box::pin(async { Outcome::Success })) + }, ) .await?; diff --git a/tests/jwt_vp.rs b/tests/jwt_vp.rs index 39846a6..a65a030 100644 --- a/tests/jwt_vp.rs +++ b/tests/jwt_vp.rs @@ -1,24 +1,27 @@ +use std::str::FromStr; use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::Result; use oid4vp::verifier::request_signer::{P256Signer, RequestSigner}; -use ssi_claims::jwt::{AnyRegisteredClaim, RegisteredClaim}; +use serde_json::json; +use ssi_claims::jwt::{AnyRegisteredClaim, Issuer, RegisteredClaim, VerifiablePresentation}; +use ssi_claims::vc::v2::syntax::VERIFIABLE_PRESENTATION_TYPE; +use ssi_claims::{JWSPayload, JWTClaims}; +// use ssi_claims::vc::v1::VerifiableCredential; use ssi_claims::{ - jwt::{Subject, VerifiableCredential}, - vc::v1::VerifiablePresentation, + jwt::{self, Subject}, ResourceProvider, }; +use ssi_dids::ssi_json_ld::syntax::{Object, Value}; +use ssi_dids::ssi_json_ld::CREDENTIALS_V1_CONTEXT; use ssi_dids::{DIDKey, DIDJWK}; use ssi_jwk::JWK; #[tokio::test] async fn test_verifiable_presentation() -> Result<()> { - // // Create a holder DID and key - // let mut holder_key = JWK::generate_p256(); - // let holder_did = DIDJWK::generate_url(&holder_key.to_public()); - - // holder_key.key_id = Some(holder_did.into()); + let verifier = JWK::from_str(include_str!("examples/verifier.jwk"))?; let signer = Arc::new( P256Signer::new( @@ -31,22 +34,88 @@ async fn test_verifiable_presentation() -> Result<()> { println!("Signer: {:?}", signer.jwk()); + let holder_jwk = JWK::from_str(std::include_str!("examples/subject.jwk"))?; 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: VerifiableCredential = + let verifiable_credential: jwt::VerifiableCredential = ssi_claims::jwt::decode_unverified(include_str!("examples/vc.jwt"))?; - let subject = Subject::extract(AnyRegisteredClaim::from(verifiable_credential.clone())); + println!("VC: {:?}", verifiable_credential); + + // let issuer = Issuer::extract(AnyRegisteredClaim::from(verifiable_credential.clone())); + + // println!("Issuer: {:?}", issuer); // assert_eq!(holder_did.as_did_url(), subject); - println!("VC: {:?}", verifiable_credential); + // TODO: There should be a more idiomatically correct way to do this, if not already implemented. + let mut verifiable_presentation = VerifiablePresentation(Value::Object(Object::new())); + + verifiable_presentation.0.as_object_mut().map(|obj| { + // The issuer is the holder of the verifiable credential (subject of the verifiable credential). + obj.insert("iss".into(), Value::String(holder_did.as_str().into())); + + // The audience is the verifier of the verifiable credential. + obj.insert("aud".into(), Value::String(verifier_did.as_str().into())); + + SystemTime::now() + .duration_since(UNIX_EPOCH) + .ok() + .map(|dur| { + // 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() + 3600).into())); + }); + + // The nonce is a random string. + obj.insert( + "nonce".into(), + Value::String(uuid::Uuid::new_v4().to_string().into()), + ); + + let mut verifiable_credential_field = Value::Object(Object::new()); + + verifiable_credential_field.as_object_mut().map(|cred| { + 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(vec![verifiable_credential.0]), + ); + }); + + obj.insert("vp".into(), verifiable_credential_field); + }); + + let claim = JWTClaims::from_private_claims(verifiable_presentation); + + let jwt = claim + .sign(&holder_jwk) + .await + .expect("Failed to sign Verifiable Presentation JWT"); + + println!("JWT: {:?}", jwt); + + // Save the JWT to the file system to be used in other tests `examples/vp.jwt` + std::fs::write("tests/examples/vp.jwt", jwt.as_str())?; - println!("Holder DID: {:?}", holder_did); + let vp: jwt::VerifiablePresentation = ssi_claims::jwt::decode_unverified(jwt.as_str())?; - println!("Subject: {:?}", subject); + println!("VP: {:?}", vp); Ok(()) } From 3423d139844443136cf546f8929781549581072f Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 13 Aug 2024 14:55:49 -0700 Subject: [PATCH 24/71] ensure range exclusive values are checked; fix inclusive range values Signed-off-by: Ryan Tate --- src/json_schema_validation.rs | 68 ++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/src/json_schema_validation.rs b/src/json_schema_validation.rs index f3a9dd4..a361106 100644 --- a/src/json_schema_validation.rs +++ b/src/json_schema_validation.rs @@ -37,10 +37,10 @@ pub struct SchemaValidator { pattern: Option, #[serde(skip_serializing_if = "Option::is_none")] minimum: Option, - #[serde(rename = "exclusiveMinimum", skip_serializing_if = "Option::is_none")] - exclusive_minimum: Option, #[serde(skip_serializing_if = "Option::is_none")] maximum: Option, + #[serde(rename = "exclusiveMinimum", skip_serializing_if = "Option::is_none")] + exclusive_minimum: Option, #[serde(rename = "exclusiveMaximum", skip_serializing_if = "Option::is_none")] exclusive_maximum: Option, #[serde(rename = "multipleOf", skip_serializing_if = "Option::is_none")] @@ -85,7 +85,7 @@ impl SchemaValidator { let s = value.as_str().context("Expected a string")?; if let Some(min_length) = self.min_length { - if s.len() < min_length { + if s.len() <= min_length { bail!( "String length {} is less than minimum {}", s.len(), @@ -95,7 +95,7 @@ impl SchemaValidator { } if let Some(max_length) = self.max_length { - if s.len() > max_length { + if s.len() >= max_length { bail!( "String length {} is greater than maximum {}", s.len(), @@ -119,17 +119,43 @@ impl SchemaValidator { let n = value.as_f64().context("Expected a number")?; if let Some(minimum) = self.minimum { - if n < minimum { + if n <= minimum { bail!("Number {} is less than minimum {}", n, minimum); } } if let Some(maximum) = self.maximum { - if n > maximum { + if n >= maximum { bail!("Number {} is greater than maximum {}", n, maximum); } } + if let Some(exclusive_minimum) = self.exclusive_minimum { + if n < exclusive_minimum { + bail!( + "Number {} is less than or equal to exclusive minimum {}", + n, + exclusive_minimum + ); + } + } + + if let Some(exclusive_maximum) = self.exclusive_maximum { + if n > exclusive_maximum { + bail!( + "Number {} is greater than or equal to exclusive maximum {}", + n, + exclusive_maximum + ); + } + } + + if let Some(multiple_of) = self.multiple_of { + if n % multiple_of != 0.0 { + bail!("Number {} is not a multiple of {}", n, multiple_of); + } + } + Ok(()) } @@ -137,17 +163,43 @@ impl SchemaValidator { let n = value.as_i64().context("Expected an integer")?; if let Some(minimum) = self.minimum { - if (n as f64) < minimum { + if n <= minimum as i64 { bail!("Integer {} is less than minimum {}", n, minimum); } } if let Some(maximum) = self.maximum { - if n as f64 > maximum { + if n >= maximum as i64 { bail!("Integer {} is greater than maximum {}", n, maximum); } } + if let Some(exclusive_minimum) = self.exclusive_minimum { + if n < exclusive_minimum as i64 { + bail!( + "Integer {} is less than or equal to exclusive minimum {}", + n, + exclusive_minimum + ); + } + } + + if let Some(exclusive_maximum) = self.exclusive_maximum { + if n > exclusive_maximum as i64 { + bail!( + "Integer {} is greater than or equal to exclusive maximum {}", + n, + exclusive_maximum + ); + } + } + + if let Some(multiple_of) = self.multiple_of { + if n % multiple_of as i64 != 0 { + bail!("Integer {} is not a multiple of {}", n, multiple_of); + } + } + Ok(()) } From 7bd0660011e33601a247d4b4b1e9246e30ff64ec Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 13 Aug 2024 15:18:56 -0700 Subject: [PATCH 25/71] ensure enum values are parsed in alpha descending order This is a fix for a bug where ClaimFormat::JwtVc was being parsed when ClaimFormat::JwtVp should have been instead. The fix is to order the enum fields in alphabetical descending order, such that VP comes BEFORE VC, and so on, for the other formats. Signed-off-by: Ryan Tate --- src/presentation_exchange.rs | 46 ++++++++++++++++++------------------ tests/e2e.rs | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index de7ad54..8a3c901 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -22,7 +22,7 @@ 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 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`. /// @@ -55,16 +55,6 @@ pub enum Predicate { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(untagged)] pub enum ClaimFormat { - #[serde(rename = "jwt_vc")] - JwtVc { - // The algorithm used to sign the JWT verifiable credential. - 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")] JwtVp { // The algorithm used to sign the JWT verifiable presentation. @@ -75,15 +65,24 @@ pub enum ClaimFormat { // Used in the OID4VP specification for wallet methods supported. alg_values_supported: Vec, }, + #[serde(rename = "jwt_vc")] + JwtVc { + // The algorithm used to sign the JWT verifiable credential. + alg: Vec, + }, + #[serde(rename = "jwt_vc_json")] + JwtVcJson { + // Used in the OID4VP specification for wallet methods supported. + alg_values_supported: Vec, + }, #[serde(rename = "jwt")] Jwt { // The algorithm used to sign the JWT. alg: Vec, }, - #[serde(rename = "ldp")] - Ldp { - // The proof type used to sign the linked data proof. - // e.g., "JsonWebSignature2020", "Ed25519Signature2018", "EcdsaSecp256k1Signature2019", "RsaSignature2018" + #[serde(rename = "ldp_vp")] + LdpVp { + // The proof type used to sign the linked data proof verifiable presentation. proof_type: Vec, }, #[serde(rename = "ldp_vc")] @@ -91,14 +90,10 @@ pub enum ClaimFormat { // 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. + #[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 = "ac_vp")] @@ -106,6 +101,11 @@ pub enum ClaimFormat { // The proof type used to sign the anoncreds 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 = "mso_mdoc")] MsoMDoc(serde_json::Value), #[serde(untagged)] diff --git a/tests/e2e.rs b/tests/e2e.rs index 079662a..70dceb8 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -31,7 +31,7 @@ async fn w3c_vc_did_client_direct_post() { ) .set_name("DID Key Identity Verification".into()) .set_purpose("Check whether your identity key has been verified.".into()) - .set_format(ClaimFormat::JwtVc { + .set_format(ClaimFormat::JwtVp { alg: vec![Algorithm::ES256.to_string()], }), ); From ce60c036369b8506c86f1dd32cd578dd79ab720d Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 13 Aug 2024 21:51:46 -0700 Subject: [PATCH 26/71] wip: verify authorized response presentation submission Signed-off-by: Ryan Tate --- src/json_schema_validation.rs | 78 +++++++++++++++++++++++ src/lib.rs | 2 +- src/presentation_exchange.rs | 115 +++++++++++++++++++++++++++------- src/verifier/mod.rs | 6 +- tests/e2e.rs | 15 +++-- tests/jwt_vc.rs | 7 ++- 6 files changed, 191 insertions(+), 32 deletions(-) diff --git a/src/json_schema_validation.rs b/src/json_schema_validation.rs index a361106..9324c6e 100644 --- a/src/json_schema_validation.rs +++ b/src/json_schema_validation.rs @@ -70,6 +70,84 @@ impl PartialEq for SchemaValidator { impl Eq for SchemaValidator {} impl SchemaValidator { + /// Creates a new schema validator with the given schema type. + pub fn new(schema_type: SchemaType) -> Self { + Self { + schema_type, + min_length: None, + max_length: None, + pattern: None, + minimum: None, + maximum: None, + exclusive_minimum: None, + exclusive_maximum: None, + multiple_of: None, + required: Vec::new(), + properties: HashMap::new(), + items: None, + } + } + + pub fn set_schema_type(mut self, schema_type: SchemaType) -> Self { + self.schema_type = schema_type; + self + } + + pub fn set_min_length(mut self, min_length: usize) -> Self { + self.min_length = Some(min_length); + self + } + + pub fn set_max_length(mut self, max_length: usize) -> Self { + self.max_length = Some(max_length); + self + } + + pub fn set_pattern(mut self, pattern: String) -> Self { + self.pattern = Some(pattern); + self + } + + pub fn set_minimum(mut self, minimum: f64) -> Self { + self.minimum = Some(minimum); + self + } + + pub fn set_maximum(mut self, maximum: f64) -> Self { + self.maximum = Some(maximum); + self + } + + pub fn set_exclusive_minimum(mut self, exclusive_minimum: f64) -> Self { + self.exclusive_minimum = Some(exclusive_minimum); + self + } + + pub fn set_exclusive_maximum(mut self, exclusive_maximum: f64) -> Self { + self.exclusive_maximum = Some(exclusive_maximum); + self + } + + pub fn set_multiple_of(mut self, multiple_of: f64) -> Self { + self.multiple_of = Some(multiple_of); + self + } + + pub fn add_required(mut self, required: String) -> Self { + self.required.push(required); + self + } + + pub fn add_property(mut self, key: String, value: SchemaValidator) -> Self { + self.properties.insert(key, Box::new(value)); + self + } + + pub fn set_items(mut self, items: Box) -> Self { + self.items = Some(items); + self + } + pub fn validate(&self, value: &Value) -> Result<()> { match self.schema_type { SchemaType::String => self.validate_string(value), diff --git a/src/lib.rs b/src/lib.rs index e148bb6..52f5bd5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ pub mod core; // pub mod holder; -mod json_schema_validation; +pub mod json_schema_validation; pub mod presentation_exchange; mod utils; pub mod verifier; diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 8a3c901..d51d164 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -7,13 +7,22 @@ use crate::{ }; use anyhow::{bail, Context, Result}; +use did_method_key::DIDKey; use serde::{Deserialize, Serialize}; use serde_json::Map; -use ssi_claims::jwt::VerifiablePresentation; -use ssi_dids::ssi_json_ld::{ - object::value::FragmentRef, - syntax::{from_value, Value}, +use ssi_claims::{ + jwt::{AnyRegisteredClaim, Issuer, RegisteredClaim, VerifiablePresentation}, + CompactJWSString, VerificationParameters, }; +use ssi_dids::{ + ssi_json_ld::{ + object::value::FragmentRef, + syntax::{from_value, Value}, + }, + VerificationMethodDIDResolver, DIDJWK, +}; +use ssi_jwk::JWK; +use ssi_verification_methods::AnyJwkMethod; /// A JSONPath is a string that represents a path to a specific value within a JSON object. /// @@ -324,7 +333,7 @@ impl PresentationDefinition { /// /// Checks the underlying presentation submission parsed from the authorization response, /// against the input descriptors of the presentation definition. - pub fn validate_authorization_response( + pub async fn validate_authorization_response( &self, auth_response: &AuthorizationResponse, ) -> Result<()> { @@ -335,8 +344,25 @@ impl PresentationDefinition { AuthorizationResponse::Unencoded(response) => { let presentation_submission = response.presentation_submission().parsed(); + let jwt = response.vp_token().0.clone(); + + // TODO: Verify the JWT. + // let jws = CompactJWSString::from_string(jwt.clone()).context("Invalid JWT.")?; + // let resolver: VerificationMethodDIDResolver = + // VerificationMethodDIDResolver::new(DIDKey); + // let params = VerificationParameters::from_resolver(resolver); + + // if let Err(e) = jws.verify(params).await { + // bail!("JWT Verification Failed: {:?}", e) + // } + let verifiable_presentation: VerifiablePresentation = - ssi_claims::jwt::decode_unverified(&response.vp_token().0)?; + ssi_claims::jwt::decode_unverified(&jwt)?; + + // let holder: Option = + // Issuer::extract(AnyRegisteredClaim::from(verifiable_presentation.clone())); + + // println!("Holder: {:?}", holder); // Ensure the definition id matches the submission's definition id. if presentation_submission.definition_id() != self.id() { @@ -484,18 +510,32 @@ impl InputDescriptor { 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")?; - // 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.") - } + if let Some(ConstraintsLimitDisclosure::Required) = self.constraints().limit_disclosure() { + if self.constraints().fields().is_none() { + bail!("Required limit disclosure must have fields.") + } + }; if let Some(field_constraints) = self.constraints().fields() { for constraint_field in field_constraints.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. @@ -507,28 +547,54 @@ impl InputDescriptor { .first() .ok_or(anyhow::anyhow!("Root element not found."))?; + println!("Root element: {:?}", root_element); + let mut map_selector = jsonpath_lib::selector(root_element); for field_path in constraint_field.path().iter() { - let Ok(field_elements) = map_selector(field_path) else { + println!("Field path: {:?}", field_path); + + 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() { + if let Some(ConstraintsLimitDisclosure::Required) = + self.constraints().limit_disclosure() + { + bail!("Field elements are empty while limit disclosure is required.") + } + // 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; + } - // bail!("Failed to select field element from verifiable presentation."); - }; + println!("Field elements: {:?}", field_elements); if let Some(filter) = constraint_field.filter() { // TODO: possible trace a warning if a field is not valid. // TODO: Check the predicate feature value. - let validated_fields = field_elements - .iter() - .find(|element| filter.validate(element).is_ok()); - - if validated_fields.is_none() && !constraint_field.optional.unwrap_or(false) - { - bail!("Field did not pass filter validation."); + let validated_fields = + field_elements + .iter() + .find(|element| match filter.validate(element) { + Err(e) => { + println!("Field did not pass filter validation: {}", e); + 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."); + } } } @@ -638,6 +704,8 @@ impl From> for ConstraintsField { 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 { @@ -764,9 +832,14 @@ impl ConstraintsField { } /// Return the optional value of the constraints field. - pub fn optional(&self) -> bool { + 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() + } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] diff --git a/src/verifier/mod.rs b/src/verifier/mod.rs index 4a49db0..dd0c716 100644 --- a/src/verifier/mod.rs +++ b/src/verifier/mod.rs @@ -119,12 +119,12 @@ impl Verifier { validator_function: F, ) -> Result<()> where - F: FnOnce(Session, AuthorizationResponse) -> Result>>, - Fut: Future, + F: FnOnce(Session, AuthorizationResponse) -> Fut, + Fut: Future>, { let session = self.session_store.get_session(reference).await?; - let outcome = validator_function(session, authorization_response)?.await; + let outcome = validator_function(session, authorization_response).await?; self.session_store .update_status(reference, Status::Complete(outcome)) diff --git a/tests/e2e.rs b/tests/e2e.rs index 70dceb8..84f5c08 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -1,3 +1,4 @@ +use oid4vp::json_schema_validation::{SchemaType, SchemaValidator}; use oid4vp::presentation_exchange::*; use oid4vp::{ @@ -23,11 +24,17 @@ async fn w3c_vc_did_client_direct_post() { "did-key-id-proof".into(), InputDescriptor::new( "did-key-id".into(), - Constraints::new().add_constraint( - ConstraintsField::new("$.vp.verifiableCredential.credentialSubject.id".into()) + Constraints::new() + .add_constraint( + ConstraintsField::new( + "$.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_purpose("Check whether your identity key has been verified.".into()) + .set_filter(SchemaValidator::new(SchemaType::String)) + .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()) diff --git a/tests/jwt_vc.rs b/tests/jwt_vc.rs index 793955e..e158b25 100644 --- a/tests/jwt_vc.rs +++ b/tests/jwt_vc.rs @@ -159,15 +159,16 @@ impl AsyncHttpClient for MockHttpClient { id.parse().context("failed to parse id")?, AuthorizationResponse::from_x_www_form_urlencoded(body) .context("failed to parse authorization response request")?, - |session, auth_response| { + |session, auth_response| async move { session .presentation_definition - .validate_authorization_response(&auth_response)?; + .validate_authorization_response(&auth_response) + .await?; println!("Session: {:?}", session); println!("Auth Response: {:?}", auth_response); - Ok(Box::pin(async { Outcome::Success })) + Ok(Outcome::Success) }, ) .await?; From cd22624a8aab7ddbb156065fa242e8f5b58f2f5f Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Wed, 14 Aug 2024 09:37:40 -0700 Subject: [PATCH 27/71] ensure json schema validator adheres to the specification Signed-off-by: Ryan Tate --- src/json_schema_validation.rs | 312 ++++++++++++++++++++++++++++++---- 1 file changed, 275 insertions(+), 37 deletions(-) diff --git a/src/json_schema_validation.rs b/src/json_schema_validation.rs index 9324c6e..7f21969 100644 --- a/src/json_schema_validation.rs +++ b/src/json_schema_validation.rs @@ -4,6 +4,19 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; +use crate::utils::NonEmptyVec; + +/// The value of this keyword MUST be either a string or an array. If it is an array, +/// elements of the array MUST be strings and MUST be unique. +/// +/// String values MUST be one of the six primitive types +/// ("null", "boolean", "object", "array", "number", or "string"), or "integer" +/// which matches any number with a zero fractional part. +/// +/// If the value of "type" is a string, then an instance validates successfully if its +/// type matches the type represented by the value of the string. If the value of "type" +/// is an array, then an instance validates successfully if its type matches any +/// of the types indicated by the strings in the array. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum SchemaType { @@ -45,12 +58,28 @@ pub struct SchemaValidator { exclusive_maximum: Option, #[serde(rename = "multipleOf", skip_serializing_if = "Option::is_none")] multiple_of: Option, - #[serde(skip_serializing_if = "Vec::is_empty", default)] - required: Vec, - #[serde(skip_serializing_if = "HashMap::is_empty", default)] - properties: HashMap>, #[serde(skip_serializing_if = "Option::is_none")] - items: Option>, + required: Option>, + #[serde(rename = "dependentRequired", skip_serializing_if = "Option::is_none")] + dependent_required: Option>>, + #[serde(rename = "maxProperties", skip_serializing_if = "Option::is_none")] + max_properties: Option, + #[serde(rename = "minProperties", skip_serializing_if = "Option::is_none")] + min_properties: Option, + #[serde(rename = "maxItems", skip_serializing_if = "Option::is_none")] + max_items: Option, + #[serde(rename = "minItems", skip_serializing_if = "Option::is_none")] + min_items: Option, + #[serde(rename = "uniqueItems", skip_serializing_if = "Option::is_none")] + unique_items: Option, + #[serde(rename = "maxContains", skip_serializing_if = "Option::is_none")] + max_contains: Option, + #[serde(rename = "minContains", skip_serializing_if = "Option::is_none")] + min_contains: Option, + #[serde(rename = "const", skip_serializing_if = "Option::is_none")] + r#const: Option, + #[serde(rename = "enum", skip_serializing_if = "Option::is_none")] + r#enum: Option>, } impl PartialEq for SchemaValidator { @@ -62,8 +91,18 @@ impl PartialEq for SchemaValidator { && self.minimum == other.minimum && self.maximum == other.maximum && self.required == other.required - && self.properties == other.properties - && self.items == other.items + && self.dependent_required == other.dependent_required + && self.max_properties == other.max_properties + && self.min_properties == other.min_properties + && self.max_items == other.max_items + && self.min_items == other.min_items + && self.unique_items == other.unique_items + && self.min_contains == other.min_contains + && self.max_contains == other.max_contains + && self.exclusive_minimum == other.exclusive_minimum + && self.exclusive_maximum == other.exclusive_maximum + && self.multiple_of == other.multiple_of + && self.r#const == other.r#const } } @@ -71,6 +110,8 @@ impl Eq for SchemaValidator {} impl SchemaValidator { /// Creates a new schema validator with the given schema type. + /// + /// A schema validator must have a schema type. pub fn new(schema_type: SchemaType) -> Self { Self { schema_type, @@ -82,73 +123,250 @@ impl SchemaValidator { exclusive_minimum: None, exclusive_maximum: None, multiple_of: None, - required: Vec::new(), - properties: HashMap::new(), - items: None, + required: None, + dependent_required: None, + max_properties: None, + min_properties: None, + max_items: None, + min_items: None, + unique_items: None, + min_contains: None, + max_contains: None, + r#const: None, + r#enum: None, } } - pub fn set_schema_type(mut self, schema_type: SchemaType) -> Self { - self.schema_type = schema_type; - self - } - + /// The value of this keyword MUST be a non-negative integer. + /// + /// A string instance is valid against this keyword if its length is greater than, or equal to, the value of this keyword. + /// + /// The length of a string instance is defined as the number of its characters as defined by RFC 8259 [RFC8259]. + /// + /// Omitting this keyword has the same behavior as a value of 0. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.2](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.2) pub fn set_min_length(mut self, min_length: usize) -> Self { self.min_length = Some(min_length); self } + /// The value of this keyword MUST be a non-negative integer. + /// + /// A string instance is valid against this keyword if its length is less than, or equal to, the value of this keyword. + /// + /// The length of a string instance is defined as the number of its characters as defined by RFC 8259 [RFC8259]. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.1](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.1) pub fn set_max_length(mut self, max_length: usize) -> Self { self.max_length = Some(max_length); self } + /// The value of this keyword MUST be a string. This string SHOULD be a valid regular expression, according to the ECMA-262 regular expression dialect. + + // A string instance is considered valid if the regular expression matches the instance successfully. + // Recall: regular expressions are not implicitly anchored. + // + // See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.3](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.3) pub fn set_pattern(mut self, pattern: String) -> Self { self.pattern = Some(pattern); self } + /// The value of "minimum" MUST be a number, representing an inclusive lower limit for a numeric instance. + /// + /// If the instance is a number, then this keyword validates only if the instance is greater than or exactly equal to "minimum". + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.4](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.4) pub fn set_minimum(mut self, minimum: f64) -> Self { self.minimum = Some(minimum); self } + /// The value of "maximum" MUST be a number, representing an inclusive upper limit for a numeric instance. + /// + /// If the instance is a number, then this keyword validates only if the instance is less than or exactly equal to "maximum". + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.2](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.2) pub fn set_maximum(mut self, maximum: f64) -> Self { self.maximum = Some(maximum); self } + /// The value of "exclusiveMinimum" MUST be a number, representing an exclusive lower limit for a numeric instance. + /// + /// If the instance is a number, then the instance is valid only if it has a value strictly greater than (not equal to) "exclusiveMinimum". + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.5](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.5) pub fn set_exclusive_minimum(mut self, exclusive_minimum: f64) -> Self { self.exclusive_minimum = Some(exclusive_minimum); self } + /// The value of "exclusiveMaximum" MUST be a number, representing an exclusive upper limit for a numeric instance. + /// + /// If the instance is a number, then the instance is valid only if it has a value strictly less than (not equal to) "exclusiveMaximum". + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.3](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.3) pub fn set_exclusive_maximum(mut self, exclusive_maximum: f64) -> Self { self.exclusive_maximum = Some(exclusive_maximum); self } + /// The value of "exclusiveMinimum" MUST be a number, representing an exclusive lower limit for a numeric instance. + /// + /// If the instance is a number, then the instance is valid only if it has a value strictly greater than (not equal to) "exclusiveMinimum". + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.1](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.1) pub fn set_multiple_of(mut self, multiple_of: f64) -> Self { self.multiple_of = Some(multiple_of); self } - pub fn add_required(mut self, required: String) -> Self { - self.required.push(required); + /// The value of this keyword MUST be an array. Elements of this array, if any, MUST be strings, and MUST be unique. + /// + /// An object instance is valid against this keyword if every item in the array is the name of a property in the instance. + /// + /// Omitting this keyword has the same behavior as an empty array. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.5.3](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.5.3) + pub fn set_required(mut self, required: Vec) -> Self { + self.required = Some(required); + self + } + + /// In addition to [SchemaValidator::set_required], push a single requirement to the list of required properties. + pub fn add_requirement(mut self, requirement: String) -> Self { + self.required.get_or_insert_with(Vec::new).push(requirement); + self + } + + /// The value of this keyword MUST be a non-negative integer. + /// + /// An object instance is valid against "maxProperties" if its number of properties is less than, or equal to, the value of this keyword. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.5.1](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.5.1) + pub fn set_max_properties(mut self, max_properties: usize) -> Self { + self.max_properties = Some(max_properties); + self + } + + /// The value of this keyword MUST be a non-negative integer. + /// + /// An object instance is valid against "minProperties" if its number of properties is greater than, or equal to, the value of this keyword. + /// + /// Omitting this keyword has the same behavior as a value of 0. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.5.2](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.5.2) + pub fn set_min_properties(mut self, min_properties: usize) -> Self { + self.min_properties = Some(min_properties); + self + } + + /// The value of this keyword MUST be a non-negative integer. + /// + /// An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.1](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.1) + pub fn set_max_items(mut self, max_items: usize) -> Self { + self.max_items = Some(max_items); + self + } + + /// The value of this keyword MUST be a non-negative integer. + /// + /// An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. + /// + /// Omitting this keyword has the same behavior as a value of 0. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.2](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.2) + pub fn set_min_items(mut self, min_items: usize) -> Self { + self.min_items = Some(min_items); + self + } + + /// The value of this keyword MUST be a boolean. + /// + /// If this keyword has boolean value false, the instance validates successfully. + /// If it has boolean value true, the instance validates successfully if all of its elements are unique. + /// + /// Omitting this keyword has the same behavior as a value of false. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.3](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.3) + pub fn set_unique_items(mut self, unique_items: bool) -> Self { + self.unique_items = Some(unique_items); self } - pub fn add_property(mut self, key: String, value: SchemaValidator) -> Self { - self.properties.insert(key, Box::new(value)); + /// The value of this keyword MUST be a non-negative integer. + /// + /// If "contains" is not present within the same schema object, then this keyword has no effect. + /// + /// An instance array is valid against "maxContains" in two ways, depending on the form of the + /// annotation result of an adjacent "contains" [json-schema] keyword. The first way is if the + /// annotation result is an array and the length of that array is less than or equal to the "maxContains" + /// value. The second way is if the annotation result is a boolean "true" and the instance array length + /// is less than or equal to the "maxContains" value. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.4](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.4) + pub fn set_max_contains(mut self, max: usize) -> Self { + self.max_contains = Some(max); self } - pub fn set_items(mut self, items: Box) -> Self { - self.items = Some(items); + /// The value of this keyword MUST be a non-negative integer. + /// + /// If "contains" is not present within the same schema object, then this keyword has no effect. + /// + /// An instance array is valid against "minContains" in two ways, depending on the form of the annotation result of an adjacent "contains" [json-schema] keyword. The first way is if the annotation result is an array and the length of that array is greater than or equal to the "minContains" value. The second way is if the annotation result is a boolean "true" and the instance array length is greater than or equal to the "minContains" value. + /// + /// A value of 0 is allowed, but is only useful for setting a range of occurrences from 0 to the value of "maxContains". A value of 0 causes "minContains" and "contains" to always pass validation (but validation can still fail against a "maxContains" keyword). + /// + /// Omitting this keyword has the same behavior as a value of 1. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.5](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.5) + pub fn set_min_contains(mut self, min: usize) -> Self { + self.min_contains = Some(min); self } + /// The value of this keyword MUST be an array. This array SHOULD have at least one element. Elements in the array SHOULD be unique. + /// + /// An instance validates successfully against this keyword if its value is equal to one of the elements in this keyword's array value. + /// + /// Elements in the array might be of any type, including null. + /// + /// https://json-schema.org/draft/2020-12/json-schema-validation#section-6.1.2 + pub fn set_enum(mut self, r#enum: NonEmptyVec) -> Self { + self.r#enum = Some(r#enum); + self + } + + /// The value of this keyword MAY be of any type, including null. + /// Use of this keyword is functionally equivalent to an "enum" (Section 6.1.2) with a single value. + /// An instance validates successfully against this keyword if its value is equal to the value of the keyword. + pub fn set_const(mut self, r#const: Value) -> Self { + self.r#const = Some(r#const); + self + } + + /// Primary method for validating a JSON value against the schema. pub fn validate(&self, value: &Value) -> Result<()> { + // Check input against const, if it exists. + if let Some(const_value) = self.r#const.as_ref() { + if value != const_value { + bail!("Value does not match const"); + } + } + + // Check input against enum, if it exists. + if let Some(enum_values) = self.r#enum.as_ref() { + if !enum_values.contains(value) { + bail!("Value does not match enum"); + } + } + match self.schema_type { SchemaType::String => self.validate_string(value), SchemaType::Number => self.validate_number(value), @@ -311,31 +529,51 @@ impl SchemaValidator { } } - if let Some(item_validator) = &self.items { - for (index, item) in arr.iter().enumerate() { - item_validator - .validate(item) - .context(format!("Error in array item {}", index))?; - } - } - Ok(()) } pub fn validate_object(&self, value: &Value) -> Result<()> { let obj = value.as_object().context("Expected an object")?; - for required_prop in &self.required { - if !obj.contains_key(required_prop) { - bail!("Missing required property: {}", required_prop); + if let Some(required) = &self.required { + for required_prop in required { + if !obj.contains_key(required_prop) { + bail!("Missing required property: {}", required_prop); + } + } + } + + if let Some(min_properties) = self.min_properties { + if obj.len() >= min_properties { + bail!( + "Object has fewer properties {} than minimum {}", + obj.len(), + min_properties + ); + } + } + + if let Some(max_properties) = self.max_properties { + if obj.len() <= max_properties { + bail!( + "Object has more properties {} than maximum {}", + obj.len(), + max_properties + ); } } - for (prop_name, prop_validator) in &self.properties { - if let Some(prop_value) = obj.get(prop_name) { - prop_validator - .validate(prop_value) - .context(format!("Error in property {}", prop_name))?; + if let Some(dependents_required) = &self.dependent_required { + for (prop, dependents) in dependents_required { + if let Some(obj) = obj.get(prop) { + let child = obj.as_object().context("Expected an object")?; + + for dependent in dependents { + if !child.contains_key(dependent) { + bail!("Dependent property {} is required", dependent); + } + } + } } } From 26dda42e67989cf5746f472cfd939183a1a44025 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Wed, 14 Aug 2024 09:38:37 -0700 Subject: [PATCH 28/71] ensure json schema validator adheres to the specification Signed-off-by: Ryan Tate --- src/json_schema_validation.rs | 441 +++++++++++++++++++++++++++++++--- src/lib.rs | 2 +- 2 files changed, 410 insertions(+), 33 deletions(-) diff --git a/src/json_schema_validation.rs b/src/json_schema_validation.rs index 298474f..7f21969 100644 --- a/src/json_schema_validation.rs +++ b/src/json_schema_validation.rs @@ -4,6 +4,19 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; +use crate::utils::NonEmptyVec; + +/// The value of this keyword MUST be either a string or an array. If it is an array, +/// elements of the array MUST be strings and MUST be unique. +/// +/// String values MUST be one of the six primitive types +/// ("null", "boolean", "object", "array", "number", or "string"), or "integer" +/// which matches any number with a zero fractional part. +/// +/// If the value of "type" is a string, then an instance validates successfully if its +/// type matches the type represented by the value of the string. If the value of "type" +/// is an array, then an instance validates successfully if its type matches any +/// of the types indicated by the strings in the array. #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum SchemaType { @@ -21,14 +34,17 @@ pub enum SchemaType { /// /// For more information, see the field constraints filter property: /// -/// https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object +/// - [https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object](https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-descriptor-object) +/// +/// - [https://json-schema.org/understanding-json-schema](https://json-schema.org/understanding-json-schema) +/// #[derive(Debug, Clone, Deserialize, Serialize)] pub struct SchemaValidator { #[serde(rename = "type")] schema_type: SchemaType, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "minLength", skip_serializing_if = "Option::is_none")] min_length: Option, - #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "maxLength", skip_serializing_if = "Option::is_none")] max_length: Option, #[serde(skip_serializing_if = "Option::is_none")] pattern: Option, @@ -36,12 +52,34 @@ pub struct SchemaValidator { minimum: Option, #[serde(skip_serializing_if = "Option::is_none")] maximum: Option, - #[serde(skip_serializing_if = "Vec::is_empty", default)] - required: Vec, - #[serde(skip_serializing_if = "HashMap::is_empty", default)] - properties: HashMap>, + #[serde(rename = "exclusiveMinimum", skip_serializing_if = "Option::is_none")] + exclusive_minimum: Option, + #[serde(rename = "exclusiveMaximum", skip_serializing_if = "Option::is_none")] + exclusive_maximum: Option, + #[serde(rename = "multipleOf", skip_serializing_if = "Option::is_none")] + multiple_of: Option, #[serde(skip_serializing_if = "Option::is_none")] - items: Option>, + required: Option>, + #[serde(rename = "dependentRequired", skip_serializing_if = "Option::is_none")] + dependent_required: Option>>, + #[serde(rename = "maxProperties", skip_serializing_if = "Option::is_none")] + max_properties: Option, + #[serde(rename = "minProperties", skip_serializing_if = "Option::is_none")] + min_properties: Option, + #[serde(rename = "maxItems", skip_serializing_if = "Option::is_none")] + max_items: Option, + #[serde(rename = "minItems", skip_serializing_if = "Option::is_none")] + min_items: Option, + #[serde(rename = "uniqueItems", skip_serializing_if = "Option::is_none")] + unique_items: Option, + #[serde(rename = "maxContains", skip_serializing_if = "Option::is_none")] + max_contains: Option, + #[serde(rename = "minContains", skip_serializing_if = "Option::is_none")] + min_contains: Option, + #[serde(rename = "const", skip_serializing_if = "Option::is_none")] + r#const: Option, + #[serde(rename = "enum", skip_serializing_if = "Option::is_none")] + r#enum: Option>, } impl PartialEq for SchemaValidator { @@ -53,15 +91,282 @@ impl PartialEq for SchemaValidator { && self.minimum == other.minimum && self.maximum == other.maximum && self.required == other.required - && self.properties == other.properties - && self.items == other.items + && self.dependent_required == other.dependent_required + && self.max_properties == other.max_properties + && self.min_properties == other.min_properties + && self.max_items == other.max_items + && self.min_items == other.min_items + && self.unique_items == other.unique_items + && self.min_contains == other.min_contains + && self.max_contains == other.max_contains + && self.exclusive_minimum == other.exclusive_minimum + && self.exclusive_maximum == other.exclusive_maximum + && self.multiple_of == other.multiple_of + && self.r#const == other.r#const } } impl Eq for SchemaValidator {} impl SchemaValidator { + /// Creates a new schema validator with the given schema type. + /// + /// A schema validator must have a schema type. + pub fn new(schema_type: SchemaType) -> Self { + Self { + schema_type, + min_length: None, + max_length: None, + pattern: None, + minimum: None, + maximum: None, + exclusive_minimum: None, + exclusive_maximum: None, + multiple_of: None, + required: None, + dependent_required: None, + max_properties: None, + min_properties: None, + max_items: None, + min_items: None, + unique_items: None, + min_contains: None, + max_contains: None, + r#const: None, + r#enum: None, + } + } + + /// The value of this keyword MUST be a non-negative integer. + /// + /// A string instance is valid against this keyword if its length is greater than, or equal to, the value of this keyword. + /// + /// The length of a string instance is defined as the number of its characters as defined by RFC 8259 [RFC8259]. + /// + /// Omitting this keyword has the same behavior as a value of 0. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.2](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.2) + pub fn set_min_length(mut self, min_length: usize) -> Self { + self.min_length = Some(min_length); + self + } + + /// The value of this keyword MUST be a non-negative integer. + /// + /// A string instance is valid against this keyword if its length is less than, or equal to, the value of this keyword. + /// + /// The length of a string instance is defined as the number of its characters as defined by RFC 8259 [RFC8259]. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.1](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.1) + pub fn set_max_length(mut self, max_length: usize) -> Self { + self.max_length = Some(max_length); + self + } + + /// The value of this keyword MUST be a string. This string SHOULD be a valid regular expression, according to the ECMA-262 regular expression dialect. + + // A string instance is considered valid if the regular expression matches the instance successfully. + // Recall: regular expressions are not implicitly anchored. + // + // See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.3](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.3) + pub fn set_pattern(mut self, pattern: String) -> Self { + self.pattern = Some(pattern); + self + } + + /// The value of "minimum" MUST be a number, representing an inclusive lower limit for a numeric instance. + /// + /// If the instance is a number, then this keyword validates only if the instance is greater than or exactly equal to "minimum". + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.4](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.4) + pub fn set_minimum(mut self, minimum: f64) -> Self { + self.minimum = Some(minimum); + self + } + + /// The value of "maximum" MUST be a number, representing an inclusive upper limit for a numeric instance. + /// + /// If the instance is a number, then this keyword validates only if the instance is less than or exactly equal to "maximum". + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.2](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.2) + pub fn set_maximum(mut self, maximum: f64) -> Self { + self.maximum = Some(maximum); + self + } + + /// The value of "exclusiveMinimum" MUST be a number, representing an exclusive lower limit for a numeric instance. + /// + /// If the instance is a number, then the instance is valid only if it has a value strictly greater than (not equal to) "exclusiveMinimum". + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.5](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.5) + pub fn set_exclusive_minimum(mut self, exclusive_minimum: f64) -> Self { + self.exclusive_minimum = Some(exclusive_minimum); + self + } + + /// The value of "exclusiveMaximum" MUST be a number, representing an exclusive upper limit for a numeric instance. + /// + /// If the instance is a number, then the instance is valid only if it has a value strictly less than (not equal to) "exclusiveMaximum". + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.3](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.3) + pub fn set_exclusive_maximum(mut self, exclusive_maximum: f64) -> Self { + self.exclusive_maximum = Some(exclusive_maximum); + self + } + + /// The value of "exclusiveMinimum" MUST be a number, representing an exclusive lower limit for a numeric instance. + /// + /// If the instance is a number, then the instance is valid only if it has a value strictly greater than (not equal to) "exclusiveMinimum". + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.1](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.2.1) + pub fn set_multiple_of(mut self, multiple_of: f64) -> Self { + self.multiple_of = Some(multiple_of); + self + } + + /// The value of this keyword MUST be an array. Elements of this array, if any, MUST be strings, and MUST be unique. + /// + /// An object instance is valid against this keyword if every item in the array is the name of a property in the instance. + /// + /// Omitting this keyword has the same behavior as an empty array. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.5.3](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.5.3) + pub fn set_required(mut self, required: Vec) -> Self { + self.required = Some(required); + self + } + + /// In addition to [SchemaValidator::set_required], push a single requirement to the list of required properties. + pub fn add_requirement(mut self, requirement: String) -> Self { + self.required.get_or_insert_with(Vec::new).push(requirement); + self + } + + /// The value of this keyword MUST be a non-negative integer. + /// + /// An object instance is valid against "maxProperties" if its number of properties is less than, or equal to, the value of this keyword. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.5.1](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.5.1) + pub fn set_max_properties(mut self, max_properties: usize) -> Self { + self.max_properties = Some(max_properties); + self + } + + /// The value of this keyword MUST be a non-negative integer. + /// + /// An object instance is valid against "minProperties" if its number of properties is greater than, or equal to, the value of this keyword. + /// + /// Omitting this keyword has the same behavior as a value of 0. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.5.2](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.5.2) + pub fn set_min_properties(mut self, min_properties: usize) -> Self { + self.min_properties = Some(min_properties); + self + } + + /// The value of this keyword MUST be a non-negative integer. + /// + /// An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.1](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.1) + pub fn set_max_items(mut self, max_items: usize) -> Self { + self.max_items = Some(max_items); + self + } + + /// The value of this keyword MUST be a non-negative integer. + /// + /// An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. + /// + /// Omitting this keyword has the same behavior as a value of 0. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.2](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.2) + pub fn set_min_items(mut self, min_items: usize) -> Self { + self.min_items = Some(min_items); + self + } + + /// The value of this keyword MUST be a boolean. + /// + /// If this keyword has boolean value false, the instance validates successfully. + /// If it has boolean value true, the instance validates successfully if all of its elements are unique. + /// + /// Omitting this keyword has the same behavior as a value of false. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.3](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.3) + pub fn set_unique_items(mut self, unique_items: bool) -> Self { + self.unique_items = Some(unique_items); + self + } + + /// The value of this keyword MUST be a non-negative integer. + /// + /// If "contains" is not present within the same schema object, then this keyword has no effect. + /// + /// An instance array is valid against "maxContains" in two ways, depending on the form of the + /// annotation result of an adjacent "contains" [json-schema] keyword. The first way is if the + /// annotation result is an array and the length of that array is less than or equal to the "maxContains" + /// value. The second way is if the annotation result is a boolean "true" and the instance array length + /// is less than or equal to the "maxContains" value. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.4](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.4) + pub fn set_max_contains(mut self, max: usize) -> Self { + self.max_contains = Some(max); + self + } + + /// The value of this keyword MUST be a non-negative integer. + /// + /// If "contains" is not present within the same schema object, then this keyword has no effect. + /// + /// An instance array is valid against "minContains" in two ways, depending on the form of the annotation result of an adjacent "contains" [json-schema] keyword. The first way is if the annotation result is an array and the length of that array is greater than or equal to the "minContains" value. The second way is if the annotation result is a boolean "true" and the instance array length is greater than or equal to the "minContains" value. + /// + /// A value of 0 is allowed, but is only useful for setting a range of occurrences from 0 to the value of "maxContains". A value of 0 causes "minContains" and "contains" to always pass validation (but validation can still fail against a "maxContains" keyword). + /// + /// Omitting this keyword has the same behavior as a value of 1. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.5](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.5) + pub fn set_min_contains(mut self, min: usize) -> Self { + self.min_contains = Some(min); + self + } + + /// The value of this keyword MUST be an array. This array SHOULD have at least one element. Elements in the array SHOULD be unique. + /// + /// An instance validates successfully against this keyword if its value is equal to one of the elements in this keyword's array value. + /// + /// Elements in the array might be of any type, including null. + /// + /// https://json-schema.org/draft/2020-12/json-schema-validation#section-6.1.2 + pub fn set_enum(mut self, r#enum: NonEmptyVec) -> Self { + self.r#enum = Some(r#enum); + self + } + + /// The value of this keyword MAY be of any type, including null. + /// Use of this keyword is functionally equivalent to an "enum" (Section 6.1.2) with a single value. + /// An instance validates successfully against this keyword if its value is equal to the value of the keyword. + pub fn set_const(mut self, r#const: Value) -> Self { + self.r#const = Some(r#const); + self + } + + /// Primary method for validating a JSON value against the schema. pub fn validate(&self, value: &Value) -> Result<()> { + // Check input against const, if it exists. + if let Some(const_value) = self.r#const.as_ref() { + if value != const_value { + bail!("Value does not match const"); + } + } + + // Check input against enum, if it exists. + if let Some(enum_values) = self.r#enum.as_ref() { + if !enum_values.contains(value) { + bail!("Value does not match enum"); + } + } + match self.schema_type { SchemaType::String => self.validate_string(value), SchemaType::Number => self.validate_number(value), @@ -76,7 +381,7 @@ impl SchemaValidator { let s = value.as_str().context("Expected a string")?; if let Some(min_length) = self.min_length { - if s.len() < min_length { + if s.len() <= min_length { bail!( "String length {} is less than minimum {}", s.len(), @@ -86,7 +391,7 @@ impl SchemaValidator { } if let Some(max_length) = self.max_length { - if s.len() > max_length { + if s.len() >= max_length { bail!( "String length {} is greater than maximum {}", s.len(), @@ -110,17 +415,43 @@ impl SchemaValidator { let n = value.as_f64().context("Expected a number")?; if let Some(minimum) = self.minimum { - if n < minimum { + if n <= minimum { bail!("Number {} is less than minimum {}", n, minimum); } } if let Some(maximum) = self.maximum { - if n > maximum { + if n >= maximum { bail!("Number {} is greater than maximum {}", n, maximum); } } + if let Some(exclusive_minimum) = self.exclusive_minimum { + if n < exclusive_minimum { + bail!( + "Number {} is less than or equal to exclusive minimum {}", + n, + exclusive_minimum + ); + } + } + + if let Some(exclusive_maximum) = self.exclusive_maximum { + if n > exclusive_maximum { + bail!( + "Number {} is greater than or equal to exclusive maximum {}", + n, + exclusive_maximum + ); + } + } + + if let Some(multiple_of) = self.multiple_of { + if n % multiple_of != 0.0 { + bail!("Number {} is not a multiple of {}", n, multiple_of); + } + } + Ok(()) } @@ -128,17 +459,43 @@ impl SchemaValidator { let n = value.as_i64().context("Expected an integer")?; if let Some(minimum) = self.minimum { - if (n as f64) < minimum { + if n <= minimum as i64 { bail!("Integer {} is less than minimum {}", n, minimum); } } if let Some(maximum) = self.maximum { - if n as f64 > maximum { + if n >= maximum as i64 { bail!("Integer {} is greater than maximum {}", n, maximum); } } + if let Some(exclusive_minimum) = self.exclusive_minimum { + if n < exclusive_minimum as i64 { + bail!( + "Integer {} is less than or equal to exclusive minimum {}", + n, + exclusive_minimum + ); + } + } + + if let Some(exclusive_maximum) = self.exclusive_maximum { + if n > exclusive_maximum as i64 { + bail!( + "Integer {} is greater than or equal to exclusive maximum {}", + n, + exclusive_maximum + ); + } + } + + if let Some(multiple_of) = self.multiple_of { + if n % multiple_of as i64 != 0 { + bail!("Integer {} is not a multiple of {}", n, multiple_of); + } + } + Ok(()) } @@ -172,31 +529,51 @@ impl SchemaValidator { } } - if let Some(item_validator) = &self.items { - for (index, item) in arr.iter().enumerate() { - item_validator - .validate(item) - .context(format!("Error in array item {}", index))?; - } - } - Ok(()) } pub fn validate_object(&self, value: &Value) -> Result<()> { let obj = value.as_object().context("Expected an object")?; - for required_prop in &self.required { - if !obj.contains_key(required_prop) { - bail!("Missing required property: {}", required_prop); + if let Some(required) = &self.required { + for required_prop in required { + if !obj.contains_key(required_prop) { + bail!("Missing required property: {}", required_prop); + } } } - for (prop_name, prop_validator) in &self.properties { - if let Some(prop_value) = obj.get(prop_name) { - prop_validator - .validate(prop_value) - .context(format!("Error in property {}", prop_name))?; + if let Some(min_properties) = self.min_properties { + if obj.len() >= min_properties { + bail!( + "Object has fewer properties {} than minimum {}", + obj.len(), + min_properties + ); + } + } + + if let Some(max_properties) = self.max_properties { + if obj.len() <= max_properties { + bail!( + "Object has more properties {} than maximum {}", + obj.len(), + max_properties + ); + } + } + + if let Some(dependents_required) = &self.dependent_required { + for (prop, dependents) in dependents_required { + if let Some(obj) = obj.get(prop) { + let child = obj.as_object().context("Expected an object")?; + + for dependent in dependents { + if !child.contains_key(dependent) { + bail!("Dependent property {} is required", dependent); + } + } + } } } diff --git a/src/lib.rs b/src/lib.rs index 0279f15..b9f4fc4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ pub mod core; -mod json_schema_validation; +pub mod json_schema_validation; pub mod presentation_exchange; mod utils; pub mod verifier; From d188434954c571953cd572c547e070e8e5524d51 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Wed, 14 Aug 2024 12:05:55 -0700 Subject: [PATCH 29/71] add unit tests for schema validator Signed-off-by: Ryan Tate --- src/json_schema_validation.rs | 556 ++++++++++++++++++++++++++++------ 1 file changed, 467 insertions(+), 89 deletions(-) diff --git a/src/json_schema_validation.rs b/src/json_schema_validation.rs index 7f21969..1883549 100644 --- a/src/json_schema_validation.rs +++ b/src/json_schema_validation.rs @@ -2,7 +2,7 @@ use anyhow::{bail, Context, Result}; use regex::Regex; use serde::{Deserialize, Serialize}; use serde_json::Value; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use crate::utils::NonEmptyVec; @@ -48,6 +48,8 @@ pub struct SchemaValidator { max_length: Option, #[serde(skip_serializing_if = "Option::is_none")] pattern: Option, + // TODO: Consider using a generic type for numbers/integers that + // can be used for minimum, maximum, exclusiveMinimum, exclusiveMaximum, multipleOf #[serde(skip_serializing_if = "Option::is_none")] minimum: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -74,6 +76,8 @@ pub struct SchemaValidator { unique_items: Option, #[serde(rename = "maxContains", skip_serializing_if = "Option::is_none")] max_contains: Option, + #[serde(rename = "contains", skip_serializing_if = "Option::is_none")] + contains: Option, #[serde(rename = "minContains", skip_serializing_if = "Option::is_none")] min_contains: Option, #[serde(rename = "const", skip_serializing_if = "Option::is_none")] @@ -132,6 +136,7 @@ impl SchemaValidator { unique_items: None, min_contains: None, max_contains: None, + contains: None, r#const: None, r#enum: None, } @@ -139,38 +144,70 @@ impl SchemaValidator { /// The value of this keyword MUST be a non-negative integer. /// - /// A string instance is valid against this keyword if its length is greater than, or equal to, the value of this keyword. + /// An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword. /// - /// The length of a string instance is defined as the number of its characters as defined by RFC 8259 [RFC8259]. + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.1](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.1) + pub fn set_max_items(mut self, max_items: usize) -> Self { + self.max_items = Some(max_items); + self + } + + /// The value of this keyword MUST be a non-negative integer. + /// + /// An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. /// /// Omitting this keyword has the same behavior as a value of 0. /// - /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.2](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.2) - pub fn set_min_length(mut self, min_length: usize) -> Self { - self.min_length = Some(min_length); + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.2](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.2) + pub fn set_min_items(mut self, min_items: usize) -> Self { + self.min_items = Some(min_items); self } - /// The value of this keyword MUST be a non-negative integer. + /// The value of this keyword MUST be a boolean. /// - /// A string instance is valid against this keyword if its length is less than, or equal to, the value of this keyword. + /// If this keyword has boolean value false, the instance validates successfully. + /// If it has boolean value true, the instance validates successfully if all of its elements are unique. /// - /// The length of a string instance is defined as the number of its characters as defined by RFC 8259 [RFC8259]. + /// Omitting this keyword has the same behavior as a value of false. /// - /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.1](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.1) - pub fn set_max_length(mut self, max_length: usize) -> Self { - self.max_length = Some(max_length); + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.3](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.3) + pub fn set_unique_items(mut self, unique_items: bool) -> Self { + self.unique_items = Some(unique_items); self } - /// The value of this keyword MUST be a string. This string SHOULD be a valid regular expression, according to the ECMA-262 regular expression dialect. + /// The value of this keyword MUST be a non-negative integer. + /// + /// If "contains" is not present within the same schema object, then this keyword has no effect. + /// + /// An instance array is valid against "maxContains" in two ways, depending on the form of the + /// annotation result of an adjacent "contains" [json-schema] keyword. The first way is if the + /// annotation result is an array and the length of that array is less than or equal to the "maxContains" + /// value. The second way is if the annotation result is a boolean "true" and the instance array length + /// is less than or equal to the "maxContains" value. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.4](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.4) + pub fn set_max_contains(mut self, max: usize, contains: SchemaType) -> Self { + self.max_contains = Some(max); + self.contains = Some(contains); + self + } - // A string instance is considered valid if the regular expression matches the instance successfully. - // Recall: regular expressions are not implicitly anchored. - // - // See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.3](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.3) - pub fn set_pattern(mut self, pattern: String) -> Self { - self.pattern = Some(pattern); + /// The value of this keyword MUST be a non-negative integer. + /// + /// If "contains" is not present within the same schema object, then this keyword has no effect. + /// + /// An instance array is valid against "minContains" in two ways, depending on the form of the annotation result of an adjacent "contains" [json-schema] keyword. The first way is if the annotation result is an array and the length of that array is greater than or equal to the "minContains" value. The second way is if the annotation result is a boolean "true" and the instance array length is greater than or equal to the "minContains" value. + /// + /// A value of 0 is allowed, but is only useful for setting a range of occurrences from 0 to the value of "maxContains". A value of 0 causes "minContains" and "contains" to always pass validation (but validation can still fail against a "maxContains" keyword). + /// + /// Omitting this keyword has the same behavior as a value of 1. + /// + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.5](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.5) + pub fn set_min_contains(mut self, min: usize, contains: SchemaType) -> Self { + self.min_contains = Some(min); + self.contains = Some(contains); self } @@ -236,12 +273,29 @@ impl SchemaValidator { self } - /// In addition to [SchemaValidator::set_required], push a single requirement to the list of required properties. + /// Push a single requirement to the list of required properties. pub fn add_requirement(mut self, requirement: String) -> Self { self.required.get_or_insert_with(Vec::new).push(requirement); self } + /// Set the dependent requirements for a property. + pub fn set_dependent_requirements( + mut self, + dependent_requirements: HashMap>, + ) -> Self { + self.dependent_required = Some(dependent_requirements); + self + } + + /// Add a dependent requirement for a property. + pub fn add_dependent_requirement(mut self, property: String, requirement: Vec) -> Self { + self.dependent_required + .get_or_insert_with(HashMap::new) + .insert(property, requirement); + self + } + /// The value of this keyword MUST be a non-negative integer. /// /// An object instance is valid against "maxProperties" if its number of properties is less than, or equal to, the value of this keyword. @@ -266,68 +320,38 @@ impl SchemaValidator { /// The value of this keyword MUST be a non-negative integer. /// - /// An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword. - /// - /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.1](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.1) - pub fn set_max_items(mut self, max_items: usize) -> Self { - self.max_items = Some(max_items); - self - } - - /// The value of this keyword MUST be a non-negative integer. + /// A string instance is valid against this keyword if its length is greater than, or equal to, the value of this keyword. /// - /// An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. + /// The length of a string instance is defined as the number of its characters as defined by RFC 8259 [RFC8259]. /// /// Omitting this keyword has the same behavior as a value of 0. /// - /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.2](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.2) - pub fn set_min_items(mut self, min_items: usize) -> Self { - self.min_items = Some(min_items); - self - } - - /// The value of this keyword MUST be a boolean. - /// - /// If this keyword has boolean value false, the instance validates successfully. - /// If it has boolean value true, the instance validates successfully if all of its elements are unique. - /// - /// Omitting this keyword has the same behavior as a value of false. - /// - /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.3](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.3) - pub fn set_unique_items(mut self, unique_items: bool) -> Self { - self.unique_items = Some(unique_items); + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.2](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.2) + pub fn set_min_length(mut self, min_length: usize) -> Self { + self.min_length = Some(min_length); self } /// The value of this keyword MUST be a non-negative integer. /// - /// If "contains" is not present within the same schema object, then this keyword has no effect. + /// A string instance is valid against this keyword if its length is less than, or equal to, the value of this keyword. /// - /// An instance array is valid against "maxContains" in two ways, depending on the form of the - /// annotation result of an adjacent "contains" [json-schema] keyword. The first way is if the - /// annotation result is an array and the length of that array is less than or equal to the "maxContains" - /// value. The second way is if the annotation result is a boolean "true" and the instance array length - /// is less than or equal to the "maxContains" value. + /// The length of a string instance is defined as the number of its characters as defined by RFC 8259 [RFC8259]. /// - /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.4](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.4) - pub fn set_max_contains(mut self, max: usize) -> Self { - self.max_contains = Some(max); + /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.1](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.1) + pub fn set_max_length(mut self, max_length: usize) -> Self { + self.max_length = Some(max_length); self } - /// The value of this keyword MUST be a non-negative integer. - /// - /// If "contains" is not present within the same schema object, then this keyword has no effect. - /// - /// An instance array is valid against "minContains" in two ways, depending on the form of the annotation result of an adjacent "contains" [json-schema] keyword. The first way is if the annotation result is an array and the length of that array is greater than or equal to the "minContains" value. The second way is if the annotation result is a boolean "true" and the instance array length is greater than or equal to the "minContains" value. - /// - /// A value of 0 is allowed, but is only useful for setting a range of occurrences from 0 to the value of "maxContains". A value of 0 causes "minContains" and "contains" to always pass validation (but validation can still fail against a "maxContains" keyword). - /// - /// Omitting this keyword has the same behavior as a value of 1. - /// - /// See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.5](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.4.5) - pub fn set_min_contains(mut self, min: usize) -> Self { - self.min_contains = Some(min); + /// The value of this keyword MUST be a string. This string SHOULD be a valid regular expression, according to the ECMA-262 regular expression dialect. + + // A string instance is considered valid if the regular expression matches the instance successfully. + // Recall: regular expressions are not implicitly anchored. + // + // See: [https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.3](https://json-schema.org/draft/2020-12/json-schema-validation#section-6.3.3) + pub fn set_pattern(mut self, pattern: String) -> Self { + self.pattern = Some(pattern); self } @@ -381,7 +405,7 @@ impl SchemaValidator { let s = value.as_str().context("Expected a string")?; if let Some(min_length) = self.min_length { - if s.len() <= min_length { + if s.len() < min_length { bail!( "String length {} is less than minimum {}", s.len(), @@ -391,7 +415,7 @@ impl SchemaValidator { } if let Some(max_length) = self.max_length { - if s.len() >= max_length { + if s.len() > max_length { bail!( "String length {} is greater than maximum {}", s.len(), @@ -400,11 +424,11 @@ impl SchemaValidator { } } - if let Some(pattern) = &self.pattern { + if let Some(pattern) = self.pattern.as_ref() { let regex_pattern = Regex::new(pattern).context("Invalid regex pattern")?; - if !regex_pattern.is_match(pattern) { - bail!("String does not match pattern: {}", pattern); + if !regex_pattern.is_match(s) { + bail!("String {s} does not match pattern: {}", regex_pattern); } } @@ -415,19 +439,19 @@ impl SchemaValidator { let n = value.as_f64().context("Expected a number")?; if let Some(minimum) = self.minimum { - if n <= minimum { + if n < minimum { bail!("Number {} is less than minimum {}", n, minimum); } } if let Some(maximum) = self.maximum { - if n >= maximum { + if n > maximum { bail!("Number {} is greater than maximum {}", n, maximum); } } if let Some(exclusive_minimum) = self.exclusive_minimum { - if n < exclusive_minimum { + if n <= exclusive_minimum { bail!( "Number {} is less than or equal to exclusive minimum {}", n, @@ -437,7 +461,7 @@ impl SchemaValidator { } if let Some(exclusive_maximum) = self.exclusive_maximum { - if n > exclusive_maximum { + if n >= exclusive_maximum { bail!( "Number {} is greater than or equal to exclusive maximum {}", n, @@ -459,19 +483,19 @@ impl SchemaValidator { let n = value.as_i64().context("Expected an integer")?; if let Some(minimum) = self.minimum { - if n <= minimum as i64 { + if n < minimum as i64 { bail!("Integer {} is less than minimum {}", n, minimum); } } if let Some(maximum) = self.maximum { - if n >= maximum as i64 { + if n > maximum as i64 { bail!("Integer {} is greater than maximum {}", n, maximum); } } if let Some(exclusive_minimum) = self.exclusive_minimum { - if n < exclusive_minimum as i64 { + if n <= exclusive_minimum as i64 { bail!( "Integer {} is less than or equal to exclusive minimum {}", n, @@ -481,7 +505,7 @@ impl SchemaValidator { } if let Some(exclusive_maximum) = self.exclusive_maximum { - if n > exclusive_maximum as i64 { + if n >= exclusive_maximum as i64 { bail!( "Integer {} is greater than or equal to exclusive maximum {}", n, @@ -509,26 +533,132 @@ impl SchemaValidator { pub fn validate_array(&self, value: &Value) -> Result<()> { let arr = value.as_array().context("Expected an array")?; - if let Some(min_length) = self.min_length { - if arr.len() < min_length { + if let Some(min_items) = self.min_items { + if arr.len() < min_items { bail!( "Array length {} is less than minimum {}", arr.len(), - min_length + min_items ); } } - if let Some(max_length) = self.max_length { - if arr.len() > max_length { + if let Some(max_items) = self.max_items { + if arr.len() > max_items { bail!( "Array length {} is greater than maximum {}", arr.len(), - max_length + max_items ); } } + if let Some(unique_items) = self.unique_items { + if unique_items { + let mut unique = HashSet::new(); + for item in arr { + if !unique.insert(item) { + bail!("Array has duplicate items"); + } + } + } + } + + if let Some(contains) = self.contains.as_ref() { + match contains { + SchemaType::String => { + let count = arr.iter().filter(|item| item.is_string()).count(); + + if let Some(max_contains) = self.max_contains { + if count > max_contains { + bail!("Array contains more than maximum number of strings"); + } + } + + if let Some(min_contains) = self.min_contains { + if count < min_contains { + bail!("Array contains fewer than minimum number of strings"); + } + } + } + SchemaType::Number => { + let count = arr.iter().filter(|item| item.is_number()).count(); + + if let Some(max_contains) = self.max_contains { + if count > max_contains { + bail!("Array contains more than maximum number of numbers"); + } + } + + if let Some(min_contains) = self.min_contains { + if count < min_contains { + bail!("Array contains fewer than minimum number of numbers"); + } + } + } + SchemaType::Integer => { + let count = arr.iter().filter(|item| item.is_i64()).count(); + + if let Some(max_contains) = self.max_contains { + if count > max_contains { + bail!("Array contains more than maximum number of integers"); + } + } + + if let Some(min_contains) = self.min_contains { + if count < min_contains { + bail!("Array contains fewer than minimum number of integers"); + } + } + } + SchemaType::Boolean => { + let count = arr.iter().filter(|item| item.is_boolean()).count(); + + if let Some(max_contains) = self.max_contains { + if count > max_contains { + bail!("Array contains more than maximum number of booleans"); + } + } + + if let Some(min_contains) = self.min_contains { + if count < min_contains { + bail!("Array contains fewer than minimum number of booleans"); + } + } + } + SchemaType::Array => { + let count = arr.iter().filter(|item| item.is_array()).count(); + + if let Some(max_contains) = self.max_contains { + if count > max_contains { + bail!("Array contains more than maximum number of arrays"); + } + } + + if let Some(min_contains) = self.min_contains { + if count < min_contains { + bail!("Array contains fewer than minimum number of arrays"); + } + } + } + SchemaType::Object => { + let count = arr.iter().filter(|item| item.is_object()).count(); + + if let Some(max_contains) = self.max_contains { + if count > max_contains { + bail!("Array contains more than maximum number of objects"); + } + } + + if let Some(min_contains) = self.min_contains { + if count < min_contains { + bail!("Array contains fewer than minimum number of objects"); + } + } + } + } + } + Ok(()) } @@ -544,7 +674,7 @@ impl SchemaValidator { } if let Some(min_properties) = self.min_properties { - if obj.len() >= min_properties { + if obj.len() < min_properties { bail!( "Object has fewer properties {} than minimum {}", obj.len(), @@ -554,7 +684,7 @@ impl SchemaValidator { } if let Some(max_properties) = self.max_properties { - if obj.len() <= max_properties { + if obj.len() > max_properties { bail!( "Object has more properties {} than maximum {}", obj.len(), @@ -580,3 +710,251 @@ impl SchemaValidator { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use anyhow::Result; + + #[test] + fn test_regex() -> Result<()> { + let regex = Regex::new(r#"(\+1|1)?[-.\s]?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}"#)?; + + assert!(regex.is_match(r#"+1 (253) 111 4321"#)); + + Ok(()) + } + + #[test] + fn test_validate_string() -> Result<()> { + let value = Value::String("hello".to_string()); + + let mut schema_validator = SchemaValidator::new(SchemaType::String).set_max_length(4); + + assert!(schema_validator.validate(&value).is_err()); + + schema_validator = schema_validator.set_max_length(5); + + assert!(schema_validator.validate(&value).is_ok()); + + schema_validator = schema_validator + .set_pattern(r#"(\+1|1)?[-.\s]?\(?[2-9]\d{2}\)?[-.\s]?\d{3}[-.\s]?\d{4}"#.to_owned()); + + assert!(schema_validator.validate(&value).is_err()); + schema_validator = schema_validator.set_max_length(17); + + let value = Value::String(r#"+1 (253) 111 4321"#.to_owned()); + + assert!(schema_validator.validate(&value).is_ok()); + + Ok(()) + } + + #[test] + fn test_validate_number() -> Result<()> { + let mut schema_validator = SchemaValidator::new(SchemaType::Number); + + let mut value = serde_json::json!(5.0); + + assert!(schema_validator.validate(&value).is_ok()); + + schema_validator = schema_validator.set_minimum(6.0).set_maximum(9.0); + + assert!(schema_validator.validate(&value).is_err()); + + value = serde_json::json!(6.0); + + assert!(schema_validator.validate(&value).is_ok()); + + schema_validator = schema_validator + .set_exclusive_minimum(6.0) + .set_exclusive_maximum(9.0); + + assert!(schema_validator.validate(&value).is_err()); + + value = serde_json::json!(7.0); + + assert!(schema_validator.validate(&value).is_ok()); + + schema_validator = schema_validator.set_multiple_of(2.0); + + assert!(schema_validator.validate(&value).is_err()); + + value = serde_json::json!(8.0); + + assert!(schema_validator.validate(&value).is_ok()); + + Ok(()) + } + + #[test] + fn test_validate_integer() -> Result<()> { + let mut schema_validator = SchemaValidator::new(SchemaType::Integer); + + let mut value = serde_json::json!(5); + + assert!(schema_validator.validate(&value).is_ok()); + + schema_validator = schema_validator.set_minimum(6.).set_maximum(9.); + + assert!(schema_validator.validate(&value).is_err()); + + value = serde_json::json!(6); + + assert!(schema_validator.validate(&value).is_ok()); + + schema_validator = schema_validator + .set_exclusive_minimum(6.) + .set_exclusive_maximum(9.); + + assert!(schema_validator.validate(&value).is_err()); + + value = serde_json::json!(7); + + assert!(schema_validator.validate(&value).is_ok()); + + schema_validator = schema_validator.set_multiple_of(2.); + + assert!(schema_validator.validate(&value).is_err()); + + value = serde_json::json!(8); + + assert!(schema_validator.validate(&value).is_ok()); + + Ok(()) + } + + #[test] + fn test_validate_array() -> Result<()> { + let mut schema_validator = SchemaValidator::new(SchemaType::Array); + + let mut value = serde_json::json!([1, 2, 3]); + + assert!(schema_validator.validate(&value).is_ok()); + + schema_validator = schema_validator.set_min_items(4); + + assert!(schema_validator.validate(&value).is_err()); + + schema_validator = schema_validator.set_max_items(5); + + value = serde_json::json!([1, 2, 3, 4, 5]); + + assert!(schema_validator.validate(&value).is_ok()); + + schema_validator = schema_validator.set_unique_items(true); + + value = serde_json::json!([1, 2, 3, 5, 5]); + + assert!(schema_validator.validate(&value).is_err()); + + schema_validator = schema_validator.set_unique_items(false); + + assert!(schema_validator.validate(&value).is_ok()); + + schema_validator = schema_validator.set_min_contains(1, SchemaType::String); + + assert!(schema_validator.validate(&value).is_err()); + + value = serde_json::json!([1, 2, 3, "Hello", ["a", "b", "c"]]); + + assert!(schema_validator.validate(&value).is_ok()); + + // NOTE: To check whether an array contains multiple different typed elements, + // we can overwrite the `min/max_contains` value to check for a new type. + schema_validator = schema_validator.set_min_contains(1, SchemaType::Array); + + assert!(schema_validator.validate(&value).is_ok()); + + schema_validator = schema_validator.set_min_contains(3, SchemaType::Number); + + assert!(schema_validator.validate(&value).is_ok()); + + Ok(()) + } + + #[test] + fn test_validate_object() -> Result<()> { + let mut schema_validator = SchemaValidator::new(SchemaType::Object); + + let value = serde_json::json!({ + "name": "John Doe", + "age": 25, + "address": { + "street": "1234 Elm St", + "city": "Springfield", + "state": "IL", + "zip": "62701" + } + }); + + assert!(schema_validator.validate(&value).is_ok()); + + schema_validator = schema_validator.set_min_properties(5); + + assert!(schema_validator.validate(&value).is_err()); + + schema_validator = schema_validator.set_min_properties(3); + + assert!(schema_validator.validate(&value).is_ok()); + + schema_validator = schema_validator.add_requirement("birthdate".into()); + + assert!(schema_validator.validate(&value).is_err()); + + // NOTE: `set_required` will overwrite existing required fields. + schema_validator = + schema_validator.set_required(vec!["name".into(), "age".into(), "address".into()]); + + assert!(schema_validator.validate(&value).is_ok()); + + schema_validator = + schema_validator.add_dependent_requirement("address".into(), vec!["street".into()]); + + assert!(schema_validator.validate(&value).is_ok()); + + // NOTE: `add_requirement` will add to the existing required fields. + schema_validator = schema_validator.add_requirement("birthdate".into()); + + assert!(schema_validator.validate(&value).is_err()); + + Ok(()) + } + + #[test] + fn test_const() -> Result<()> { + let mut schema_validator = SchemaValidator::new(SchemaType::String); + + let value = serde_json::json!("Hello, world!"); + + assert!(schema_validator.validate(&value).is_ok()); + + schema_validator = schema_validator.set_const(serde_json::json!("Hello, World!")); + + assert!(schema_validator.validate(&value).is_err()); + + Ok(()) + } + + #[test] + fn test_enum() -> Result<()> { + let mut schema_validator = SchemaValidator::new(SchemaType::String); + + let value = serde_json::json!("Hello, world!"); + + assert!(schema_validator.validate(&value).is_ok()); + + let mut enums = NonEmptyVec::new(serde_json::json!("Hello, World!")); + + schema_validator = schema_validator.set_enum(enums.clone()); + + assert!(schema_validator.validate(&value).is_err()); + + enums.push(serde_json::json!("Hello, world!")); + schema_validator = schema_validator.set_enum(enums); + + assert!(schema_validator.validate(&value).is_ok()); + + Ok(()) + } +} From 89db3b238a56dc5bd2ae43cee9cb41f9fd526817 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Wed, 14 Aug 2024 12:11:46 -0700 Subject: [PATCH 30/71] use serde default value for constraints field if not found during deserialization Signed-off-by: Ryan Tate --- src/presentation_exchange.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 9361260..a5df0e5 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -283,6 +283,7 @@ impl PresentationDefinition { #[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, From d4781030acf29712593a7c27d534eddc67c76f08 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Wed, 14 Aug 2024 12:43:42 -0700 Subject: [PATCH 31/71] remove unused imports Signed-off-by: Ryan Tate --- src/presentation_exchange.rs | 33 +++++++++------------------------ src/verifier/mod.rs | 2 +- tests/e2e.rs | 8 +++++--- 3 files changed, 15 insertions(+), 28 deletions(-) diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 0c19d33..45ed220 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -1,28 +1,13 @@ use std::collections::HashMap; pub use crate::utils::NonEmptyVec; -use crate::{ - core::response::{AuthorizationResponse, UnencodedAuthorizationResponse}, - json_schema_validation::SchemaValidator, -}; +use crate::{core::response::AuthorizationResponse, json_schema_validation::SchemaValidator}; use anyhow::{bail, Context, Result}; -use did_method_key::DIDKey; use serde::{Deserialize, Serialize}; use serde_json::Map; -use ssi_claims::{ - jwt::{AnyRegisteredClaim, Issuer, RegisteredClaim, VerifiablePresentation}, - CompactJWSString, VerificationParameters, -}; -use ssi_dids::{ - ssi_json_ld::{ - object::value::FragmentRef, - syntax::{from_value, Value}, - }, - VerificationMethodDIDResolver, DIDJWK, -}; -use ssi_jwk::JWK; -use ssi_verification_methods::AnyJwkMethod; +use ssi_claims::jwt::VerifiablePresentation; +use ssi_dids::ssi_json_ld::syntax::from_value; /// A JSONPath is a string that represents a path to a specific value within a JSON object. /// @@ -69,11 +54,6 @@ pub enum ClaimFormat { // The algorithm used to sign the JWT verifiable presentation. alg: Vec, }, - #[serde(rename = "jwt_vp_json")] - JwtVpJson { - // Used in the OID4VP specification for wallet methods supported. - alg_values_supported: Vec, - }, #[serde(rename = "jwt_vc")] JwtVc { // The algorithm used to sign the JWT verifiable credential. @@ -84,6 +64,11 @@ pub enum ClaimFormat { // 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 = "jwt")] Jwt { // The algorithm used to sign the JWT. @@ -338,7 +323,7 @@ impl PresentationDefinition { auth_response: &AuthorizationResponse, ) -> Result<()> { match auth_response { - AuthorizationResponse::Jwt(jwt) => { + AuthorizationResponse::Jwt(_jwt) => { bail!("Authorization Response Presentation Definition Validation Not Implemented.") } AuthorizationResponse::Unencoded(response) => { diff --git a/src/verifier/mod.rs b/src/verifier/mod.rs index dd0c716..725f880 100644 --- a/src/verifier/mod.rs +++ b/src/verifier/mod.rs @@ -1,4 +1,4 @@ -use std::{fmt::Debug, future::Future, pin::Pin, sync::Arc}; +use std::{fmt::Debug, future::Future, sync::Arc}; use anyhow::{bail, Context, Result}; use client::Client; diff --git a/tests/e2e.rs b/tests/e2e.rs index 84f5c08..f0fd84e 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -31,15 +31,17 @@ async fn w3c_vc_did_client_direct_post() { ) .set_name("Verify Identity Key".into()) .set_purpose("Check whether your identity key has been verified.".into()) - .set_filter(SchemaValidator::new(SchemaType::String)) + .set_filter( + SchemaValidator::new(SchemaType::String).set_pattern("did:key:.*".into()), + ) .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(ClaimFormat::JwtVp { - alg: vec![Algorithm::ES256.to_string()], + .set_format(ClaimFormat::JwtVcJson { + alg_values_supported: vec![Algorithm::ES256.to_string()], }), ); From e276d6a78b251d0d5219ba3aef8f29fa3e742a23 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Wed, 14 Aug 2024 12:49:31 -0700 Subject: [PATCH 32/71] remove unsed imports in test files Signed-off-by: Ryan Tate --- tests/jwt_vp.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/jwt_vp.rs b/tests/jwt_vp.rs index a65a030..a2a4ffe 100644 --- a/tests/jwt_vp.rs +++ b/tests/jwt_vp.rs @@ -5,18 +5,14 @@ use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::Result; use oid4vp::verifier::request_signer::{P256Signer, RequestSigner}; -use serde_json::json; -use ssi_claims::jwt::{AnyRegisteredClaim, Issuer, RegisteredClaim, VerifiablePresentation}; +use ssi_claims::jwt::VerifiablePresentation; use ssi_claims::vc::v2::syntax::VERIFIABLE_PRESENTATION_TYPE; use ssi_claims::{JWSPayload, JWTClaims}; // use ssi_claims::vc::v1::VerifiableCredential; -use ssi_claims::{ - jwt::{self, Subject}, - ResourceProvider, -}; +use ssi_claims::jwt; use ssi_dids::ssi_json_ld::syntax::{Object, Value}; use ssi_dids::ssi_json_ld::CREDENTIALS_V1_CONTEXT; -use ssi_dids::{DIDKey, DIDJWK}; +use ssi_dids::DIDKey; use ssi_jwk::JWK; #[tokio::test] From 3d93a0e37bf80695e7edc52663d668c82bace05e Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Wed, 14 Aug 2024 12:50:41 -0700 Subject: [PATCH 33/71] update vp token Signed-off-by: Ryan Tate --- tests/examples/vp.jwt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/examples/vp.jwt b/tests/examples/vp.jwt index 1ad4664..abc2f22 100644 --- a/tests/examples/vp.jwt +++ b/tests/examples/vp.jwt @@ -1 +1 @@ -eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6a2V5OnpEbmFlZnFUMUJyR0dzSkVaR3dBaXVlb3VxTWg2TXFzWmhhTDFtZDVoa0hndGZ6YjIjekRuYWVmcVQxQnJHR3NKRVpHd0FpdWVvdXFNaDZNcXNaaGFMMW1kNWhrSGd0ZnpiMiIsImF1ZCI6ImRpZDprZXk6ekRuYWVhRGozWXBQUjRKWG9zMmtDQ05QUzg2aGRFTGVONVBaaDk3S0drb0Z6VXRHbiN6RG5hZWFEajNZcFBSNEpYb3Mya0NDTlBTODZoZEVMZU41UFpoOTdLR2tvRnpVdEduIiwiaWF0IjoxNzIzNjYzNDQ0LCJleHAiOjE3MjM2NjcwNDQsIm5vbmNlIjoiOGYwMWVjZDItZjU3Mi00ODc4LWE3YzAtMzg0YTUzMWNlYTYwIiwidnAiOnsiQGNvbnRleHQiOiJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsInR5cGUiOiJWZXJpZmlhYmxlUHJlc2VudGF0aW9uIiwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlt7ImlzcyI6ImRpZDprZXk6ekRuYWVlZXg5TUFWYmhvV2VEY2JiR1pkek0xenhxWnFwQzM4N2pXb0xoVXIxQmRTVCIsIm5iZiI6MTcwNDA2NzIwMC4wLCJqdGkiOiIyYzI4MmViYS1kMTQ3LTQyMmMtOWNlNS05OTFmMTk5ODAwYzUiLCJzdWIiOiJkaWQ6a2V5OnpEbmFlZnFUMUJyR0dzSkVaR3dBaXVlb3VxTWg2TXFzWmhhTDFtZDVoa0hndGZ6YjIiLCJ2YyI6eyJAY29udGV4dCI6Imh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaWQiOiIyYzI4MmViYS1kMTQ3LTQyMmMtOWNlNS05OTFmMTk5ODAwYzUiLCJ0eXBlIjoiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ekRuYWVmcVQxQnJHR3NKRVpHd0FpdWVvdXFNaDZNcXNaaGFMMW1kNWhrSGd0ZnpiMiJ9LCJpc3N1ZXIiOiJkaWQ6a2V5OnpEbmFlZWV4OU1BVmJob1dlRGNiYkdaZHpNMXp4cVpxcEMzODdqV29MaFVyMUJkU1QiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTAxLTAxVDAwOjAwOjAwKzAwOjAwIn19XX19.2eOnpNdiRaxZJ6dtOa7tIU3onIN61NH5V7U1sMaCQUGBWnAeTwLQxeZJR953H15XHXdmF7qQsMqvwuqu_8XrxA \ No newline at end of file +eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6a2V5OnpEbmFlZnFUMUJyR0dzSkVaR3dBaXVlb3VxTWg2TXFzWmhhTDFtZDVoa0hndGZ6YjIjekRuYWVmcVQxQnJHR3NKRVpHd0FpdWVvdXFNaDZNcXNaaGFMMW1kNWhrSGd0ZnpiMiIsImF1ZCI6ImRpZDprZXk6ekRuYWVhRGozWXBQUjRKWG9zMmtDQ05QUzg2aGRFTGVONVBaaDk3S0drb0Z6VXRHbiN6RG5hZWFEajNZcFBSNEpYb3Mya0NDTlBTODZoZEVMZU41UFpoOTdLR2tvRnpVdEduIiwiaWF0IjoxNzIzNjY0OTgyLCJleHAiOjE3MjM2Njg1ODIsIm5vbmNlIjoiNTgwNjM5YTctYzgzOC00YjBhLTg1YWMtM2U1ODE3YTE5YmM5IiwidnAiOnsiQGNvbnRleHQiOiJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsInR5cGUiOiJWZXJpZmlhYmxlUHJlc2VudGF0aW9uIiwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlt7ImlzcyI6ImRpZDprZXk6ekRuYWVlZXg5TUFWYmhvV2VEY2JiR1pkek0xenhxWnFwQzM4N2pXb0xoVXIxQmRTVCIsIm5iZiI6MTcwNDA2NzIwMC4wLCJqdGkiOiIyYzI4MmViYS1kMTQ3LTQyMmMtOWNlNS05OTFmMTk5ODAwYzUiLCJzdWIiOiJkaWQ6a2V5OnpEbmFlZnFUMUJyR0dzSkVaR3dBaXVlb3VxTWg2TXFzWmhhTDFtZDVoa0hndGZ6YjIiLCJ2YyI6eyJAY29udGV4dCI6Imh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaWQiOiIyYzI4MmViYS1kMTQ3LTQyMmMtOWNlNS05OTFmMTk5ODAwYzUiLCJ0eXBlIjoiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ekRuYWVmcVQxQnJHR3NKRVpHd0FpdWVvdXFNaDZNcXNaaGFMMW1kNWhrSGd0ZnpiMiJ9LCJpc3N1ZXIiOiJkaWQ6a2V5OnpEbmFlZWV4OU1BVmJob1dlRGNiYkdaZHpNMXp4cVpxcEMzODdqV29MaFVyMUJkU1QiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTAxLTAxVDAwOjAwOjAwKzAwOjAwIn19XX19.cMgxo6dZMFeJp5TsviNkzIMj9ydTWGkvb-vp5_bZq0JzL0TYdLJGV2p08IOpG3bWa6Ep36qlpGjSfoHYkOtw4A \ No newline at end of file From 0843f664878c2dea806f4bda15c2b41c19b079c5 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Wed, 14 Aug 2024 12:52:07 -0700 Subject: [PATCH 34/71] rebase with main Signed-off-by: Ryan Tate --- tests/jwt_vc.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/jwt_vc.rs b/tests/jwt_vc.rs index e158b25..af6b7f5 100644 --- a/tests/jwt_vc.rs +++ b/tests/jwt_vc.rs @@ -168,7 +168,9 @@ impl AsyncHttpClient for MockHttpClient { println!("Session: {:?}", session); println!("Auth Response: {:?}", auth_response); - Ok(Outcome::Success) + Ok(Outcome::Success { + info: serde_json::Value::Null, + }) }, ) .await?; From 096d3832c87e14884b39c77183b46ce37c4aae33 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Fri, 16 Aug 2024 11:29:02 -0700 Subject: [PATCH 35/71] remove unused dependencies Signed-off-by: Ryan Tate --- Cargo.toml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2b081e9..171e37b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,24 +16,18 @@ p256 = ["dep:p256"] anyhow = "1.0.75" async-trait = "0.1.73" base64 = "0.21.4" -did-method-key = "0.2" -did-web = "0.2.2" http = "1.1.0" jsonpath_lib = "0.3.0" jsonschema = "0.18.0" p256 = { version = "0.13.2", features = ["jwk"], optional = true } -regex = "1.10.6" reqwest = { version = "0.12.5", features = ["rustls-tls"], optional = true } 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-claims = "0.1.0" ssi-dids = "0.2.0" ssi-jwk = { version = "0.2.1", features = ["secp256r1"] } ssi-verification-methods = "0.1.1" -thiserror = "1.0.49" tokio = "1.32.0" tracing = "0.1.37" url = { version = "2.4.1", features = ["serde"] } From f82fa26463f3cb3ef4c88c310569801e8dbe48d3 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 19 Aug 2024 19:18:10 -0700 Subject: [PATCH 36/71] add rand crate and provide random nonce method using Rng trait Signed-off-by: Ryan Tate --- Cargo.toml | 9 +++++++++ src/core/authorization_request/parameters.rs | 14 +++++++++++--- tests/e2e.rs | 10 ++++++++-- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 171e37b..0cd6c87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,8 +9,10 @@ repository = "https://github.com/spruceid/oidc4vp-rs/" documentation = "https://docs.rs/oid4vp/" [features] +default = [] reqwest = ["dep:reqwest"] p256 = ["dep:p256"] +rand = ["dep:rand"] [dependencies] anyhow = "1.0.75" @@ -20,6 +22,7 @@ http = "1.1.0" jsonpath_lib = "0.3.0" jsonschema = "0.18.0" p256 = { version = "0.13.2", features = ["jwk"], optional = true } +rand = { version = "0.8.5", optional = true } reqwest = { version = "0.12.5", features = ["rustls-tls"], optional = true } serde = "1.0.188" serde_json = "1.0.107" @@ -44,3 +47,9 @@ uuid = { version = "1.2", features = ["v4", "serde", "js"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] uuid = { version = "1.2", features = ["v4", "serde"] } + +# For Development Purpose, patch the ssi* crates to use the local versions +# [patch.crates-io] +# ssi-claims = { path = "../ssi/crates/claims" } +# ssi-dids = { path = "../ssi/crates/dids" } +# ssi-jwk = { path = "../ssi/crates/jwk" } diff --git a/src/core/authorization_request/parameters.rs b/src/core/authorization_request/parameters.rs index c00fed7..ea8f27f 100644 --- a/src/core/authorization_request/parameters.rs +++ b/src/core/authorization_request/parameters.rs @@ -201,6 +201,12 @@ impl From for Nonce { } } +impl From<&str> for Nonce { + fn from(value: &str) -> Self { + Self(value.to_string()) + } +} + impl Deref for Nonce { type Target = String; @@ -210,9 +216,11 @@ impl Deref for Nonce { } impl Nonce { - pub fn random() -> Self { - // NOTE: consider replacing with rng rand crate. - Self(uuid::Uuid::new_v4().to_string()) + #[cfg(feature = "rand")] + pub fn random(rng: &mut impl rand::Rng) -> Self { + use rand::distributions::Alphanumeric; + + Self((0..16).map(|_| rng.sample(Alphanumeric) as char).collect()) } } diff --git a/tests/e2e.rs b/tests/e2e.rs index abcd0cc..cdaa226 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -52,12 +52,18 @@ async fn w3c_vc_did_client_direct_post() { let client_metadata = UntypedObject::default(); + #[cfg(feature = "rand")] + let nonce = Nonce::random(&mut rand::thread_rng()); + + #[cfg(not(feature = "rand"))] + 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::random()) + .with_request_parameter(nonce) .with_request_parameter(ClientMetadata(client_metadata)) .build(wallet.metadata().clone()) .await @@ -89,7 +95,7 @@ async fn w3c_vc_did_client_direct_post() { // 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().clone(), + descriptor.id().to_string(), ClaimFormatDesignation::JwtVc, "$".into(), ) From b07b71579449b8de6bf95c945b4ec4272dd22f2e Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 19 Aug 2024 19:47:55 -0700 Subject: [PATCH 37/71] remove todos and update comments, use JWKResolver instead of VerificationMethodDIDResolver Signed-off-by: Ryan Tate --- src/core/authorization_request/parameters.rs | 6 ++++ .../authorization_request/verification/did.rs | 14 ++------- src/presentation_exchange.rs | 29 ++++++++++--------- src/verifier/client.rs | 15 +++------- tests/e2e.rs | 5 ++++ tests/examples/vp.jwt | 2 +- tests/jwt_vp.rs | 16 ++++------ 7 files changed, 39 insertions(+), 48 deletions(-) diff --git a/src/core/authorization_request/parameters.rs b/src/core/authorization_request/parameters.rs index ea8f27f..54a1eeb 100644 --- a/src/core/authorization_request/parameters.rs +++ b/src/core/authorization_request/parameters.rs @@ -215,6 +215,12 @@ impl Deref for Nonce { } } +impl ToString for Nonce { + fn to_string(&self) -> String { + self.0.clone() + } +} + impl Nonce { #[cfg(feature = "rand")] pub fn random(rng: &mut impl rand::Rng) -> Self { diff --git a/src/core/authorization_request/verification/did.rs b/src/core/authorization_request/verification/did.rs index ff0b7ac..2ea7f50 100644 --- a/src/core/authorization_request/verification/did.rs +++ b/src/core/authorization_request/verification/did.rs @@ -7,12 +7,7 @@ use anyhow::{bail, Context, Result}; use base64::prelude::*; use serde_json::{Map, Value as Json}; -use ssi_dids::{DIDResolver, VerificationMethodDIDResolver}; use ssi_jwk::JWKResolver; -use ssi_verification_methods::{ - GenericVerificationMethod, InvalidVerificationMethod, MaybeJwkVerificationMethod, - VerificationMethodSet, -}; /// Default implementation of request validation for `client_id_scheme` `did`. pub async fn verify_with_resolver( @@ -20,12 +15,7 @@ pub async fn verify_with_resolver( request_object: &AuthorizationRequestObject, request_jwt: String, trusted_dids: Option<&[String]>, - resolver: &VerificationMethodDIDResolver< - impl DIDResolver, - impl MaybeJwkVerificationMethod - + VerificationMethodSet - + TryFrom, - >, + resolver: impl JWKResolver, ) -> Result<()> { let (headers_b64, _, _) = ssi_claims::jws::split_jws(&request_jwt)?; @@ -80,7 +70,7 @@ pub async fn verify_with_resolver( .await .context("unable to resolve key from verification method")?; - let _: Json = ssi_claims::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/presentation_exchange.rs b/src/presentation_exchange.rs index adcb0d6..8933015 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -439,8 +439,8 @@ impl InputDescriptor { } /// Return the id of the input descriptor. - pub fn id(&self) -> &String { - &self.id + pub fn id(&self) -> &str { + self.id.as_str() } /// Return the constraints of the input descriptor. @@ -560,8 +560,6 @@ impl InputDescriptor { // If a filter is available with a valid schema, handle the field validation. if let Some(Ok(schema_validator)) = validator.as_ref() { - // TODO: possible trace a warning if a field is not valid. - // TODO: Check the predicate feature value. let validated_fields = field_elements.iter().find(|element| { match schema_validator.validate(element) { Err(errors) => { @@ -586,9 +584,6 @@ impl InputDescriptor { } } } - - // TODO: Check limit disclosure of data requested. Do not provide more data - // than is necessary to satisfy the constraints. } } } @@ -604,7 +599,6 @@ impl InputDescriptor { /// /// 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() } @@ -660,6 +654,16 @@ impl Constraints { 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 { + if let Some(fields) = self.fields() { + fields.iter().any(|field| field.is_required()) + } else { + false + } + } } /// 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. @@ -822,10 +826,9 @@ impl ConstraintsField { /// If no filter is provided on the constraint field, this /// will return None. /// - /// If the filter schema is invalid, this will also return None. + /// # Errors /// - /// NOTE: Errors are not handled if the filter schema is invalid, - /// instead the method will return None on an invalid filter. + /// If the filter is invalid, this will return an error. pub fn validator(&self) -> Option> { self.filter.as_ref().map(JSONSchema::compile) } @@ -935,9 +938,9 @@ impl DescriptorMap { /// 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 { + pub fn new(id: impl Into, format: ClaimFormatDesignation, path: JsonPath) -> Self { Self { - id, + id: id.into(), format, path, path_nested: None, diff --git a/src/verifier/client.rs b/src/verifier/client.rs index 8b71208..7bad783 100644 --- a/src/verifier/client.rs +++ b/src/verifier/client.rs @@ -4,12 +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_dids::{DIDResolver, VerificationMethodDIDResolver}; use ssi_jwk::JWKResolver; -use ssi_verification_methods::{ - GenericVerificationMethod, InvalidVerificationMethod, MaybeJwkVerificationMethod, - VerificationMethodSet, -}; + use tracing::debug; use x509_cert::{ der::Encode, @@ -48,13 +44,10 @@ impl DIDClient { pub async fn new( vm: String, signer: Arc, - resolver: &VerificationMethodDIDResolver< - impl DIDResolver, - impl MaybeJwkVerificationMethod - + VerificationMethodSet - + TryFrom, - >, + resolver: impl JWKResolver, ) -> Result { + // use ssi_jwk::JWKResolver; + let (id, _f) = vm.rsplit_once('#').context(format!( "expected a DID verification method, received '{vm}'" ))?; diff --git a/tests/e2e.rs b/tests/e2e.rs index cdaa226..0a0ab78 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -96,6 +96,11 @@ async fn w3c_vc_did_client_direct_post() { // 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::JwtVc, "$".into(), ) diff --git a/tests/examples/vp.jwt b/tests/examples/vp.jwt index 72119bd..afe06ec 100644 --- a/tests/examples/vp.jwt +++ b/tests/examples/vp.jwt @@ -1 +1 @@ -eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6a2V5OnpEbmFlZnFUMUJyR0dzSkVaR3dBaXVlb3VxTWg2TXFzWmhhTDFtZDVoa0hndGZ6YjIjekRuYWVmcVQxQnJHR3NKRVpHd0FpdWVvdXFNaDZNcXNaaGFMMW1kNWhrSGd0ZnpiMiIsImF1ZCI6ImRpZDprZXk6ekRuYWVhRGozWXBQUjRKWG9zMmtDQ05QUzg2aGRFTGVONVBaaDk3S0drb0Z6VXRHbiN6RG5hZWFEajNZcFBSNEpYb3Mya0NDTlBTODZoZEVMZU41UFpoOTdLR2tvRnpVdEduIiwiaWF0IjoxNzIzODMyNjI2LCJleHAiOjE3MjM4MzYyMjYsIm5vbmNlIjoiZGMxZTI2MDAtZjlmZi00YWM0LWI3N2EtYmI1YWYxMzg2YmRmIiwidnAiOnsiQGNvbnRleHQiOiJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsInR5cGUiOiJWZXJpZmlhYmxlUHJlc2VudGF0aW9uIiwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlt7ImlzcyI6ImRpZDprZXk6ekRuYWVlZXg5TUFWYmhvV2VEY2JiR1pkek0xenhxWnFwQzM4N2pXb0xoVXIxQmRTVCIsIm5iZiI6MTcwNDA2NzIwMC4wLCJqdGkiOiIyYzI4MmViYS1kMTQ3LTQyMmMtOWNlNS05OTFmMTk5ODAwYzUiLCJzdWIiOiJkaWQ6a2V5OnpEbmFlZnFUMUJyR0dzSkVaR3dBaXVlb3VxTWg2TXFzWmhhTDFtZDVoa0hndGZ6YjIiLCJ2YyI6eyJAY29udGV4dCI6Imh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaWQiOiIyYzI4MmViYS1kMTQ3LTQyMmMtOWNlNS05OTFmMTk5ODAwYzUiLCJ0eXBlIjoiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ekRuYWVmcVQxQnJHR3NKRVpHd0FpdWVvdXFNaDZNcXNaaGFMMW1kNWhrSGd0ZnpiMiJ9LCJpc3N1ZXIiOiJkaWQ6a2V5OnpEbmFlZWV4OU1BVmJob1dlRGNiYkdaZHpNMXp4cVpxcEMzODdqV29MaFVyMUJkU1QiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTAxLTAxVDAwOjAwOjAwKzAwOjAwIn19XX19.0eEgYc87wDosi6k8m_gcDyfVY1OSUxqtY2KfNrc5557xyjt97Dq5ag8Ba00b9rrhACYwpTpiWlzXU7tlV54fpQ \ No newline at end of file +eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6a2V5OnpEbmFlZnFUMUJyR0dzSkVaR3dBaXVlb3VxTWg2TXFzWmhhTDFtZDVoa0hndGZ6YjIjekRuYWVmcVQxQnJHR3NKRVpHd0FpdWVvdXFNaDZNcXNaaGFMMW1kNWhrSGd0ZnpiMiIsImF1ZCI6ImRpZDprZXk6ekRuYWVhRGozWXBQUjRKWG9zMmtDQ05QUzg2aGRFTGVONVBaaDk3S0drb0Z6VXRHbiN6RG5hZWFEajNZcFBSNEpYb3Mya0NDTlBTODZoZEVMZU41UFpoOTdLR2tvRnpVdEduIiwiaWF0IjoxNzI0MTAxOTEzLCJleHAiOjE3MjQxMDU1MTMsIm5vbmNlIjoiOTYyNWQ0NDEtMWRhNS00ZmEzLWFiMzgtMzY1Y2JlNzJiMjNlIiwidnAiOnsiQGNvbnRleHQiOiJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsInR5cGUiOiJWZXJpZmlhYmxlUHJlc2VudGF0aW9uIiwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlt7ImlzcyI6ImRpZDprZXk6ekRuYWVlZXg5TUFWYmhvV2VEY2JiR1pkek0xenhxWnFwQzM4N2pXb0xoVXIxQmRTVCIsIm5iZiI6MTcwNDA2NzIwMC4wLCJqdGkiOiIyYzI4MmViYS1kMTQ3LTQyMmMtOWNlNS05OTFmMTk5ODAwYzUiLCJzdWIiOiJkaWQ6a2V5OnpEbmFlZnFUMUJyR0dzSkVaR3dBaXVlb3VxTWg2TXFzWmhhTDFtZDVoa0hndGZ6YjIiLCJ2YyI6eyJAY29udGV4dCI6Imh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaWQiOiIyYzI4MmViYS1kMTQ3LTQyMmMtOWNlNS05OTFmMTk5ODAwYzUiLCJ0eXBlIjoiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ekRuYWVmcVQxQnJHR3NKRVpHd0FpdWVvdXFNaDZNcXNaaGFMMW1kNWhrSGd0ZnpiMiJ9LCJpc3N1ZXIiOiJkaWQ6a2V5OnpEbmFlZWV4OU1BVmJob1dlRGNiYkdaZHpNMXp4cVpxcEMzODdqV29MaFVyMUJkU1QiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTAxLTAxVDAwOjAwOjAwKzAwOjAwIn19XX19.XyUgg9iAL4ETaT89UEDSSsAyzDHcePFmac6pVr6NELppeBOCywHtafomsuUOFUsQte5ASjuBUNwiyHEFMpXZHg \ No newline at end of file diff --git a/tests/jwt_vp.rs b/tests/jwt_vp.rs index a2a4ffe..821ecd2 100644 --- a/tests/jwt_vp.rs +++ b/tests/jwt_vp.rs @@ -4,6 +4,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::Result; +use oid4vp::core::authorization_request::parameters::Nonce; use oid4vp::verifier::request_signer::{P256Signer, RequestSigner}; use ssi_claims::jwt::VerifiablePresentation; use ssi_claims::vc::v2::syntax::VERIFIABLE_PRESENTATION_TYPE; @@ -42,13 +43,9 @@ async fn test_verifiable_presentation() -> Result<()> { println!("VC: {:?}", verifiable_credential); - // let issuer = Issuer::extract(AnyRegisteredClaim::from(verifiable_credential.clone())); - - // println!("Issuer: {:?}", issuer); - - // assert_eq!(holder_did.as_did_url(), subject); - // TODO: There should be a more idiomatically correct way to do this, if not already implemented. + // NOTE: There is an unused `VerifiablePresentationBuilder` in the holder module, however, these methods + // may best be implemented as methods on the `VerifiablePresentation` struct itself. let mut verifiable_presentation = VerifiablePresentation(Value::Object(Object::new())); verifiable_presentation.0.as_object_mut().map(|obj| { @@ -69,11 +66,8 @@ async fn test_verifiable_presentation() -> Result<()> { obj.insert("exp".into(), Value::Number((dur.as_secs() + 3600).into())); }); - // The nonce is a random string. - obj.insert( - "nonce".into(), - Value::String(uuid::Uuid::new_v4().to_string().into()), - ); + let nonce = Nonce::from("random_nonce"); + obj.insert("nonce".into(), Value::String(nonce.to_string().into())); let mut verifiable_credential_field = Value::Object(Object::new()); From fcb4ed9a8f802a47102115c04d5ccc697df3e22d Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 19 Aug 2024 20:04:49 -0700 Subject: [PATCH 38/71] fix clippy warnings Signed-off-by: Ryan Tate --- src/core/authorization_request/parameters.rs | 6 +++--- src/core/authorization_request/verification/did.rs | 2 +- src/core/metadata/mod.rs | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/core/authorization_request/parameters.rs b/src/core/authorization_request/parameters.rs index 54a1eeb..0dfe80e 100644 --- a/src/core/authorization_request/parameters.rs +++ b/src/core/authorization_request/parameters.rs @@ -215,9 +215,9 @@ impl Deref for Nonce { } } -impl ToString for Nonce { - fn to_string(&self) -> String { - self.0.clone() +impl std::fmt::Display for Nonce { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) } } diff --git a/src/core/authorization_request/verification/did.rs b/src/core/authorization_request/verification/did.rs index 2ea7f50..5817881 100644 --- a/src/core/authorization_request/verification/did.rs +++ b/src/core/authorization_request/verification/did.rs @@ -70,7 +70,7 @@ pub async fn verify_with_resolver( .await .context("unable to resolve key from verification method")?; - let _: Json = ssi_claims::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/metadata/mod.rs b/src/core/metadata/mod.rs index 0d83542..45b4f7a 100644 --- a/src/core/metadata/mod.rs +++ b/src/core/metadata/mod.rs @@ -41,6 +41,11 @@ impl WalletMetadata { &self.2 } + /// Returns whether the claim format is supported. + pub fn is_claim_format_supported(&self, designation: &ClaimFormatDesignation) -> bool { + self.vp_formats_supported().0.contains_key(designation) + } + /// The static wallet metadata bound to `openid4vp:`: /// ```json /// { From 1398cadffd13c2a03fc69fc8514656a7830c3f73 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 19 Aug 2024 20:16:37 -0700 Subject: [PATCH 39/71] verify jwt in validate_authorization_response presentation definition method Signed-off-by: Ryan Tate --- src/presentation_exchange.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 8933015..cb7f412 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -7,8 +7,9 @@ use anyhow::{bail, Context, Result}; use jsonschema::{JSONSchema, ValidationError}; use serde::{Deserialize, Serialize}; use serde_json::Map; -use ssi_claims::jwt::VerifiablePresentation; -use ssi_dids::ssi_json_ld::syntax::from_value; +use ssi_claims::{jwt::VerifiablePresentation, CompactJWSString, VerificationParameters}; +use ssi_dids::{ssi_json_ld::syntax::from_value, DIDKey, VerificationMethodDIDResolver}; +use ssi_verification_methods::AnyJwkMethod; /// A JSONPath is a string that represents a path to a specific value within a JSON object. /// @@ -338,15 +339,15 @@ impl PresentationDefinition { let jwt = response.vp_token().0.clone(); - // TODO: Verify the JWT. - // let jws = CompactJWSString::from_string(jwt.clone()).context("Invalid JWT.")?; - // let resolver: VerificationMethodDIDResolver = - // VerificationMethodDIDResolver::new(DIDKey); - // let params = VerificationParameters::from_resolver(resolver); + let jws = CompactJWSString::from_string(jwt.clone()).context("Invalid JWT.")?; + let resolver: VerificationMethodDIDResolver = + VerificationMethodDIDResolver::new(DIDKey); - // if let Err(e) = jws.verify(params).await { - // bail!("JWT Verification Failed: {:?}", e) - // } + let params = VerificationParameters::from_resolver(resolver); + + if let Err(e) = jws.verify(params).await { + bail!("JWT Verification Failed: {:?}", e) + } let verifiable_presentation: VerifiablePresentation = ssi_claims::jwt::decode_unverified(&jwt)?; From be2bbed88567504fdd29e703943457b7ab4ec1e6 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 20 Aug 2024 06:16:04 -0700 Subject: [PATCH 40/71] Update tests/e2e.rs Co-authored-by: Jacob --- tests/e2e.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e.rs b/tests/e2e.rs index 0a0ab78..b44eacc 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -43,7 +43,7 @@ async fn w3c_vc_did_client_direct_post() { .set_format((|| { let mut map = ClaimFormatMap::new(); map.insert( - ClaimFormatDesignation::JwtVp, + ClaimFormatDesignation::JwtVcJson, ClaimFormatPayload::Alg(vec![Algorithm::ES256.to_string()]), ); map From 32a49b79f34ffe7f0ef4b03c22a67af9c232bf44 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 20 Aug 2024 07:34:56 -0700 Subject: [PATCH 41/71] update descriptor map nested path in e2e example Signed-off-by: Ryan Tate --- src/presentation_exchange.rs | 10 ++++------ tests/e2e.rs | 31 ++++++++++++++++++++++++++----- tests/examples/vp.jwt | 2 +- tests/jwt_vp.rs | 16 +++++++--------- 4 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index cb7f412..17b5dc2 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -974,15 +974,13 @@ impl DescriptorMap { /// /// 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.") - } + 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 = self.id.clone(); self.path_nested = Some(Box::new(path_nested)); - Ok(self) + self } } diff --git a/tests/e2e.rs b/tests/e2e.rs index b44eacc..ee0588f 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -1,3 +1,4 @@ +use jwt_vp::create_test_verifiable_presentation; use oid4vp::presentation_exchange::*; use oid4vp::{ @@ -96,14 +97,29 @@ async fn w3c_vc_did_client_direct_post() { // 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 + // 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::JwtVc, - "$".into(), + 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. + "$.vp".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 + "$.verifiableCredential[0]".into(), + )) }) .collect(); @@ -115,7 +131,12 @@ async fn w3c_vc_did_client_direct_post() { let response = AuthorizationResponse::Unencoded(UnencodedAuthorizationResponse( Default::default(), - VpToken(include_str!("examples/vp.jwt").to_owned()), + VpToken( + create_test_verifiable_presentation() + .await + .expect("failed to create verifiable presentation") + .to_string(), + ), presentation_submission.try_into().unwrap(), )); diff --git a/tests/examples/vp.jwt b/tests/examples/vp.jwt index afe06ec..28eaf73 100644 --- a/tests/examples/vp.jwt +++ b/tests/examples/vp.jwt @@ -1 +1 @@ -eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6a2V5OnpEbmFlZnFUMUJyR0dzSkVaR3dBaXVlb3VxTWg2TXFzWmhhTDFtZDVoa0hndGZ6YjIjekRuYWVmcVQxQnJHR3NKRVpHd0FpdWVvdXFNaDZNcXNaaGFMMW1kNWhrSGd0ZnpiMiIsImF1ZCI6ImRpZDprZXk6ekRuYWVhRGozWXBQUjRKWG9zMmtDQ05QUzg2aGRFTGVONVBaaDk3S0drb0Z6VXRHbiN6RG5hZWFEajNZcFBSNEpYb3Mya0NDTlBTODZoZEVMZU41UFpoOTdLR2tvRnpVdEduIiwiaWF0IjoxNzI0MTAxOTEzLCJleHAiOjE3MjQxMDU1MTMsIm5vbmNlIjoiOTYyNWQ0NDEtMWRhNS00ZmEzLWFiMzgtMzY1Y2JlNzJiMjNlIiwidnAiOnsiQGNvbnRleHQiOiJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsInR5cGUiOiJWZXJpZmlhYmxlUHJlc2VudGF0aW9uIiwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlt7ImlzcyI6ImRpZDprZXk6ekRuYWVlZXg5TUFWYmhvV2VEY2JiR1pkek0xenhxWnFwQzM4N2pXb0xoVXIxQmRTVCIsIm5iZiI6MTcwNDA2NzIwMC4wLCJqdGkiOiIyYzI4MmViYS1kMTQ3LTQyMmMtOWNlNS05OTFmMTk5ODAwYzUiLCJzdWIiOiJkaWQ6a2V5OnpEbmFlZnFUMUJyR0dzSkVaR3dBaXVlb3VxTWg2TXFzWmhhTDFtZDVoa0hndGZ6YjIiLCJ2YyI6eyJAY29udGV4dCI6Imh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaWQiOiIyYzI4MmViYS1kMTQ3LTQyMmMtOWNlNS05OTFmMTk5ODAwYzUiLCJ0eXBlIjoiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ekRuYWVmcVQxQnJHR3NKRVpHd0FpdWVvdXFNaDZNcXNaaGFMMW1kNWhrSGd0ZnpiMiJ9LCJpc3N1ZXIiOiJkaWQ6a2V5OnpEbmFlZWV4OU1BVmJob1dlRGNiYkdaZHpNMXp4cVpxcEMzODdqV29MaFVyMUJkU1QiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTAxLTAxVDAwOjAwOjAwKzAwOjAwIn19XX19.XyUgg9iAL4ETaT89UEDSSsAyzDHcePFmac6pVr6NELppeBOCywHtafomsuUOFUsQte5ASjuBUNwiyHEFMpXZHg \ No newline at end of file +eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkaWQ6a2V5OnpEbmFlZnFUMUJyR0dzSkVaR3dBaXVlb3VxTWg2TXFzWmhhTDFtZDVoa0hndGZ6YjIjekRuYWVmcVQxQnJHR3NKRVpHd0FpdWVvdXFNaDZNcXNaaGFMMW1kNWhrSGd0ZnpiMiIsImF1ZCI6ImRpZDprZXk6ekRuYWVhRGozWXBQUjRKWG9zMmtDQ05QUzg2aGRFTGVONVBaaDk3S0drb0Z6VXRHbiN6RG5hZWFEajNZcFBSNEpYb3Mya0NDTlBTODZoZEVMZU41UFpoOTdLR2tvRnpVdEduIiwiaWF0IjoxNzI0MTI0MDc0LCJleHAiOjE3MjQxMjc2NzQsIm5vbmNlIjoicmFuZG9tX25vbmNlIiwidnAiOnsiQGNvbnRleHQiOiJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsInR5cGUiOiJWZXJpZmlhYmxlUHJlc2VudGF0aW9uIiwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlt7ImlzcyI6ImRpZDprZXk6ekRuYWVlZXg5TUFWYmhvV2VEY2JiR1pkek0xenhxWnFwQzM4N2pXb0xoVXIxQmRTVCIsIm5iZiI6MTcwNDA2NzIwMC4wLCJqdGkiOiIyYzI4MmViYS1kMTQ3LTQyMmMtOWNlNS05OTFmMTk5ODAwYzUiLCJzdWIiOiJkaWQ6a2V5OnpEbmFlZnFUMUJyR0dzSkVaR3dBaXVlb3VxTWg2TXFzWmhhTDFtZDVoa0hndGZ6YjIiLCJ2YyI6eyJAY29udGV4dCI6Imh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaWQiOiIyYzI4MmViYS1kMTQ3LTQyMmMtOWNlNS05OTFmMTk5ODAwYzUiLCJ0eXBlIjoiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ekRuYWVmcVQxQnJHR3NKRVpHd0FpdWVvdXFNaDZNcXNaaGFMMW1kNWhrSGd0ZnpiMiJ9LCJpc3N1ZXIiOiJkaWQ6a2V5OnpEbmFlZWV4OU1BVmJob1dlRGNiYkdaZHpNMXp4cVpxcEMzODdqV29MaFVyMUJkU1QiLCJpc3N1YW5jZURhdGUiOiIyMDI0LTAxLTAxVDAwOjAwOjAwKzAwOjAwIn19XX19.tsP3YHS6CouT-Fe-p2E16HRUY0qKLZYi79V8-pUw0tuGEhL4i5BPCZo14vigthtk37pJGb-rM2qB_NlsDyJJkQ \ No newline at end of file diff --git a/tests/jwt_vp.rs b/tests/jwt_vp.rs index 821ecd2..b69dc19 100644 --- a/tests/jwt_vp.rs +++ b/tests/jwt_vp.rs @@ -8,7 +8,7 @@ use oid4vp::core::authorization_request::parameters::Nonce; use oid4vp::verifier::request_signer::{P256Signer, RequestSigner}; use ssi_claims::jwt::VerifiablePresentation; use ssi_claims::vc::v2::syntax::VERIFIABLE_PRESENTATION_TYPE; -use ssi_claims::{JWSPayload, JWTClaims}; +use ssi_claims::{CompactJWSString, JWSPayload, JWTClaims}; // use ssi_claims::vc::v1::VerifiableCredential; use ssi_claims::jwt; use ssi_dids::ssi_json_ld::syntax::{Object, Value}; @@ -16,8 +16,7 @@ use ssi_dids::ssi_json_ld::CREDENTIALS_V1_CONTEXT; use ssi_dids::DIDKey; use ssi_jwk::JWK; -#[tokio::test] -async fn test_verifiable_presentation() -> Result<()> { +pub async fn create_test_verifiable_presentation() -> Result { let verifier = JWK::from_str(include_str!("examples/verifier.jwk"))?; let signer = Arc::new( @@ -66,8 +65,10 @@ async fn test_verifiable_presentation() -> Result<()> { obj.insert("exp".into(), Value::Number((dur.as_secs() + 3600).into())); }); - let nonce = Nonce::from("random_nonce"); - obj.insert("nonce".into(), Value::String(nonce.to_string().into())); + obj.insert( + "nonce".into(), + Value::String(Nonce::from("random_nonce").to_string().into()), + ); let mut verifiable_credential_field = Value::Object(Object::new()); @@ -100,12 +101,9 @@ async fn test_verifiable_presentation() -> Result<()> { println!("JWT: {:?}", jwt); - // Save the JWT to the file system to be used in other tests `examples/vp.jwt` - std::fs::write("tests/examples/vp.jwt", jwt.as_str())?; - let vp: jwt::VerifiablePresentation = ssi_claims::jwt::decode_unverified(jwt.as_str())?; println!("VP: {:?}", vp); - Ok(()) + Ok(jwt) } From d0503559e02355c95a13dd34dd5b5d8126ec80f4 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 20 Aug 2024 07:35:44 -0700 Subject: [PATCH 42/71] remove dependency patches Signed-off-by: Ryan Tate --- Cargo.toml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0cd6c87..882d01e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,9 +47,3 @@ uuid = { version = "1.2", features = ["v4", "serde", "js"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] uuid = { version = "1.2", features = ["v4", "serde"] } - -# For Development Purpose, patch the ssi* crates to use the local versions -# [patch.crates-io] -# ssi-claims = { path = "../ssi/crates/claims" } -# ssi-dids = { path = "../ssi/crates/dids" } -# ssi-jwk = { path = "../ssi/crates/jwk" } From 4a0217b3ec5ca64355cc9765fc241e58ad4d7420 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 20 Aug 2024 07:50:48 -0700 Subject: [PATCH 43/71] add paths to example for input descriptor constraints field Signed-off-by: Ryan Tate --- src/presentation_exchange.rs | 8 +++++--- tests/e2e.rs | 23 +++++++++++++---------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 17b5dc2..9c25607 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -667,7 +667,9 @@ impl Constraints { } } -/// 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. +/// 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)] @@ -968,9 +970,9 @@ impl DescriptorMap { /// 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. + /// > 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) + /// 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. diff --git a/tests/e2e.rs b/tests/e2e.rs index ee0588f..bd7bb47 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -26,16 +26,19 @@ async fn w3c_vc_did_client_direct_post() { "did-key-id".into(), Constraints::new() .add_constraint( - ConstraintsField::new( - "$.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), + // 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), ) From 9997486535fbf5ffe1c7f4968f280aef4f9f9337 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 20 Aug 2024 08:02:49 -0700 Subject: [PATCH 44/71] use top level json path for jwt_vp_json Signed-off-by: Ryan Tate --- tests/e2e.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e.rs b/tests/e2e.rs index bd7bb47..d519640 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -110,7 +110,7 @@ async fn w3c_vc_did_client_direct_post() { // 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. - "$.vp".into(), + "$".into(), ) .set_path_nested(DescriptorMap::new( // Descriptor map id must be the same as the parent descriptor map id. @@ -121,7 +121,7 @@ async fn w3c_vc_did_client_direct_post() { // The nested path is relative to the `vp` key. // // See: https://identity.foundation/presentation-exchange/spec/v2.0.0/#processing-of-submission-entries - "$.verifiableCredential[0]".into(), + "$.vp.verifiableCredential[0]".into(), )) }) .collect(); From 2a937502f2d341a49f52f8c5dba02488fafaee2f Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 20 Aug 2024 08:28:01 -0700 Subject: [PATCH 45/71] Update src/verifier/client.rs Co-authored-by: Jacob --- src/verifier/client.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/verifier/client.rs b/src/verifier/client.rs index 7bad783..66873c4 100644 --- a/src/verifier/client.rs +++ b/src/verifier/client.rs @@ -46,7 +46,6 @@ impl DIDClient { signer: Arc, resolver: impl JWKResolver, ) -> Result { - // use ssi_jwk::JWKResolver; let (id, _f) = vm.rsplit_once('#').context(format!( "expected a DID verification method, received '{vm}'" From f490b8ba855786676b682fc753237aa53e14dcb3 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 20 Aug 2024 08:30:35 -0700 Subject: [PATCH 46/71] rebase Signed-off-by: Ryan Tate --- src/core/metadata/parameters/wallet.rs | 1 - src/verifier/client.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/core/metadata/parameters/wallet.rs b/src/core/metadata/parameters/wallet.rs index 005e680..bf2c288 100644 --- a/src/core/metadata/parameters/wallet.rs +++ b/src/core/metadata/parameters/wallet.rs @@ -135,7 +135,6 @@ impl From for Json { } } -// TODO: Better types #[derive(Debug, Clone)] pub struct VpFormatsSupported(pub ClaimFormatMap); diff --git a/src/verifier/client.rs b/src/verifier/client.rs index 66873c4..1488561 100644 --- a/src/verifier/client.rs +++ b/src/verifier/client.rs @@ -46,7 +46,6 @@ impl DIDClient { signer: Arc, resolver: impl JWKResolver, ) -> Result { - let (id, _f) = vm.rsplit_once('#').context(format!( "expected a DID verification method, received '{vm}'" ))?; From 347259d764ebeaf6e69da46ec031b8cf40f864a1 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 20 Aug 2024 08:35:17 -0700 Subject: [PATCH 47/71] revert validation function async signature to use boxed pin future Signed-off-by: Ryan Tate --- src/verifier/mod.rs | 8 ++++---- tests/jwt_vc.rs | 23 ++++++++++++----------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/verifier/mod.rs b/src/verifier/mod.rs index 725f880..d179856 100644 --- a/src/verifier/mod.rs +++ b/src/verifier/mod.rs @@ -1,4 +1,4 @@ -use std::{fmt::Debug, future::Future, sync::Arc}; +use std::{fmt::Debug, future::Future, pin::Pin, sync::Arc}; use anyhow::{bail, Context, Result}; use client::Client; @@ -119,12 +119,12 @@ impl Verifier { validator_function: F, ) -> Result<()> where - F: FnOnce(Session, AuthorizationResponse) -> Fut, - Fut: Future>, + F: FnOnce(Session, AuthorizationResponse) -> Pin>, + Fut: Future, { let session = self.session_store.get_session(reference).await?; - let outcome = validator_function(session, authorization_response).await?; + let outcome = validator_function(session, authorization_response).await; self.session_store .update_status(reference, Status::Complete(outcome)) diff --git a/tests/jwt_vc.rs b/tests/jwt_vc.rs index af6b7f5..7559b73 100644 --- a/tests/jwt_vc.rs +++ b/tests/jwt_vc.rs @@ -159,17 +159,18 @@ impl AsyncHttpClient for MockHttpClient { id.parse().context("failed to parse id")?, AuthorizationResponse::from_x_www_form_urlencoded(body) .context("failed to parse authorization response request")?, - |session, auth_response| async move { - session - .presentation_definition - .validate_authorization_response(&auth_response) - .await?; - - println!("Session: {:?}", session); - println!("Auth Response: {:?}", auth_response); - - Ok(Outcome::Success { - info: serde_json::Value::Null, + |session, auth_response| { + Box::pin(async move { + match session + .presentation_definition + .validate_authorization_response(&auth_response) + .await + { + Ok(_) => Outcome::Success { + info: serde_json::Value::Null, + }, + Err(e) => Outcome::Error { cause: Arc::new(e) }, + } }) }, ) From 05d550effb43563a29cab5f8ed9c6d63dbefda84 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Thu, 22 Aug 2024 08:48:47 -0700 Subject: [PATCH 48/71] add helper methods Signed-off-by: Ryan Tate --- src/core/metadata/mod.rs | 39 +++++++++++++++++++++++--- src/core/metadata/parameters/wallet.rs | 10 +++++-- src/core/util/mod.rs | 1 + src/presentation_exchange.rs | 26 +++++++++++++++++ 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/src/core/metadata/mod.rs b/src/core/metadata/mod.rs index 45b4f7a..a749362 100644 --- a/src/core/metadata/mod.rs +++ b/src/core/metadata/mod.rs @@ -1,6 +1,6 @@ use std::ops::{Deref, DerefMut}; -use anyhow::Error; +use anyhow::{bail, Error, Result}; use parameters::wallet::{RequestObjectSigningAlgValuesSupported, ResponseTypesSupported}; use serde::{Deserialize, Serialize}; use ssi_jwk::Algorithm; @@ -37,13 +37,32 @@ impl WalletMetadata { &self.1 } + /// Return a reference to the vp formats supported. pub fn vp_formats_supported(&self) -> &VpFormatsSupported { &self.2 } - /// Returns whether the claim format is supported. - pub fn is_claim_format_supported(&self, designation: &ClaimFormatDesignation) -> bool { - self.vp_formats_supported().0.contains_key(designation) + /// Return a mutable reference to the vp formats supported. + pub fn vp_formats_supported_mut(&mut self) -> &mut VpFormatsSupported { + &mut self.2 + } + + // /// Returns whether the claim format is supported. + // pub fn is_claim_format_supported(&self, designation: &ClaimFormatDesignation) -> bool { + // self.vp_formats_supported() + // .is_claim_format_supported(designation) + // } + + /// Adds a new algorithm to the list of supported request object signing algorithms. + pub fn add_request_object_signing_alg(&mut self, alg: String) -> Result<()> { + self.0 + .get::() + .transpose()? + .map(|x| x.0) + .get_or_insert_with(Vec::new) + .push(alg); + + Ok(()) } /// The static wallet metadata bound to `openid4vp:`: @@ -98,6 +117,18 @@ impl WalletMetadata { // Unwrap safety: unit tested. object.try_into().unwrap() } + + /// Return the `request_object_signing_alg_values_supported` + /// field from the wallet metadata. + pub fn request_object_signing_alg_values_supported(&self) -> Result, Error> { + let Some(Ok(algs)) = self.get::() else { + bail!( + "Failed to parse request object signing algorithms supported from wallet metadata." + ) + }; + + Ok(algs.0) + } } impl From for UntypedObject { diff --git a/src/core/metadata/parameters/wallet.rs b/src/core/metadata/parameters/wallet.rs index bf2c288..444d624 100644 --- a/src/core/metadata/parameters/wallet.rs +++ b/src/core/metadata/parameters/wallet.rs @@ -3,7 +3,7 @@ use crate::{ authorization_request::parameters::{ClientIdScheme, ResponseType}, object::TypedParameter, }, - presentation_exchange::ClaimFormatMap, + presentation_exchange::{ClaimFormatDesignation, ClaimFormatMap}, }; use anyhow::{bail, Error, Result}; use serde_json::Value as Json; @@ -135,7 +135,7 @@ impl From for Json { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct VpFormatsSupported(pub ClaimFormatMap); impl TypedParameter for VpFormatsSupported { @@ -158,6 +158,12 @@ impl TryFrom for Json { } } +impl VpFormatsSupported { + pub fn is_claim_format_supported(&self, designation: &ClaimFormatDesignation) -> bool { + self.0.contains_key(designation) + } +} + #[derive(Debug, Clone)] pub struct AuthorizationEncryptionAlgValuesSupported(pub Vec); diff --git a/src/core/util/mod.rs b/src/core/util/mod.rs index 0fb529d..fe7b7e6 100644 --- a/src/core/util/mod.rs +++ b/src/core/util/mod.rs @@ -17,6 +17,7 @@ pub(crate) fn base_request() -> http::request::Builder { } #[cfg(feature = "reqwest")] +#[derive(Debug)] pub struct ReqwestClient(reqwest::Client); #[cfg(feature = "reqwest")] diff --git a/src/presentation_exchange.rs b/src/presentation_exchange.rs index 9c25607..c96c9a6 100644 --- a/src/presentation_exchange.rs +++ b/src/presentation_exchange.rs @@ -145,6 +145,32 @@ pub enum ClaimFormatPayload { Json(serde_json::Value), } +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) { + match self { + Self::Alg(algs) | Self::AlgValuesSupported(algs) => { + algs.push(alg); + } + _ => {} // Noop + } + } + + /// 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) { + match self { + Self::ProofType(proof_types) => { + proof_types.push(proof_type); + } + _ => {} // Noop + } + } +} + /// 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 From 129d22c8dbceb2180ccbb2538b3bc44505593328 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Thu, 22 Aug 2024 08:49:47 -0700 Subject: [PATCH 49/71] debug: jwt claim signing does not include public key Signed-off-by: Ryan Tate --- tests/jwt_vp.rs | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/tests/jwt_vp.rs b/tests/jwt_vp.rs index b69dc19..a2e375a 100644 --- a/tests/jwt_vp.rs +++ b/tests/jwt_vp.rs @@ -6,6 +6,7 @@ use anyhow::Result; use oid4vp::core::authorization_request::parameters::Nonce; use oid4vp::verifier::request_signer::{P256Signer, RequestSigner}; +use ssi_claims::jws::JWSSigner; use ssi_claims::jwt::VerifiablePresentation; use ssi_claims::vc::v2::syntax::VERIFIABLE_PRESENTATION_TYPE; use ssi_claims::{CompactJWSString, JWSPayload, JWTClaims}; @@ -19,20 +20,16 @@ use ssi_jwk::JWK; pub async fn create_test_verifiable_presentation() -> Result { let verifier = JWK::from_str(include_str!("examples/verifier.jwk"))?; - let signer = Arc::new( - P256Signer::new( - p256::SecretKey::from_jwk_str(include_str!("examples/subject.jwk")) - .unwrap() - .into(), - ) - .unwrap(), - ); + let signer = P256Signer::new( + p256::SecretKey::from_jwk_str(include_str!("examples/subject.jwk")) + .unwrap() + .into(), + ) + .unwrap(); - println!("Signer: {:?}", signer.jwk()); - - let holder_jwk = JWK::from_str(std::include_str!("examples/subject.jwk"))?; - let holder_did = DIDKey::generate_url(&signer.jwk())?; + println!("Signer: {:?}", signer); + 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 @@ -95,7 +92,7 @@ pub async fn create_test_verifiable_presentation() -> Result { let claim = JWTClaims::from_private_claims(verifiable_presentation); let jwt = claim - .sign(&holder_jwk) + .sign(&signer) .await .expect("Failed to sign Verifiable Presentation JWT"); From a3bf2153e2d1d3184700344f24016c969b138027 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Thu, 22 Aug 2024 08:50:54 -0700 Subject: [PATCH 50/71] make request signer methods return a result Signed-off-by: Ryan Tate --- src/verifier/client.rs | 20 ++++++++---- src/verifier/request_signer.rs | 60 ++++++++++++++++++++++++++++++---- 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/src/verifier/client.rs b/src/verifier/client.rs index 1488561..94fcb11 100644 --- a/src/verifier/client.rs +++ b/src/verifier/client.rs @@ -37,13 +37,13 @@ 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, + signer: Arc + Send + Sync>, resolver: impl JWKResolver, ) -> Result { let (id, _f) = vm.rsplit_once('#').context(format!( @@ -55,7 +55,7 @@ impl DIDClient { .await .context("unable to resolve key from verification method")?; - if &*jwk != 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" ) @@ -74,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]; @@ -145,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, @@ -172,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_signer.rs b/src/verifier/request_signer.rs index 3b0550d..a02d8ba 100644 --- a/src/verifier/request_signer.rs +++ b/src/verifier/request_signer.rs @@ -3,17 +3,34 @@ use anyhow::Result; use async_trait::async_trait; #[cfg(feature = "p256")] use p256::ecdsa::{signature::Signer, Signature, SigningKey}; +#[cfg(feature = "p256")] +use ssi_claims::jws::{JWSSigner, JWSSignerInfo}; +#[cfg(feature = "p256")] +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")] @@ -30,17 +47,28 @@ impl P256Signer { 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 + .map(|alg| alg) + .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 +76,23 @@ impl RequestSigner for P256Signer { sig.to_vec() } } + +#[cfg(feature = "p256")] +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).into()) + }) + } +} From 7733f657845a07cd3d6b9db761fcf3098c223354 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Fri, 23 Aug 2024 15:01:18 -0700 Subject: [PATCH 51/71] refactor presentation exchange file into smaller modules Signed-off-by: Ryan Tate --- src/core/authorization_request/parameters.rs | 13 +- src/core/credential_format/mod.rs | 261 ++++++++++ src/core/input_descriptor.rs | 505 +++++++++++++++++++ src/core/metadata/mod.rs | 4 +- src/core/metadata/parameters/verifier.rs | 5 +- src/core/metadata/parameters/wallet.rs | 11 +- src/core/mod.rs | 3 + src/core/presentation_definition.rs | 185 +++++++ src/core/presentation_submission.rs | 161 ++++++ src/core/response/parameters.rs | 19 +- src/lib.rs | 4 +- src/{presentation_exchange.rs => tests.rs} | 74 ++- src/verifier/request_builder.rs | 2 +- src/verifier/session.rs | 6 +- 14 files changed, 1206 insertions(+), 47 deletions(-) create mode 100644 src/core/input_descriptor.rs create mode 100644 src/core/presentation_definition.rs create mode 100644 src/core/presentation_submission.rs rename src/{presentation_exchange.rs => tests.rs} (96%) diff --git a/src/core/authorization_request/parameters.rs b/src/core/authorization_request/parameters.rs index 0dfe80e..a2735cc 100644 --- a/src/core/authorization_request/parameters.rs +++ b/src/core/authorization_request/parameters.rs @@ -2,6 +2,7 @@ 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}; @@ -467,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/credential_format/mod.rs b/src/core/credential_format/mod.rs index 863258e..1d9b6c6 100644 --- a/src/core/credential_format/mod.rs +++ b/src/core/credential_format/mod.rs @@ -1,3 +1,7 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + /// A credential format that can be transmitted using OID4VP. pub trait CredentialFormat { /// The ID of the credential format. @@ -15,3 +19,260 @@ pub struct JwtVc; impl CredentialFormat for JwtVc { const ID: &'static str = "jwt_vc"; } + +/// 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 = "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), + 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 value + let format = value + .get("format") + .and_then(|format| format.as_str()) + // If a `format` property is not present, default to "unknown" + .unwrap_or("unknown"); + + ClaimFormatDesignation::Other(format.to_string()) + } + } + } +} + +/// 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), +} + +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) { + match self { + Self::Alg(algs) | Self::AlgValuesSupported(algs) => { + algs.push(alg); + } + _ => {} // Noop + } + } + + /// 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) { + match self { + Self::ProofType(proof_types) => { + proof_types.push(proof_type); + } + _ => {} // Noop + } + } +} + +/// 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(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 Into for ClaimFormatDesignation { + fn into(self) -> String { + match self { + Self::AcVc => "ac_vc".to_string(), + Self::AcVp => "ac_vp".to_string(), + Self::Jwt => "jwt".to_string(), + Self::JwtVc => "jwt_vc".to_string(), + Self::JwtVp => "jwt_vp".to_string(), + Self::JwtVcJson => "jwt_vc_json".to_string(), + Self::JwtVpJson => "jwt_vp_json".to_string(), + Self::Ldp => "ldp".to_string(), + Self::LdpVc => "ldp_vc".to_string(), + Self::LdpVp => "ldp_vp".to_string(), + Self::MsoMDoc => "mso_mdoc".to_string(), + Self::Other(s) => s, + } + } +} + +impl std::fmt::Display for ClaimFormatDesignation { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self) + } +} diff --git a/src/core/input_descriptor.rs b/src/core/input_descriptor.rs new file mode 100644 index 0000000..08cf8a6 --- /dev/null +++ b/src/core/input_descriptor.rs @@ -0,0 +1,505 @@ +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 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(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) -> &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 = Some(format); + 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_none() { + bail!("Required limit disclosure must have fields.") + } + }; + + if let Some(constraint_fields) = self.constraints.fields() { + for constraint_field in constraint_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(); + + 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() { + if let Some(ConstraintsLimitDisclosure::Required) = + self.constraints.limit_disclosure + { + bail!("Field elements are empty while limit disclosure is required.") + } + + // 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; + } + + // 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."); + } + } + } + } + } + } + + 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) -> 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() + } + + /// Returns if the constraints fields contain non-optional + /// fields that must be satisfied. + pub fn is_required(&self) -> bool { + if let Some(fields) = self.fields() { + fields.iter().any(|field| field.is_required()) + } else if let Some(ConstraintsLimitDisclosure::Required) = self.limit_disclosure() { + true + } else { + false + } + } +} + +/// 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 { + 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(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, + predicate: None, + optional: None, + intent_to_retain: None, + } + } +} + +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), + id: None, + purpose: None, + name: None, + filter: None, + predicate: 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 + } + + /// 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() + } +} + +#[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 a749362..8f51ec0 100644 --- a/src/core/metadata/mod.rs +++ b/src/core/metadata/mod.rs @@ -1,3 +1,5 @@ +use super::credential_format::*; + use std::ops::{Deref, DerefMut}; use anyhow::{bail, Error, Result}; @@ -5,8 +7,6 @@ use parameters::wallet::{RequestObjectSigningAlgValuesSupported, ResponseTypesSu use serde::{Deserialize, Serialize}; use ssi_jwk::Algorithm; -use crate::presentation_exchange::{ClaimFormatDesignation, ClaimFormatMap, ClaimFormatPayload}; - use self::parameters::wallet::{AuthorizationEndpoint, VpFormatsSupported}; use super::{ diff --git a/src/core/metadata/parameters/verifier.rs b/src/core/metadata/parameters/verifier.rs index fb4604c..9698ba8 100644 --- a/src/core/metadata/parameters/verifier.rs +++ b/src/core/metadata/parameters/verifier.rs @@ -1,9 +1,10 @@ +use crate::core::credential_format::ClaimFormatMap; +use crate::core::object::TypedParameter; + use anyhow::{Context, Error}; use serde::{Deserialize, Serialize}; use serde_json::{Map, Value as Json}; -use crate::{core::object::TypedParameter, presentation_exchange::ClaimFormatMap}; - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct VpFormats(pub ClaimFormatMap); diff --git a/src/core/metadata/parameters/wallet.rs b/src/core/metadata/parameters/wallet.rs index 444d624..5eff2e5 100644 --- a/src/core/metadata/parameters/wallet.rs +++ b/src/core/metadata/parameters/wallet.rs @@ -1,10 +1,9 @@ -use crate::{ - core::{ - authorization_request::parameters::{ClientIdScheme, ResponseType}, - object::TypedParameter, - }, - presentation_exchange::{ClaimFormatDesignation, ClaimFormatMap}, +use crate::core::{ + authorization_request::parameters::{ClientIdScheme, ResponseType}, + credential_format::{ClaimFormatDesignation, ClaimFormatMap}, + object::TypedParameter, }; + use anyhow::{bail, Error, Result}; use serde_json::Value as Json; use url::Url; 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/presentation_definition.rs b/src/core/presentation_definition.rs new file mode 100644 index 0000000..188f8f7 --- /dev/null +++ b/src/core/presentation_definition.rs @@ -0,0 +1,185 @@ +use super::credential_format::*; +use super::input_descriptor::*; +use super::presentation_submission::*; +use super::response::AuthorizationResponse; + +use std::collections::HashMap; + +use anyhow::{bail, Context, Result}; +use serde::{Deserialize, Serialize}; +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, 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 + } + + /// Validate a presentation submission against the presentation definition. + /// + /// Checks the underlying presentation submission parsed from the authorization response, + /// against the input descriptors of the presentation definition. + pub async fn validate_authorization_response( + &self, + auth_response: &AuthorizationResponse, + ) -> Result<()> { + match auth_response { + AuthorizationResponse::Jwt(_jwt) => { + // TODO: Handle JWT Encoded authorization response. + + bail!("Authorization Response Presentation Definition Validation Not Implemented.") + } + AuthorizationResponse::Unencoded(response) => { + let presentation_submission = response.presentation_submission().parsed(); + + let jwt = response.vp_token().0.clone(); + + let verifiable_presentation: VerifiablePresentation = + ssi_claims::jwt::decode_unverified(&jwt)?; + + // Ensure the definition id matches the submission's definition id. + if presentation_submission.definition_id() != self.id() { + bail!("Presentation Definition ID does not match the Presentation Submission.") + } + + let descriptor_map: HashMap = presentation_submission + .descriptor_map() + .iter() + .map(|descriptor_map| (descriptor_map.id().to_owned(), descriptor_map.clone())) + .collect(); + + for input_descriptor in self.input_descriptors().iter() { + match descriptor_map.get(input_descriptor.id()) { + None => { + // TODO: Determine whether input descriptor must have a corresponding descriptor map. + bail!("Input Descriptor ID not found in Descriptor Map.") + } + Some(descriptor) => { + input_descriptor + .validate_verifiable_presentation( + &verifiable_presentation, + descriptor, + ) + .context("Input Descriptor Validation Failed.")?; + } + } + } + } + } + + Ok(()) + } + + /// 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() + } +} diff --git a/src/core/presentation_submission.rs b/src/core/presentation_submission.rs new file mode 100644 index 0000000..872d1ec --- /dev/null +++ b/src/core/presentation_submission.rs @@ -0,0 +1,161 @@ +use super::{credential_format::*, input_descriptor::*}; + +use serde::{Deserialize, Serialize}; +use serde_json::Map; + +/// 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: String, + 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: String, 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 + } +} + +/// 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 [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) -> &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. + /// + /// 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, mut path_nested: DescriptorMap) -> Self { + // Ensure the nested path has the same id as the parent. + path_nested.id = self.id.clone(); + + self.path_nested = Some(Box::new(path_nested)); + + 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, +} diff --git a/src/core/response/parameters.rs b/src/core/response/parameters.rs index b9d6b8b..bb40385 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); @@ -49,25 +50,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/lib.rs b/src/lib.rs index 551dda8..eeabe7e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,7 @@ pub mod core; -pub mod presentation_exchange; +// pub mod presentation_exchange; +#[cfg(test)] +pub(crate) mod tests; mod utils; pub mod verifier; pub mod wallet; diff --git a/src/presentation_exchange.rs b/src/tests.rs similarity index 96% rename from src/presentation_exchange.rs rename to src/tests.rs index c96c9a6..c452e6c 100644 --- a/src/presentation_exchange.rs +++ b/src/tests.rs @@ -7,9 +7,8 @@ use anyhow::{bail, Context, Result}; use jsonschema::{JSONSchema, ValidationError}; use serde::{Deserialize, Serialize}; use serde_json::Map; -use ssi_claims::{jwt::VerifiablePresentation, CompactJWSString, VerificationParameters}; -use ssi_dids::{ssi_json_ld::syntax::from_value, DIDKey, VerificationMethodDIDResolver}; -use ssi_verification_methods::AnyJwkMethod; +use ssi_claims::jwt::VerifiablePresentation; +use ssi_dids::ssi_json_ld::syntax::from_value; /// A JSONPath is a string that represents a path to a specific value within a JSON object. /// @@ -125,7 +124,16 @@ impl ClaimFormat { ClaimFormat::AcVc { .. } => ClaimFormatDesignation::AcVc, ClaimFormat::AcVp { .. } => ClaimFormatDesignation::AcVp, ClaimFormat::MsoMDoc(_) => ClaimFormatDesignation::MsoMDoc, - ClaimFormat::Other(_) => ClaimFormatDesignation::Other, + ClaimFormat::Other(value) => { + // parse the format from the value + let format = value + .get("format") + .and_then(|format| format.as_str()) + // If a `format` property is not present, default to "unknown" + .unwrap_or("unknown"); + + ClaimFormatDesignation::Other(format.to_string()) + } } } } @@ -237,7 +245,51 @@ pub enum ClaimFormatDesignation { /// Other claim format designations not covered by the above. /// /// The value of this variant is the name of the claim format designation. - Other, + 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 Into for ClaimFormatDesignation { + fn into(self) -> String { + match self { + Self::AcVc => "ac_vc".to_string(), + Self::AcVp => "ac_vp".to_string(), + Self::Jwt => "jwt".to_string(), + Self::JwtVc => "jwt_vc".to_string(), + Self::JwtVp => "jwt_vp".to_string(), + Self::JwtVcJson => "jwt_vc_json".to_string(), + Self::JwtVpJson => "jwt_vp_json".to_string(), + Self::Ldp => "ldp".to_string(), + Self::LdpVc => "ldp_vc".to_string(), + Self::LdpVp => "ldp_vp".to_string(), + Self::MsoMDoc => "mso_mdoc".to_string(), + Self::Other(s) => s, + } + } +} + +impl std::fmt::Display for ClaimFormatDesignation { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self) + } } /// 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). @@ -365,16 +417,6 @@ impl PresentationDefinition { let jwt = response.vp_token().0.clone(); - let jws = CompactJWSString::from_string(jwt.clone()).context("Invalid JWT.")?; - let resolver: VerificationMethodDIDResolver = - VerificationMethodDIDResolver::new(DIDKey); - - let params = VerificationParameters::from_resolver(resolver); - - if let Err(e) = jws.verify(params).await { - bail!("JWT Verification Failed: {:?}", e) - } - let verifiable_presentation: VerifiablePresentation = ssi_claims::jwt::decode_unverified(&jwt)?; @@ -687,6 +729,8 @@ impl Constraints { pub fn is_required(&self) -> bool { if let Some(fields) = self.fields() { fields.iter().any(|field| field.is_required()) + } else if let Some(ConstraintsLimitDisclosure::Required) = self.limit_disclosure() { + true } else { false } 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/session.rs b/src/verifier/session.rs index 41fc454..909871e 100644 --- a/src/verifier/session.rs +++ b/src/verifier/session.rs @@ -6,9 +6,9 @@ use serde_json::Value as Json; 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)] From 54c0f79bf0f407d2a8d8c3b92d18d6b2a39ede36 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Fri, 23 Aug 2024 16:13:20 -0700 Subject: [PATCH 52/71] fix clippy warnings Signed-off-by: Ryan Tate --- src/core/credential_format/mod.rs | 50 +- src/core/input_descriptor.rs | 26 +- src/core/metadata/parameters/verifier.rs | 6 +- src/core/metadata/parameters/wallet.rs | 6 +- src/core/presentation_submission.rs | 2 +- src/holder/verifiable_presentation_builder.rs | 252 ++-- src/lib.rs | 2 +- src/tests.rs | 1320 ++--------------- tests/e2e.rs | 7 +- tests/jwt_vp.rs | 83 +- 10 files changed, 310 insertions(+), 1444 deletions(-) diff --git a/src/core/credential_format/mod.rs b/src/core/credential_format/mod.rs index 1d9b6c6..7199544 100644 --- a/src/core/credential_format/mod.rs +++ b/src/core/credential_format/mod.rs @@ -143,11 +143,8 @@ impl ClaimFormatPayload { /// /// This method is a no-op if self is not of type `AlgValuesSupported` or `Alg`. pub fn add_alg(&mut self, alg: String) { - match self { - Self::Alg(algs) | Self::AlgValuesSupported(algs) => { - algs.push(alg); - } - _ => {} // Noop + if let Self::Alg(algs) | Self::AlgValuesSupported(algs) = self { + algs.push(alg); } } @@ -155,11 +152,8 @@ impl ClaimFormatPayload { /// /// This method is a no-op if self is not of type `ProofType`. pub fn add_proof_type(&mut self, proof_type: String) { - match self { - Self::ProofType(proof_types) => { - proof_types.push(proof_type); - } - _ => {} // Noop + if let Self::ProofType(proof_types) = self { + proof_types.push(proof_type); } } } @@ -252,27 +246,21 @@ impl From<&str> for ClaimFormatDesignation { } } -impl Into for ClaimFormatDesignation { - fn into(self) -> String { - match self { - Self::AcVc => "ac_vc".to_string(), - Self::AcVp => "ac_vp".to_string(), - Self::Jwt => "jwt".to_string(), - Self::JwtVc => "jwt_vc".to_string(), - Self::JwtVp => "jwt_vp".to_string(), - Self::JwtVcJson => "jwt_vc_json".to_string(), - Self::JwtVpJson => "jwt_vp_json".to_string(), - Self::Ldp => "ldp".to_string(), - Self::LdpVc => "ldp_vc".to_string(), - Self::LdpVp => "ldp_vp".to_string(), - Self::MsoMDoc => "mso_mdoc".to_string(), - Self::Other(s) => s, +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, } } } - -impl std::fmt::Display for ClaimFormatDesignation { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self) - } -} diff --git a/src/core/input_descriptor.rs b/src/core/input_descriptor.rs index 08cf8a6..edc6107 100644 --- a/src/core/input_descriptor.rs +++ b/src/core/input_descriptor.rs @@ -175,24 +175,22 @@ impl InputDescriptor { 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() { - if let Some(ConstraintsLimitDisclosure::Required) = - self.constraints.limit_disclosure - { - bail!("Field elements are empty while limit disclosure is required.") - } - // 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| { @@ -220,6 +218,15 @@ impl InputDescriptor { } } } + + // 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.") + } + } } } @@ -295,10 +302,11 @@ impl Constraints { pub fn is_required(&self) -> bool { if let Some(fields) = self.fields() { fields.iter().any(|field| field.is_required()) - } else if let Some(ConstraintsLimitDisclosure::Required) = self.limit_disclosure() { - true } else { - false + matches!( + self.limit_disclosure(), + Some(ConstraintsLimitDisclosure::Required) + ) } } } diff --git a/src/core/metadata/parameters/verifier.rs b/src/core/metadata/parameters/verifier.rs index 9698ba8..97afa4a 100644 --- a/src/core/metadata/parameters/verifier.rs +++ b/src/core/metadata/parameters/verifier.rs @@ -121,8 +121,10 @@ impl From for Json { mod test { use serde_json::json; - use crate::core::object::UntypedObject; - use crate::presentation_exchange::{ClaimFormatDesignation, ClaimFormatPayload}; + use crate::core::{ + credential_format::{ClaimFormatDesignation, ClaimFormatPayload}, + object::UntypedObject, + }; use super::*; diff --git a/src/core/metadata/parameters/wallet.rs b/src/core/metadata/parameters/wallet.rs index 5eff2e5..09af0cd 100644 --- a/src/core/metadata/parameters/wallet.rs +++ b/src/core/metadata/parameters/wallet.rs @@ -209,9 +209,9 @@ impl From for Json { mod test { use serde_json::json; - use crate::{ - core::object::UntypedObject, - presentation_exchange::{ClaimFormatDesignation, ClaimFormatPayload}, + use crate::core::{ + credential_format::{ClaimFormatDesignation, ClaimFormatPayload}, + object::UntypedObject, }; use super::*; diff --git a/src/core/presentation_submission.rs b/src/core/presentation_submission.rs index 872d1ec..f366c98 100644 --- a/src/core/presentation_submission.rs +++ b/src/core/presentation_submission.rs @@ -113,7 +113,7 @@ impl DescriptorMap { /// - The id of the nested path must be the same as the parent id. 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 = self.id.clone(); + path_nested.id.clone_from(self.id()); self.path_nested = Some(Box::new(path_nested)); diff --git a/src/holder/verifiable_presentation_builder.rs b/src/holder/verifiable_presentation_builder.rs index dd31cbc..e7db9d3 100644 --- a/src/holder/verifiable_presentation_builder.rs +++ b/src/holder/verifiable_presentation_builder.rs @@ -1,152 +1,164 @@ -use anyhow::{Context, Result}; +use std::time::{SystemTime, UNIX_EPOCH}; + use serde::{Deserialize, Serialize}; -use ssi_claims::vc::{v1::VerifiableCredential, v2::syntax::VERIFIABLE_CREDENTIAL_TYPE}; -use ssi_dids::{DIDURLBuf, DIDURL}; - -pub const VERIFIABLE_PRESENTATION_CONTEXT_V1: &str = "https://www.w3.org/2018/credentials/v1"; - -// NOTE: This may make more sense to be moved to ssi_claims lib. -pub const VERIFIABLE_PRESENTATION_TYPE: &str = "VerifiablePresentation"; - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct VerifiablePresentationBuilder { - /// The issuer of the presentation. - #[serde(skip_serializing_if = "Option::is_none")] - iss: Option, // TODO: Should this be a DIDURLBuf or IRI/URI type? - /// The Json Web Token ID of the presentation. - #[serde(skip_serializing_if = "Option::is_none")] - jti: Option, - /// The audience of the presentation. - #[serde(skip_serializing_if = "Option::is_none")] - aud: Option, // TODO: Should this be a DIDURLBuf? - /// The issuance date of the presentation. - #[serde(skip_serializing_if = "Option::is_none")] - iat: Option, - /// The expiration date of the presentation. - #[serde(skip_serializing_if = "Option::is_none")] - exp: Option, - /// The nonce of the presentation. - #[serde(skip_serializing_if = "Option::is_none")] - nonce: Option, - /// The verifiable presentation format. - #[serde(skip_serializing_if = "Option::is_none")] - vp: Option, +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, Deserialize, Serialize)] -pub struct VerifiablePresentationCredentialBuilder { - /// The context of the presentation. - #[serde(rename = "@context")] - context: Vec, - /// The type of the presentation. - #[serde(rename = "type")] - type_: Vec, - /// The verifiable credentials list of the presentation. - verifiable_credential: Vec, +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VerifiablePresentationBuilder(VerifiablePresentation); + +impl From for VerifiablePresentation { + fn from(builder: VerifiablePresentationBuilder) -> Self { + builder.0 + } } -impl Default for VerifiablePresentationCredentialBuilder { +impl Default for VerifiablePresentationBuilder { fn default() -> Self { - Self { - context: vec![VERIFIABLE_PRESENTATION_CONTEXT_V1.into()], - type_: vec![VERIFIABLE_PRESENTATION_TYPE.into()], - verifiable_credential: vec![], - } + Self::new() } } -impl VerifiablePresentationCredentialBuilder { +impl VerifiablePresentationBuilder { + /// Returns an empty verifiable presentation builder. pub fn new() -> Self { - Self::default() + Self(VerifiablePresentation(Value::Object(Object::new()))) } - /// Add a verifiable credential to the presentation. - pub fn add_verifiable_credential( - mut self, - verifiable_credential: VerifiableCredentialBuilder, - ) -> Self { - self.verifiable_credential.push(verifiable_credential); - self - } -} + /// 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())); -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct VerifiableCredentialBuilder { - /// The context of the credential. - #[serde(rename = "@context")] - context: Vec, - /// The type of the credential. - #[serde(rename = "type")] - type_: Vec, - /// The issuer of the credential. - issuer: Option, - // TODO: Determine if we should use a DateTime type here, use chrono lib? - #[serde(rename = "issuanceDate")] - issuance_date: Option, - #[serde(rename = "credentialSubject")] - credential_subject: Option, -} + 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())); -impl Default for VerifiableCredentialBuilder { - fn default() -> Self { - Self { - context: vec![VERIFIABLE_PRESENTATION_CONTEXT_V1.into()], - type_: vec![VERIFIABLE_CREDENTIAL_TYPE.into()], - issuer: None, - issuance_date: None, - credential_subject: None, + // 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); } - } -} -impl VerifiableCredentialBuilder { - pub fn new() -> Self { - Self::default() + verifiable_presentation } - /// Add a credential to the credential builder, e.g. `IdentityCredential` or `mDL`. + /// Add an issuer to the verifiable presentation. /// - /// By default, the `VerifiableCredential` type is added to the credential. - pub fn add_type(mut self, credential_type: String) -> Self { - self.type_.push(credential_type); + /// 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 } - /// Set the issuer of the credential. - /// - /// The value of the issuer property MUST be either a URI or an object containing an id property. - /// It is RECOMMENDED that the URI in the issuer or its id be one which, if dereferenced, results - /// in a document containing machine-readable information about the issuer that can be used to verify - /// the information expressed in the credential. + /// Add a subject to the verifiable presentation. /// - /// See: [https://www.w3.org/TR/vc-data-model-1.0/#issuer](https://www.w3.org/TR/vc-data-model-1.0/#issuer) - pub fn set_issuer(mut self, issuer: String) -> Self { - self.issuer = Some(issuer); + /// 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 } - /// Set the issuance date of the credential. - /// - /// A credential MUST have an issuanceDate property. - /// The value of the issuanceDate property MUST be a string value of an [RFC3339](https://www.w3.org/TR/vc-data-model-1.0/#bib-rfc3339) - /// combined date and time string representing the date and time the credential becomes valid, - /// which could be a date and time in the future. Note that this value represents the earliest - /// point in time at which the information associated with the credentialSubject property becomes valid. - /// - /// See: [https://www.w3.org/TR/vc-data-model-1.0/#issuance-date](https://www.w3.org/TR/vc-data-model-1.0/#issuance-date) - pub fn set_issuance_date(mut self, issuance_date: String) -> Self { - self.issuance_date = Some(issuance_date); + /// 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 credential subject of the credential. - /// - /// The value of the credentialSubject property is defined as a set of objects that contain - /// one or more properties that are each related to a subject of the verifiable credential. - /// Each object MAY contain an id, as described in [Section § 4.2 Identifiers](https://www.w3.org/TR/vc-data-model-1.0/#identifiers) - /// section of the specification. - pub fn set_credential_subject(mut self, credential_subject: serde_json::Value) -> Self { - self.credential_subject = Some(credential_subject); + /// 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 eeabe7e..f62a444 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ pub mod core; -// pub mod presentation_exchange; +pub mod holder; #[cfg(test)] pub(crate) mod tests; mod utils; diff --git a/src/tests.rs b/src/tests.rs index c452e6c..70d2149 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,1233 +1,143 @@ -use std::collections::HashMap; +use serde::Deserialize; -use crate::core::response::AuthorizationResponse; -pub use crate::utils::NonEmptyVec; +// use crate::core::response::AuthorizationResponse; +// pub use crate::utils::NonEmptyVec; -use anyhow::{bail, Context, Result}; -use jsonschema::{JSONSchema, ValidationError}; -use serde::{Deserialize, Serialize}; -use serde_json::Map; -use ssi_claims::jwt::VerifiablePresentation; -use ssi_dids::ssi_json_ld::syntax::from_value; +// use anyhow::{bail, Context, Result}; +// use jsonschema::{JSONSchema, ValidationError}; -/// 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; +// use serde_json::Map; +// use ssi_claims::jwt::VerifiablePresentation; +// use ssi_dids::ssi_json_ld::syntax::from_value; -/// 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, -} -/// 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 = "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), - Other(serde_json::Value), -} +use crate::core::{presentation_definition::PresentationDefinition, presentation_submission::*}; -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 value - let format = value - .get("format") - .and_then(|format| format.as_str()) - // If a `format` property is not present, default to "unknown" - .unwrap_or("unknown"); +use serde_json::json; +use std::{ + ffi::OsStr, + fs::{self, File}, +}; - ClaimFormatDesignation::Other(format.to_string()) - } - } - } -} - -/// 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), -} - -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) { - match self { - Self::Alg(algs) | Self::AlgValuesSupported(algs) => { - algs.push(alg); - } - _ => {} // Noop - } - } - - /// 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) { - match self { - Self::ProofType(proof_types) => { - proof_types.push(proof_type); - } - _ => {} // Noop - } - } -} - -/// 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(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 Into for ClaimFormatDesignation { - fn into(self) -> String { - match self { - Self::AcVc => "ac_vc".to_string(), - Self::AcVp => "ac_vp".to_string(), - Self::Jwt => "jwt".to_string(), - Self::JwtVc => "jwt_vc".to_string(), - Self::JwtVp => "jwt_vp".to_string(), - Self::JwtVcJson => "jwt_vc_json".to_string(), - Self::JwtVpJson => "jwt_vp_json".to_string(), - Self::Ldp => "ldp".to_string(), - Self::LdpVc => "ldp_vc".to_string(), - Self::LdpVp => "ldp_vp".to_string(), - Self::MsoMDoc => "mso_mdoc".to_string(), - Self::Other(s) => s, - } - } -} - -impl std::fmt::Display for ClaimFormatDesignation { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self) - } -} - -/// 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 - } - - /// Validate a presentation submission against the presentation definition. - /// - /// Checks the underlying presentation submission parsed from the authorization response, - /// against the input descriptors of the presentation definition. - pub async fn validate_authorization_response( - &self, - auth_response: &AuthorizationResponse, - ) -> Result<()> { - match auth_response { - AuthorizationResponse::Jwt(_jwt) => { - // TODO: Handle JWT Encoded authorization response. - - bail!("Authorization Response Presentation Definition Validation Not Implemented.") - } - AuthorizationResponse::Unencoded(response) => { - let presentation_submission = response.presentation_submission().parsed(); - - let jwt = response.vp_token().0.clone(); - - let verifiable_presentation: VerifiablePresentation = - ssi_claims::jwt::decode_unverified(&jwt)?; - - // Ensure the definition id matches the submission's definition id. - if presentation_submission.definition_id() != self.id() { - bail!("Presentation Definition ID does not match the Presentation Submission.") - } - - let descriptor_map: HashMap = presentation_submission - .descriptor_map() - .iter() - .map(|descriptor_map| (descriptor_map.id().to_owned(), descriptor_map.clone())) - .collect(); - - for input_descriptor in self.input_descriptors().iter() { - match descriptor_map.get(input_descriptor.id()) { - None => { - // TODO: Determine whether input descriptor must have a corresponding descriptor map. - bail!("Input Descriptor ID not found in Descriptor Map.") - } - Some(descriptor) => { - input_descriptor - .validate_verifiable_presentation( - &verifiable_presentation, - descriptor, - ) - .context("Input Descriptor Validation Failed.")?; - } - } - } - } - } - - Ok(()) - } - - /// 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) -> &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 = Some(format); - 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_none() { - bail!("Required limit disclosure must have fields.") - } - }; - - if let Some(constraint_fields) = self.constraints.fields() { - for constraint_field in constraint_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(); - - 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() { - if let Some(ConstraintsLimitDisclosure::Required) = - self.constraints.limit_disclosure - { - bail!("Field elements are empty while limit disclosure is required.") +#[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" + ] } - - // 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; - } - - // 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 + }, + "constraints": { + "fields": [ + { + "path": [ + "$.type" + ], + "filter": { + "type": "string", + "pattern": "IDCardCredential" } - 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."); } - } + ] } } - } + ] } - - 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) -> 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() - } - - /// Returns if the constraints fields contain non-optional - /// fields that must be satisfied. - pub fn is_required(&self) -> bool { - if let Some(fields) = self.fields() { - fields.iter().any(|field| field.is_required()) - } else if let Some(ConstraintsLimitDisclosure::Required) = self.limit_disclosure() { - true - } else { - false - } - } -} - -/// 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 { - 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(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, - predicate: None, - optional: None, - intent_to_retain: None, - } - } -} - -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), - id: None, - purpose: None, - name: None, - filter: None, - predicate: 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 - } - - /// 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() - } -} - -#[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: String, - 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: String, 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 - } -} - -/// 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 [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) -> &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. - /// - /// 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, mut path_nested: DescriptorMap) -> Self { - // Ensure the nested path has the same id as the parent. - path_nested.id = self.id.clone(); - - self.path_nested = Some(Box::new(path_nested)); - - 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, - }, + ); + let _: PresentationDefinition = serde_json::from_value(value).unwrap(); } #[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!( +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()) { - "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; - } + 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!("✅") } + 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; - } +#[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!("✅") } + 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; - } +#[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!("✅") } + 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/tests/e2e.rs b/tests/e2e.rs index d519640..8e2df83 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -1,13 +1,14 @@ use jwt_vp::create_test_verifiable_presentation; -use oid4vp::presentation_exchange::*; - 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, }; diff --git a/tests/jwt_vp.rs b/tests/jwt_vp.rs index a2e375a..cd3dabf 100644 --- a/tests/jwt_vp.rs +++ b/tests/jwt_vp.rs @@ -1,19 +1,12 @@ use std::str::FromStr; -use std::sync::Arc; -use std::time::{SystemTime, UNIX_EPOCH}; use anyhow::Result; - -use oid4vp::core::authorization_request::parameters::Nonce; -use oid4vp::verifier::request_signer::{P256Signer, RequestSigner}; -use ssi_claims::jws::JWSSigner; -use ssi_claims::jwt::VerifiablePresentation; -use ssi_claims::vc::v2::syntax::VERIFIABLE_PRESENTATION_TYPE; -use ssi_claims::{CompactJWSString, JWSPayload, JWTClaims}; -// use ssi_claims::vc::v1::VerifiableCredential; +use oid4vp::holder::verifiable_presentation_builder::{ + VerifiablePresentationBuilder, VerifiablePresentationBuilderOptions, +}; +use oid4vp::verifier::request_signer::P256Signer; use ssi_claims::jwt; -use ssi_dids::ssi_json_ld::syntax::{Object, Value}; -use ssi_dids::ssi_json_ld::CREDENTIALS_V1_CONTEXT; +use ssi_claims::{CompactJWSString, JWSPayload, JWTClaims}; use ssi_dids::DIDKey; use ssi_jwk::JWK; @@ -27,8 +20,6 @@ pub async fn create_test_verifiable_presentation() -> Result { ) .unwrap(); - println!("Signer: {:?}", signer); - let holder_did = DIDKey::generate_url(signer.jwk())?; let verifier_did = DIDKey::generate_url(&verifier)?; @@ -37,58 +28,16 @@ pub async fn create_test_verifiable_presentation() -> Result { let verifiable_credential: jwt::VerifiableCredential = ssi_claims::jwt::decode_unverified(include_str!("examples/vc.jwt"))?; - println!("VC: {:?}", verifiable_credential); - - // TODO: There should be a more idiomatically correct way to do this, if not already implemented. - // NOTE: There is an unused `VerifiablePresentationBuilder` in the holder module, however, these methods - // may best be implemented as methods on the `VerifiablePresentation` struct itself. - let mut verifiable_presentation = VerifiablePresentation(Value::Object(Object::new())); - - verifiable_presentation.0.as_object_mut().map(|obj| { - // The issuer is the holder of the verifiable credential (subject of the verifiable credential). - obj.insert("iss".into(), Value::String(holder_did.as_str().into())); - - // The audience is the verifier of the verifiable credential. - obj.insert("aud".into(), Value::String(verifier_did.as_str().into())); - - SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok() - .map(|dur| { - // 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() + 3600).into())); - }); - - obj.insert( - "nonce".into(), - Value::String(Nonce::from("random_nonce").to_string().into()), - ); - - let mut verifiable_credential_field = Value::Object(Object::new()); - - verifiable_credential_field.as_object_mut().map(|cred| { - 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(vec![verifiable_credential.0]), - ); + 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(), }); - obj.insert("vp".into(), verifiable_credential_field); - }); - let claim = JWTClaims::from_private_claims(verifiable_presentation); let jwt = claim @@ -96,11 +45,7 @@ pub async fn create_test_verifiable_presentation() -> Result { .await .expect("Failed to sign Verifiable Presentation JWT"); - println!("JWT: {:?}", jwt); - - let vp: jwt::VerifiablePresentation = ssi_claims::jwt::decode_unverified(jwt.as_str())?; - - println!("VP: {:?}", vp); + let _: jwt::VerifiablePresentation = ssi_claims::jwt::decode_unverified(jwt.as_str())?; Ok(jwt) } From a7aed1166dedebc1f4c579806ba3a83e0ff30aaf Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Fri, 23 Aug 2024 16:33:15 -0700 Subject: [PATCH 53/71] fix outcome error cause Signed-off-by: Ryan Tate --- tests/jwt_vc.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/jwt_vc.rs b/tests/jwt_vc.rs index 7559b73..3c5768a 100644 --- a/tests/jwt_vc.rs +++ b/tests/jwt_vc.rs @@ -169,7 +169,9 @@ impl AsyncHttpClient for MockHttpClient { Ok(_) => Outcome::Success { info: serde_json::Value::Null, }, - Err(e) => Outcome::Error { cause: Arc::new(e) }, + Err(e) => Outcome::Error { + cause: e.to_string(), + }, } }) }, From 2e453ca68f2b8253858d8b2d78de998dba9be26d Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Mon, 26 Aug 2024 10:43:33 -0700 Subject: [PATCH 54/71] add credential format and add requested fields helper method to input descriptor Signed-off-by: Ryan Tate --- src/core/credential_format/mod.rs | 40 ++++++++++++++++ src/core/input_descriptor.rs | 72 +++++++++++++++++++++++++++++ src/core/presentation_definition.rs | 55 ++++++++++++++++------ 3 files changed, 154 insertions(+), 13 deletions(-) diff --git a/src/core/credential_format/mod.rs b/src/core/credential_format/mod.rs index 7199544..1b7a599 100644 --- a/src/core/credential_format/mod.rs +++ b/src/core/credential_format/mod.rs @@ -264,3 +264,43 @@ impl From for String { } } } + +/// Credential types that may be requested in a credential request. +/// +/// Credential types can be presented in a number of formats. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum CredentialType { + /// an ISO 18013-5:2021 mobile driving license (mDL) Credential + #[serde(rename = "org.iso.18013.5.1.mDL")] + Iso18013_5_1mDl, + /// A vehicle title credential + /// + /// Given there is no universal standard for how to present a vehicle title credential, + /// the inner String provides a dynamic way to represent a vehicle title credential. + #[serde(rename = "vehicle_title")] + VehicleTitle(String), + // Add additional credential types here. + // + // Fallback to a string for any other credential type. + Other(String), +} + +impl From<&str> for CredentialType { + fn from(s: &str) -> Self { + match s { + s if s.contains("org.iso.18013.5.1.mDL") => Self::Iso18013_5_1mDl, + s if s.contains("vehicle_title.") => Self::VehicleTitle(s.to_string()), + s => Self::Other(s.to_string()), + } + } +} + +impl From for String { + fn from(cred_type: CredentialType) -> Self { + match cred_type { + CredentialType::Iso18013_5_1mDl => "org.iso.18013.5.1.mDL".to_string(), + CredentialType::VehicleTitle(title) => format!("vehicle_title.{title}"), + CredentialType::Other(s) => s, + } + } +} diff --git a/src/core/input_descriptor.rs b/src/core/input_descriptor.rs index edc6107..3c68d0e 100644 --- a/src/core/input_descriptor.rs +++ b/src/core/input_descriptor.rs @@ -503,6 +503,78 @@ impl ConstraintsField { pub fn is_required(&self) -> bool { !self.is_optional() } + + /// Set the intent to retain the constraints field. + pub fn set_retained(mut self, intent_to_retain: bool) -> Self { + self.intent_to_retain = Some(intent_to_retain); + self + } + + /// Return the intent to retain the constraints field. + pub fn is_intended_to_retain(&self) -> bool { + self.intent_to_retain.unwrap_or(false) + } + + /// 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() + .into_iter() + .map(|path| path.split(&['-', '.', ':', '@'][..]).last()) + .flatten() + .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(' ') + .map(|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!("{} ", word.trim_end()) + }) + .collect::() + .trim_end() + .to_string() + }) + .collect() + } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] diff --git a/src/core/presentation_definition.rs b/src/core/presentation_definition.rs index 188f8f7..b1ab2a4 100644 --- a/src/core/presentation_definition.rs +++ b/src/core/presentation_definition.rs @@ -115,10 +115,50 @@ impl PresentationDefinition { 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() + } + + /// 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() + .filter_map(|input_descriptor| { + input_descriptor.constraints().fields().map(|fields| { + fields + .iter() + .map(|constraint| constraint.requested_fields()) + }) + }) + .flat_map(|field| field.into_iter()) + .flatten() + .map(|field| field.to_string()) + .collect() + } + /// Validate a presentation submission against the presentation definition. /// /// Checks the underlying presentation submission parsed from the authorization response, /// against the input descriptors of the presentation definition. + #[deprecated( + note = "This method is to be replaced by a top-level function that takes a presentation definition and a presentation submission." + )] pub async fn validate_authorization_response( &self, auth_response: &AuthorizationResponse, @@ -134,6 +174,8 @@ impl PresentationDefinition { let jwt = response.vp_token().0.clone(); + // TODO: Validate the VP JWT Signature against the holder's key? + let verifiable_presentation: VerifiablePresentation = ssi_claims::jwt::decode_unverified(&jwt)?; @@ -169,17 +211,4 @@ impl PresentationDefinition { Ok(()) } - - /// 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() - } } From a3bfdcb67148184c9cdebb1680275883813d08b9 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 27 Aug 2024 14:20:15 -0700 Subject: [PATCH 55/71] Update src/core/presentation_definition.rs Co-authored-by: Jacob --- src/core/presentation_definition.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/presentation_definition.rs b/src/core/presentation_definition.rs index b1ab2a4..f50cff2 100644 --- a/src/core/presentation_definition.rs +++ b/src/core/presentation_definition.rs @@ -167,7 +167,7 @@ impl PresentationDefinition { AuthorizationResponse::Jwt(_jwt) => { // TODO: Handle JWT Encoded authorization response. - bail!("Authorization Response Presentation Definition Validation Not Implemented.") + bail!("Authorization Response Presentation Definition validation not implemented.") } AuthorizationResponse::Unencoded(response) => { let presentation_submission = response.presentation_submission().parsed(); From 7a337ae536590dd511f1dc7482db596546a38178 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 27 Aug 2024 14:20:34 -0700 Subject: [PATCH 56/71] Update src/core/presentation_definition.rs Co-authored-by: Jacob --- src/core/presentation_definition.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/presentation_definition.rs b/src/core/presentation_definition.rs index f50cff2..8628e0d 100644 --- a/src/core/presentation_definition.rs +++ b/src/core/presentation_definition.rs @@ -202,7 +202,7 @@ impl PresentationDefinition { &verifiable_presentation, descriptor, ) - .context("Input Descriptor Validation Failed.")?; + .context("Input Descriptor validation failed.")?; } } } From 76e3a3acacc5e4705354582e1ffd37790132c9ff Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 27 Aug 2024 14:23:02 -0700 Subject: [PATCH 57/71] wip: add notes on required fields parsing Signed-off-by: Ryan Tate --- src/core/input_descriptor.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/input_descriptor.rs b/src/core/input_descriptor.rs index 3c68d0e..28d107d 100644 --- a/src/core/input_descriptor.rs +++ b/src/core/input_descriptor.rs @@ -529,6 +529,10 @@ impl ConstraintsField { pub fn requested_fields(&self) -> Vec { self.path() .into_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. .map(|path| path.split(&['-', '.', ':', '@'][..]).last()) .flatten() .map(|path| { From 51e11e8d0476b2eb4d644c8a3e5f51a207489ad7 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 27 Aug 2024 17:37:11 -0700 Subject: [PATCH 58/71] update vp token base64 encoding and check for multiple vp payloads Signed-off-by: Ryan Tate --- Cargo.toml | 2 + src/core/credential_format/mod.rs | 18 -------- src/core/input_descriptor.rs | 10 ++--- src/core/presentation_definition.rs | 66 ++++++++++++++++++++--------- src/core/presentation_submission.rs | 5 +++ src/core/response/parameters.rs | 23 ++++++++++ tests/e2e.rs | 3 ++ tests/jwt_vp.rs | 17 +++----- 8 files changed, 90 insertions(+), 54 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f5c7fdc..6c2497d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,8 @@ anyhow = "1.0.75" async-trait = "0.1.73" base64 = "0.21.4" 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" } diff --git a/src/core/credential_format/mod.rs b/src/core/credential_format/mod.rs index 1b7a599..ab45346 100644 --- a/src/core/credential_format/mod.rs +++ b/src/core/credential_format/mod.rs @@ -2,24 +2,6 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; -/// A credential format that can be transmitted using OID4VP. -pub trait CredentialFormat { - /// The ID of the credential format. - const ID: &'static str; -} - -pub struct MsoMdoc; - -impl CredentialFormat for MsoMdoc { - const ID: &'static str = "mso_mdoc"; -} - -pub struct JwtVc; - -impl CredentialFormat for JwtVc { - const ID: &'static str = "jwt_vc"; -} - /// A Json object of claim formats. pub type ClaimFormatMap = HashMap; diff --git a/src/core/input_descriptor.rs b/src/core/input_descriptor.rs index 28d107d..da1defb 100644 --- a/src/core/input_descriptor.rs +++ b/src/core/input_descriptor.rs @@ -528,13 +528,12 @@ impl ConstraintsField { /// pub fn requested_fields(&self) -> Vec { self.path() - .into_iter() + .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. - .map(|path| path.split(&['-', '.', ':', '@'][..]).last()) - .flatten() + .filter_map(|path| path.split(&['-', '.', ':', '@'][..]).last()) .map(|path| { path.chars() .fold(String::new(), |mut acc, c| { @@ -555,7 +554,7 @@ impl ConstraintsField { }) // Split the path based on empty spaces and uppercase the first letter of each word. .split(' ') - .map(|word| { + .fold(String::new(), |desc, word| { let word = word.chars() .enumerate() @@ -571,9 +570,8 @@ impl ConstraintsField { acc }); - format!("{} ", word.trim_end()) + format!("{desc} {}", word.trim_end()) }) - .collect::() .trim_end() .to_string() }) diff --git a/src/core/presentation_definition.rs b/src/core/presentation_definition.rs index 8628e0d..ce3912a 100644 --- a/src/core/presentation_definition.rs +++ b/src/core/presentation_definition.rs @@ -172,40 +172,66 @@ impl PresentationDefinition { AuthorizationResponse::Unencoded(response) => { let presentation_submission = response.presentation_submission().parsed(); - let jwt = response.vp_token().0.clone(); - - // TODO: Validate the VP JWT Signature against the holder's key? - - let verifiable_presentation: VerifiablePresentation = - ssi_claims::jwt::decode_unverified(&jwt)?; - // Ensure the definition id matches the submission's definition id. if presentation_submission.definition_id() != self.id() { bail!("Presentation Definition ID does not match the Presentation Submission.") } + // Parse the descriptor map into a HashMap for easier access let descriptor_map: HashMap = presentation_submission .descriptor_map() .iter() .map(|descriptor_map| (descriptor_map.id().to_owned(), descriptor_map.clone())) .collect(); - for input_descriptor in self.input_descriptors().iter() { - match descriptor_map.get(input_descriptor.id()) { - None => { - // TODO: Determine whether input descriptor must have a corresponding descriptor map. - bail!("Input Descriptor ID not found in Descriptor Map.") - } - Some(descriptor) => { - input_descriptor - .validate_verifiable_presentation( - &verifiable_presentation, - descriptor, - ) - .context("Input Descriptor validation failed.")?; + // Parse the VP Token according to the Spec, here: + // https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-6.1-2.2 + let vp_payload = response.vp_token().parse()?; + + // Check if the vp_payload is an array of VPs + match vp_payload.as_array() { + None => { + // handle a single verifiable presentation + self.validate_definition_map( + VerifiablePresentation(json_syntax::Value::from(vp_payload)), + &descriptor_map, + ) + } + Some(vps) => { + // Each item in the array is a VP + for vp in vps { + // handle the verifiable presentation + self.validate_definition_map( + VerifiablePresentation(json_syntax::Value::from(vp.clone())), + &descriptor_map, + )?; } + + Ok(()) + } + } + } + } + } + + /// Validate a presentation submission against the presentation definition. + fn validate_definition_map( + &self, + verifiable_presentation: VerifiablePresentation, + descriptor_map: &HashMap, + ) -> Result<()> { + for input_descriptor in self.input_descriptors().iter() { + match descriptor_map.get(input_descriptor.id()) { + None => { + if input_descriptor.constraints().is_required() { + bail!("Required Input Descriptor ID not found in Descriptor Map.") } } + Some(descriptor) => { + input_descriptor + .validate_verifiable_presentation(&verifiable_presentation, descriptor) + .context("Input Descriptor validation failed.")?; + } } } diff --git a/src/core/presentation_submission.rs b/src/core/presentation_submission.rs index f366c98..9e8d68c 100644 --- a/src/core/presentation_submission.rs +++ b/src/core/presentation_submission.rs @@ -91,6 +91,11 @@ impl DescriptorMap { } /// 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 } diff --git a/src/core/response/parameters.rs b/src/core/response/parameters.rs index bb40385..e4c9fb1 100644 --- a/src/core/response/parameters.rs +++ b/src/core/response/parameters.rs @@ -3,6 +3,7 @@ use crate::core::object::TypedParameter; use crate::core::presentation_submission::PresentationSubmission as PresentationSubmissionParsed; use anyhow::Error; +use base64::prelude::*; use serde_json::Value as Json; #[derive(Debug, Clone)] @@ -47,6 +48,28 @@ impl From for Json { } } +impl VpToken { + /// Parse the VP Token as a JSON object. + /// + /// This will attempt to decode the token as base64, and if that fails, it + /// will attempt to parse the token as a JSON object. + /// + /// If you want to check for decode errors, use [VpToken::decode_base64]. + pub fn parse(&self) -> Result { + match self.decode_base64() { + Ok(decoded) => Ok(decoded), + Err(_) => Ok(serde_json::from_str(&self.0)?), + } + } + + /// Return the Verifiable Presentation Token as a JSON object from a base64 + /// encoded string. + pub fn decode_base64(&self) -> Result { + let decoded = BASE64_STANDARD.decode(&self.0)?; + Ok(serde_json::from_slice(&decoded)?) + } +} + #[derive(Debug, Clone)] pub struct PresentationSubmission { raw: Json, diff --git a/tests/e2e.rs b/tests/e2e.rs index 2aeb5a4..163c585 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -152,5 +152,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/jwt_vp.rs b/tests/jwt_vp.rs index cd3dabf..1d8267a 100644 --- a/tests/jwt_vp.rs +++ b/tests/jwt_vp.rs @@ -1,16 +1,16 @@ use std::str::FromStr; use anyhow::Result; +use base64::{encode_engine_string, prelude::*}; use oid4vp::holder::verifiable_presentation_builder::{ VerifiablePresentationBuilder, VerifiablePresentationBuilderOptions, }; use oid4vp::verifier::request_signer::P256Signer; use ssi_claims::jwt; -use ssi_claims::{CompactJWSString, JWSPayload, JWTClaims}; use ssi_dids::DIDKey; use ssi_jwk::JWK; -pub async fn create_test_verifiable_presentation() -> Result { +pub async fn create_test_verifiable_presentation() -> Result { let verifier = JWK::from_str(include_str!("examples/verifier.jwk"))?; let signer = P256Signer::new( @@ -38,14 +38,11 @@ pub async fn create_test_verifiable_presentation() -> Result { nonce: "random_nonce".into(), }); - let claim = JWTClaims::from_private_claims(verifiable_presentation); + // Encode the verifiable presentation as base64 encoded payload. + let vp_token = verifiable_presentation.0.to_string(); - let jwt = claim - .sign(&signer) - .await - .expect("Failed to sign Verifiable Presentation JWT"); + // encode as base64. + let base64_encoded_vp = BASE64_STANDARD.encode(vp_token); - let _: jwt::VerifiablePresentation = ssi_claims::jwt::decode_unverified(jwt.as_str())?; - - Ok(jwt) + Ok(base64_encoded_vp) } From 65e1b11c7097d5f12ee961855cc81140420bea6f Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 27 Aug 2024 18:03:12 -0700 Subject: [PATCH 59/71] move validation to auth response impl instead of presentation definition impl Signed-off-by: Ryan Tate --- src/core/credential_format/mod.rs | 35 ++++++------ src/core/presentation_definition.rs | 65 +--------------------- src/core/response/mod.rs | 84 +++++++++++++++++++++++++++-- tests/jwt_vc.rs | 6 +-- tests/jwt_vp.rs | 2 +- 5 files changed, 102 insertions(+), 90 deletions(-) diff --git a/src/core/credential_format/mod.rs b/src/core/credential_format/mod.rs index ab45346..6598156 100644 --- a/src/core/credential_format/mod.rs +++ b/src/core/credential_format/mod.rs @@ -2,6 +2,9 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; +// TODO: Does the `isomdl` crate provide this constant value, or another crate? +const ORG_ISO_18013_5_1_MDL: &str = "org.iso.18013.5.1.mDL"; + /// A Json object of claim formats. pub type ClaimFormatMap = HashMap; @@ -20,53 +23,53 @@ pub type ClaimFormatMap = HashMap; pub enum ClaimFormat { #[serde(rename = "jwt")] Jwt { - // The algorithm used to sign the JWT. + /// The algorithm used to sign the JWT. alg: Vec, }, #[serde(rename = "jwt_vc")] JwtVc { - // The algorithm used to sign the JWT verifiable credential. + /// 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. + /// 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. + /// 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. + /// 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" + /// 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. + /// 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. + /// 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. + /// 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. + /// The proof type used to sign the anoncreds verifiable presentation. proof_type: Vec, }, #[serde(rename = "mso_mdoc")] @@ -259,18 +262,18 @@ pub enum CredentialType { /// /// Given there is no universal standard for how to present a vehicle title credential, /// the inner String provides a dynamic way to represent a vehicle title credential. + // TODO: is there a standard identifier for a vehicle title credential instead of `vehicle_title`? #[serde(rename = "vehicle_title")] VehicleTitle(String), - // Add additional credential types here. - // - // Fallback to a string for any other credential type. + // TODO: Add additional credential types here. Fallback to a string for any other credential type. + /// Other credential types not covered by the above. Other(String), } impl From<&str> for CredentialType { fn from(s: &str) -> Self { match s { - s if s.contains("org.iso.18013.5.1.mDL") => Self::Iso18013_5_1mDl, + s if s.contains(ORG_ISO_18013_5_1_MDL) => Self::Iso18013_5_1mDl, s if s.contains("vehicle_title.") => Self::VehicleTitle(s.to_string()), s => Self::Other(s.to_string()), } @@ -280,7 +283,7 @@ impl From<&str> for CredentialType { impl From for String { fn from(cred_type: CredentialType) -> Self { match cred_type { - CredentialType::Iso18013_5_1mDl => "org.iso.18013.5.1.mDL".to_string(), + CredentialType::Iso18013_5_1mDl => ORG_ISO_18013_5_1_MDL.to_string(), CredentialType::VehicleTitle(title) => format!("vehicle_title.{title}"), CredentialType::Other(s) => s, } diff --git a/src/core/presentation_definition.rs b/src/core/presentation_definition.rs index ce3912a..07fa562 100644 --- a/src/core/presentation_definition.rs +++ b/src/core/presentation_definition.rs @@ -1,7 +1,6 @@ use super::credential_format::*; use super::input_descriptor::*; use super::presentation_submission::*; -use super::response::AuthorizationResponse; use std::collections::HashMap; @@ -153,69 +152,7 @@ impl PresentationDefinition { } /// Validate a presentation submission against the presentation definition. - /// - /// Checks the underlying presentation submission parsed from the authorization response, - /// against the input descriptors of the presentation definition. - #[deprecated( - note = "This method is to be replaced by a top-level function that takes a presentation definition and a presentation submission." - )] - pub async fn validate_authorization_response( - &self, - auth_response: &AuthorizationResponse, - ) -> Result<()> { - match auth_response { - AuthorizationResponse::Jwt(_jwt) => { - // TODO: Handle JWT Encoded authorization response. - - bail!("Authorization Response Presentation Definition validation not implemented.") - } - AuthorizationResponse::Unencoded(response) => { - let presentation_submission = response.presentation_submission().parsed(); - - // Ensure the definition id matches the submission's definition id. - if presentation_submission.definition_id() != self.id() { - bail!("Presentation Definition ID does not match the Presentation Submission.") - } - - // Parse the descriptor map into a HashMap for easier access - let descriptor_map: HashMap = presentation_submission - .descriptor_map() - .iter() - .map(|descriptor_map| (descriptor_map.id().to_owned(), descriptor_map.clone())) - .collect(); - - // Parse the VP Token according to the Spec, here: - // https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-6.1-2.2 - let vp_payload = response.vp_token().parse()?; - - // Check if the vp_payload is an array of VPs - match vp_payload.as_array() { - None => { - // handle a single verifiable presentation - self.validate_definition_map( - VerifiablePresentation(json_syntax::Value::from(vp_payload)), - &descriptor_map, - ) - } - Some(vps) => { - // Each item in the array is a VP - for vp in vps { - // handle the verifiable presentation - self.validate_definition_map( - VerifiablePresentation(json_syntax::Value::from(vp.clone())), - &descriptor_map, - )?; - } - - Ok(()) - } - } - } - } - } - - /// Validate a presentation submission against the presentation definition. - fn validate_definition_map( + pub fn validate_definition_map( &self, verifiable_presentation: VerifiablePresentation, descriptor_map: &HashMap, diff --git a/src/core/response/mod.rs b/src/core/response/mod.rs index 7a752a3..6f3986b 100644 --- a/src/core/response/mod.rs +++ b/src/core/response/mod.rs @@ -1,14 +1,19 @@ -use std::collections::BTreeMap; +use super::{ + object::{ParsingErrorContext, UntypedObject}, + presentation_definition::PresentationDefinition, + presentation_submission::DescriptorMap, +}; -use anyhow::{Context, Error, Result}; +use std::collections::{BTreeMap, HashMap}; + +use anyhow::{bail, Context, Error, Result}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use ssi_claims::jwt::VerifiablePresentation; use url::Url; use self::parameters::{PresentationSubmission, VpToken}; -use super::object::{ParsingErrorContext, UntypedObject}; - pub mod parameters; #[derive(Debug, Clone)] @@ -35,6 +40,77 @@ impl AuthorizationResponse { Ok(Self::Unencoded(UntypedObject(map).try_into()?)) } + + /// Validate an authorization response against a presentation definition. + /// + /// This method will parse the presentation submission from the auth response and + /// validate it against the provided presentation definition. + /// + /// # Parameters + /// + /// - `self` - The authorization response to validate. + /// - `presentation_definition` - The presentation definition to validate against. + /// + /// # Errors + /// + /// This method will return an error if the presentation submission does not match the + /// presentation definition. + /// + /// # Returns + /// + /// This method will return `Ok(())` if the presentation submission matches the presentation + /// definition. + pub fn validate(&self, presentation_definition: &PresentationDefinition) -> Result<()> { + match self { + AuthorizationResponse::Jwt(_jwt) => { + // TODO: Handle JWT Encoded authorization response. + + bail!("Authorization Response Presentation Definition validation not implemented.") + } + AuthorizationResponse::Unencoded(response) => { + let presentation_submission = response.presentation_submission().parsed(); + + // Ensure the definition id matches the submission's definition id. + if presentation_submission.definition_id() != presentation_definition.id() { + bail!("Presentation Definition ID does not match the Presentation Submission.") + } + + // Parse the descriptor map into a HashMap for easier access + let descriptor_map: HashMap = presentation_submission + .descriptor_map() + .iter() + .map(|descriptor_map| (descriptor_map.id().to_owned(), descriptor_map.clone())) + .collect(); + + // Parse the VP Token according to the Spec, here: + // https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-6.1-2.2 + let vp_payload = response.vp_token().parse()?; + + // Check if the vp_payload is an array of VPs + match vp_payload.as_array() { + None => { + // handle a single verifiable presentation + presentation_definition.validate_definition_map( + VerifiablePresentation(json_syntax::Value::from(vp_payload)), + &descriptor_map, + ) + } + Some(vps) => { + // Each item in the array is a VP + for vp in vps { + // handle the verifiable presentation + presentation_definition.validate_definition_map( + VerifiablePresentation(json_syntax::Value::from(vp.clone())), + &descriptor_map, + )?; + } + + Ok(()) + } + } + } + } + } } #[derive(Debug, Clone)] diff --git a/tests/jwt_vc.rs b/tests/jwt_vc.rs index 3c5768a..b7ffd5c 100644 --- a/tests/jwt_vc.rs +++ b/tests/jwt_vc.rs @@ -161,11 +161,7 @@ impl AsyncHttpClient for MockHttpClient { .context("failed to parse authorization response request")?, |session, auth_response| { Box::pin(async move { - match session - .presentation_definition - .validate_authorization_response(&auth_response) - .await - { + match auth_response.validate(&session.presentation_definition) { Ok(_) => Outcome::Success { info: serde_json::Value::Null, }, diff --git a/tests/jwt_vp.rs b/tests/jwt_vp.rs index 1d8267a..42253b3 100644 --- a/tests/jwt_vp.rs +++ b/tests/jwt_vp.rs @@ -1,7 +1,7 @@ use std::str::FromStr; use anyhow::Result; -use base64::{encode_engine_string, prelude::*}; +use base64::prelude::*; use oid4vp::holder::verifiable_presentation_builder::{ VerifiablePresentationBuilder, VerifiablePresentationBuilderOptions, }; From d0c74391562f17ee57a1f70db054a306a7d374f5 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 27 Aug 2024 18:10:04 -0700 Subject: [PATCH 60/71] remove feature gated non-optional deps Signed-off-by: Ryan Tate --- Cargo.toml | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6c2497d..2e512d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,9 +10,6 @@ documentation = "https://docs.rs/oid4vp/" [features] default = [] -reqwest = ["dep:reqwest"] -p256 = ["dep:p256"] -rand = ["dep:rand"] [dependencies] anyhow = "1.0.75" @@ -24,9 +21,9 @@ 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 } -rand = { version = "0.8.5", optional = true } -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_json = "1.0.107" serde_urlencoded = "0.7.1" @@ -43,7 +40,7 @@ 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"] } +oid4vp = { path = "." } [target.'cfg(target_arch = "wasm32")'.dependencies] uuid = { version = "1.2", features = ["v4", "serde", "js"] } From 033fd5e2aaf9095bc90a971f14e774604a3a33ae Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 27 Aug 2024 18:58:49 -0700 Subject: [PATCH 61/71] remove cfg features Signed-off-by: Ryan Tate --- src/core/authorization_request/parameters.rs | 8 ++++---- src/core/authorization_request/verification/verifier.rs | 4 ---- src/verifier/request_signer.rs | 8 -------- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/core/authorization_request/parameters.rs b/src/core/authorization_request/parameters.rs index a2735cc..58429c2 100644 --- a/src/core/authorization_request/parameters.rs +++ b/src/core/authorization_request/parameters.rs @@ -223,11 +223,11 @@ impl std::fmt::Display for Nonce { } impl Nonce { - #[cfg(feature = "rand")] - pub fn random(rng: &mut impl rand::Rng) -> Self { - use rand::distributions::Alphanumeric; + /// 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((0..16).map(|_| rng.sample(Alphanumeric) as char).collect()) + Self(Alphanumeric.sample_string(rng, length)) } } 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/verifier/request_signer.rs b/src/verifier/request_signer.rs index a02d8ba..4945b94 100644 --- a/src/verifier/request_signer.rs +++ b/src/verifier/request_signer.rs @@ -1,11 +1,7 @@ -#[cfg(feature = "p256")] use anyhow::Result; use async_trait::async_trait; -#[cfg(feature = "p256")] use p256::ecdsa::{signature::Signer, Signature, SigningKey}; -#[cfg(feature = "p256")] use ssi_claims::jws::{JWSSigner, JWSSignerInfo}; -#[cfg(feature = "p256")] use ssi_jwk::Algorithm; use ssi_jwk::JWK; @@ -33,14 +29,12 @@ pub trait RequestSigner: Debug { } } -#[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(); @@ -53,7 +47,6 @@ impl P256Signer { } } -#[cfg(feature = "p256")] #[async_trait] impl RequestSigner for P256Signer { type Error = anyhow::Error; @@ -77,7 +70,6 @@ impl RequestSigner for P256Signer { } } -#[cfg(feature = "p256")] impl JWSSigner for P256Signer { async fn fetch_info(&self) -> std::result::Result { let algorithm = self.jwk.algorithm.unwrap_or(Algorithm::ES256); From d98d2a4056fe8bbe4f7cd7cf18ecc59fb212ebdc Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 27 Aug 2024 22:00:16 -0700 Subject: [PATCH 62/71] wip: handle groups in presentation definition, input descriptor tests Signed-off-by: Ryan Tate --- src/core/input_descriptor.rs | 36 ++++++++++---- src/core/presentation_definition.rs | 50 ++++++++++++++++++- src/core/presentation_submission.rs | 48 +++--------------- src/core/response/mod.rs | 10 +--- src/tests.rs | 77 +++++++++++++++++++++++------ src/verifier/request_signer.rs | 13 ++--- tests/e2e.rs | 4 +- 7 files changed, 154 insertions(+), 84 deletions(-) diff --git a/src/core/input_descriptor.rs b/src/core/input_descriptor.rs index da1defb..805e14c 100644 --- a/src/core/input_descriptor.rs +++ b/src/core/input_descriptor.rs @@ -52,6 +52,8 @@ pub struct InputDescriptor { purpose: Option, #[serde(skip_serializing_if = "Option::is_none")] format: Option, + #[serde(skip_serializing_if = "Option::is_none")] + group: Option>, } impl InputDescriptor { @@ -128,6 +130,24 @@ impl InputDescriptor { self } + /// Set the group of the constraints field. + pub fn set_group(mut self, group: Vec) -> Self { + self.group = Some(group); + self + } + + /// Return the group of the constraints field. + pub fn group(&self) -> Option<&Vec> { + self.group.as_ref() + } + + /// Return a mutable reference to the group of the constraints field. + pub fn add_to_group(mut self, member: String) -> Self { + self.group.get_or_insert_with(Vec::new).push(member); + + self + } + /// Validate the input descriptor against the verifiable presentation and the descriptor map. pub fn validate_verifiable_presentation( &self, @@ -300,14 +320,9 @@ impl Constraints { /// Returns if the constraints fields contain non-optional /// fields that must be satisfied. pub fn is_required(&self) -> bool { - if let Some(fields) = self.fields() { - fields.iter().any(|field| field.is_required()) - } else { - matches!( - self.limit_disclosure(), - Some(ConstraintsLimitDisclosure::Required) - ) - } + self.fields() + .map(|fields| fields.iter().any(|field| field.is_required())) + .unwrap_or(false) } } @@ -505,13 +520,16 @@ impl ConstraintsField { } /// 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 = Some(intent_to_retain); self } /// Return the intent to retain the constraints field. - pub fn is_intended_to_retain(&self) -> bool { + pub fn intent_to_retain(&self) -> bool { self.intent_to_retain.unwrap_or(false) } diff --git a/src/core/presentation_definition.rs b/src/core/presentation_definition.rs index 07fa562..115917e 100644 --- a/src/core/presentation_definition.rs +++ b/src/core/presentation_definition.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use anyhow::{bail, Context, 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). @@ -18,11 +19,13 @@ use ssi_claims::jwt::VerifiablePresentation; /// 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)] +#[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, @@ -155,11 +158,15 @@ impl PresentationDefinition { pub fn validate_definition_map( &self, verifiable_presentation: VerifiablePresentation, - descriptor_map: &HashMap, + descriptor_map: &HashMap, ) -> Result<()> { for input_descriptor in self.input_descriptors().iter() { match descriptor_map.get(input_descriptor.id()) { None => { + println!("Input Descriptor: {}", input_descriptor.id()); + + // TODO: check for groups in submission requirements + if input_descriptor.constraints().is_required() { bail!("Required Input Descriptor ID not found in Descriptor Map.") } @@ -175,3 +182,42 @@ impl PresentationDefinition { 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: String, + #[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), +} + +#[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 index 9e8d68c..d624a26 100644 --- a/src/core/presentation_submission.rs +++ b/src/core/presentation_submission.rs @@ -1,7 +1,6 @@ use super::{credential_format::*, input_descriptor::*}; use serde::{Deserialize, Serialize}; -use serde_json::Map; /// Presentation Submissions are objects embedded within target /// [Claim](https://identity.foundation/presentation-exchange/spec/v2.0.0/#term:claim) negotiation @@ -55,6 +54,14 @@ impl PresentationSubmission { 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. + 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). @@ -125,42 +132,3 @@ impl DescriptorMap { 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, -} diff --git a/src/core/response/mod.rs b/src/core/response/mod.rs index 6f3986b..0a3dfe9 100644 --- a/src/core/response/mod.rs +++ b/src/core/response/mod.rs @@ -1,10 +1,9 @@ use super::{ object::{ParsingErrorContext, UntypedObject}, presentation_definition::PresentationDefinition, - presentation_submission::DescriptorMap, }; -use std::collections::{BTreeMap, HashMap}; +use std::collections::BTreeMap; use anyhow::{bail, Context, Error, Result}; use serde::{Deserialize, Serialize}; @@ -75,12 +74,7 @@ impl AuthorizationResponse { bail!("Presentation Definition ID does not match the Presentation Submission.") } - // Parse the descriptor map into a HashMap for easier access - let descriptor_map: HashMap = presentation_submission - .descriptor_map() - .iter() - .map(|descriptor_map| (descriptor_map.id().to_owned(), descriptor_map.clone())) - .collect(); + let descriptor_map = presentation_submission.descriptor_map_by_id(); // Parse the VP Token according to the Spec, here: // https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-6.1-2.2 diff --git a/src/tests.rs b/src/tests.rs index 70d2149..a249fd4 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,23 +1,19 @@ -use serde::Deserialize; - -// use crate::core::response::AuthorizationResponse; -// pub use crate::utils::NonEmptyVec; - -// use anyhow::{bail, Context, Result}; -// use jsonschema::{JSONSchema, ValidationError}; - -// use serde_json::Map; -// use ssi_claims::jwt::VerifiablePresentation; -// use ssi_dids::ssi_json_ld::syntax::from_value; - -use crate::core::{presentation_definition::PresentationDefinition, presentation_submission::*}; +use crate::core::{ + presentation_definition::{PresentationDefinition, SubmissionRequirement}, + presentation_submission::*, +}; -use serde_json::json; 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!( @@ -141,3 +137,56 @@ fn submission_requirements_suite() { println!("✅") } } + +#[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/input_descriptors_example.json" + ); + + println!("Input Descriptors: {:?}", input_descriptors); + 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(|v| serde_json::from_value(v)) + }) + .flatten() + .expect("failed to parse presentation definition")?; + + println!("Presentation Definition: {:?}", presentation_definition); + + let presentation_submission = include_str!( + "../tests/presentation-exchange/test/presentation-submission/appendix_VP_example.json" + ); + + println!("Presentation Submission: {:?}", presentation_submission); + + 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")?; + + println!("Presentation Submission: {:?}", presentation_submission); + + let descriptor_map = presentation_submission.descriptor_map_by_id(); + + let verifiable_presentation: VerifiablePresentation = serde_json::from_value(value)?; + + println!("Verifiable Presentation: {verifiable_presentation:?}"); + + presentation_definition + .validate_definition_map(verifiable_presentation, &descriptor_map) + .expect("Failed to validate definition map"); + + Ok(()) +} diff --git a/src/verifier/request_signer.rs b/src/verifier/request_signer.rs index 4945b94..7fbba83 100644 --- a/src/verifier/request_signer.rs +++ b/src/verifier/request_signer.rs @@ -52,12 +52,7 @@ impl RequestSigner for P256Signer { type Error = anyhow::Error; fn alg(&self) -> Result { - Ok(self - .jwk - .algorithm - .map(|alg| alg) - .unwrap_or(Algorithm::ES256) - .to_string()) + Ok(self.jwk.algorithm.unwrap_or(Algorithm::ES256).to_string()) } fn jwk(&self) -> Result { @@ -83,8 +78,8 @@ impl JWSSigner for P256Signer { &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).into()) - }) + self.try_sign(signing_bytes) + .await + .map_err(|e| ssi_claims::SignatureError::Other(format!("Failed to sign bytes: {}", e))) } } diff --git a/tests/e2e.rs b/tests/e2e.rs index 163c585..8b06e9f 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -84,8 +84,8 @@ async fn w3c_vc_did_client_direct_post() { .unwrap(); assert_eq!( - &presentation_definition, - parsed_presentation_definition.parsed() + presentation_definition.id(), + parsed_presentation_definition.parsed().id() ); assert_eq!(&ResponseType::VpToken, request.response_type()); From 915b92264d31990fb9797755ebe8ee599b663cac Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 27 Aug 2024 22:04:15 -0700 Subject: [PATCH 63/71] remove cfg feature tags Signed-off-by: Ryan Tate --- src/core/util/mod.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core/util/mod.rs b/src/core/util/mod.rs index fe7b7e6..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,11 +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() @@ -31,7 +28,6 @@ impl ReqwestClient { } } -#[cfg(feature = "reqwest")] #[async_trait] impl AsyncHttpClient for ReqwestClient { async fn execute(&self, request: Request>) -> Result>> { From 3d7b1f541c6f11b1777e27f80a2c4b7c235f2a98 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Wed, 28 Aug 2024 10:17:11 -0700 Subject: [PATCH 64/71] remove extraneous metadata helper methods; use UntypedObject for dereferencing Signed-off-by: Ryan Tate --- src/core/metadata/mod.rs | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/src/core/metadata/mod.rs b/src/core/metadata/mod.rs index 8f51ec0..8696803 100644 --- a/src/core/metadata/mod.rs +++ b/src/core/metadata/mod.rs @@ -47,24 +47,6 @@ impl WalletMetadata { &mut self.2 } - // /// Returns whether the claim format is supported. - // pub fn is_claim_format_supported(&self, designation: &ClaimFormatDesignation) -> bool { - // self.vp_formats_supported() - // .is_claim_format_supported(designation) - // } - - /// Adds a new algorithm to the list of supported request object signing algorithms. - pub fn add_request_object_signing_alg(&mut self, alg: String) -> Result<()> { - self.0 - .get::() - .transpose()? - .map(|x| x.0) - .get_or_insert_with(Vec::new) - .push(alg); - - Ok(()) - } - /// The static wallet metadata bound to `openid4vp:`: /// ```json /// { @@ -117,18 +99,6 @@ impl WalletMetadata { // Unwrap safety: unit tested. object.try_into().unwrap() } - - /// Return the `request_object_signing_alg_values_supported` - /// field from the wallet metadata. - pub fn request_object_signing_alg_values_supported(&self) -> Result, Error> { - let Some(Ok(algs)) = self.get::() else { - bail!( - "Failed to parse request object signing algorithms supported from wallet metadata." - ) - }; - - Ok(algs.0) - } } impl From for UntypedObject { From e9b33dfadb7547ee16573e21d443c348c968b1bb Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Wed, 28 Aug 2024 18:01:11 -0700 Subject: [PATCH 65/71] add submission requirement check for presentation validation Signed-off-by: Ryan Tate --- src/core/input_descriptor.rs | 13 +- src/core/metadata/mod.rs | 2 +- src/core/presentation_definition.rs | 191 ++++++++++++++++-- src/core/presentation_submission.rs | 29 ++- src/core/response/mod.rs | 10 +- src/tests.rs | 58 ++++-- .../presentation-submission/definition_1.json | 28 +++ .../presentation-submission/definition_2.json | 50 +++++ .../presentation-submission/definition_3.json | 42 ++++ .../presentation-submission/submission_1.json | 16 ++ .../presentation-submission/submission_2.json | 21 ++ .../presentation-submission/submission_3.json | 21 ++ tests/presentation-submission/vp_1.json | 26 +++ tests/presentation-submission/vp_2.json | 36 ++++ tests/presentation-submission/vp_3.json | 42 ++++ 15 files changed, 536 insertions(+), 49 deletions(-) create mode 100644 tests/presentation-submission/definition_1.json create mode 100644 tests/presentation-submission/definition_2.json create mode 100644 tests/presentation-submission/definition_3.json create mode 100644 tests/presentation-submission/submission_1.json create mode 100644 tests/presentation-submission/submission_2.json create mode 100644 tests/presentation-submission/submission_3.json create mode 100644 tests/presentation-submission/vp_1.json create mode 100644 tests/presentation-submission/vp_2.json create mode 100644 tests/presentation-submission/vp_3.json diff --git a/src/core/input_descriptor.rs b/src/core/input_descriptor.rs index 805e14c..956f66b 100644 --- a/src/core/input_descriptor.rs +++ b/src/core/input_descriptor.rs @@ -7,6 +7,11 @@ 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) @@ -53,7 +58,7 @@ pub struct InputDescriptor { #[serde(skip_serializing_if = "Option::is_none")] format: Option, #[serde(skip_serializing_if = "Option::is_none")] - group: Option>, + group: Option>, } impl InputDescriptor { @@ -131,18 +136,18 @@ impl InputDescriptor { } /// Set the group of the constraints field. - pub fn set_group(mut self, group: Vec) -> Self { + pub fn set_group(mut self, group: Vec) -> Self { self.group = Some(group); self } /// Return the group of the constraints field. - pub fn group(&self) -> Option<&Vec> { + pub fn groups(&self) -> Option<&Vec> { self.group.as_ref() } /// Return a mutable reference to the group of the constraints field. - pub fn add_to_group(mut self, member: String) -> Self { + pub fn add_to_group(mut self, member: GroupId) -> Self { self.group.get_or_insert_with(Vec::new).push(member); self diff --git a/src/core/metadata/mod.rs b/src/core/metadata/mod.rs index 8696803..5909ced 100644 --- a/src/core/metadata/mod.rs +++ b/src/core/metadata/mod.rs @@ -2,7 +2,7 @@ use super::credential_format::*; use std::ops::{Deref, DerefMut}; -use anyhow::{bail, Error, Result}; +use anyhow::{Error, Result}; use parameters::wallet::{RequestObjectSigningAlgValuesSupported, ResponseTypesSupported}; use serde::{Deserialize, Serialize}; use ssi_jwk::Algorithm; diff --git a/src/core/presentation_definition.rs b/src/core/presentation_definition.rs index 115917e..223c677 100644 --- a/src/core/presentation_definition.rs +++ b/src/core/presentation_definition.rs @@ -4,7 +4,7 @@ use super::presentation_submission::*; use std::collections::HashMap; -use anyhow::{bail, Context, Result}; +use anyhow::{bail, Result}; use serde::{Deserialize, Serialize}; use serde_json::Map; use ssi_claims::jwt::VerifiablePresentation; @@ -64,11 +64,65 @@ impl PresentationDefinition { &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: &Vec, + ) -> 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 @@ -155,26 +209,35 @@ impl PresentationDefinition { } /// Validate a presentation submission against the presentation definition. - pub fn validate_definition_map( + /// + /// 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: &HashMap, + descriptor_map: &Vec, ) -> Result<()> { - for input_descriptor in self.input_descriptors().iter() { - match descriptor_map.get(input_descriptor.id()) { - None => { - println!("Input Descriptor: {}", input_descriptor.id()); + // Validate the submission requirements + self.validate_submission_requirements(descriptor_map)?; - // TODO: check for groups in submission requirements + let input_descript_map = self.input_descriptors_map(); - if input_descriptor.constraints().is_required() { - bail!("Required Input Descriptor ID not found in Descriptor 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(descriptor) => { + Some(input_descriptor) => { input_descriptor - .validate_verifiable_presentation(&verifiable_presentation, descriptor) - .context("Input Descriptor validation failed.")?; + .validate_verifiable_presentation(&verifiable_presentation, descriptor)?; } } } @@ -195,7 +258,7 @@ pub struct SubmissionRequirementObject { #[serde(untagged)] pub enum SubmissionRequirementBase { From { - from: String, + from: GroupId, #[serde(flatten)] submission_requirement_base: SubmissionRequirementObject, }, @@ -213,11 +276,103 @@ pub enum SubmissionRequirement { 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() + .map(|input_group| input_group.contains(group)) + .unwrap_or(false) + }) + .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}."); + } + } + } + + Ok(()) + } + + /// Validate a submission requirement against an input descriptor and a descriptor map. + pub fn validate( + &self, + input_descriptors: &Vec, + decriptor_map: &Vec, + ) -> 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, + pub count: Option, + pub min: Option, + pub max: Option, } diff --git a/src/core/presentation_submission.rs b/src/core/presentation_submission.rs index d624a26..d79f8ec 100644 --- a/src/core/presentation_submission.rs +++ b/src/core/presentation_submission.rs @@ -2,6 +2,9 @@ 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 @@ -15,7 +18,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct PresentationSubmission { id: uuid::Uuid, - definition_id: String, + definition_id: DescriptorMapId, descriptor_map: Vec, } @@ -27,7 +30,11 @@ impl PresentationSubmission { /// /// 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: String, descriptor_map: Vec) -> Self { + pub fn new( + id: uuid::Uuid, + definition_id: DescriptorMapId, + descriptor_map: Vec, + ) -> Self { Self { id, definition_id, @@ -56,7 +63,13 @@ impl PresentationSubmission { } /// Returns the descriptor map as a mapping of descriptor map id to descriptor map. - pub fn descriptor_map_by_id(&self) -> std::collections::HashMap { + /// + /// 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)) @@ -69,7 +82,7 @@ impl PresentationSubmission { /// 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, + id: DescriptorMapId, format: ClaimFormatDesignation, path: JsonPath, path_nested: Option>, @@ -83,7 +96,11 @@ impl DescriptorMap { /// 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 { + pub fn new( + id: impl Into, + format: ClaimFormatDesignation, + path: JsonPath, + ) -> Self { Self { id: id.into(), format, @@ -93,7 +110,7 @@ impl DescriptorMap { } /// Return the id of the descriptor map. - pub fn id(&self) -> &String { + pub fn id(&self) -> &DescriptorMapId { &self.id } diff --git a/src/core/response/mod.rs b/src/core/response/mod.rs index 0a3dfe9..d611369 100644 --- a/src/core/response/mod.rs +++ b/src/core/response/mod.rs @@ -74,7 +74,7 @@ impl AuthorizationResponse { bail!("Presentation Definition ID does not match the Presentation Submission.") } - let descriptor_map = presentation_submission.descriptor_map_by_id(); + let descriptor_map = presentation_submission.descriptor_map(); // Parse the VP Token according to the Spec, here: // https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-6.1-2.2 @@ -84,18 +84,18 @@ impl AuthorizationResponse { match vp_payload.as_array() { None => { // handle a single verifiable presentation - presentation_definition.validate_definition_map( + presentation_definition.validate_presentation( VerifiablePresentation(json_syntax::Value::from(vp_payload)), - &descriptor_map, + descriptor_map, ) } Some(vps) => { // Each item in the array is a VP for vp in vps { // handle the verifiable presentation - presentation_definition.validate_definition_map( + presentation_definition.validate_presentation( VerifiablePresentation(json_syntax::Value::from(vp.clone())), - &descriptor_map, + descriptor_map, )?; } diff --git a/src/tests.rs b/src/tests.rs index a249fd4..1565cdc 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -138,33 +138,64 @@ fn submission_requirements_suite() { } } +#[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/input_descriptors_example.json" + "../tests/presentation-exchange/test/presentation-definition/multi_group_example.json" ); - println!("Input Descriptors: {:?}", input_descriptors); 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(|v| serde_json::from_value(v)) + .map(serde_json::from_value) }) .flatten() .expect("failed to parse presentation definition")?; - println!("Presentation Definition: {:?}", presentation_definition); - let presentation_submission = include_str!( "../tests/presentation-exchange/test/presentation-submission/appendix_VP_example.json" ); - println!("Presentation Submission: {:?}", presentation_submission); - let value: Value = serde_json::from_str(presentation_submission)?; let presentation_submission: PresentationSubmission = value @@ -176,17 +207,14 @@ fn test_input_descriptor_validation() -> Result<()> { .flatten() .expect("failed to parse presentation submission")?; - println!("Presentation Submission: {:?}", presentation_submission); - - let descriptor_map = presentation_submission.descriptor_map_by_id(); + let descriptor_map = presentation_submission.descriptor_map(); let verifiable_presentation: VerifiablePresentation = serde_json::from_value(value)?; - println!("Verifiable Presentation: {verifiable_presentation:?}"); - - presentation_definition - .validate_definition_map(verifiable_presentation, &descriptor_map) - .expect("Failed to validate definition map"); + // 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/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" + } + } + ] +} From 64f87eb9c595bcfdb6c832e64d4ca24b0f9ab425 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Wed, 28 Aug 2024 21:10:29 -0700 Subject: [PATCH 66/71] add validate method to vp token; ensure submission requirement all rule is enforced. Signed-off-by: Ryan Tate --- src/core/presentation_definition.rs | 22 +++++++++------- src/core/response/mod.rs | 30 ++++------------------ src/core/response/parameters.rs | 40 ++++++++++++++++++++++++++++- 3 files changed, 57 insertions(+), 35 deletions(-) diff --git a/src/core/presentation_definition.rs b/src/core/presentation_definition.rs index 223c677..7ffa987 100644 --- a/src/core/presentation_definition.rs +++ b/src/core/presentation_definition.rs @@ -108,10 +108,7 @@ impl PresentationDefinition { } /// Validate submission requirements provided an input descriptor and descriptor map. - pub fn validate_submission_requirements( - &self, - descriptor_map: &Vec, - ) -> Result<()> { + pub fn validate_submission_requirements(&self, descriptor_map: &[DescriptorMap]) -> Result<()> { match self.submission_requirements.as_ref() { None => Ok(()), Some(requirements) => { @@ -218,9 +215,10 @@ impl PresentationDefinition { pub fn validate_presentation( &self, verifiable_presentation: VerifiablePresentation, - descriptor_map: &Vec, + descriptor_map: &[DescriptorMap], ) -> Result<()> { - // Validate the submission requirements + // 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(); @@ -324,16 +322,22 @@ impl SubmissionRequirement { 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 an input descriptor and a descriptor map. + /// Validate a submission requirement against a input descriptors and descriptor maps. pub fn validate( &self, - input_descriptors: &Vec, - decriptor_map: &Vec, + input_descriptors: &[InputDescriptor], + decriptor_map: &[DescriptorMap], ) -> Result<()> { // Validate the submission requirement against the grouped descriptor maps. match self { diff --git a/src/core/response/mod.rs b/src/core/response/mod.rs index d611369..092c99b 100644 --- a/src/core/response/mod.rs +++ b/src/core/response/mod.rs @@ -8,7 +8,6 @@ use std::collections::BTreeMap; use anyhow::{bail, Context, Error, Result}; use serde::{Deserialize, Serialize}; use serde_json::Value; -use ssi_claims::jwt::VerifiablePresentation; use url::Url; use self::parameters::{PresentationSubmission, VpToken}; @@ -78,30 +77,11 @@ impl AuthorizationResponse { // Parse the VP Token according to the Spec, here: // https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-6.1-2.2 - let vp_payload = response.vp_token().parse()?; - - // Check if the vp_payload is an array of VPs - match vp_payload.as_array() { - None => { - // handle a single verifiable presentation - presentation_definition.validate_presentation( - VerifiablePresentation(json_syntax::Value::from(vp_payload)), - descriptor_map, - ) - } - Some(vps) => { - // Each item in the array is a VP - for vp in vps { - // handle the verifiable presentation - presentation_definition.validate_presentation( - VerifiablePresentation(json_syntax::Value::from(vp.clone())), - descriptor_map, - )?; - } - - Ok(()) - } - } + response + .vp_token() + .validate(presentation_definition, descriptor_map)?; + + Ok(()) } } } diff --git a/src/core/response/parameters.rs b/src/core/response/parameters.rs index e4c9fb1..7c37a51 100644 --- a/src/core/response/parameters.rs +++ b/src/core/response/parameters.rs @@ -1,10 +1,14 @@ pub use crate::core::authorization_request::parameters::State; use crate::core::object::TypedParameter; -use crate::core::presentation_submission::PresentationSubmission as PresentationSubmissionParsed; +use crate::core::presentation_definition::PresentationDefinition; +use crate::core::presentation_submission::{ + DescriptorMap, PresentationSubmission as PresentationSubmissionParsed, +}; use anyhow::Error; use base64::prelude::*; use serde_json::Value as Json; +use ssi_claims::jwt::VerifiablePresentation; #[derive(Debug, Clone)] pub struct IdToken(pub String); @@ -54,6 +58,8 @@ impl VpToken { /// This will attempt to decode the token as base64, and if that fails, it /// will attempt to parse the token as a JSON object. /// + /// See: https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-6.1-2.2 + /// /// If you want to check for decode errors, use [VpToken::decode_base64]. pub fn parse(&self) -> Result { match self.decode_base64() { @@ -68,6 +74,38 @@ impl VpToken { let decoded = BASE64_STANDARD.decode(&self.0)?; Ok(serde_json::from_slice(&decoded)?) } + + /// Validate the Verifiable Presentation Token. + pub fn validate( + &self, + presentation_definition: &PresentationDefinition, + descriptor_map: &[DescriptorMap], + ) -> Result<(), Error> { + let vp_payload = self.parse()?; + + // Check if the vp_payload is an array of VPs + match vp_payload.as_array() { + None => { + // handle a single verifiable presentation + presentation_definition.validate_presentation( + VerifiablePresentation(json_syntax::Value::from(vp_payload)), + descriptor_map, + )?; + } + Some(vps) => { + // Each item in the array is a VP + for vp in vps { + // handle the verifiable presentation + presentation_definition.validate_presentation( + VerifiablePresentation(json_syntax::Value::from(vp.clone())), + descriptor_map, + )?; + } + } + } + + Ok(()) + } } #[derive(Debug, Clone)] From d3dce7ffd7290dfaf00ebcb8bacefc11c8e47bd5 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Thu, 29 Aug 2024 08:01:17 -0700 Subject: [PATCH 67/71] add vp token validate unencoded method. fix minor todos. Signed-off-by: Ryan Tate --- src/core/presentation_submission.rs | 3 --- src/core/response/mod.rs | 12 +++++------- src/core/response/parameters.rs | 18 ++++++++++++++++-- tests/e2e.rs | 4 ---- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/core/presentation_submission.rs b/src/core/presentation_submission.rs index d79f8ec..dd1fb53 100644 --- a/src/core/presentation_submission.rs +++ b/src/core/presentation_submission.rs @@ -137,9 +137,6 @@ impl DescriptorMap { /// > 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) - /// - /// Errors: - /// - The id of the nested path must be the same as the parent id. 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()); diff --git a/src/core/response/mod.rs b/src/core/response/mod.rs index 092c99b..04a2adb 100644 --- a/src/core/response/mod.rs +++ b/src/core/response/mod.rs @@ -73,13 +73,11 @@ impl AuthorizationResponse { bail!("Presentation Definition ID does not match the Presentation Submission.") } - let descriptor_map = presentation_submission.descriptor_map(); - - // Parse the VP Token according to the Spec, here: - // https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-6.1-2.2 - response - .vp_token() - .validate(presentation_definition, descriptor_map)?; + // Validate the VP Token as an unencoded payload. + response.vp_token().validate_unencoded( + presentation_definition, + presentation_submission.descriptor_map(), + )?; Ok(()) } diff --git a/src/core/response/parameters.rs b/src/core/response/parameters.rs index 7c37a51..cdd6f2b 100644 --- a/src/core/response/parameters.rs +++ b/src/core/response/parameters.rs @@ -75,8 +75,22 @@ impl VpToken { Ok(serde_json::from_slice(&decoded)?) } - /// Validate the Verifiable Presentation Token. - pub fn validate( + /// Validate an unencoded Verifiable Presentation Token. + /// + /// This method assumtes the VP token is not encoded as a JWT. + /// + /// # Returns + /// + /// This method will return `Ok(())` if the VP token is valid. + /// + /// # Errors + /// + /// This method will return an error if the VP token is invalid, + /// or if the verifiable presentation is invalid or does not meet + /// the requirements of the presentation definition. + /// + /// + pub fn validate_unencoded( &self, presentation_definition: &PresentationDefinition, descriptor_map: &[DescriptorMap], diff --git a/tests/e2e.rs b/tests/e2e.rs index 8b06e9f..b17df19 100644 --- a/tests/e2e.rs +++ b/tests/e2e.rs @@ -57,10 +57,6 @@ async fn w3c_vc_did_client_direct_post() { let client_metadata = UntypedObject::default(); - #[cfg(feature = "rand")] - let nonce = Nonce::random(&mut rand::thread_rng()); - - #[cfg(not(feature = "rand"))] let nonce = Nonce::from("random_nonce"); let (id, request) = verifier From 458ff5f664f3a7751b295d747108dee664e3c700 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Thu, 12 Sep 2024 14:29:49 -0700 Subject: [PATCH 68/71] Update Cargo.toml Co-authored-by: Jacob --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2e512d0..bec8d49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 = "." } [target.'cfg(target_arch = "wasm32")'.dependencies] uuid = { version = "1.2", features = ["v4", "serde", "js"] } From 165b1c7625c543f836f352a4ac1598dc32767bf8 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Thu, 12 Sep 2024 16:45:42 -0700 Subject: [PATCH 69/71] fix other claim format serde Signed-off-by: Ryan Tate --- src/core/credential_format/mod.rs | 102 +++++++++++++++++------------- 1 file changed, 58 insertions(+), 44 deletions(-) diff --git a/src/core/credential_format/mod.rs b/src/core/credential_format/mod.rs index 6598156..d3239a7 100644 --- a/src/core/credential_format/mod.rs +++ b/src/core/credential_format/mod.rs @@ -2,12 +2,15 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; -// TODO: Does the `isomdl` crate provide this constant value, or another crate? -const ORG_ISO_18013_5_1_MDL: &str = "org.iso.18013.5.1.mDL"; - /// 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. @@ -74,6 +77,10 @@ pub enum ClaimFormat { }, #[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), } @@ -95,14 +102,14 @@ impl ClaimFormat { ClaimFormat::AcVp { .. } => ClaimFormatDesignation::AcVp, ClaimFormat::MsoMDoc(_) => ClaimFormatDesignation::MsoMDoc, ClaimFormat::Other(value) => { - // parse the format from the value + // Parse the format from the first key found in the value map. let format = value - .get("format") - .and_then(|format| format.as_str()) - // If a `format` property is not present, default to "unknown" - .unwrap_or("unknown"); + .as_object() + .and_then(|map| map.keys().next()) + .map(ToOwned::to_owned) + .unwrap_or("other".into()); - ClaimFormatDesignation::Other(format.to_string()) + ClaimFormatDesignation::Other(format) } } } @@ -209,6 +216,7 @@ pub enum ClaimFormatDesignation { /// 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), } @@ -250,42 +258,48 @@ impl From for String { } } -/// Credential types that may be requested in a credential request. -/// -/// Credential types can be presented in a number of formats. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum CredentialType { - /// an ISO 18013-5:2021 mobile driving license (mDL) Credential - #[serde(rename = "org.iso.18013.5.1.mDL")] - Iso18013_5_1mDl, - /// A vehicle title credential - /// - /// Given there is no universal standard for how to present a vehicle title credential, - /// the inner String provides a dynamic way to represent a vehicle title credential. - // TODO: is there a standard identifier for a vehicle title credential instead of `vehicle_title`? - #[serde(rename = "vehicle_title")] - VehicleTitle(String), - // TODO: Add additional credential types here. Fallback to a string for any other credential type. - /// Other credential types not covered by the above. - Other(String), -} +#[cfg(test)] +mod tests { + use super::*; -impl From<&str> for CredentialType { - fn from(s: &str) -> Self { - match s { - s if s.contains(ORG_ISO_18013_5_1_MDL) => Self::Iso18013_5_1mDl, - s if s.contains("vehicle_title.") => Self::VehicleTitle(s.to_string()), - s => Self::Other(s.to_string()), - } - } -} + use serde_json::json; -impl From for String { - fn from(cred_type: CredentialType) -> Self { - match cred_type { - CredentialType::Iso18013_5_1mDl => ORG_ISO_18013_5_1_MDL.to_string(), - CredentialType::VehicleTitle(title) => format!("vehicle_title.{title}"), - CredentialType::Other(s) => s, - } + #[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"); + + 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() + )) + ); } } From 0e0f985da4e5e0827efedf86509a424f25cc4816 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Thu, 12 Sep 2024 21:55:16 -0700 Subject: [PATCH 70/71] use Vec::is_empty versus Option::is_none for various serialization fields This commit also removes validation logic from vp token response struct. Signed-off-by: Ryan Tate --- src/core/input_descriptor.rs | 170 +++++++++++++--------------- src/core/presentation_definition.rs | 37 +++--- src/core/response/mod.rs | 52 +-------- src/core/response/parameters.rs | 87 ++------------ src/utils.rs | 2 +- tests/jwt_vc.rs | 11 +- 6 files changed, 110 insertions(+), 249 deletions(-) diff --git a/src/core/input_descriptor.rs b/src/core/input_descriptor.rs index 956f66b..7a8a870 100644 --- a/src/core/input_descriptor.rs +++ b/src/core/input_descriptor.rs @@ -55,10 +55,10 @@ pub struct InputDescriptor { name: Option, #[serde(skip_serializing_if = "Option::is_none")] purpose: Option, - #[serde(skip_serializing_if = "Option::is_none")] - format: Option, - #[serde(skip_serializing_if = "Option::is_none")] - group: Option>, + #[serde(default, skip_serializing_if = "ClaimFormatMap::is_empty")] + format: ClaimFormatMap, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + group: Vec, } impl InputDescriptor { @@ -131,24 +131,24 @@ impl InputDescriptor { /// 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.format = format; self } /// Set the group of the constraints field. pub fn set_group(mut self, group: Vec) -> Self { - self.group = Some(group); + self.group = group; self } /// Return the group of the constraints field. - pub fn groups(&self) -> Option<&Vec> { + 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.get_or_insert_with(Vec::new).push(member); + self.group.push(member); self } @@ -170,87 +170,83 @@ impl InputDescriptor { 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_none() { + if self.constraints.fields().is_empty() { bail!("Required limit disclosure must have fields.") } }; - if let Some(constraint_fields) = self.constraints.fields() { - for constraint_field in constraint_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.") - } + 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); + 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.") - }; + // 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 root_element = root_element + .first() + .ok_or(anyhow::anyhow!("Root element not found."))?; - let mut map_selector = jsonpath_lib::selector(root_element); + let mut map_selector = jsonpath_lib::selector(root_element); - let validator = constraint_field.validator(); + let validator = constraint_field.validator(); - let mut found_elements = false; + 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.")?; + 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; - } + // 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 + 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}", + ); } - 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."); + 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.") - } + // 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.") } } } @@ -266,8 +262,8 @@ impl InputDescriptor { /// /// 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() + pub fn format(&self) -> &ClaimFormatMap { + &self.format } } @@ -278,8 +274,8 @@ impl InputDescriptor { /// 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 = "Vec::is_empty")] + fields: Vec, #[serde(skip_serializing_if = "Option::is_none")] limit_disclosure: Option, } @@ -292,12 +288,12 @@ impl Constraints { /// 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.fields.push(field); self } /// Returns the fields of the constraints object. - pub fn fields(&self) -> Option<&Vec> { + pub fn fields(&self) -> &Vec { self.fields.as_ref() } @@ -325,9 +321,7 @@ impl Constraints { /// Returns if the constraints fields contain non-optional /// fields that must be satisfied. pub fn is_required(&self) -> bool { - self.fields() - .map(|fields| fields.iter().any(|field| field.is_required())) - .unwrap_or(false) + self.fields.iter().any(|field| field.is_required()) } } @@ -336,7 +330,7 @@ impl Constraints { /// 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)] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] pub struct ConstraintsField { path: NonEmptyVec, #[serde(skip_serializing_if = "Option::is_none")] @@ -351,8 +345,8 @@ pub struct ConstraintsField { filter: Option, #[serde(skip_serializing_if = "Option::is_none")] optional: Option, - #[serde(skip_serializing_if = "Option::is_none")] - intent_to_retain: Option, + #[serde(default)] + intent_to_retain: bool, } pub type ConstraintsFields = Vec; @@ -367,7 +361,7 @@ impl From> for ConstraintsField { filter: None, predicate: None, optional: None, - intent_to_retain: None, + intent_to_retain: false, } } } @@ -382,13 +376,7 @@ impl ConstraintsField { pub fn new(path: JsonPath) -> ConstraintsField { ConstraintsField { path: NonEmptyVec::new(path), - id: None, - purpose: None, - name: None, - filter: None, - predicate: None, - optional: None, - intent_to_retain: None, + ..Default::default() } } @@ -529,13 +517,13 @@ impl ConstraintsField { /// 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 = Some(intent_to_retain); + 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.unwrap_or(false) + self.intent_to_retain } /// Return the humanly-readable requested fields of the constraints field. diff --git a/src/core/presentation_definition.rs b/src/core/presentation_definition.rs index 7ffa987..ae97f22 100644 --- a/src/core/presentation_definition.rs +++ b/src/core/presentation_definition.rs @@ -29,8 +29,8 @@ pub struct PresentationDefinition { name: Option, #[serde(skip_serializing_if = "Option::is_none")] purpose: Option, - #[serde(skip_serializing_if = "Option::is_none")] - format: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + format: Vec, } impl PresentationDefinition { @@ -163,21 +163,19 @@ impl PresentationDefinition { /// 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); + pub fn set_format(mut self, format: Vec) -> Self { + self.format = 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); + pub fn add_format(mut self, value: ClaimFormat) -> Self { + self.format.push(value); self } /// Return the format of the presentation definition. - pub fn format(&self) -> Option<&ClaimFormatMap> { + pub fn format(&self) -> &Vec { self.format.as_ref() } @@ -192,16 +190,14 @@ impl PresentationDefinition { pub fn requested_fields(&self) -> Vec { self.input_descriptors .iter() - .filter_map(|input_descriptor| { - input_descriptor.constraints().fields().map(|fields| { - fields - .iter() - .map(|constraint| constraint.requested_fields()) - }) + .flat_map(|input_descriptor| { + input_descriptor + .constraints() + .fields() + .iter() + .map(|constraint| constraint.requested_fields()) }) - .flat_map(|field| field.into_iter()) .flatten() - .map(|field| field.to_string()) .collect() } @@ -286,12 +282,7 @@ impl SubmissionRequirement { // 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() - .map(|input_group| input_group.contains(group)) - .unwrap_or(false) - }) + .filter(|input_descriptor| input_descriptor.groups().contains(group)) .collect::>(); // Filter for the descriptor maps that match the grouped input descriptors. diff --git a/src/core/response/mod.rs b/src/core/response/mod.rs index 04a2adb..e448e37 100644 --- a/src/core/response/mod.rs +++ b/src/core/response/mod.rs @@ -1,11 +1,8 @@ -use super::{ - object::{ParsingErrorContext, UntypedObject}, - presentation_definition::PresentationDefinition, -}; +use super::object::{ParsingErrorContext, UntypedObject}; use std::collections::BTreeMap; -use anyhow::{bail, Context, Error, Result}; +use anyhow::{Context, Error, Result}; use serde::{Deserialize, Serialize}; use serde_json::Value; use url::Url; @@ -38,51 +35,6 @@ impl AuthorizationResponse { Ok(Self::Unencoded(UntypedObject(map).try_into()?)) } - - /// Validate an authorization response against a presentation definition. - /// - /// This method will parse the presentation submission from the auth response and - /// validate it against the provided presentation definition. - /// - /// # Parameters - /// - /// - `self` - The authorization response to validate. - /// - `presentation_definition` - The presentation definition to validate against. - /// - /// # Errors - /// - /// This method will return an error if the presentation submission does not match the - /// presentation definition. - /// - /// # Returns - /// - /// This method will return `Ok(())` if the presentation submission matches the presentation - /// definition. - pub fn validate(&self, presentation_definition: &PresentationDefinition) -> Result<()> { - match self { - AuthorizationResponse::Jwt(_jwt) => { - // TODO: Handle JWT Encoded authorization response. - - bail!("Authorization Response Presentation Definition validation not implemented.") - } - AuthorizationResponse::Unencoded(response) => { - let presentation_submission = response.presentation_submission().parsed(); - - // Ensure the definition id matches the submission's definition id. - if presentation_submission.definition_id() != presentation_definition.id() { - bail!("Presentation Definition ID does not match the Presentation Submission.") - } - - // Validate the VP Token as an unencoded payload. - response.vp_token().validate_unencoded( - presentation_definition, - presentation_submission.descriptor_map(), - )?; - - Ok(()) - } - } - } } #[derive(Debug, Clone)] diff --git a/src/core/response/parameters.rs b/src/core/response/parameters.rs index cdd6f2b..8fd36e2 100644 --- a/src/core/response/parameters.rs +++ b/src/core/response/parameters.rs @@ -1,14 +1,9 @@ pub use crate::core::authorization_request::parameters::State; use crate::core::object::TypedParameter; -use crate::core::presentation_definition::PresentationDefinition; -use crate::core::presentation_submission::{ - DescriptorMap, PresentationSubmission as PresentationSubmissionParsed, -}; +use crate::core::presentation_submission::PresentationSubmission as PresentationSubmissionParsed; use anyhow::Error; -use base64::prelude::*; use serde_json::Value as Json; -use ssi_claims::jwt::VerifiablePresentation; #[derive(Debug, Clone)] pub struct IdToken(pub String); @@ -31,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); @@ -52,76 +57,6 @@ impl From for Json { } } -impl VpToken { - /// Parse the VP Token as a JSON object. - /// - /// This will attempt to decode the token as base64, and if that fails, it - /// will attempt to parse the token as a JSON object. - /// - /// See: https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#section-6.1-2.2 - /// - /// If you want to check for decode errors, use [VpToken::decode_base64]. - pub fn parse(&self) -> Result { - match self.decode_base64() { - Ok(decoded) => Ok(decoded), - Err(_) => Ok(serde_json::from_str(&self.0)?), - } - } - - /// Return the Verifiable Presentation Token as a JSON object from a base64 - /// encoded string. - pub fn decode_base64(&self) -> Result { - let decoded = BASE64_STANDARD.decode(&self.0)?; - Ok(serde_json::from_slice(&decoded)?) - } - - /// Validate an unencoded Verifiable Presentation Token. - /// - /// This method assumtes the VP token is not encoded as a JWT. - /// - /// # Returns - /// - /// This method will return `Ok(())` if the VP token is valid. - /// - /// # Errors - /// - /// This method will return an error if the VP token is invalid, - /// or if the verifiable presentation is invalid or does not meet - /// the requirements of the presentation definition. - /// - /// - pub fn validate_unencoded( - &self, - presentation_definition: &PresentationDefinition, - descriptor_map: &[DescriptorMap], - ) -> Result<(), Error> { - let vp_payload = self.parse()?; - - // Check if the vp_payload is an array of VPs - match vp_payload.as_array() { - None => { - // handle a single verifiable presentation - presentation_definition.validate_presentation( - VerifiablePresentation(json_syntax::Value::from(vp_payload)), - descriptor_map, - )?; - } - Some(vps) => { - // Each item in the array is a VP - for vp in vps { - // handle the verifiable presentation - presentation_definition.validate_presentation( - VerifiablePresentation(json_syntax::Value::from(vp.clone())), - descriptor_map, - )?; - } - } - } - - Ok(()) - } -} - #[derive(Debug, Clone)] pub struct PresentationSubmission { raw: Json, 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/tests/jwt_vc.rs b/tests/jwt_vc.rs index b7ffd5c..9d60245 100644 --- a/tests/jwt_vc.rs +++ b/tests/jwt_vc.rs @@ -159,15 +159,10 @@ impl AsyncHttpClient for MockHttpClient { id.parse().context("failed to parse id")?, AuthorizationResponse::from_x_www_form_urlencoded(body) .context("failed to parse authorization response request")?, - |session, auth_response| { + |_, _| { Box::pin(async move { - match auth_response.validate(&session.presentation_definition) { - Ok(_) => Outcome::Success { - info: serde_json::Value::Null, - }, - Err(e) => Outcome::Error { - cause: e.to_string(), - }, + Outcome::Success { + info: serde_json::Value::Null, } }) }, From b8e9141099f6e92b55eebcf8d328f6630d33f22b Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Thu, 12 Sep 2024 22:13:00 -0700 Subject: [PATCH 71/71] revert to use of ClaimFormatMap to pass presentation defintion test suite Signed-off-by: Ryan Tate --- src/core/presentation_definition.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/presentation_definition.rs b/src/core/presentation_definition.rs index ae97f22..caa402a 100644 --- a/src/core/presentation_definition.rs +++ b/src/core/presentation_definition.rs @@ -29,8 +29,8 @@ pub struct PresentationDefinition { name: Option, #[serde(skip_serializing_if = "Option::is_none")] purpose: Option, - #[serde(skip_serializing_if = "Vec::is_empty")] - format: Vec, + #[serde(default, skip_serializing_if = "ClaimFormatMap::is_empty")] + format: ClaimFormatMap, } impl PresentationDefinition { @@ -163,20 +163,20 @@ impl PresentationDefinition { /// 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: Vec) -> Self { + 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, value: ClaimFormat) -> Self { - self.format.push(value); + 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) -> &Vec { - self.format.as_ref() + pub fn format(&self) -> &ClaimFormatMap { + &self.format } /// Return the human-readable string representation of the fields requested