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: Resolve credentials from presentation submission #2612

Merged
merged 4 commits into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
36 changes: 36 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,35 @@ 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)
} else {
reinkrul marked this conversation as resolved.
Show resolved Hide resolved
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
83 changes: 83 additions & 0 deletions vcr/pe/presentation_submission.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ 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"
Expand Down Expand Up @@ -169,3 +170,85 @@ 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(inputDescriptor.Id, 0, 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(descriptorID string, level int, mapping InputDescriptorMappingObject, value interface{}) (*vc.VerifiableCredential, error) {
reinkrul marked this conversation as resolved.
Show resolved Hide resolved
reinkrul marked this conversation as resolved.
Show resolved Hide resolved
targetValueRaw, err := jsonpath.Get(mapping.Path, value)
if err != nil {
return nil, fmt.Errorf("unable to get value for path %s: %w", mapping.Path, 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", mapping.Path, 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", mapping.Path, 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' (level %d): %w", mapping.Path, level, 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' (level %d): %w", mapping.Path, level, err)
}
}
}
if decodedTargetValue == nil {
return nil, fmt.Errorf("value of Go type '%T' at path '%s' (level %d) can't be decoded using format '%s'", targetValueRaw, mapping.Path, level, mapping.Format)
}
if mapping.PathNested == nil {
if decodedCredential, ok := decodedTargetValue.(*vc.VerifiableCredential); ok {
return decodedCredential, nil
} else {
return nil, fmt.Errorf("path '%s' (level %d) does not reference a credential", mapping.Path, level)
}
} else {
reinkrul marked this conversation as resolved.
Show resolved Hide resolved
// 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(descriptorID, level+1, *mapping.PathNested, decodedValueMap)
}
}
Loading
Loading