From 51e11e8d0476b2eb4d644c8a3e5f51a207489ad7 Mon Sep 17 00:00:00 2001 From: Ryan Tate Date: Tue, 27 Aug 2024 17:37:11 -0700 Subject: [PATCH] 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) }