Skip to content

Commit

Permalink
PEX: Support matching JWT VCs
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul committed Nov 10, 2023
1 parent ec09115 commit 797a72f
Show file tree
Hide file tree
Showing 2 changed files with 306 additions and 122 deletions.
85 changes: 62 additions & 23 deletions vcr/pe/presentation_definition.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/lestrrat-go/jwx/v2/jwa"
"github.com/lestrrat-go/jwx/v2/jws"
"strings"

"github.com/PaesslerAG/jsonpath"
Expand Down Expand Up @@ -49,6 +51,7 @@ type PresentationContext struct {
// It implements §5 of the Presentation Exchange specification (v2.x.x pre-Draft, 2023-07-29) (https://identity.foundation/presentation-exchange/#presentation-definition)
// It supports the following:
// - ldp_vc format
// - jwt_vc format
// - pattern, const and enum only on string fields
// - number, boolean, array and string JSON schema types
// - Submission Requirements Feature
Expand Down Expand Up @@ -221,32 +224,41 @@ func (presentationDefinition PresentationDefinition) groups() []groupCandidates
}

// matchFormat checks if the credential matches the Format from the presentationDefinition.
// if one of format['ldp_vc'] or format['jwt_vc'] is present, the VC must match that format.
// If the VC is of the required format, the alg or proofType must also match.
// vp formats are ignored.
// This might not be fully interoperable, but the spec at https://identity.foundation/presentation-exchange/#presentation-definition is not clear on this.
func matchFormat(format *PresentationDefinitionClaimFormatDesignations, credential vc.VerifiableCredential) bool {
if format == nil {
if format == nil || len(*format) == 0 {
return true
}

asMap := map[string]map[string][]string(*format)
// we're only interested in the jwt_vc and ldp_vc formats
if asMap["jwt_vc"] == nil && asMap["ldp_vc"] == nil {
return true
}

// only ldp_vc supported for now
if entry := asMap["ldp_vc"]; entry != nil {
if proofTypes := entry["proof_type"]; proofTypes != nil {
for _, proofType := range proofTypes {
if matchProofType(proofType, credential) {
return true
switch credential.Format() {
case vc.JSONLDCredentialProofFormat:
if entry := asMap["ldp_vc"]; entry != nil {
if proofTypes := entry["proof_type"]; proofTypes != nil {
for _, proofType := range proofTypes {
if matchProofType(proofType, credential) {
return true
}
}
}
}
case vc.JWTCredentialProofFormat:
// Get signing algorithm used to sign the JWT
message, _ := jws.ParseString(credential.Raw()) // can't really fail, JWT has been parsed before.
signingAlgorithm, _ := message.Signatures()[0].ProtectedHeaders().Get("alg")
// Check that the signing algorithm is specified by the presentation definition
if entry := asMap["jwt_vc"]; entry != nil {
if supportedAlgorithms := entry["alg"]; supportedAlgorithms != nil {
for _, supportedAlgorithm := range supportedAlgorithms {
if signingAlgorithm == jwa.SignatureAlgorithm(supportedAlgorithm) {
return true
}
}
}
}
}

return false
}

Expand Down Expand Up @@ -289,10 +301,26 @@ func matchCredential(descriptor InputDescriptor, credential vc.VerifiableCredent
// IsHolder, SameSubject, SubjectIsIssuer, Statuses are not supported for now.
// LimitDisclosure is not supported for now.
func matchConstraint(constraint *Constraints, credential vc.VerifiableCredential) (bool, error) {
// jsonpath works on interfaces, so convert the VC to an interface
var credentialAsMap map[string]interface{}
var err error
switch credential.Format() {
case vc.JWTCredentialProofFormat:
// JWT-VCs marshal to a JSON string, so marshal an alias to make sure we get a JSON object with the VC properties,
// instead of a JWT string.
type Alias vc.VerifiableCredential
credentialAsMap, err = remarshalToMap(Alias(credential))
case vc.JSONLDCredentialProofFormat:
credentialAsMap, err = remarshalToMap(credential)
}
if err != nil {
return false, err
}

// for each field in constraint.fields:
// a vc must match the field
for _, field := range constraint.Fields {
match, err := matchField(field, credential)
match, err := matchField(field, credentialAsMap)
if err != nil {
return false, err
}
Expand All @@ -305,18 +333,13 @@ func matchConstraint(constraint *Constraints, credential vc.VerifiableCredential

// matchField matches the field against the VC.
// All fields need to match unless optional is set to true and no values are found for all the paths.
func matchField(field Field, credential vc.VerifiableCredential) (bool, error) {
// jsonpath works on interfaces, so convert the VC to an interface
asJSON, _ := json.Marshal(credential)
var asInterface interface{}
_ = json.Unmarshal(asJSON, &asInterface)

func matchField(field Field, credential map[string]interface{}) (bool, error) {
// for each path in field.paths:
// a vc must match one of the path
var optionalInvalid int
for _, path := range field.Path {
// if path is not found continue
value, err := getValueAtPath(path, asInterface)
value, err := getValueAtPath(path, credential)
if err != nil {
return false, err
}
Expand Down Expand Up @@ -351,7 +374,10 @@ func matchField(field Field, credential vc.VerifiableCredential) (bool, error) {
func getValueAtPath(path string, vcAsInterface interface{}) (interface{}, error) {
value, err := jsonpath.Get(path, vcAsInterface)
// jsonpath.Get returns some errors if the path is not found, or it has a different type as expected
if err != nil && (strings.HasPrefix(err.Error(), "unknown key") || strings.HasPrefix(err.Error(), "unsupported value type")) {
if err != nil && (strings.HasPrefix(err.Error(), "unknown key") ||
strings.HasPrefix(err.Error(), "unsupported value type") ||
// Then a JSON path points to an array, but the expression doesn't specify an index
strings.HasPrefix(err.Error(), "could not select value, invalid key: expected number but got")) {
return nil, nil
}
return value, err
Expand Down Expand Up @@ -457,3 +483,16 @@ func vcEqual(a, b vc.VerifiableCredential) bool {
bJSON, _ := json.Marshal(b)
return string(aJSON) == string(bJSON)
}

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

0 comments on commit 797a72f

Please sign in to comment.