diff --git a/go.mod b/go.mod index 0dadd30..55d5b5d 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/multiformats/go-multibase v0.1.1 github.com/piprate/json-gold v0.5.1-0.20230111113000-6ddbe6e6f19f + github.com/samber/lo v1.47.0 github.com/stretchr/testify v1.8.2 github.com/tidwall/gjson v1.14.3 github.com/tidwall/sjson v1.1.4 @@ -61,6 +62,7 @@ require ( github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect golang.org/x/crypto v0.17.0 // indirect golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.16.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect rsc.io/tmplfunc v0.0.3 // indirect diff --git a/go.sum b/go.sum index 78109cc..205305f 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,8 @@ github.com/pquerna/cachecontrol v0.1.0 h1:yJMy84ti9h/+OEWa752kBTKv4XC30OtVVHYv/8 github.com/pquerna/cachecontrol v0.1.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -168,6 +170,8 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= diff --git a/presexch/definition.go b/presexch/definition.go index 23a924c..089afe1 100644 --- a/presexch/definition.go +++ b/presexch/definition.go @@ -18,6 +18,7 @@ import ( "github.com/google/uuid" jsonpathkeys "github.com/kawamuray/jsonpath" "github.com/piprate/json-gold/ld" + "github.com/samber/lo" "github.com/tidwall/gjson" "github.com/tidwall/sjson" "github.com/xeipuuv/gojsonschema" @@ -559,13 +560,27 @@ func presentationData( } func presentation(credentials ...*verifiable.Credential) (*verifiable.Presentation, error) { - vp, e := verifiable.NewPresentation(verifiable.WithCredentials(credentials...)) + baseContext, err := getBaseContext(credentials) + if err != nil { + return nil, fmt.Errorf("get base context: %w", err) + } + + vp, e := verifiable.NewPresentation( + verifiable.WithCredentials(credentials...), + verifiable.WithBaseContext(baseContext), + ) if e != nil { return nil, e } - vp.Context = append(vp.Context, PresentationSubmissionJSONLDContextIRI) - vp.Type = append(vp.Type, PresentationSubmissionJSONLDType) + if !lo.Contains(vp.Context, PresentationSubmissionJSONLDContextIRI) { + vp.Context = append(vp.Context, PresentationSubmissionJSONLDContextIRI) + } + + if !lo.Contains(vp.Type, PresentationSubmissionJSONLDType) { + vp.Type = append(vp.Type, PresentationSubmissionJSONLDType) + } + vp.ID = uuid.NewString() return vp, nil @@ -1792,3 +1807,26 @@ func getContext(contextURI string, documentLoader ld.DocumentLoader) (*ld.Contex return activeCtx, nil } + +func getBaseContext(credentials []*verifiable.Credential) (string, error) { + var baseContext string + + for _, cred := range credentials { + baseCtx, err := verifiable.GetBaseContext(cred.Contents().Context) + if err != nil { + return "", err + } + + if baseContext == "" { + baseContext = baseCtx + } else if baseContext != baseCtx { + return "", errors.New("credentials have different base contexts") + } + } + + if baseContext == "" { + return "", errors.New("base context is required") + } + + return baseContext, nil +} diff --git a/presexch/definition_test.go b/presexch/definition_test.go index f860fa5..2822ac6 100644 --- a/presexch/definition_test.go +++ b/presexch/definition_test.go @@ -2256,6 +2256,77 @@ func TestPresentationDefinition_CreateVP_V1Credential(t *testing.T) { checkVP(t, vp) }) + + t.Run("request two VCs that have different base contexts -> error", func(t *testing.T) { + requirements := []*SubmissionRequirement{ + { + Rule: All, + From: "A", + }, + { + Rule: All, + From: "B", + }, + } + + makeInputDescriptor := func(claim string, groups ...string) *InputDescriptor { + return &InputDescriptor{ + ID: "get_" + claim, + Group: groups, + Schema: []*Schema{{ + URI: fmt.Sprintf("%s#%s", verifiable.V1ContextID, verifiable.VCType), + }}, + Constraints: &Constraints{ + Fields: []*Field{{ + Path: []string{"$." + claim}, + }}, + }, + } + } + + makeCredential := func(baseContext string, claims ...string) *verifiable.Credential { + selfIssuedID := uuid.NewString() + + customFields := map[string]interface{}{} + + for _, claim := range claims { + customFields[claim] = "foo" + } + + vc := createTestCredential(t, credentialProto{ + Context: []string{baseContext}, + Types: []string{verifiable.VCType}, + ID: "https://example.com/credential/" + uuid.NewString(), + Subject: []verifiable.Subject{{ID: selfIssuedID}}, + Issued: &utiltime.TimeWrapper{ + Time: time.Now(), + }, + Issuer: &verifiable.Issuer{ + ID: selfIssuedID, + }, + CustomFields: customFields, + }) + + return vc + } + + pd := &PresentationDefinition{ + ID: uuid.NewString(), + SubmissionRequirements: requirements, + InputDescriptors: []*InputDescriptor{ + makeInputDescriptor("A", "A"), + makeInputDescriptor("B", "B"), + }, + } + + credentials := []*verifiable.Credential{ + makeCredential(verifiable.V1ContextURI, "A"), + makeCredential(verifiable.V2ContextURI, "B"), + } + + _, err := pd.CreateVP(credentials, lddl) + require.ErrorContains(t, err, "credentials have different base contexts") + }) } func TestPresentationDefinition_CreateVPArray(t *testing.T) { diff --git a/verifiable/presentation.go b/verifiable/presentation.go index 45758a7..3f481df 100644 --- a/verifiable/presentation.go +++ b/verifiable/presentation.go @@ -362,6 +362,15 @@ func WithCredentials(cs ...*Credential) CreatePresentationOpt { } } +// WithBaseContext sets the base context of the presentation. +func WithBaseContext(ctx string) CreatePresentationOpt { + return func(p *Presentation) error { + p.Context = []string{ctx} + + return nil + } +} + // MarshalJSON converts Verifiable Presentation to JSON bytes. func (vp *Presentation) MarshalJSON() ([]byte, error) { if vp.JWT != "" { diff --git a/verifiable/testdata/v2_valid_credential.jsonld b/verifiable/testdata/v2_valid_credential.jsonld index f8d4e0e..961ee42 100644 --- a/verifiable/testdata/v2_valid_credential.jsonld +++ b/verifiable/testdata/v2_valid_credential.jsonld @@ -23,5 +23,11 @@ "lang": "fr" }] } + }, + "termsOfUse": { + "type": "TrustFrameworkPolicy", + "trustFramework": "Employment&Life", + "policyId": "https://policy.example/policies/125", + "legalBasis": "professional qualifications directive" } } \ No newline at end of file