Skip to content

Commit

Permalink
PEX: Provide ParseEnvelope to correctly parse PEX VP envelopes (#2620)
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul authored Nov 27, 2023
1 parent 30a3bd6 commit 6211184
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 64 deletions.
13 changes: 8 additions & 5 deletions vcr/pe/presentation_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,14 +468,17 @@ func vcEqual(a, b vc.VerifiableCredential) bool {
return string(aJSON) == string(bJSON)
}

func remarshalToMap(v interface{}) (map[string]interface{}, error) {
asJSON, err := json.Marshal(v)
func remarshal(src interface{}, dst interface{}) error {
asJSON, err := json.Marshal(src)
if err != nil {
return nil, err
return err
}
return json.Unmarshal(asJSON, &dst)
}

func remarshalToMap(v interface{}) (map[string]interface{}, error) {
var result map[string]interface{}
err = json.Unmarshal(asJSON, &result)
if err != nil {
if err := remarshal(v, &result); err != nil {
return nil, err
}
return result, nil
Expand Down
33 changes: 4 additions & 29 deletions vcr/pe/presentation_submission.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,21 +177,10 @@ func (b *PresentationSubmissionBuilder) Build(format string) (PresentationSubmis
// Resolve returns a map where each of the input descriptors is mapped to the corresponding VerifiableCredential.
// If an input descriptor can't be mapped to a VC, an error is returned.
// This function is specified by https://identity.foundation/presentation-exchange/#processing-of-submission-entries
func (s PresentationSubmission) Resolve(envelope interface{}) (map[string]vc.VerifiableCredential, error) {
switch envelope.(type) {
case []interface{}:
// list of VPs
case map[string]interface{}:
// single VP (JSON)
case string:
// single VP (JWT)
default:
return nil, errors.New("invalid Presentation Exchange envelope")
}

func (s PresentationSubmission) Resolve(envelope Envelope) (map[string]vc.VerifiableCredential, error) {
result := make(map[string]vc.VerifiableCredential)
for _, inputDescriptor := range s.DescriptorMap {
resolvedCredential, err := resolveCredential(nil, inputDescriptor, envelope)
resolvedCredential, err := resolveCredential(nil, inputDescriptor, envelope.asInterface)
if err != nil {
return nil, fmt.Errorf("unable to resolve credential for input descriptor '%s': %w", inputDescriptor.Id, err)
}
Expand Down Expand Up @@ -260,29 +249,15 @@ func resolveCredential(path []string, mapping InputDescriptorMappingObject, valu
// The credentials will be returned as map with the InputDescriptor.Id as key.
// The Presentation Definitions are passed in the envelope, as specified by the PEX specification.
// It assumes credentials of the presentations only map in 1 way to the input descriptors.
func (s PresentationSubmission) Validate(envelope interface{}, definition PresentationDefinition) (map[string]vc.VerifiableCredential, error) {
func (s PresentationSubmission) Validate(envelope Envelope, definition PresentationDefinition) (map[string]vc.VerifiableCredential, error) {
actualCredentials, err := s.Resolve(envelope)
if err != nil {
return nil, fmt.Errorf("resolve credentials from presentation submission: %w", err)
}

// Create a new presentation submission: the submission being validated should have the same input descriptor mapping.
// First, create a new submission
var presentations []vc.VerifiablePresentation
envelopeJSON, _ := json.Marshal(envelope)
if _, ok := envelope.([]interface{}); ok {
if err := json.Unmarshal(envelopeJSON, &presentations); err != nil {
return nil, fmt.Errorf("unable to unmarshal envelope: %w", err)
}
} else {
var presentation vc.VerifiablePresentation
if err := json.Unmarshal(envelopeJSON, &presentation); err != nil {
return nil, fmt.Errorf("unable to unmarshal envelope: %w", err)
}
presentations = []vc.VerifiablePresentation{presentation}
}
submissionBuilder := definition.PresentationSubmissionBuilder()
for _, presentation := range presentations {
for _, presentation := range envelope.Presentations {
signer, err := credential.PresentationSigner(presentation)
if err != nil {
return nil, fmt.Errorf("unable to derive presentation signer: %w", err)
Expand Down
48 changes: 18 additions & 30 deletions vcr/pe/presentation_submission_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ func TestPresentationSubmission_Resolve(t *testing.T) {
var submission PresentationSubmission
require.NoError(t, json.Unmarshal([]byte(submissionJSON), &submission))

credentials, err := submission.Resolve(remarshalToInterface(vp))
credentials, err := submission.Resolve(toEnvelope(t, vp))

require.NoError(t, err)
assert.Len(t, credentials, 1)
Expand Down Expand Up @@ -241,7 +241,7 @@ func TestPresentationSubmission_Resolve(t *testing.T) {
var submission PresentationSubmission
require.NoError(t, json.Unmarshal([]byte(submissionJSON), &submission))

credentials, err := submission.Resolve(remarshalToInterface(vp))
credentials, err := submission.Resolve(toEnvelope(t, vp))

require.NoError(t, err)
assert.Len(t, credentials, 2)
Expand Down Expand Up @@ -284,7 +284,7 @@ func TestPresentationSubmission_Resolve(t *testing.T) {
var submission PresentationSubmission
require.NoError(t, json.Unmarshal([]byte(submissionJSON), &submission))

credentials, err := submission.Resolve(remarshalToInterface([]vc.VerifiablePresentation{vp1, vp2}))
credentials, err := submission.Resolve(toEnvelope(t, []interface{}{vp1, vp2}))

require.NoError(t, err)
assert.Len(t, credentials, 2)
Expand All @@ -309,7 +309,7 @@ func TestPresentationSubmission_Resolve(t *testing.T) {
var submission PresentationSubmission
require.NoError(t, json.Unmarshal([]byte(submissionJSON), &submission))

credentials, err := submission.Resolve(remarshalToInterface(vp))
credentials, err := submission.Resolve(toEnvelope(t, vp))

require.EqualError(t, err, "unable to resolve credential for input descriptor '1': path '$.verifiableCredential' does not reference a credential")
assert.Nil(t, credentials)
Expand All @@ -332,7 +332,7 @@ func TestPresentationSubmission_Resolve(t *testing.T) {
var submission PresentationSubmission
require.NoError(t, json.Unmarshal([]byte(submissionJSON), &submission))

credentials, err := submission.Resolve(remarshalToInterface(vp))
credentials, err := submission.Resolve(toEnvelope(t, vp))

require.ErrorContains(t, err, "unable to resolve credential for input descriptor '1': invalid JSON-LD credential at path")
assert.Nil(t, credentials)
Expand All @@ -355,7 +355,7 @@ func TestPresentationSubmission_Resolve(t *testing.T) {
var submission PresentationSubmission
require.NoError(t, json.Unmarshal([]byte(submissionJSON), &submission))

credentials, err := submission.Resolve(remarshalToInterface(vp))
credentials, err := submission.Resolve(toEnvelope(t, vp))

require.ErrorContains(t, err, "unable to resolve credential for input descriptor '1': invalid JSON-LD presentation at path")
assert.Nil(t, credentials)
Expand All @@ -378,7 +378,7 @@ func TestPresentationSubmission_Resolve(t *testing.T) {
var submission PresentationSubmission
require.NoError(t, json.Unmarshal([]byte(submissionJSON), &submission))

credentials, err := submission.Resolve(remarshalToInterface(vp))
credentials, err := submission.Resolve(toEnvelope(t, vp))

assert.EqualError(t, err, "unable to resolve credential for input descriptor '1': value of Go type 'string' at path '$.verifiableCredential.expirationDate' can't be decoded using format 'ldp_vc'")
assert.Nil(t, credentials)
Expand Down Expand Up @@ -426,7 +426,7 @@ func TestPresentationSubmission_Validate(t *testing.T) {
},
}

credentials, err := submission.Validate(remarshalToInterface(vp), definition)
credentials, err := submission.Validate(toEnvelope(t, vp), definition)

require.NoError(t, err)
require.Len(t, credentials, 1)
Expand Down Expand Up @@ -501,7 +501,7 @@ func TestPresentationSubmission_Validate(t *testing.T) {
},
}

credentials, err := submission.Validate(remarshalToInterface([]vc.VerifiablePresentation{vp, secondVP}), definition)
credentials, err := submission.Validate(toEnvelope(t, []vc.VerifiablePresentation{vp, secondVP}), definition)

require.NoError(t, err)
require.Len(t, credentials, 2)
Expand Down Expand Up @@ -529,7 +529,7 @@ func TestPresentationSubmission_Validate(t *testing.T) {
},
}

credentials, err := PresentationSubmission{}.Validate(remarshalToInterface([]vc.VerifiablePresentation{vp}), definition)
credentials, err := PresentationSubmission{}.Validate(toEnvelope(t, []vc.VerifiablePresentation{vp}), definition)

assert.EqualError(t, err, "presentation submission doesn't match presentation definition")
assert.Empty(t, credentials)
Expand Down Expand Up @@ -590,23 +590,11 @@ func TestPresentationSubmission_Validate(t *testing.T) {
},
}

credentials, err := submission.Validate(remarshalToInterface(vp), definition)
credentials, err := submission.Validate(toEnvelope(t, vp), definition)

assert.EqualError(t, err, "incorrect mapping for input descriptor: 2")
assert.Empty(t, credentials)
})
t.Run("envelope contains an invalid presentation", func(t *testing.T) {
credentials, err := PresentationSubmission{}.Validate(map[string]interface{}{"id": true}, PresentationDefinition{})

assert.EqualError(t, err, "unable to unmarshal envelope: json: cannot unmarshal bool into Go struct field Alias.id of type string")
assert.Empty(t, credentials)
})
t.Run("envelope contains an invalid presentation (envelope is array)", func(t *testing.T) {
credentials, err := PresentationSubmission{}.Validate([]interface{}{map[string]interface{}{"id": true}}, PresentationDefinition{})

assert.EqualError(t, err, "unable to unmarshal envelope: json: cannot unmarshal bool into Go struct field Alias.id of type string")
assert.Empty(t, credentials)
})
t.Run("submission contains mappings for non-existing input descriptors", func(t *testing.T) {
definition := PresentationDefinition{
InputDescriptors: []*InputDescriptor{
Expand Down Expand Up @@ -651,23 +639,23 @@ func TestPresentationSubmission_Validate(t *testing.T) {
},
}

credentials, err := submission.Validate(remarshalToInterface(vp), definition)
credentials, err := submission.Validate(toEnvelope(t, vp), definition)

assert.EqualError(t, err, "expected 1 credentials, got 2")
assert.Empty(t, credentials)
})
t.Run("unable to derive presentation signer", func(t *testing.T) {
vp = vc.VerifiablePresentation{}
credentials, err := PresentationSubmission{}.Validate(remarshalToInterface(vp), PresentationDefinition{})
credentials, err := PresentationSubmission{}.Validate(toEnvelope(t, vp), PresentationDefinition{})

assert.EqualError(t, err, "unable to derive presentation signer: presentation should have exactly 1 proof, got 0")
assert.Empty(t, credentials)
})
}

func remarshalToInterface(input interface{}) interface{} {
bytes, _ := json.Marshal(input)
var result interface{}
_ = json.Unmarshal(bytes, &result)
return result
func toEnvelope(t *testing.T, presentations interface{}) Envelope {
vpBytes, _ := json.Marshal(presentations)
envelope, err := ParseEnvelope(vpBytes)
require.NoError(t, err)
return *envelope
}
139 changes: 139 additions & 0 deletions vcr/pe/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright (C) 2023 Nuts community
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/

package pe

import (
"encoding/json"
"fmt"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/nuts-foundation/go-did/vc"
)

// Envelope is a parsed Presentation Exchange envelope, containing zero or more Verifiable Presentations that are referenced by the Presentation Submission.
type Envelope struct {
// Presentations contains the Verifiable Presentations that were parsed from the envelope.
Presentations []vc.VerifiablePresentation
asInterface interface{}
}

// ParseEnvelope parses a Presentation Exchange envelope, which is a JSON type that encompasses zero or more Verifiable Presentations.
// It returns the envelope as interface{} for use in PresentationSubmission.Validate() and PresentationSubmission.Resolve().
// It also returns the parsed Verifiable Presentations.
// Parsing is complicated since a Presentation Submission has multiple ways to reference a Verifiable Credential:
// - single VP as JSON object: $.verifiableCredential
// - multiple VPs as JSON array (with path_nested): $[0] -> $.verifiableCredential
// And since JWT VPs aren't JSON objects, we need to parse them separately.
func ParseEnvelope(envelopeBytes []byte) (*Envelope, error) {
jsonArray := tryParseJSONArray(envelopeBytes)
if jsonArray != nil {
// Array of Verifiable Presentations
asInterface, presentations, err := parseJSONArrayEnvelope(jsonArray)
if err != nil {
return nil, err
}
return &Envelope{
asInterface: asInterface,
Presentations: presentations,
}, nil
}
// Single Verifiable Presentation
asInterface, presentation, err := parseJSONObjectOrStringEnvelope(envelopeBytes)
if err != nil {
return nil, err
}
return &Envelope{
asInterface: asInterface,
Presentations: []vc.VerifiablePresentation{*presentation},
}, nil
}

// parseEnvelopeEntry parses a single Verifiable Presentation in a Presentation Exchange envelope.
// It takes into account custom unmarshalling required for JWT VPs.
func parseJSONArrayEnvelope(arr []interface{}) (interface{}, []vc.VerifiablePresentation, error) {
var presentations []vc.VerifiablePresentation
var asInterfaces []interface{}
for _, entry := range arr {
// Each entry can be a VP as JWT (string) or JSON (object)
var entryBytes []byte
switch entry.(type) {
case string:
// JWT
entryBytes = []byte(entry.(string))
default:
var err error
entryBytes, err = json.Marshal(entry)
if err != nil {
return nil, nil, err
}
}
asInterface, presentation, err := parseJSONObjectOrStringEnvelope(entryBytes)
if err != nil {
return nil, nil, err
}
asInterfaces = append(asInterfaces, asInterface)
presentations = append(presentations, *presentation)
}
return asInterfaces, presentations, nil
}

// parseJSONObjectOrStringEnvelope parses a single Verifiable Presentation in a Presentation Exchange envelope.
// It takes into account custom unmarshalling required for JWT VPs (since they're JSON strings, not objects).
func parseJSONObjectOrStringEnvelope(envelopeBytes []byte) (interface{}, *vc.VerifiablePresentation, error) {
presentation, err := vc.ParseVerifiablePresentation(string(envelopeBytes))
if err != nil {
return nil, nil, fmt.Errorf("unable to parse PEX envelope as verifiable presentation: %w", err)
}
// TODO: This should be part of go-did library; we need to decode a JWT VP (and maybe later VC) and get the properties
// (in this case as map) without losing original cardinality.
// Part of https://github.com/nuts-foundation/go-did/issues/85
if presentation.Format() == vc.JWTPresentationProofFormat {
token, err := jwt.Parse(envelopeBytes, jwt.WithVerify(false), jwt.WithValidate(false))
if err != nil {
return nil, nil, fmt.Errorf("unable to parse PEX envelope as JWT verifiable presentation: %w", err)
}
asMap := make(map[string]interface{})
// use the 'vp' claim as base Verifiable Presentation properties
innerVPAsMap, _ := token.PrivateClaims()["vp"].(map[string]interface{})
for key, value := range innerVPAsMap {
asMap[key] = value
}
if jti, ok := token.Get(jwt.JwtIDKey); ok {
asMap["id"] = jti
}
return asMap, presentation, nil
}
// For other formats, we can just parse the JSON to get the interface{} for JSON Path to work on
var asMap interface{}
if err := json.Unmarshal(envelopeBytes, &asMap); err != nil {
// Can't actually fail?
return nil, nil, err
}
return asMap, presentation, nil
}

// tryParseJSONArray tries to parse the given bytes as a JSON array.
// It returns the array as []interface{} if the bytes are a JSON array, or nil otherwise.
func tryParseJSONArray(bytes []byte) []interface{} {
var asInterface interface{}
if err := json.Unmarshal(bytes, &asInterface); err != nil {
return nil
}
arr, _ := asInterface.([]interface{})
return arr
}
Loading

0 comments on commit 6211184

Please sign in to comment.