Skip to content

Commit

Permalink
PEX: Resolve credentials from presentation submission (#2612)
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul authored Nov 21, 2023
1 parent 005fb65 commit 8c7654f
Show file tree
Hide file tree
Showing 5 changed files with 425 additions and 1 deletion.
35 changes: 35 additions & 0 deletions vcr/credential/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
package credential

import (
"errors"
"fmt"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/vcr/signature/proof"
)

// FindValidator finds the Validator the provided credential based on its Type
Expand Down Expand Up @@ -52,3 +56,34 @@ func ExtractTypes(credential vc.VerifiableCredential) []string {

return vcTypes
}

// PresentationSigner returns the DID of the signer of the presentation.
// It does not do any signature validation.
// For JWTs it returns the issuer (iss) of the JWT.
// For JSON-LD it returns the verification method of the proof.
func PresentationSigner(presentation vc.VerifiablePresentation) (*did.DID, error) {
switch presentation.Format() {
case vc.JWTPresentationProofFormat:
token := presentation.JWT()
issuer := token.Issuer()
if issuer == "" {
return nil, errors.New("JWT presentation does not have 'iss' claim")
}
return did.ParseDID(issuer)
case vc.JSONLDCredentialProofFormat:
fallthrough
default:
var proofs []proof.LDProof
if err := presentation.UnmarshalProofValue(&proofs); err != nil {
return nil, fmt.Errorf("invalid LD-proof for presentation: %w", err)
}
if len(proofs) != 1 {
return nil, fmt.Errorf("presentation should have exactly 1 proof, got %d", len(proofs))
}
verificationMethod, err := did.ParseDIDURL(proofs[0].VerificationMethod.String())
if err != nil || verificationMethod.DID.Empty() {
return nil, fmt.Errorf("invalid verification method for JSON-LD presentation: %w", err)
}
return &verificationMethod.DID, nil
}
}
90 changes: 90 additions & 0 deletions vcr/credential/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@
package credential

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/nuts-node/vcr/signature/proof"
"github.com/stretchr/testify/require"
"testing"

ssi "github.com/nuts-foundation/go-did"
Expand Down Expand Up @@ -59,3 +67,85 @@ func TestExtractTypes(t *testing.T) {
assert.Len(t, types, 1)
assert.Equal(t, NutsOrganizationCredentialType, types[0])
}

func TestPresentationSigner(t *testing.T) {
keyID := did.MustParseDIDURL("did:example:issuer#1")
t.Run("JWT", func(t *testing.T) {
privateKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
t.Run("ok", func(t *testing.T) {
token := jwt.New()
require.NoError(t, token.Set(jwt.IssuerKey, keyID.DID.String()))
signedToken, err := jwt.Sign(token, jwt.WithKey(jwa.ES256, privateKey))
require.NoError(t, err)
presentation, err := vc.ParseVerifiablePresentation(string(signedToken))
require.NoError(t, err)

actual, err := PresentationSigner(*presentation)
require.NoError(t, err)
assert.Equal(t, keyID.DID, *actual)
})
t.Run("missing 'iss' claim", func(t *testing.T) {
token := jwt.New()
signedToken, err := jwt.Sign(token, jwt.WithKey(jwa.ES256, privateKey))
require.NoError(t, err)
presentation, err := vc.ParseVerifiablePresentation(string(signedToken))
require.NoError(t, err)

actual, err := PresentationSigner(*presentation)
assert.EqualError(t, err, "JWT presentation does not have 'iss' claim")
assert.Nil(t, actual)
})
})
t.Run("JSON-LD", func(t *testing.T) {
t.Run("ok", func(t *testing.T) {
presentation := vc.VerifiablePresentation{
Proof: []interface{}{proof.LDProof{
VerificationMethod: keyID.URI(),
}},
}
actual, err := PresentationSigner(presentation)
assert.NoError(t, err)
assert.Equal(t, keyID.DID, *actual)
})
t.Run("too many proofs", func(t *testing.T) {
presentation := vc.VerifiablePresentation{
Proof: []interface{}{proof.LDProof{
VerificationMethod: keyID.URI(),
}, proof.LDProof{
VerificationMethod: keyID.URI(),
}},
}
actual, err := PresentationSigner(presentation)
assert.EqualError(t, err, "presentation should have exactly 1 proof, got 2")
assert.Nil(t, actual)
})
t.Run("not a JSON-LD proof", func(t *testing.T) {
presentation := vc.VerifiablePresentation{
Proof: []interface{}{5},
}
actual, err := PresentationSigner(presentation)
assert.EqualError(t, err, "invalid LD-proof for presentation: json: cannot unmarshal number into Go value of type proof.LDProof")
assert.Nil(t, actual)
})
t.Run("invalid DID in proof", func(t *testing.T) {
presentation := vc.VerifiablePresentation{
Proof: []interface{}{proof.LDProof{
VerificationMethod: ssi.MustParseURI("foo"),
}},
}
actual, err := PresentationSigner(presentation)
assert.EqualError(t, err, "invalid verification method for JSON-LD presentation: invalid DID")
assert.Nil(t, actual)
})
t.Run("empty VerificationMethod", func(t *testing.T) {
presentation := vc.VerifiablePresentation{
Proof: []interface{}{proof.LDProof{
VerificationMethod: ssi.MustParseURI(""),
}},
}
actual, err := PresentationSigner(presentation)
assert.ErrorContains(t, err, "invalid verification method for JSON-LD presentation")
assert.Nil(t, actual)
})
})
}
2 changes: 1 addition & 1 deletion vcr/pe/presentation_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ func (presentationDefinition PresentationDefinition) matchSubmissionRequirements
}
for _, group := range presentationDefinition.groups() {
if _, ok := availableGroups[group.Name]; !ok {
return nil, nil, fmt.Errorf("group %s is required but not available", group.Name)
return nil, nil, fmt.Errorf("group '%s' is required but not available", group.Name)
}
}

Expand Down
85 changes: 85 additions & 0 deletions vcr/pe/presentation_submission.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ package pe
import (
"encoding/json"
"fmt"
"github.com/PaesslerAG/jsonpath"
"github.com/google/uuid"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
v2 "github.com/nuts-foundation/nuts-node/vcr/pe/schema/v2"
"strings"
)

// ParsePresentationSubmission validates the given JSON and parses it into a PresentationSubmission.
Expand Down Expand Up @@ -169,3 +171,86 @@ func (b *PresentationSubmissionBuilder) Build(format string) (PresentationSubmis

return presentationSubmission, nonEmptySignInstructions, nil
}

// 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(presentations []vc.VerifiablePresentation) (map[string]vc.VerifiableCredential, error) {
var envelopeJSON []byte
if len(presentations) == 1 {
// TODO: This might not be right, caller might even use a JSON array as envelope with a single VP?
envelopeJSON, _ = json.Marshal(presentations[0])
} else {
envelopeJSON, _ = json.Marshal(presentations)
}
var envelope interface{}
if err := json.Unmarshal(envelopeJSON, &envelope); err != nil {
return nil, fmt.Errorf("unable to convert presentations to an interface: %w", err)
}

result := make(map[string]vc.VerifiableCredential)
for _, inputDescriptor := range s.DescriptorMap {
resolvedCredential, err := resolveCredential(nil, inputDescriptor, envelope)
if err != nil {
return nil, fmt.Errorf("unable to resolve credential for input descriptor '%s': %w", inputDescriptor.Id, err)
}
result[inputDescriptor.Id] = *resolvedCredential
}
return result, nil
}

func resolveCredential(path []string, mapping InputDescriptorMappingObject, value interface{}) (*vc.VerifiableCredential, error) {
fullPath := append(path, mapping.Path)
fullPathString := strings.Join(fullPath, "/")

targetValueRaw, err := jsonpath.Get(mapping.Path, value)
if err != nil {
return nil, fmt.Errorf("unable to get value for path %s: %w", fullPathString, err)
}

var decodedTargetValue interface{}
switch targetValue := targetValueRaw.(type) {
case string:
// must be JWT VC or VP
if mapping.Format == vc.JWTCredentialProofFormat {
decodedTargetValue, err = vc.ParseVerifiableCredential(targetValue)
if err != nil {
return nil, fmt.Errorf("invalid JWT credential at path '%s': %w", fullPathString, err)
}
} else if mapping.Format == vc.JWTPresentationProofFormat {
decodedTargetValue, err = vc.ParseVerifiablePresentation(targetValue)
if err != nil {
return nil, fmt.Errorf("invalid JWT presentation at path '%s': %w", fullPathString, err)
}
}
case map[string]interface{}:
// must be JSON-LD
targetValueAsJSON, _ := json.Marshal(targetValue)
if mapping.Format == vc.JSONLDCredentialProofFormat {
decodedTargetValue, err = vc.ParseVerifiableCredential(string(targetValueAsJSON))
if err != nil {
return nil, fmt.Errorf("invalid JSON-LD credential at path '%s': %w", fullPathString, err)
}
} else if mapping.Format == vc.JSONLDPresentationProofFormat {
decodedTargetValue, err = vc.ParseVerifiablePresentation(string(targetValueAsJSON))
if err != nil {
return nil, fmt.Errorf("invalid JSON-LD presentation at path '%s': %w", fullPathString, err)
}
}
}
if decodedTargetValue == nil {
return nil, fmt.Errorf("value of Go type '%T' at path '%s' can't be decoded using format '%s'", targetValueRaw, fullPathString, mapping.Format)
}
if mapping.PathNested == nil {
if decodedCredential, ok := decodedTargetValue.(*vc.VerifiableCredential); ok {
return decodedCredential, nil
}
return nil, fmt.Errorf("path '%s' does not reference a credential", fullPathString)
}
// path_nested implies the credential is not found at the evaluated JSON path, but further down.
// We need to decode the value at the path (could be a credential or presentation in JWT or VP format) and evaluate the nested path.
decodedValueJSON, _ := json.Marshal(decodedTargetValue)
var decodedValueMap map[string]interface{}
_ = json.Unmarshal(decodedValueJSON, &decodedValueMap)
return resolveCredential(fullPath, *mapping.PathNested, decodedValueMap)
}
Loading

0 comments on commit 8c7654f

Please sign in to comment.