Skip to content

Commit

Permalink
update vp token base64 encoding and check for multiple vp payloads
Browse files Browse the repository at this point in the history
Signed-off-by: Ryan Tate <[email protected]>
  • Loading branch information
Ryanmtate committed Aug 28, 2024
1 parent 76e3a3a commit 51e11e8
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 54 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
18 changes: 0 additions & 18 deletions src/core/credential_format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ClaimFormatDesignation, ClaimFormatPayload>;

Expand Down
10 changes: 4 additions & 6 deletions src/core/input_descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -528,13 +528,12 @@ impl ConstraintsField {
///
pub fn requested_fields(&self) -> Vec<String> {
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| {
Expand All @@ -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()
Expand All @@ -571,9 +570,8 @@ impl ConstraintsField {
acc
});

format!("{} ", word.trim_end())
format!("{desc} {}", word.trim_end())
})
.collect::<String>()
.trim_end()
.to_string()
})
Expand Down
66 changes: 46 additions & 20 deletions src/core/presentation_definition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, DescriptorMap> = 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<String, DescriptorMap>,
) -> 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.")?;
}
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/core/presentation_submission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
23 changes: 23 additions & 0 deletions src/core/response/parameters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -47,6 +48,28 @@ impl From<VpToken> 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<Json, Error> {
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<Json, Error> {
let decoded = BASE64_STANDARD.decode(&self.0)?;
Ok(serde_json::from_slice(&decoded)?)
}
}

#[derive(Debug, Clone)]
pub struct PresentationSubmission {
raw: Json,
Expand Down
3 changes: 3 additions & 0 deletions tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 { .. })))
}
17 changes: 7 additions & 10 deletions tests/jwt_vp.rs
Original file line number Diff line number Diff line change
@@ -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<CompactJWSString> {
pub async fn create_test_verifiable_presentation() -> Result<String> {
let verifier = JWK::from_str(include_str!("examples/verifier.jwk"))?;

let signer = P256Signer::new(
Expand Down Expand Up @@ -38,14 +38,11 @@ pub async fn create_test_verifiable_presentation() -> Result<CompactJWSString> {
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)
}

0 comments on commit 51e11e8

Please sign in to comment.