Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PEX: Provide ParseEnvelope to correctly parse PEX VP envelopes #2620

Merged
merged 2 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
reinkrul marked this conversation as resolved.
Show resolved Hide resolved
// Presentations contains the Verifiable Presentations that were parsed from the envelope.
Presentations []vc.VerifiablePresentation
reinkrul marked this conversation as resolved.
Show resolved Hide resolved
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
Loading