diff --git a/vcr/pe/presentation_definition.go b/vcr/pe/presentation_definition.go
index 89ed2db436..19705eadbe 100644
--- a/vcr/pe/presentation_definition.go
+++ b/vcr/pe/presentation_definition.go
@@ -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
diff --git a/vcr/pe/presentation_submission.go b/vcr/pe/presentation_submission.go
index 2b4fd863f6..fdd1bf6cdc 100644
--- a/vcr/pe/presentation_submission.go
+++ b/vcr/pe/presentation_submission.go
@@ -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)
}
@@ -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)
diff --git a/vcr/pe/presentation_submission_test.go b/vcr/pe/presentation_submission_test.go
index 1f7116ce43..8724e2b0ee 100644
--- a/vcr/pe/presentation_submission_test.go
+++ b/vcr/pe/presentation_submission_test.go
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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{
@@ -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
}
diff --git a/vcr/pe/util.go b/vcr/pe/util.go
new file mode 100644
index 0000000000..6600a12fe5
--- /dev/null
+++ b/vcr/pe/util.go
@@ -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 .
+ *
+ */
+
+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
+}
diff --git a/vcr/pe/util_test.go b/vcr/pe/util_test.go
new file mode 100644
index 0000000000..291e8f0054
--- /dev/null
+++ b/vcr/pe/util_test.go
@@ -0,0 +1,80 @@
+/*
+ * 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 .
+ *
+ */
+
+package pe
+
+import (
+ "encoding/json"
+ "github.com/nuts-foundation/go-did/did"
+ "github.com/nuts-foundation/nuts-node/vcr/credential"
+ "github.com/nuts-foundation/nuts-node/vcr/test"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "testing"
+)
+
+func TestParseEnvelope(t *testing.T) {
+ t.Run("JWT", func(t *testing.T) {
+ presentation := test.CreateJWTPresentation(t, did.MustParseDID("did:example:1"), credential.ValidNutsOrganizationCredential(t))
+ envelope, err := ParseEnvelope([]byte(presentation.Raw()))
+ require.NoError(t, err)
+ require.Equal(t, presentation.ID.String(), envelope.asInterface.(map[string]interface{})["id"])
+ require.Len(t, envelope.Presentations, 1)
+ })
+ t.Run("invalid JWT", func(t *testing.T) {
+ envelope, err := ParseEnvelope([]byte(`eyINVALID`))
+ assert.EqualError(t, err, "unable to parse PEX envelope as verifiable presentation: invalid JWT")
+ assert.Nil(t, envelope)
+ })
+ t.Run("JSON object", func(t *testing.T) {
+ envelope, err := ParseEnvelope([]byte(`{"id": "value"}`))
+ require.NoError(t, err)
+ require.Equal(t, map[string]interface{}{"id": "value"}, envelope.asInterface)
+ require.Len(t, envelope.Presentations, 1)
+ })
+ t.Run("invalid VP as JSON object", func(t *testing.T) {
+ envelope, err := ParseEnvelope([]byte(`{"id": true}`))
+ assert.ErrorContains(t, err, "unable to parse PEX envelope as verifiable presentation")
+ assert.Nil(t, envelope)
+ })
+ t.Run("JSON array with objects", func(t *testing.T) {
+ envelope, err := ParseEnvelope([]byte(`[{"id": "value"}]`))
+ require.NoError(t, err)
+ require.Equal(t, []interface{}{map[string]interface{}{"id": "value"}}, envelope.asInterface)
+ require.Len(t, envelope.Presentations, 1)
+ })
+ t.Run("JSON array with JWTs", func(t *testing.T) {
+ presentation := test.CreateJWTPresentation(t, did.MustParseDID("did:example:1"), credential.ValidNutsOrganizationCredential(t))
+ presentations := []string{presentation.Raw(), presentation.Raw()}
+ listJSON, _ := json.Marshal(presentations)
+ envelope, err := ParseEnvelope(listJSON)
+ require.NoError(t, err)
+ require.Len(t, envelope.asInterface, 2)
+ require.Len(t, envelope.Presentations, 2)
+ })
+ t.Run("invalid VPs list as JSON", func(t *testing.T) {
+ envelope, err := ParseEnvelope([]byte(`[{"id": true}]`))
+ assert.ErrorContains(t, err, "unable to parse PEX envelope as verifiable presentation")
+ assert.Nil(t, envelope)
+ })
+ t.Run("invalid format", func(t *testing.T) {
+ envelope, err := ParseEnvelope([]byte(`true`))
+ assert.EqualError(t, err, "unable to parse PEX envelope as verifiable presentation: invalid JWT")
+ assert.Nil(t, envelope)
+ })
+}
diff --git a/vcr/test/test.go b/vcr/test/test.go
new file mode 100644
index 0000000000..9699576e45
--- /dev/null
+++ b/vcr/test/test.go
@@ -0,0 +1,61 @@
+/*
+ * 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 .
+ *
+ */
+
+package test
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "github.com/google/uuid"
+ "github.com/lestrrat-go/jwx/v2/jwa"
+ "github.com/lestrrat-go/jwx/v2/jws"
+ "github.com/lestrrat-go/jwx/v2/jwt"
+ ssi "github.com/nuts-foundation/go-did"
+ "github.com/nuts-foundation/go-did/did"
+ "github.com/nuts-foundation/go-did/vc"
+ "github.com/stretchr/testify/require"
+ "testing"
+ "time"
+)
+
+// CreateJWTPresentation creates a JWT presentation with the given subject DID and credentials.
+func CreateJWTPresentation(t *testing.T, subjectDID did.DID, credentials ...vc.VerifiableCredential) vc.VerifiablePresentation {
+ headers := jws.NewHeaders()
+ require.NoError(t, headers.Set(jws.TypeKey, "JWT"))
+ claims := map[string]interface{}{
+ jwt.IssuerKey: subjectDID.String(),
+ jwt.SubjectKey: subjectDID.String(),
+ jwt.JwtIDKey: subjectDID.String() + "#" + uuid.NewString(),
+ jwt.NotBeforeKey: time.Now().Unix(),
+ jwt.ExpirationKey: time.Now().Add(time.Hour).Unix(),
+ "vp": vc.VerifiablePresentation{
+ Type: []ssi.URI{vc.VerifiablePresentationTypeV1URI()},
+ VerifiableCredential: credentials,
+ },
+ }
+ unsignedToken := jwt.New()
+ for k, v := range claims {
+ require.NoError(t, unsignedToken.Set(k, v))
+ }
+ privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ token, _ := jwt.Sign(unsignedToken, jwt.WithKey(jwa.ES256, privateKey, jws.WithProtectedHeaders(headers)))
+ result, err := vc.ParseVerifiablePresentation(string(token))
+ require.NoError(t, err)
+ return *result
+}