Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Commit

Permalink
Presentation Exchange: support for json schema filtering and relation…
Browse files Browse the repository at this point in the history
…al constraints (#351)

* constraint filter

* subject is issuer constraint

* fix 165 too

* fix annoying pathing issues

* update tests
  • Loading branch information
decentralgabe authored Apr 13, 2023
1 parent 82a731c commit dd225d5
Show file tree
Hide file tree
Showing 18 changed files with 472 additions and 172 deletions.
3 changes: 1 addition & 2 deletions credential/exchange/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"github.com/stretchr/testify/assert"

"github.com/TBD54566975/ssi-sdk/crypto"
"github.com/TBD54566975/ssi-sdk/util"
)

func TestPresentationDefinitionBuilder(t *testing.T) {
Expand Down Expand Up @@ -425,5 +424,5 @@ func TestPE(t *testing.T) {
assert.NoError(t, err)
definition, err := builder.Build()
assert.NoError(t, err)
println(util.PrettyJSON(definition))
assert.NotEmpty(t, definition)
}
30 changes: 20 additions & 10 deletions credential/exchange/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package exchange
import (
"reflect"

"github.com/goccy/go-json"
"github.com/pkg/errors"

"github.com/TBD54566975/ssi-sdk/crypto"
Expand Down Expand Up @@ -260,29 +261,30 @@ type Constraints struct {
LimitDisclosure *Preference `json:"limit_disclosure,omitempty"`

// https://identity.foundation/presentation-exchange/#relational-constraint-feature
SubjectIsIssuer *Preference `json:"subject_is_issuer,omitempty"`
IsHolder *RelationalConstraint `json:"is_holder,omitempty" validate:"omitempty,dive"`
SameSubject *RelationalConstraint `json:"same_subject,omitempty"`
SubjectIsIssuer *Preference `json:"subject_is_issuer,omitempty"`
IsHolder []RelationalConstraint `json:"is_holder,omitempty" validate:"omitempty,dive"`
SameSubject []RelationalConstraint `json:"same_subject,omitempty"`

// https://identity.foundation/presentation-exchange/#credential-status-constraint-feature
Statuses *CredentialStatus `json:"statuses,omitempty"`
}

type Field struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Path []string `json:"path,omitempty" validate:"required"`
Purpose string `json:"purpose,omitempty"`
Optional bool `json:"optional,omitempty"`
IntentToRetain bool `json:"intent_to_retain,omitempty"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Path []string `json:"path,omitempty" validate:"required"`
Purpose string `json:"purpose,omitempty"`
Optional bool `json:"optional,omitempty"`
// https://identity.foundation/presentation-exchange/spec/v2.0.0/#retention-feature
IntentToRetain bool `json:"intent_to_retain,omitempty"`
// If a predicate property is present, filter must be too
// https://identity.foundation/presentation-exchange/#predicate-feature
Predicate *Preference `json:"predicate,omitempty"`
Filter *Filter `json:"filter,omitempty"`
}

type RelationalConstraint struct {
FieldID string `json:"field_id" validate:"required"`
FieldID []string `json:"field_id" validate:"required"`
Directive *Preference `json:"directive" validate:"required"`
}

Expand All @@ -306,6 +308,14 @@ type Filter struct {
OneOf any `json:"oneOf,omitempty"`
}

func (f Filter) ToJSON() (string, error) {
jsonBytes, err := json.Marshal(f)
if err != nil {
return "", err
}
return string(jsonBytes), nil
}

// CredentialStatus https://identity.foundation/presentation-exchange/#credential-status-constraint-feature
type CredentialStatus struct {
Active *struct {
Expand Down
12 changes: 6 additions & 6 deletions credential/exchange/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const (

// Presentation Request Option types

TargetOption PresentationRequestOptionType = "target"
AudienceOption PresentationRequestOptionType = "audience"
)

type PresentationRequestOptionType string
Expand All @@ -46,16 +46,16 @@ func BuildPresentationRequest(signer any, pt PresentationRequestType, def Presen
if len(opts) > 1 {
return nil, fmt.Errorf("only one option supported")
}
var target string
var audience string
if len(opts) == 1 {
opt := opts[0]
if opt.Type != TargetOption {
if opt.Type != AudienceOption {
return nil, fmt.Errorf("unsupported option type: %s", opt.Type)
}
var ok bool
target, ok = opt.Value.(string)
audience, ok = opt.Value.(string)
if !ok {
return nil, fmt.Errorf("target option value must be a string")
return nil, fmt.Errorf("audience option value must be a string")
}
}

Expand All @@ -68,7 +68,7 @@ func BuildPresentationRequest(signer any, pt PresentationRequestType, def Presen
if !ok {
return nil, errors.New("signer is not a JWTSigner")
}
return BuildJWTPresentationRequest(jwtSigner, def, target)
return BuildJWTPresentationRequest(jwtSigner, def, audience)
default:
return nil, fmt.Errorf("presentation request type <%s> is not implemented", pt)
}
Expand Down
2 changes: 1 addition & 1 deletion credential/exchange/request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func TestBuildPresentationRequest(t *testing.T) {

testDef := getDummyPresentationDefinition()
_, err = BuildPresentationRequest(*signer, "bad", testDef, PresentationRequestOption{
Type: TargetOption,
Type: AudienceOption,
Value: "did:test:abcd",
})
assert.Error(t, err)
Expand Down
29 changes: 18 additions & 11 deletions credential/exchange/submission.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/TBD54566975/ssi-sdk/util"
"github.com/goccy/go-json"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/v2/jwt"
"github.com/oliveagle/jsonpath"
"github.com/pkg/errors"
)
Expand Down Expand Up @@ -41,31 +42,36 @@ type PresentationClaim struct {
LDPFormat *LinkedDataFormat

// If we have a token, we assume we have a JWT format value
TokenJSON *string
JWTFormat *JWTFormat
TokenBytes []byte
JWTFormat *JWTFormat

// The algorithm or Linked Data proof type by which the claim was signed must be present
SignatureAlgorithmOrProofType string
}

func (pc *PresentationClaim) IsEmpty() bool {
if pc == nil || (pc.Credential == nil && pc.Presentation == nil && pc.TokenJSON == nil) {
if pc == nil || (pc.Credential == nil && pc.Presentation == nil && len(pc.TokenBytes) == 0) {
return true
}
return reflect.DeepEqual(pc, &PresentationClaim{})
}

// GetClaimValue returns the value of the claim, since PresentationClaim is a union type. An error is returned if
// no value is present in any of the possible embedded types.
// GetClaimValue returns the value of the claim as JSON. Since PresentationClaim is a union type. An error
// is returned if no value is present in any of the possible embedded types.
func (pc *PresentationClaim) GetClaimValue() (any, error) {
if pc.Credential != nil {
return *pc.Credential, nil
}
if pc.Presentation != nil {
return *pc.Presentation, nil
}
if pc.TokenJSON != nil {
return *pc.TokenJSON, nil
if pc.TokenBytes != nil {
switch pc.JWTFormat.String() {
case JWT.String(), JWTVC.String(), JWTVP.String():
return jwt.Parse(pc.TokenBytes, jwt.WithValidate(false), jwt.WithVerify(false))
default:
return nil, fmt.Errorf("unsupported JWT format: %s", pc.JWTFormat)
}
}
return nil, errors.New("claim is empty")
}
Expand All @@ -86,7 +92,7 @@ func (pc *PresentationClaim) GetClaimFormat() (string, error) {
}
return string(*pc.LDPFormat), nil
}
if pc.TokenJSON != nil {
if pc.TokenBytes != nil {
if pc.JWTFormat == nil {
return "", errors.New("JWT claim has no JWT format set")
}
Expand Down Expand Up @@ -472,8 +478,8 @@ func canProcessDefinition(def PresentationDefinition) error {
}
if len(id.Constraints.Fields) > 0 {
for _, field := range id.Constraints.Fields {
if field.Predicate != nil || field.Filter != nil {
return errors.New("predicate and filter features not supported")
if field.Predicate != nil {
return errors.New("predicate feature not supported")
}
}
}
Expand All @@ -496,11 +502,12 @@ func canProcessDefinition(def PresentationDefinition) error {
}

// hasRelationalConstraint checks a constraint property for relational constraint field values
// except for subject is issuer, which is supported
func hasRelationalConstraint(constraints *Constraints) bool {
if constraints == nil {
return false
}
return constraints.IsHolder != nil || constraints.SameSubject != nil || constraints.SubjectIsIssuer != nil
return constraints.IsHolder != nil || constraints.SameSubject != nil
}

func IsSupportedEmbedTarget(et EmbedTarget) bool {
Expand Down
79 changes: 44 additions & 35 deletions credential/exchange/submission_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package exchange

import (
"strings"
"testing"

"github.com/goccy/go-json"
Expand Down Expand Up @@ -233,9 +232,13 @@ func TestBuildPresentationSubmissionVP(t *testing.T) {
Constraints: &Constraints{
Fields: []Field{
{
Path: []string{"$.vc.credentialSubject.color"},
ID: "color-input-descriptor",
Purpose: "need to check the color",
Path: []string{"$.vc.credentialSubject.name"},
ID: "name-input-descriptor",
Purpose: "need to check the name contains Jim",
Filter: &Filter{
Type: "string",
Pattern: "Jim*",
},
},
},
},
Expand All @@ -252,7 +255,7 @@ func TestBuildPresentationSubmissionVP(t *testing.T) {
}
testVCJWT := getTestJWTVerifiableCredential()
presentationClaimJWT := PresentationClaim{
TokenJSON: &testVCJWT,
TokenBytes: testVCJWT,
JWTFormat: JWTVC.Ptr(),
SignatureAlgorithmOrProofType: string(crypto.EdDSA),
}
Expand Down Expand Up @@ -293,7 +296,7 @@ func TestBuildPresentationSubmissionVP(t *testing.T) {
assert.NotEmpty(tt, asVCJWT)

assert.Equal(tt, "did:example:456", asVCJWT["sub"])
assert.Equal(tt, "yellow", asVCJWT["vc"].(map[string]any)["credentialSubject"].(map[string]any)["color"])
assert.Equal(tt, "JimBobertson", asVCJWT["vc"].(map[string]any)["credentialSubject"].(map[string]any)["name"])
})
}

Expand Down Expand Up @@ -487,7 +490,7 @@ func TestCanProcessDefinition(tt *testing.T) {
}
err := canProcessDefinition(def)
assert.Error(tt, err)
assert.Contains(tt, err.Error(), "predicate and filter features not supported")
assert.Contains(tt, err.Error(), "predicate feature not supported")
})

tt.Run("With Relational Constraint", func(tt *testing.T) {
Expand All @@ -497,9 +500,11 @@ func TestCanProcessDefinition(tt *testing.T) {
{
ID: "id-with-relational-constraint",
Constraints: &Constraints{
IsHolder: &RelationalConstraint{
FieldID: "field-id",
Directive: Allowed.Ptr(),
IsHolder: []RelationalConstraint{
{
FieldID: []string{"field-id"},
Directive: Allowed.Ptr(),
},
},
},
},
Expand Down Expand Up @@ -688,7 +693,7 @@ func TestNormalizePresentationClaims(t *testing.T) {
assert.NotEmpty(tt, jwtVC)

presentationClaim := PresentationClaim{
TokenJSON: &jwtVC,
TokenBytes: jwtVC,
JWTFormat: JWTVC.Ptr(),
SignatureAlgorithmOrProofType: string(crypto.EdDSA),
}
Expand Down Expand Up @@ -741,30 +746,34 @@ func TestNormalizePresentationClaims(t *testing.T) {
})
}

func getTestJWTVerifiableCredential() string {
literalToken := `{
"exp": 1925061804,
"iss": "did:example:123",
"nbf": 1609529004,
"sub": "did:example:456",
"vc": {
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://w3id.org/security/suites/jws-2020/v1"
],
"credentialSubject": {
"id": "did:example:456",
"color": "yellow"
},
"expirationDate": "2031-01-01T19:23:24Z",
"issuanceDate": "2021-01-01T19:23:24Z",
"issuer": "did:example:123",
"type": ["VerifiableCredential"]
}
}`
noNewLines := strings.ReplaceAll(literalToken, "\n", "")
noTabs := strings.ReplaceAll(noNewLines, "\t", "")
return strings.ReplaceAll(noTabs, " ", "")
func getTestJWTVerifiableCredential() []byte {
// {
// "alg": "EdDSA",
// "typ": "JWT"
// }
// {
// "iat": 1609529004,
// "iss": "did:example:123",
// "jti": "http://example.edu/credentials/1872",
// "nbf": 1609529004,
// "nonce": "24976372-adc4-4808-90c2-d86ea805e11b",
// "sub": "did:example:456",
// "vc": {
// "@context": [
// "https://www.w3.org/2018/credentials/v1",
// "https://w3id.org/security/suites/jws-2020/v1"
// ],
// "type": [
// "VerifiableCredential"
// ],
// "issuer": "",
// "issuanceDate": "",
// "credentialSubject": {
// "name": "JimBobertson"
// }
// }
// }
return []byte("eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2MDk1MjkwMDQsImlzcyI6ImRpZDpleGFtcGxlOjEyMyIsImp0aSI6Imh0dHA6Ly9leGFtcGxlLmVkdS9jcmVkZW50aWFscy8xODcyIiwibmJmIjoxNjA5NTI5MDA0LCJub25jZSI6IjI0OTc2MzcyLWFkYzQtNDgwOC05MGMyLWQ4NmVhODA1ZTExYiIsInN1YiI6ImRpZDpleGFtcGxlOjQ1NiIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly93M2lkLm9yZy9zZWN1cml0eS9zdWl0ZXMvandzLTIwMjAvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCJdLCJpc3N1ZXIiOiIiLCJpc3N1YW5jZURhdGUiOiIiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJuYW1lIjoiSmltQm9iZXJ0c29uIn19fQ.STf2oFVPTwEyEhCpU_u9Qy52VzAwHlWtxq2NrlXhzvh0aJIbr5astEagEY2PRZ_S6Og-7Q4sYTT7sq6HJSjLBA")
}

func getGenericTestClaim() map[string]any {
Expand Down
Loading

0 comments on commit dd225d5

Please sign in to comment.