From 66d9bf30de2f5cd6116adaac27f277b45077f26f Mon Sep 17 00:00:00 2001 From: Volodymyr Kubiv <62515092+vkubiv@users.noreply.github.com> Date: Fri, 5 Aug 2022 20:57:10 +0300 Subject: [PATCH 1/3] feat: support of presentation exchange v2. (#3312) Signed-off-by: Volodymyr Kubiv --- pkg/doc/presexch/definition.go | 130 +++- pkg/doc/presexch/definition_test.go | 8 +- .../{example_test.go => example_v1_test.go} | 2 +- pkg/doc/presexch/example_v2_test.go | 669 ++++++++++++++++++ pkg/doc/presexch/schema.go | 211 +++++- 5 files changed, 996 insertions(+), 24 deletions(-) rename pkg/doc/presexch/{example_test.go => example_v1_test.go} (99%) create mode 100644 pkg/doc/presexch/example_v2_test.go diff --git a/pkg/doc/presexch/definition.go b/pkg/doc/presexch/definition.go index f6ce69ade2..ea2b5eaed3 100644 --- a/pkg/doc/presexch/definition.go +++ b/pkg/doc/presexch/definition.go @@ -27,6 +27,7 @@ import ( ) const ( + // All rule`s value. All Selection = "all" // Pick rule`s value. @@ -38,6 +39,8 @@ const ( Preferred Preference = "preferred" tmpEnding = "tmp_unique_id_" + + credentialSchema = "credentialSchema" ) var errPathNotApplicable = errors.New("path not applicable") @@ -93,6 +96,8 @@ type PresentationDefinition struct { // Format is an object with one or more properties matching the registered Claim Format Designations // (jwt, jwt_vc, jwt_vp, etc.) to inform the Holder of the claim format configurations the Verifier can process. Format *Format `json:"format,omitempty"` + // Frame is used for JSON-LD document framing. + Frame map[string]interface{} `json:"frame,omitempty"` // SubmissionRequirements must conform to the Submission Requirement Format. // If not present, all inputs listed in the InputDescriptors array are required for submission. SubmissionRequirements []*SubmissionRequirement `json:"submission_requirements,omitempty"` @@ -121,6 +126,7 @@ type InputDescriptor struct { Metadata map[string]interface{} `json:"metadata,omitempty"` Schema []*Schema `json:"schema,omitempty"` Constraints *Constraints `json:"constraints,omitempty"` + Format *Format `json:"format,omitempty"` } // Schema input descriptor schema. @@ -145,11 +151,12 @@ type Constraints struct { // Field describes Constraints`s Fields field. type Field struct { - Path []string `json:"path,omitempty"` - ID string `json:"id,omitempty"` - Purpose string `json:"purpose,omitempty"` - Filter *Filter `json:"filter,omitempty"` - Predicate *Preference `json:"predicate,omitempty"` + Path []string `json:"path,omitempty"` + ID string `json:"id,omitempty"` + Purpose string `json:"purpose,omitempty"` + Filter *Filter `json:"filter,omitempty"` + Predicate *Preference `json:"predicate,omitempty"` + IntentToRetain bool `json:"intent_to_retain,omitempty"` } // Filter describes filter. @@ -171,11 +178,21 @@ type Filter struct { // ValidateSchema validates presentation definition. func (pd *PresentationDefinition) ValidateSchema() error { result, err := gojsonschema.Validate( - gojsonschema.NewStringLoader(DefinitionJSONSchema), + gojsonschema.NewStringLoader(DefinitionJSONSchemaV1), gojsonschema.NewGoLoader(struct { PD *PresentationDefinition `json:"presentation_definition"` }{PD: pd}), ) + + if err != nil || !result.Valid() { + result, err = gojsonschema.Validate( + gojsonschema.NewStringLoader(DefinitionJSONSchemaV2), + gojsonschema.NewGoLoader(struct { + PD *PresentationDefinition `json:"presentation_definition"` + }{PD: pd}), + ) + } + if err != nil { return err } @@ -310,7 +327,7 @@ func (pd *PresentationDefinition) CreateVP(credentials []*verifiable.Credential, return nil, err } - result, err := applyRequirement(req, credentials, documentLoader, opts...) + result, err := pd.applyRequirement(req, credentials, documentLoader, opts...) if err != nil { return nil, err } @@ -340,14 +357,33 @@ func (pd *PresentationDefinition) CreateVP(credentials []*verifiable.Credential, var ErrNoCredentials = errors.New("credentials do not satisfy requirements") // nolint: gocyclo,funlen,gocognit -func applyRequirement(req *requirement, creds []*verifiable.Credential, +func (pd *PresentationDefinition) applyRequirement(req *requirement, creds []*verifiable.Credential, documentLoader ld.DocumentLoader, opts ...verifiable.CredentialOpt) (map[string][]*verifiable.Credential, error) { result := make(map[string][]*verifiable.Credential) for _, descriptor := range req.InputDescriptors { - filtered := filterSchema(descriptor.Schema, creds, documentLoader) + format := pd.Format + if descriptor.Format != nil { + format = descriptor.Format + } + + filtered := creds + + filtered, err := frameCreds(pd.Frame, filtered, opts...) + if err != nil { + return nil, err + } + + if format != nil { + filtered = filterFormat(format, filtered) + } + + // Validate schema only for v1 + if descriptor.Schema != nil { + filtered = filterSchema(descriptor.Schema, filtered, documentLoader) + } - filtered, err := filterConstraints(descriptor.Constraints, filtered, opts...) + filtered, err = filterConstraints(descriptor.Constraints, filtered, opts...) if err != nil { return nil, err } @@ -371,7 +407,7 @@ func applyRequirement(req *requirement, creds []*verifiable.Credential, set := map[string]map[string]string{} for _, r := range req.Nested { - res, err := applyRequirement(r, creds, documentLoader, opts...) + res, err := pd.applyRequirement(r, creds, documentLoader, opts...) if errors.Is(err, ErrNoCredentials) { continue } @@ -594,10 +630,30 @@ func filterConstraints(constraints *Constraints, creds []*verifiable.Credential, return result, nil } +func frameCreds(frame map[string]interface{}, creds []*verifiable.Credential, + opts ...verifiable.CredentialOpt) ([]*verifiable.Credential, error) { + if frame == nil { + return creds, nil + } + + var result []*verifiable.Credential + + for _, credential := range creds { + bbsVC, err := credential.GenerateBBSSelectiveDisclosure(frame, nil, opts...) + if err != nil { + return nil, err + } + + result = append(result, bbsVC) + } + + return result, nil +} + func toSubject(subject interface{}) interface{} { sub, ok := subject.([]verifiable.Subject) if ok && len(sub) == 1 { - return sub[0] + return verifiable.Subject{ID: sub[0].ID} } return subject @@ -650,6 +706,10 @@ func createNewCredential(constraints *Constraints, src, limitedCred []byte, } for _, path := range jPaths { + if strings.Contains(path[0], credentialSchema) { + continue + } + var val interface{} = true if !modifiedByPredicate { @@ -772,6 +832,16 @@ func hasBBS(vc *verifiable.Credential) bool { return false } +func hasProofWithType(vc *verifiable.Credential, proofType string) bool { + for _, proof := range vc.Proofs { + if proof["type"] == proofType { + return true + } + } + + return false +} + func filterField(f *Field, credential map[string]interface{}) error { var schema gojsonschema.JSONLoader @@ -779,19 +849,23 @@ func filterField(f *Field, credential map[string]interface{}) error { schema = gojsonschema.NewGoLoader(*f.Filter) } + var lastErr error + for _, path := range f.Path { patch, err := jsonpath.Get(path, credential) - if err != nil { - return errPathNotApplicable - } + if err == nil { + err = validatePatch(schema, patch) + if err == nil { + return nil + } - err = validatePatch(schema, patch) - if err != nil { - return err + lastErr = err + } else { + lastErr = errPathNotApplicable } } - return nil + return lastErr } func validatePatch(schema gojsonschema.JSONLoader, patch interface{}) error { @@ -888,6 +962,24 @@ func (a byID) Len() int { return len(a) } func (a byID) Less(i, j int) bool { return a[i].ID < a[j].ID } func (a byID) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func filterFormat(format *Format, credentials []*verifiable.Credential) []*verifiable.Credential { + var result []*verifiable.Credential + + if format.LdpVP == nil { + return result + } + + for _, credential := range credentials { + for _, proofType := range format.LdpVP.ProofType { + if hasProofWithType(credential, proofType) { + result = append(result, credential) + } + } + } + + return result +} + // nolint: gocyclo func filterSchema(schemas []*Schema, credentials []*verifiable.Credential, documentLoader ld.DocumentLoader) []*verifiable.Credential { diff --git a/pkg/doc/presexch/definition_test.go b/pkg/doc/presexch/definition_test.go index d7653ceb22..d773150939 100644 --- a/pkg/doc/presexch/definition_test.go +++ b/pkg/doc/presexch/definition_test.go @@ -1032,7 +1032,9 @@ func TestPresentationDefinition_CreateVP(t *testing.T) { Constraints: &Constraints{ SubjectIsIssuer: &subIsIssuerRequired, Fields: []*Field{{ - Path: []string{"$.first_name", "$.last_name"}, + Path: []string{"$.first_name"}, + }, { + Path: []string{"$.last_name"}, }}, }, }, { @@ -1042,7 +1044,9 @@ func TestPresentationDefinition_CreateVP(t *testing.T) { }}, Constraints: &Constraints{ Fields: []*Field{{ - Path: []string{"$.first_name", "$.last_name"}, + Path: []string{"$.first_name"}, + }, { + Path: []string{"$.last_name"}, }}, }, }}, diff --git a/pkg/doc/presexch/example_test.go b/pkg/doc/presexch/example_v1_test.go similarity index 99% rename from pkg/doc/presexch/example_test.go rename to pkg/doc/presexch/example_v1_test.go index b4392cd833..cde04f782f 100644 --- a/pkg/doc/presexch/example_test.go +++ b/pkg/doc/presexch/example_v1_test.go @@ -22,7 +22,7 @@ import ( const dummy = "DUMMY" -func ExamplePresentationDefinition_CreateVP() { +func ExamplePresentationDefinition_CreateVP_v1() { required := Required pd := &PresentationDefinition{ diff --git a/pkg/doc/presexch/example_v2_test.go b/pkg/doc/presexch/example_v2_test.go new file mode 100644 index 0000000000..138e417b59 --- /dev/null +++ b/pkg/doc/presexch/example_v2_test.go @@ -0,0 +1,669 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package presexch_test + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/piprate/json-gold/ld" + "github.com/stretchr/testify/require" + + "github.com/hyperledger/aries-framework-go/pkg/crypto/primitive/bbs12381g2pub" + . "github.com/hyperledger/aries-framework-go/pkg/doc/presexch" + "github.com/hyperledger/aries-framework-go/pkg/doc/signature/jsonld" + "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite" + "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/bbsblssignature2020" + "github.com/hyperledger/aries-framework-go/pkg/doc/util" + jsonutil "github.com/hyperledger/aries-framework-go/pkg/doc/util/json" + "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" + "github.com/hyperledger/aries-framework-go/pkg/internal/ldtestutil" +) + +func ExamplePresentationDefinition_CreateVP_v2() { + required := Required + + pd := &PresentationDefinition{ + ID: "c1b88ce1-8460-4baf-8f16-4759a2f055fd", + Purpose: "To sell you a drink we need to know that you are an adult.", + InputDescriptors: []*InputDescriptor{{ + ID: "age_descriptor", + Purpose: "Your age should be greater or equal to 18.", + Constraints: &Constraints{ + LimitDisclosure: &required, + Fields: []*Field{ + { + Path: []string{"$.credentialSubject.age", "$.vc.credentialSubject.age", "$.age"}, + Predicate: &required, + Filter: &Filter{ + Type: &intFilterType, + Minimum: 18, + }, + }, + { + Path: []string{"$.credentialSchema[0].id", "$.credentialSchema.id", "$.vc.credentialSchema.id"}, + Filter: &Filter{ + Type: &strFilterType, + Const: "hub://did:foo:123/Collections/schema.us.gov/passport.json", + }, + }, + }, + }, + }}, + } + + loader, err := ldtestutil.DocumentLoader() + if err != nil { + panic(err) + } + + vp, err := pd.CreateVP([]*verifiable.Credential{ + { + ID: "http://example.edu/credentials/777", + Context: []string{verifiable.ContextURI}, + Types: []string{verifiable.VCType}, + Issuer: verifiable.Issuer{ + ID: "did:example:76e12ec712ebc6f1c221ebfeb1f", + }, + Issued: &util.TimeWrapper{ + Time: time.Time{}, + }, + Schemas: []verifiable.TypedID{{ + ID: "hub://did:foo:123/Collections/schema.us.gov/passport.json", + Type: "JsonSchemaValidator2018", + }}, + + Subject: map[string]interface{}{ + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "first_name": "Jesse", + "last_name": "Pinkman", + "age": 21, + }, + }, + }, loader, verifiable.WithJSONLDDocumentLoader(loader)) + if err != nil { + panic(err) + } + + vp.CustomFields["presentation_submission"].(*PresentationSubmission).ID = dummy + + vpBytes, err := json.MarshalIndent(vp, "", "\t") + if err != nil { + panic(err) + } + + fmt.Println(string(vpBytes)) + // Output: + //{ + // "@context": [ + // "https://www.w3.org/2018/credentials/v1", + // "https://identity.foundation/presentation-exchange/submission/v1" + // ], + // "presentation_submission": { + // "id": "DUMMY", + // "definition_id": "c1b88ce1-8460-4baf-8f16-4759a2f055fd", + // "descriptor_map": [ + // { + // "id": "age_descriptor", + // "format": "ldp_vp", + // "path": "$.verifiableCredential[0]" + // } + // ] + // }, + // "type": [ + // "VerifiablePresentation", + // "PresentationSubmission" + // ], + // "verifiableCredential": [ + // { + // "@context": [ + // "https://www.w3.org/2018/credentials/v1" + // ], + // "credentialSubject": { + // "age": true, + // "first_name": "Jesse", + // "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + // "last_name": "Pinkman" + // }, + // "id": "http://example.edu/credentials/777", + // "issuanceDate": "0001-01-01T00:00:00Z", + // "issuer": "did:example:76e12ec712ebc6f1c221ebfeb1f", + // "type": "VerifiableCredential" + // } + // ] + //} +} + +func ExamplePresentationDefinition_CreateVP_withFormat() { + required := Required + + pd := &PresentationDefinition{ + ID: "c1b88ce1-8460-4baf-8f16-4759a2f055fd", + Purpose: "To sell you a drink we need to know that you are an adult.", + Format: &Format{ + LdpVP: &LdpType{ + ProofType: []string{"Ed25519Signature2018"}, + }, + }, + InputDescriptors: []*InputDescriptor{{ + ID: "age_descriptor", + Purpose: "Your age should be greater or equal to 18.", + Constraints: &Constraints{ + LimitDisclosure: &required, + Fields: []*Field{ + { + Path: []string{"$.credentialSubject.age", "$.vc.credentialSubject.age", "$.age"}, + Predicate: &required, + Filter: &Filter{ + Type: &intFilterType, + Minimum: 18, + }, + }, + { + Path: []string{"$.credentialSchema[0].id", "$.credentialSchema.id", "$.vc.credentialSchema.id"}, + Filter: &Filter{ + Type: &strFilterType, + Const: "hub://did:foo:123/Collections/schema.us.gov/passport.json", + }, + }, + }, + }, + }}, + } + + loader, err := ldtestutil.DocumentLoader() + if err != nil { + panic(err) + } + + vp, err := pd.CreateVP([]*verifiable.Credential{ + { + ID: "http://example.edu/credentials/777", + Context: []string{verifiable.ContextURI}, + Types: []string{verifiable.VCType}, + Issuer: verifiable.Issuer{ + ID: "did:example:76e12ec712ebc6f1c221ebfeb1f", + }, + Issued: &util.TimeWrapper{ + Time: time.Time{}, + }, + Schemas: []verifiable.TypedID{{ + ID: "hub://did:foo:123/Collections/schema.us.gov/passport.json", + Type: "JsonSchemaValidator2018", + }}, + + Subject: map[string]interface{}{ + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "first_name": "Jesse", + "last_name": "Pinkman", + "age": 21, + }, + Proofs: []verifiable.Proof{ + {"type": "Ed25519Signature2018"}, + }, + }, + }, loader, verifiable.WithJSONLDDocumentLoader(loader)) + if err != nil { + panic(err) + } + + vp.CustomFields["presentation_submission"].(*PresentationSubmission).ID = dummy + + vpBytes, err := json.MarshalIndent(vp, "", "\t") + if err != nil { + panic(err) + } + + fmt.Println(string(vpBytes)) + // Output: + //{ + // "@context": [ + // "https://www.w3.org/2018/credentials/v1", + // "https://identity.foundation/presentation-exchange/submission/v1" + // ], + // "presentation_submission": { + // "id": "DUMMY", + // "definition_id": "c1b88ce1-8460-4baf-8f16-4759a2f055fd", + // "descriptor_map": [ + // { + // "id": "age_descriptor", + // "format": "ldp_vp", + // "path": "$.verifiableCredential[0]" + // } + // ] + // }, + // "type": [ + // "VerifiablePresentation", + // "PresentationSubmission" + // ], + // "verifiableCredential": [ + // { + // "@context": [ + // "https://www.w3.org/2018/credentials/v1" + // ], + // "credentialSubject": { + // "age": true, + // "first_name": "Jesse", + // "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + // "last_name": "Pinkman" + // }, + // "id": "http://example.edu/credentials/777", + // "issuanceDate": "0001-01-01T00:00:00Z", + // "issuer": "did:example:76e12ec712ebc6f1c221ebfeb1f", + // "type": "VerifiableCredential" + // } + // ] + //} +} + +func ExamplePresentationDefinition_CreateVP_withFormatInInputDescriptor() { + required := Required + + pd := &PresentationDefinition{ + ID: "c1b88ce1-8460-4baf-8f16-4759a2f055fd", + Purpose: "To sell you a drink we need to know that you are an adult.", + InputDescriptors: []*InputDescriptor{{ + ID: "age_descriptor", + Purpose: "Your age should be greater or equal to 18.", + Format: &Format{ + LdpVP: &LdpType{ + ProofType: []string{"Ed25519Signature2018"}, + }, + }, + Constraints: &Constraints{ + LimitDisclosure: &required, + Fields: []*Field{ + { + Path: []string{"$.credentialSubject.age", "$.vc.credentialSubject.age", "$.age"}, + Predicate: &required, + Filter: &Filter{ + Type: &intFilterType, + Minimum: 18, + }, + }, + { + Path: []string{"$.credentialSchema[0].id", "$.credentialSchema.id", "$.vc.credentialSchema.id"}, + Filter: &Filter{ + Type: &strFilterType, + Const: "hub://did:foo:123/Collections/schema.us.gov/passport.json", + }, + }, + }, + }, + }}, + } + + loader, err := ldtestutil.DocumentLoader() + if err != nil { + panic(err) + } + + vp, err := pd.CreateVP([]*verifiable.Credential{ + { + ID: "http://example.edu/credentials/777", + Context: []string{verifiable.ContextURI}, + Types: []string{verifiable.VCType}, + Issuer: verifiable.Issuer{ + ID: "did:example:76e12ec712ebc6f1c221ebfeb1f", + }, + Issued: &util.TimeWrapper{ + Time: time.Time{}, + }, + Schemas: []verifiable.TypedID{{ + ID: "hub://did:foo:123/Collections/schema.us.gov/passport.json", + Type: "JsonSchemaValidator2018", + }}, + + Subject: map[string]interface{}{ + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "first_name": "Jesse", + "last_name": "Pinkman", + "age": 21, + }, + Proofs: []verifiable.Proof{ + {"type": "Ed25519Signature2018"}, + }, + }, + }, loader, verifiable.WithJSONLDDocumentLoader(loader)) + if err != nil { + panic(err) + } + + vp.CustomFields["presentation_submission"].(*PresentationSubmission).ID = dummy + + vpBytes, err := json.MarshalIndent(vp, "", "\t") + if err != nil { + panic(err) + } + + fmt.Println(string(vpBytes)) + // Output: + //{ + // "@context": [ + // "https://www.w3.org/2018/credentials/v1", + // "https://identity.foundation/presentation-exchange/submission/v1" + // ], + // "presentation_submission": { + // "id": "DUMMY", + // "definition_id": "c1b88ce1-8460-4baf-8f16-4759a2f055fd", + // "descriptor_map": [ + // { + // "id": "age_descriptor", + // "format": "ldp_vp", + // "path": "$.verifiableCredential[0]" + // } + // ] + // }, + // "type": [ + // "VerifiablePresentation", + // "PresentationSubmission" + // ], + // "verifiableCredential": [ + // { + // "@context": [ + // "https://www.w3.org/2018/credentials/v1" + // ], + // "credentialSubject": { + // "age": true, + // "first_name": "Jesse", + // "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + // "last_name": "Pinkman" + // }, + // "id": "http://example.edu/credentials/777", + // "issuanceDate": "0001-01-01T00:00:00Z", + // "issuer": "did:example:76e12ec712ebc6f1c221ebfeb1f", + // "type": "VerifiableCredential" + // } + // ] + //} +} + +func TestExamplePresentationDefinition_CreateVPWithFormat_NoMatch(t *testing.T) { + required := Required + + pd := &PresentationDefinition{ + ID: "c1b88ce1-8460-4baf-8f16-4759a2f055fd", + Purpose: "To sell you a drink we need to know that you are an adult.", + Format: &Format{ + LdpVP: &LdpType{ + ProofType: []string{"Ed25519Signature2018"}, + }, + }, + InputDescriptors: []*InputDescriptor{{ + ID: "age_descriptor", + Purpose: "Your age should be greater or equal to 18.", + Constraints: &Constraints{ + LimitDisclosure: &required, + Fields: []*Field{ + { + Path: []string{"$.credentialSubject.age", "$.vc.credentialSubject.age", "$.age"}, + Predicate: &required, + Filter: &Filter{ + Type: &intFilterType, + Minimum: 18, + }, + }, + { + Path: []string{"$.credentialSchema[0].id", "$.credentialSchema.id", "$.vc.credentialSchema.id"}, + Filter: &Filter{ + Type: &strFilterType, + Const: "hub://did:foo:123/Collections/schema.us.gov/passport.json", + }, + }, + }, + }, + }}, + } + + loader, err := ldtestutil.DocumentLoader() + if err != nil { + panic(err) + } + + _, err = pd.CreateVP([]*verifiable.Credential{ + { + ID: "http://example.edu/credentials/777", + Context: []string{verifiable.ContextURI}, + Types: []string{verifiable.VCType}, + Issuer: verifiable.Issuer{ + ID: "did:example:76e12ec712ebc6f1c221ebfeb1f", + }, + Issued: &util.TimeWrapper{ + Time: time.Time{}, + }, + Schemas: []verifiable.TypedID{{ + ID: "hub://did:foo:123/Collections/schema.us.gov/passport.json", + Type: "JsonSchemaValidator2018", + }}, + + Subject: map[string]interface{}{ + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "first_name": "Jesse", + "last_name": "Pinkman", + "age": 21, + }, + Proofs: []verifiable.Proof{ + {"type": "JsonWebSignature2020"}, + }, + }, + }, loader, verifiable.WithJSONLDDocumentLoader(loader)) + + require.EqualError(t, err, "credentials do not satisfy requirements") +} + +func ExamplePresentationDefinition_CreateVP_withFrame() { + vcJSON := ` + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/citizenship/v1", + "https://w3id.org/security/bbs/v1" + ], + "id": "https://issuer.oidp.uscis.gov/credentials/83627465", + "type": [ + "VerifiableCredential", + "PermanentResidentCard" + ], + "issuer": "did:example:489398593", + "identifier": "83627465", + "name": "Permanent Resident Card", + "description": "Government of Example Permanent Resident Card.", + "issuanceDate": "2019-12-03T12:19:52Z", + "expirationDate": "2029-12-03T12:19:52Z", + "credentialSubject": { + "id": "did:example:b34ca6cd37bbf23", + "type": [ + "PermanentResident", + "Person" + ], + "givenName": "JOHN", + "familyName": "SMITH", + "gender": "Male", + "image": "data:image/png;base64,iVBORw0KGgokJggg==", + "residentSince": "2015-01-01", + "lprCategory": "C09", + "lprNumber": "999-999-999", + "commuterClassification": "C1", + "birthCountry": "Bahamas", + "birthDate": "1958-07-17" + } + } + ` + + frameJSONWithMissingIssuer := ` +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/citizenship/v1", + "https://w3id.org/security/bbs/v1" + ], + "type": ["VerifiableCredential", "PermanentResidentCard"], + "@explicit": true, + "identifier": {}, + "issuer": {}, + "issuanceDate": {}, + "credentialSubject": { + "@explicit": true, + "type": ["PermanentResident", "Person"], + "givenName": {}, + "familyName": {}, + "gender": {}, + "birthCountry": {} + } +} +` + + frameDoc, err := jsonutil.ToMap(frameJSONWithMissingIssuer) + if err != nil { + panic(err) + } + + required := Required + + pd := &PresentationDefinition{ + ID: "c1b88ce1-8460-4baf-8f16-4759a2f055fd", + Frame: frameDoc, + InputDescriptors: []*InputDescriptor{{ + ID: "country_descriptor", + Constraints: &Constraints{ + Fields: []*Field{ + { + Path: []string{"$.credentialSubject.birthCountry", "$.vc.credentialSubject.birthCountry"}, + Predicate: &required, + Filter: &Filter{ + Type: &strFilterType, + Const: "Bahamas", + }, + }, + }, + }, + }}, + } + + loader, err := ldtestutil.DocumentLoader() + if err != nil { + panic(err) + } + + vc, err := verifiable.ParseCredential([]byte(vcJSON), verifiable.WithJSONLDDocumentLoader(loader)) + if err != nil { + panic(err) + } + + pubKey, privKey, err := bbs12381g2pub.GenerateKeyPair(sha256.New, nil) + if err != nil { + panic(err) + } + + pubKeyBytes, err := pubKey.Marshal() + if err != nil { + panic(err) + } + + signVCWithBBS(privKey, vc, loader) + + vp, err := pd.CreateVP([]*verifiable.Credential{vc}, loader, verifiable.WithJSONLDDocumentLoader(loader), verifiable.WithPublicKeyFetcher( + verifiable.SingleKey(pubKeyBytes, "Bls12381G2Key2020"))) + if err != nil { + panic(err) + } + + vp.CustomFields["presentation_submission"].(*PresentationSubmission).ID = dummy + vp.Credentials()[0].(*verifiable.Credential).Proofs[0]["created"] = dummy + vp.Credentials()[0].(*verifiable.Credential).Proofs[0]["proofValue"] = dummy + + vpBytes, err := json.MarshalIndent(vp, "", "\t") + if err != nil { + panic(err) + } + + fmt.Println(string(vpBytes)) + // Output: + //{ + // "@context": [ + // "https://www.w3.org/2018/credentials/v1", + // "https://identity.foundation/presentation-exchange/submission/v1" + // ], + // "presentation_submission": { + // "id": "DUMMY", + // "definition_id": "c1b88ce1-8460-4baf-8f16-4759a2f055fd", + // "descriptor_map": [ + // { + // "id": "country_descriptor", + // "format": "ldp_vp", + // "path": "$.verifiableCredential[0]" + // } + // ] + // }, + // "type": [ + // "VerifiablePresentation", + // "PresentationSubmission" + // ], + // "verifiableCredential": [ + // { + // "@context": [ + // "https://www.w3.org/2018/credentials/v1", + // "https://w3id.org/citizenship/v1", + // "https://w3id.org/security/bbs/v1" + // ], + // "credentialSubject": { + // "birthCountry": true, + // "familyName": "SMITH", + // "gender": "Male", + // "givenName": "JOHN", + // "id": "did:example:b34ca6cd37bbf23", + // "type": [ + // "Person", + // "PermanentResident" + // ] + // }, + // "id": "https://issuer.oidp.uscis.gov/credentials/83627465", + // "identifier": "83627465", + // "issuanceDate": "2019-12-03T12:19:52Z", + // "issuer": "did:example:489398593", + // "proof": { + // "created": "DUMMY", + // "nonce": "", + // "proofPurpose": "assertionMethod", + // "proofValue": "DUMMY", + // "type": "BbsBlsSignatureProof2020", + // "verificationMethod": "did:example:123456#key1" + // }, + // "type": [ + // "PermanentResidentCard", + // "VerifiableCredential" + // ] + // } + // ] + //} +} + +func signVCWithBBS(privKey *bbs12381g2pub.PrivateKey, vc *verifiable.Credential, documentLoader ld.DocumentLoader) { + bbsSigner, err := newBBSSigner(privKey) + if err != nil { + panic(err) + } + + sigSuite := bbsblssignature2020.New( + suite.WithSigner(bbsSigner), + suite.WithVerifier(bbsblssignature2020.NewG2PublicKeyVerifier())) + + ldpContext := &verifiable.LinkedDataProofContext{ + SignatureType: "BbsBlsSignature2020", + SignatureRepresentation: verifiable.SignatureProofValue, + Suite: sigSuite, + VerificationMethod: "did:example:123456#key1", + } + + err = vc.AddLinkedDataProof(ldpContext, jsonld.WithDocumentLoader(documentLoader)) + if err != nil { + panic(err) + } +} diff --git a/pkg/doc/presexch/schema.go b/pkg/doc/presexch/schema.go index c98f7702c3..85e32a2026 100644 --- a/pkg/doc/presexch/schema.go +++ b/pkg/doc/presexch/schema.go @@ -6,10 +6,10 @@ SPDX-License-Identifier: Apache-2.0 package presexch -// DefinitionJSONSchema is the JSONSchema definition for PresentationDefinition. +// DefinitionJSONSchemaV1 is the JSONSchema definition for PresentationDefinition. // nolint:lll // https://github.com/decentralized-identity/presentation-exchange/blob/9a6abc6d2b0f08b6339c9116132fa94c4c834418/test/presentation-definition/schema.json -const DefinitionJSONSchema = ` +const DefinitionJSONSchemaV1 = ` { "$schema":"http://json-schema.org/draft-07/schema#", "title":"Presentation Definition", @@ -470,3 +470,210 @@ const DefinitionJSONSchema = ` } } }` + +// DefinitionJSONSchemaV2 is the JSONSchema definition for PresentationDefinition. +// nolint:lll +const DefinitionJSONSchemaV2 = ` +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Presentation Definition Envelope", + "definitions": { + "status_directive": { + "type": "object", + "additionalProperties": false, + "properties": { + "directive": { + "type": "string", + "enum": ["required", "allowed", "disallowed"] + }, + "type": { + "type": "array", + "minItems": 1, + "items": { "type": "string" } + } + } + }, + "field": { + "type": "object", + "oneOf": [ + { + "properties": { + "id": { "type": "string" }, + "path": { + "type": "array", + "items": { "type": "string" } + }, + "purpose": { "type": "string" }, + "intent_to_retain": { "type": "boolean" }, + "filter": { "$ref": "http://json-schema.org/draft-07/schema#" } + }, + "required": ["path"], + "additionalProperties": false + }, + { + "properties": { + "id": { "type": "string" }, + "path": { + "type": "array", + "items": { "type": "string" } + }, + "purpose": { "type": "string" }, + "intent_to_retain": { "type": "boolean" }, + "filter": { "$ref": "http://json-schema.org/draft-07/schema#" }, + "predicate": { + "type": "string", + "enum": ["required", "preferred"] + } + }, + "required": ["path", "filter", "predicate"], + "additionalProperties": false + } + ] + }, + "input_descriptor": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "purpose": { "type": "string" }, + "format": { + "$ref": "http://identity.foundation/claim-format-registry/schemas/presentation-definition-claim-format-designations.json" + }, + "group": { "type": "array", "items": { "type": "string" } }, + "constraints": { + "type": "object", + "additionalProperties": false, + "properties": { + "limit_disclosure": { "type": "string", "enum": ["required", "preferred"] }, + "statuses": { + "type": "object", + "additionalProperties": false, + "properties": { + "active": { "$ref": "#/definitions/status_directive" }, + "suspended": { "$ref": "#/definitions/status_directive" }, + "revoked": { "$ref": "#/definitions/status_directive" } + } + }, + "fields": { + "type": "array", + "items": { "$ref": "#/definitions/field" } + }, + "subject_is_issuer": { "type": "string", "enum": ["required", "preferred"] }, + "is_holder": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "field_id": { + "type": "array", + "items": { "type": "string" } + }, + "directive": { + "type": "string", + "enum": ["required", "preferred"] + } + }, + "required": ["field_id", "directive"] + } + }, + "same_subject": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "field_id": { + "type": "array", + "items": { "type": "string" } + }, + "directive": { + "type": "string", + "enum": ["required", "preferred"] + } + }, + "required": ["field_id", "directive"] + } + } + } + } + }, + "required": ["id"] + }, + "submission_requirement": { + "type": "object", + "oneOf": [ + { + "properties": { + "name": { "type": "string" }, + "purpose": { "type": "string" }, + "rule": { + "type": "string", + "enum": ["all", "pick"] + }, + "count": { "type": "integer", "minimum": 1 }, + "min": { "type": "integer", "minimum": 0 }, + "max": { "type": "integer", "minimum": 0 }, + "from": { "type": "string" } + }, + "required": ["rule", "from"], + "additionalProperties": false + }, + { + "properties": { + "name": { "type": "string" }, + "purpose": { "type": "string" }, + "rule": { + "type": "string", + "enum": ["all", "pick"] + }, + "count": { "type": "integer", "minimum": 1 }, + "min": { "type": "integer", "minimum": 0 }, + "max": { "type": "integer", "minimum": 0 }, + "from_nested": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/submission_requirement" + } + } + }, + "required": ["rule", "from_nested"], + "additionalProperties": false + } + ] + }, + "presentation_definition": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "purpose": { "type": "string" }, + "format": { + "$ref": "http://identity.foundation/claim-format-registry/schemas/presentation-definition-claim-format-designations.json#" + }, + "frame": { + "type": "object", + "additionalProperties": true + }, + "submission_requirements": { + "type": "array", + "items": { + "$ref": "#/definitions/submission_requirement" + } + }, + "input_descriptors": { + "type": "array", + "items": { "$ref": "#/definitions/input_descriptor" } + } + }, + "required": ["id", "input_descriptors"], + "additionalProperties": false + } + }, + "type": "object", + "properties": { + "presentation_definition": {"$ref": "#/definitions/presentation_definition"} + } +}` From 1d1ff784e8b8e8d9428fbbb24d1b6cade01eeada Mon Sep 17 00:00:00 2001 From: HeidiHan0000 <44453261+HeidiHan0000@users.noreply.github.com> Date: Fri, 5 Aug 2022 16:25:10 -0400 Subject: [PATCH 2/3] fix: small change in schema format for clearness (#3316) Signed-off-by: heidihan0000 --- pkg/doc/cm/credentialmanifest.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/doc/cm/credentialmanifest.go b/pkg/doc/cm/credentialmanifest.go index 4f9a765f4b..87d898dc32 100644 --- a/pkg/doc/cm/credentialmanifest.go +++ b/pkg/doc/cm/credentialmanifest.go @@ -106,10 +106,10 @@ type LabeledDisplayMappingObject struct { // Schema represents Type and (optional) Format information for a DisplayMappingObject that uses the Paths field, // as defined in https://identity.foundation/credential-manifest/wallet-rendering/#using-path. type Schema struct { - Type string `json:"type,omitempty"` // MUST be here - Format string `json:"format,omitempty"` // MAY be here if the Type is "string". - ContentMediaType string `json:"mediatype,omitempty"` // MAY be here if the Type is "string". - ContentEncoding string `json:"encoding,omitempty"` // MAY be here if the Type is "string". + Type string `json:"type,omitempty"` // MUST be here + Format string `json:"format,omitempty"` // MAY be here if the Type is "string". + ContentMediaType string `json:"contentMediaType,omitempty"` // MAY be here if the Type is "string". + ContentEncoding string `json:"contentEncoding,omitempty"` // MAY be here if the Type is "string". } type staticDisplayMappingObjects struct { From 1daefcc2be6472de61ed7f9cae82aef55d85cb43 Mon Sep 17 00:00:00 2001 From: Baha <29608896+Baha-sk@users.noreply.github.com> Date: Mon, 8 Aug 2022 16:03:14 -0400 Subject: [PATCH 3/3] fix: presentation with empty credential fix (#3319) This change fixes how a presentation is marshalled with empty credential field which wrongly outputs: '"verifiableCredential": null' This change skips this field if the presentation Credential field is empty Signed-off-by: Baha Shaaban --- pkg/doc/verifiable/presentation.go | 11 ++++-- pkg/doc/verifiable/presentation_test.go | 45 +++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/pkg/doc/verifiable/presentation.go b/pkg/doc/verifiable/presentation.go index da2f2d6250..e25cb93b01 100644 --- a/pkg/doc/verifiable/presentation.go +++ b/pkg/doc/verifiable/presentation.go @@ -286,17 +286,22 @@ func (vp *Presentation) raw() (*rawPresentation, error) { return nil, err } - return &rawPresentation{ + rp := &rawPresentation{ // TODO single value contexts should be compacted as part of Issue [#1730] // Not compacting now to support interoperability Context: vp.Context, ID: vp.ID, Type: typesToRaw(vp.Type), - Credential: vp.credentials, Holder: vp.Holder, Proof: proof, CustomFields: vp.CustomFields, - }, nil + } + + if len(vp.credentials) > 0 { + rp.Credential = vp.credentials + } + + return rp, nil } // rawPresentation is a basic verifiable credential. diff --git a/pkg/doc/verifiable/presentation_test.go b/pkg/doc/verifiable/presentation_test.go index ae09636b1e..28211d560a 100644 --- a/pkg/doc/verifiable/presentation_test.go +++ b/pkg/doc/verifiable/presentation_test.go @@ -54,6 +54,19 @@ const validPresentation = ` } ` +const presentationWithoutCredentials = ` +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + "https://trustbloc.github.io/context/vc/examples-v1.jsonld" + ], + "id": "urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5", + "type": "VerifiablePresentation", + "holder": "did:example:ebfeb1f712ebc6f1c276e12ec21" +} +` + const validPresentationWithCustomFields = ` { "@context": [ @@ -130,6 +143,38 @@ func TestParsePresentation(t *testing.T) { require.Equal(t, "did:example:ebfeb1f712ebc6f1c276e12ec21", vp.Holder) }) + t.Run("creates a new Verifiable Presentation from valid JSON without credentials", func(t *testing.T) { + vp, err := newTestPresentation(t, []byte(presentationWithoutCredentials), WithPresStrictValidation()) + require.NoError(t, err) + require.NotNil(t, vp) + + // validate @context + require.Equal(t, []string{ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + "https://trustbloc.github.io/context/vc/examples-v1.jsonld", + }, vp.Context) + + // check id + require.Equal(t, "urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5", vp.ID) + + // check type + require.Equal(t, []string{"VerifiablePresentation"}, vp.Type) + + // check verifiableCredentials + require.Nil(t, vp.Credentials()) + require.Empty(t, vp.Credentials()) + + // check holder + require.Equal(t, "did:example:ebfeb1f712ebc6f1c276e12ec21", vp.Holder) + + // check rawPresentation + rp, err := vp.raw() + require.NoError(t, err) + + require.IsType(t, nil, rp.Credential) + }) + t.Run("creates a new Verifiable Presentation with custom/additional fields", func(t *testing.T) { verify := func(t *testing.T, vp *Presentation) { require.Len(t, vp.CustomFields, 1)