From 474b51727960a166c7e6f777b561c58f4abf2afd Mon Sep 17 00:00:00 2001 From: Volodymyr Kubiv Date: Mon, 13 Jun 2022 11:28:15 +0300 Subject: [PATCH] feat: validate data model of objects saved in a wallet. Signed-off-by: Volodymyr Kubiv --- .../testdata/context/context3.jsonld | 0 .../testdata/context/context4.jsonld | 0 .../testdata/context/context5.jsonld | 0 .../testdata/context/context6.jsonld | 0 .../jsonld/testdata/context/wallet_v1.jsonld | 58 ++++++ pkg/doc/jsonld/validate.go | 189 ++++++++++++++++++ .../validate_test.go} | 101 ++++++---- pkg/doc/ldcontext/embed/embed_contexts.go | 7 + .../w3c-ccg.github.io/wallet-v1.jsonld | 58 ++++++ pkg/doc/{verifiable => util/json}/json.go | 26 +-- .../{verifiable => util/json}/json_test.go | 18 +- pkg/doc/verifiable/common.go | 5 +- pkg/doc/verifiable/credential.go | 26 ++- pkg/doc/verifiable/credential_bbs.go | 3 +- pkg/doc/verifiable/credential_bbs_test.go | 5 +- pkg/doc/verifiable/credential_jwt.go | 3 +- pkg/doc/verifiable/credential_ldp_test.go | 7 +- pkg/doc/verifiable/credential_test.go | 9 +- pkg/doc/verifiable/jsonld.go | 135 ------------- pkg/doc/verifiable/presentation.go | 12 +- pkg/doc/verifiable/presentation_ldp_test.go | 3 +- pkg/wallet/contents.go | 32 ++- pkg/wallet/contents_test.go | 125 ++++++++---- pkg/wallet/options.go | 10 + pkg/wallet/wallet.go | 2 +- pkg/wallet/wallet_test.go | 2 +- 26 files changed, 567 insertions(+), 269 deletions(-) rename pkg/doc/{verifiable => jsonld}/testdata/context/context3.jsonld (100%) rename pkg/doc/{verifiable => jsonld}/testdata/context/context4.jsonld (100%) rename pkg/doc/{verifiable => jsonld}/testdata/context/context5.jsonld (100%) rename pkg/doc/{verifiable => jsonld}/testdata/context/context6.jsonld (100%) create mode 100644 pkg/doc/jsonld/testdata/context/wallet_v1.jsonld create mode 100644 pkg/doc/jsonld/validate.go rename pkg/doc/{verifiable/jsonld_test.go => jsonld/validate_test.go} (83%) create mode 100644 pkg/doc/ldcontext/embed/third_party/w3c-ccg.github.io/wallet-v1.jsonld rename pkg/doc/{verifiable => util/json}/json.go (70%) rename pkg/doc/{verifiable => util/json}/json_test.go (83%) diff --git a/pkg/doc/verifiable/testdata/context/context3.jsonld b/pkg/doc/jsonld/testdata/context/context3.jsonld similarity index 100% rename from pkg/doc/verifiable/testdata/context/context3.jsonld rename to pkg/doc/jsonld/testdata/context/context3.jsonld diff --git a/pkg/doc/verifiable/testdata/context/context4.jsonld b/pkg/doc/jsonld/testdata/context/context4.jsonld similarity index 100% rename from pkg/doc/verifiable/testdata/context/context4.jsonld rename to pkg/doc/jsonld/testdata/context/context4.jsonld diff --git a/pkg/doc/verifiable/testdata/context/context5.jsonld b/pkg/doc/jsonld/testdata/context/context5.jsonld similarity index 100% rename from pkg/doc/verifiable/testdata/context/context5.jsonld rename to pkg/doc/jsonld/testdata/context/context5.jsonld diff --git a/pkg/doc/verifiable/testdata/context/context6.jsonld b/pkg/doc/jsonld/testdata/context/context6.jsonld similarity index 100% rename from pkg/doc/verifiable/testdata/context/context6.jsonld rename to pkg/doc/jsonld/testdata/context/context6.jsonld diff --git a/pkg/doc/jsonld/testdata/context/wallet_v1.jsonld b/pkg/doc/jsonld/testdata/context/wallet_v1.jsonld new file mode 100644 index 0000000000..51efb0dc46 --- /dev/null +++ b/pkg/doc/jsonld/testdata/context/wallet_v1.jsonld @@ -0,0 +1,58 @@ +{ + "@context": [ + { + "@version": 1.1 + }, + { + "id": "@id", + "type": "@type", + + "UniversalWallet2020": "https://w3id.org/wallet#UniversalWallet2020", + "encryptedWalletContents": { + "@id": "https://w3id.org/wallet#encryptedWalletContents", + "@type": "@json" + }, + + "Key": "https://w3id.org/wallet#Key", + "Secret": "https://w3id.org/wallet#Secret", + "Entropy": "https://w3id.org/wallet#Entropy", + "Profile": "https://w3id.org/wallet#Profile", + "Mnemonic": "https://w3id.org/wallet#Mnemonic", + "MetaData": "https://w3id.org/wallet#MetaData", + + "correlation": "https://w3id.org/wallet#correlation", + "tags": "https://w3id.org/wallet#tags", + "note": "https://w3id.org/wallet#note", + "target": "https://w3id.org/wallet#target", + "quorum": "https://w3id.org/wallet#quorum", + "multibase": "https://w3id.org/wallet#multibase", + "hdPath": "https://w3id.org/wallet#hdPath", + + "amount": "https://schema.org/amount", + "currency": "https://schema.org/currency", + "value": "https://schema.org/value", + + "publicKeyJwk": { + "@id": "https://w3id.org/security#publicKeyJwk", + "@type": "@json" + }, + "privateKeyJwk": { + "@id": "https://w3id.org/security#privateKeyJwk", + "@type": "@json" + }, + "privateKeyBase58": "https://w3id.org/security#privateKeyBase58", + "privateKeyWebKms": "https://w3id.org/security#privateKeyWebKms", + "privateKeySecureEnclave": "https://w3id.org/security#privateKeySecureEnclave", + + "Organization": "http://schema.org/Organization", + "Person": "http://schema.org/Person", + "name": "http://schema.org/name", + "description": "http://schema.org/description", + "identifier": "http://schema.org/identifier", + "image": { + "@id": "http://schema.org/image", + "@type": "@id" + } + } + ] +} \ No newline at end of file diff --git a/pkg/doc/jsonld/validate.go b/pkg/doc/jsonld/validate.go new file mode 100644 index 0000000000..36a315e7bf --- /dev/null +++ b/pkg/doc/jsonld/validate.go @@ -0,0 +1,189 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +package jsonld + +import ( + "errors" + "fmt" + "reflect" + + "github.com/piprate/json-gold/ld" + + "github.com/hyperledger/aries-framework-go/pkg/doc/signature/jsonld" + "github.com/hyperledger/aries-framework-go/pkg/doc/util/json" +) + +type validateOpts struct { + strict bool + jsonldDocumentLoader ld.DocumentLoader + externalContext []string +} + +// ValidateOpts sets jsonld validation options. +type ValidateOpts func(opts *validateOpts) + +// WithDocumentLoader option is for passing custom JSON-LD document loader. +func WithDocumentLoader(jsonldDocumentLoader ld.DocumentLoader) ValidateOpts { + return func(opts *validateOpts) { + opts.jsonldDocumentLoader = jsonldDocumentLoader + } +} + +// WithExternalContext option is for definition of external context when doing JSON-LD operations. +func WithExternalContext(externalContext []string) ValidateOpts { + return func(opts *validateOpts) { + opts.externalContext = externalContext + } +} + +// WithStrictValidation sets if strict validation should be used. +func WithStrictValidation(checkStructure bool) ValidateOpts { + return func(opts *validateOpts) { + opts.strict = checkStructure + } +} + +func getValidateOpts(options []ValidateOpts) *validateOpts { + result := &validateOpts{ + strict: true, + } + + for _, opt := range options { + opt(result) + } + + return result +} + +// ValidateJSONLD validates jsonld structure. +func ValidateJSONLD(doc string, options ...ValidateOpts) error { + opts := getValidateOpts(options) + + docMap, err := json.ToMap(doc) + if err != nil { + return fmt.Errorf("convert JSON-LD doc to map: %w", err) + } + + jsonldProc := jsonld.Default() + + docCompactedMap, err := jsonldProc.Compact(docMap, + nil, jsonld.WithDocumentLoader(opts.jsonldDocumentLoader), + jsonld.WithExternalContext(opts.externalContext...)) + if err != nil { + return fmt.Errorf("compact JSON-LD document: %w", err) + } + + if opts.strict && !mapsHaveSameStructure(docMap, docCompactedMap) { + return errors.New("JSON-LD doc has different structure after compaction") + } + + return nil +} + +func mapsHaveSameStructure(originalMap, compactedMap map[string]interface{}) bool { + original := compactMap(originalMap) + compacted := compactMap(compactedMap) + + if reflect.DeepEqual(original, compacted) { + return true + } + + if len(original) != len(compacted) { + return false + } + + for k, v1 := range original { + v1Map, isMap := v1.(map[string]interface{}) + if !isMap { + continue + } + + v2, present := compacted[k] + if !present { // special case - the name of the map was mapped, cannot guess what's a new name + continue + } + + v2Map, isMap := v2.(map[string]interface{}) + if !isMap { + return false + } + + if !mapsHaveSameStructure(v1Map, v2Map) { + return false + } + } + + return true +} + +func compactMap(m map[string]interface{}) map[string]interface{} { + mCopy := make(map[string]interface{}) + + for k, v := range m { + // ignore context + if k == "@context" { + continue + } + + vNorm := compactValue(v) + + switch kv := vNorm.(type) { + case []interface{}: + mCopy[k] = compactSlice(kv) + + case map[string]interface{}: + mCopy[k] = compactMap(kv) + + default: + mCopy[k] = vNorm + } + } + + return mCopy +} + +func compactSlice(s []interface{}) []interface{} { + sCopy := make([]interface{}, len(s)) + + for i := range s { + sItem := compactValue(s[i]) + + switch sItem := sItem.(type) { + case map[string]interface{}: + sCopy[i] = compactMap(sItem) + + default: + sCopy[i] = sItem + } + } + + return sCopy +} + +func compactValue(v interface{}) interface{} { + switch cv := v.(type) { + case []interface{}: + // consists of only one element + if len(cv) == 1 { + return compactValue(cv[0]) + } + + return cv + + case map[string]interface{}: + // contains "id" element only + if len(cv) == 1 { + if _, ok := cv["id"]; ok { + return cv["id"] + } + } + + return cv + + default: + return cv + } +} diff --git a/pkg/doc/verifiable/jsonld_test.go b/pkg/doc/jsonld/validate_test.go similarity index 83% rename from pkg/doc/verifiable/jsonld_test.go rename to pkg/doc/jsonld/validate_test.go index 6dd54d3695..606aa77c45 100644 --- a/pkg/doc/verifiable/jsonld_test.go +++ b/pkg/doc/jsonld/validate_test.go @@ -3,7 +3,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -package verifiable +package jsonld import ( _ "embed" @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/hyperledger/aries-framework-go/pkg/doc/ld" "github.com/hyperledger/aries-framework-go/pkg/doc/ldcontext" "github.com/hyperledger/aries-framework-go/pkg/internal/ldtestutil" ) @@ -26,9 +27,12 @@ var ( context5 []byte //go:embed testdata/context/context6.jsonld context6 []byte + + //go:embed testdata/context/wallet_v1.jsonld + walletV1Context []byte ) -func Test_compactJSONLD(t *testing.T) { +func Test_ValidateJSONLD(t *testing.T) { t.Run("Extended both basic VC and subject model", func(t *testing.T) { contextURL := "http://127.0.0.1?context=3" @@ -101,9 +105,7 @@ func Test_compactJSONLD(t *testing.T) { Content: context3, }) - opts := &jsonldCredentialOpts{jsonldDocumentLoader: loader} - - err := compactJSONLD(vc, opts, true) + err := ValidateJSONLD(vc, WithDocumentLoader(loader)) require.NoError(t, err) }) @@ -136,14 +138,12 @@ func Test_compactJSONLD(t *testing.T) { Content: context4, }) - opts := &jsonldCredentialOpts{jsonldDocumentLoader: loader} - - err := compactJSONLD(vcJSON, opts, true) + err := ValidateJSONLD(vcJSON, WithDocumentLoader(loader)) require.NoError(t, err) }) } -func Test_compactJSONLDWithExtraUndefinedFields(t *testing.T) { +func Test_ValidateJSONLDWithExtraUndefinedFields(t *testing.T) { contextURL := "http://127.0.0.1?context=5" vcJSONTemplate := ` @@ -171,14 +171,12 @@ func Test_compactJSONLDWithExtraUndefinedFields(t *testing.T) { Content: context5, }) - opts := &jsonldCredentialOpts{jsonldDocumentLoader: loader} - - err := compactJSONLD(vc, opts, true) + err := ValidateJSONLD(vc, WithDocumentLoader(loader)) require.Error(t, err) require.EqualError(t, err, "JSON-LD doc has different structure after compaction") } -func Test_compactJSONLDWithExtraUndefinedSubjectFields(t *testing.T) { +func Test_ValidateJSONLDWithExtraUndefinedSubjectFields(t *testing.T) { contextURL := "http://127.0.0.1?context=6" loader := createTestDocumentLoader(t, ldcontext.Document{ @@ -214,9 +212,8 @@ func Test_compactJSONLDWithExtraUndefinedSubjectFields(t *testing.T) { ` vcJSON := fmt.Sprintf(vcJSONTemplate, contextURL) - opts := &jsonldCredentialOpts{jsonldDocumentLoader: loader} - err := compactJSONLD(vcJSON, opts, true) + err := ValidateJSONLD(vcJSON, WithDocumentLoader(loader)) require.Error(t, err) require.EqualError(t, err, "JSON-LD doc has different structure after compaction") }) @@ -248,15 +245,14 @@ func Test_compactJSONLDWithExtraUndefinedSubjectFields(t *testing.T) { ` vcJSON := fmt.Sprintf(vcJSONTemplate, contextURL) - opts := &jsonldCredentialOpts{jsonldDocumentLoader: loader} - err := compactJSONLD(vcJSON, opts, true) + err := ValidateJSONLD(vcJSON, WithDocumentLoader(loader)) require.Error(t, err) require.EqualError(t, err, "JSON-LD doc has different structure after compaction") }) } -func Test_compactJSONLD_WithExtraUndefinedFieldsInProof(t *testing.T) { +func Test_ValidateJSONLD_WithExtraUndefinedFieldsInProof(t *testing.T) { vcJSONWithValidProof := ` { "@context": [ @@ -283,7 +279,7 @@ func Test_compactJSONLD_WithExtraUndefinedFieldsInProof(t *testing.T) { } ` - err := compactJSONLD(vcJSONWithValidProof, defaultOpts(t), true) + err := ValidateJSONLD(vcJSONWithValidProof, WithDocumentLoader(createTestDocumentLoader(t))) require.NoError(t, err) // "newProp" field is present in the proof @@ -312,14 +308,15 @@ func Test_compactJSONLD_WithExtraUndefinedFieldsInProof(t *testing.T) { } }` - err = compactJSONLD(vcJSONWithInvalidProof, defaultOpts(t), true) + err = ValidateJSONLD(vcJSONWithInvalidProof, WithDocumentLoader(createTestDocumentLoader(t))) + require.Error(t, err) require.EqualError(t, err, "JSON-LD doc has different structure after compaction") } -func Test_compactJSONLD_CornerErrorCases(t *testing.T) { +func Test_ValidateJSONLD_CornerErrorCases(t *testing.T) { t.Run("Invalid JSON input", func(t *testing.T) { - err := compactJSONLD("not a json", defaultOpts(t), true) + err := ValidateJSONLD("not a json", WithDocumentLoader(createTestDocumentLoader(t))) require.Error(t, err) require.Contains(t, err.Error(), "convert JSON-LD doc to map") }) @@ -340,25 +337,16 @@ func Test_compactJSONLD_CornerErrorCases(t *testing.T) { } ` - err := compactJSONLD(vcJSONTemplate, defaultOpts(t), true) + err := ValidateJSONLD(vcJSONTemplate, WithDocumentLoader(createTestDocumentLoader(t))) require.Error(t, err) require.Contains(t, err.Error(), "compact JSON-LD document") }) } -func defaultOpts(t *testing.T) *jsonldCredentialOpts { - t.Helper() - - loader, err := ldtestutil.DocumentLoader() - require.NoError(t, err) - - return &jsonldCredentialOpts{jsonldDocumentLoader: loader} -} - // nolint:gochecknoglobals // needed to avoid Go compiler perf optimizations for benchmarks. var MajorSink string -func Benchmark_compactJSONLD(b *testing.B) { +func Benchmark_ValidateJSONLD(b *testing.B) { var sink string b.Run("Extended both basic VC and subject model", func(b *testing.B) { @@ -439,9 +427,7 @@ func Benchmark_compactJSONLD(b *testing.B) { }) require.NoError(b, err) - opts := &jsonldCredentialOpts{jsonldDocumentLoader: loader} - - err = compactJSONLD(vc, opts, true) + err = ValidateJSONLD(vc, WithDocumentLoader(loader)) require.NoError(b, err) sink = "basic_compact_test" @@ -485,9 +471,7 @@ func Benchmark_compactJSONLD(b *testing.B) { }) require.NoError(b, err) - opts := &jsonldCredentialOpts{jsonldDocumentLoader: loader} - - err = compactJSONLD(vcJSON, opts, true) + err = ValidateJSONLD(vcJSON, WithDocumentLoader(loader)) require.NoError(b, err) sink = "extended_compact_test" @@ -496,4 +480,43 @@ func Benchmark_compactJSONLD(b *testing.B) { MajorSink = sink }) }) + + b.Run("Extended both basic VC and subject model", func(b *testing.B) { + const testMetadata = `{ + "@context": ["https://w3id.org/wallet/v1"], + "id": "test-id", + "type": "Person", + "name": "John Smith", + "image": "https://via.placeholder.com/150", + "description" : "Professional software developer for Acme Corp." + }` + + b.RunParallel(func(pb *testing.PB) { + b.ResetTimer() + + for pb.Next() { + loader, err := ldtestutil.DocumentLoader(ldcontext.Document{ + URL: "https://w3id.org/wallet/v1", + Content: walletV1Context, + }) + require.NoError(b, err) + + err = ValidateJSONLD(testMetadata, WithDocumentLoader(loader)) + require.NoError(b, err) + + sink = "basic_compact_test" + } + + MajorSink = sink + }) + }) +} + +func createTestDocumentLoader(t *testing.T, extraContexts ...ldcontext.Document) *ld.DocumentLoader { + t.Helper() + + loader, err := ldtestutil.DocumentLoader(extraContexts...) + require.NoError(t, err) + + return loader } diff --git a/pkg/doc/ldcontext/embed/embed_contexts.go b/pkg/doc/ldcontext/embed/embed_contexts.go index 95dbd89de8..47f67e65f7 100644 --- a/pkg/doc/ldcontext/embed/embed_contexts.go +++ b/pkg/doc/ldcontext/embed/embed_contexts.go @@ -24,6 +24,8 @@ var ( w3idDIDv1 []byte //go:embed third_party/w3c-ccg.github.io/did_docres_v1.jsonld w3idDIDDocRes []byte + //go:embed third_party/w3c-ccg.github.io/wallet-v1.jsonld + w3idWalletV1 []byte //go:embed third_party/w3c-ccg.github.io/ldp-bbs2020_v1.jsonld ldpBBS2020 []byte //go:embed third_party/w3c-ccg.github.io/lds-jws2020_v1.jsonld @@ -79,6 +81,11 @@ var Contexts = []ldcontext.Document{ //nolint:gochecknoglobals DocumentURL: "https://w3c-ccg.github.io/did-resolution/contexts/did-resolution-v1.json", Content: w3idDIDDocRes, }, + { + URL: "https://w3id.org/wallet/v1", + DocumentURL: "https://w3c-ccg.github.io/universal-wallet-interop-spec/contexts/wallet-v1.json", + Content: w3idWalletV1, + }, { URL: "https://w3id.org/security/bbs/v1", DocumentURL: "https://w3c-ccg.github.io/ldp-bbs2020/contexts/v1/", diff --git a/pkg/doc/ldcontext/embed/third_party/w3c-ccg.github.io/wallet-v1.jsonld b/pkg/doc/ldcontext/embed/third_party/w3c-ccg.github.io/wallet-v1.jsonld new file mode 100644 index 0000000000..51efb0dc46 --- /dev/null +++ b/pkg/doc/ldcontext/embed/third_party/w3c-ccg.github.io/wallet-v1.jsonld @@ -0,0 +1,58 @@ +{ + "@context": [ + { + "@version": 1.1 + }, + { + "id": "@id", + "type": "@type", + + "UniversalWallet2020": "https://w3id.org/wallet#UniversalWallet2020", + "encryptedWalletContents": { + "@id": "https://w3id.org/wallet#encryptedWalletContents", + "@type": "@json" + }, + + "Key": "https://w3id.org/wallet#Key", + "Secret": "https://w3id.org/wallet#Secret", + "Entropy": "https://w3id.org/wallet#Entropy", + "Profile": "https://w3id.org/wallet#Profile", + "Mnemonic": "https://w3id.org/wallet#Mnemonic", + "MetaData": "https://w3id.org/wallet#MetaData", + + "correlation": "https://w3id.org/wallet#correlation", + "tags": "https://w3id.org/wallet#tags", + "note": "https://w3id.org/wallet#note", + "target": "https://w3id.org/wallet#target", + "quorum": "https://w3id.org/wallet#quorum", + "multibase": "https://w3id.org/wallet#multibase", + "hdPath": "https://w3id.org/wallet#hdPath", + + "amount": "https://schema.org/amount", + "currency": "https://schema.org/currency", + "value": "https://schema.org/value", + + "publicKeyJwk": { + "@id": "https://w3id.org/security#publicKeyJwk", + "@type": "@json" + }, + "privateKeyJwk": { + "@id": "https://w3id.org/security#privateKeyJwk", + "@type": "@json" + }, + "privateKeyBase58": "https://w3id.org/security#privateKeyBase58", + "privateKeyWebKms": "https://w3id.org/security#privateKeyWebKms", + "privateKeySecureEnclave": "https://w3id.org/security#privateKeySecureEnclave", + + "Organization": "http://schema.org/Organization", + "Person": "http://schema.org/Person", + "name": "http://schema.org/name", + "description": "http://schema.org/description", + "identifier": "http://schema.org/identifier", + "image": { + "@id": "http://schema.org/image", + "@type": "@id" + } + } + ] +} \ No newline at end of file diff --git a/pkg/doc/verifiable/json.go b/pkg/doc/util/json/json.go similarity index 70% rename from pkg/doc/verifiable/json.go rename to pkg/doc/util/json/json.go index 755dd8765b..f5be38ada0 100644 --- a/pkg/doc/verifiable/json.go +++ b/pkg/doc/util/json/json.go @@ -3,16 +3,16 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -package verifiable +package json import ( "encoding/json" ) -// marshalWithCustomFields marshals value merged with custom fields defined in the map into JSON bytes. -func marshalWithCustomFields(v interface{}, cf map[string]interface{}) ([]byte, error) { +// MarshalWithCustomFields marshals value merged with custom fields defined in the map into JSON bytes. +func MarshalWithCustomFields(v interface{}, cf map[string]interface{}) ([]byte, error) { // Merge value and custom fields into the joint map. - vm, err := mergeCustomFields(v, cf) + vm, err := MergeCustomFields(v, cf) if err != nil { return nil, err } @@ -21,9 +21,9 @@ func marshalWithCustomFields(v interface{}, cf map[string]interface{}) ([]byte, return json.Marshal(vm) } -// unmarshalWithCustomFields unmarshals JSON into value v and puts all JSON fields which do not belong to value +// UnmarshalWithCustomFields unmarshals JSON into value v and puts all JSON fields which do not belong to value // into custom fields map cf. -func unmarshalWithCustomFields(data []byte, v interface{}, cf map[string]interface{}) error { +func UnmarshalWithCustomFields(data []byte, v interface{}, cf map[string]interface{}) error { err := json.Unmarshal(data, v) if err != nil { return err @@ -60,9 +60,9 @@ func unmarshalWithCustomFields(data []byte, v interface{}, cf map[string]interfa return nil } -// mergeCustomFields converts value to the JSON-like map and merges it with custom fields map cf. -func mergeCustomFields(v interface{}, cf map[string]interface{}) (map[string]interface{}, error) { - kf, err := toMap(v) +// MergeCustomFields converts value to the JSON-like map and merges it with custom fields map cf. +func MergeCustomFields(v interface{}, cf map[string]interface{}) (map[string]interface{}, error) { + kf, err := ToMap(v) if err != nil { return nil, err } @@ -77,7 +77,8 @@ func mergeCustomFields(v interface{}, cf map[string]interface{}) (map[string]int return kf, nil } -func toMap(v interface{}) (map[string]interface{}, error) { +// ToMap convert object, string or bytes to json object represented by map. +func ToMap(v interface{}) (map[string]interface{}, error) { var ( b []byte err error @@ -105,11 +106,12 @@ func toMap(v interface{}) (map[string]interface{}, error) { return m, nil } -func toMaps(v []interface{}) ([]map[string]interface{}, error) { +// ToMaps convert array to array of json objects. +func ToMaps(v []interface{}) ([]map[string]interface{}, error) { maps := make([]map[string]interface{}, len(v)) for i := range v { - m, err := toMap(v[i]) + m, err := ToMap(v[i]) if err != nil { return nil, err } diff --git a/pkg/doc/verifiable/json_test.go b/pkg/doc/util/json/json_test.go similarity index 83% rename from pkg/doc/verifiable/json_test.go rename to pkg/doc/util/json/json_test.go index 77ec62718f..461cf1e195 100644 --- a/pkg/doc/verifiable/json_test.go +++ b/pkg/doc/util/json/json_test.go @@ -3,7 +3,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -package verifiable +package json import ( "encoding/json" @@ -33,7 +33,7 @@ func Test_marshalJSON(t *testing.T) { "boolValue": false, "intValue": 8, } - actual, err := marshalWithCustomFields(&v, cf) + actual, err := MarshalWithCustomFields(&v, cf) require.NoError(t, err) expectedMap := map[string]interface{}{ @@ -49,7 +49,7 @@ func Test_marshalJSON(t *testing.T) { t.Run("Failed JSON marshall", func(t *testing.T) { // artificial example - pass smth which cannot be marshalled - jsonBytes, err := marshalWithCustomFields(make(chan int), map[string]interface{}{}) + jsonBytes, err := MarshalWithCustomFields(make(chan int), map[string]interface{}{}) require.Error(t, err) require.Nil(t, jsonBytes) }) @@ -68,7 +68,7 @@ func Test_unmarshalJSON(t *testing.T) { t.Run("Successful JSON unmarshalling", func(t *testing.T) { v := new(testJSON) cf := make(map[string]interface{}) - err := unmarshalWithCustomFields(data, v, cf) + err := UnmarshalWithCustomFields(data, v, cf) require.NoError(t, err) expectedV := testJSON{ @@ -86,15 +86,15 @@ func Test_unmarshalJSON(t *testing.T) { cf := make(map[string]interface{}) // invalid JSON - err := unmarshalWithCustomFields([]byte("not JSON"), "", cf) + err := UnmarshalWithCustomFields([]byte("not JSON"), "", cf) require.Error(t, err) // unmarshallable value - err = unmarshalWithCustomFields(data, make(chan int), cf) + err = UnmarshalWithCustomFields(data, make(chan int), cf) require.Error(t, err) // incompatible structure of value - err = unmarshalWithCustomFields(data, new(testJSONInvalid), cf) + err = UnmarshalWithCustomFields(data, new(testJSONInvalid), cf) require.Error(t, err) }) } @@ -111,7 +111,7 @@ func Test_toMaps(t *testing.T) { }, } - maps, err := toMaps(v) + maps, err := ToMaps(v) require.NoError(t, err) require.Len(t, maps, 2) require.Equal(t, []map[string]interface{}{ @@ -119,7 +119,7 @@ func Test_toMaps(t *testing.T) { {"a": "b"}, }, maps) - maps, err = toMaps([]interface{}{make(chan int)}) + maps, err = ToMaps([]interface{}{make(chan int)}) require.Error(t, err) require.Empty(t, maps) } diff --git a/pkg/doc/verifiable/common.go b/pkg/doc/verifiable/common.go index 2bb6e85d1f..66f56498c9 100644 --- a/pkg/doc/verifiable/common.go +++ b/pkg/doc/verifiable/common.go @@ -23,6 +23,7 @@ import ( "github.com/xeipuuv/gojsonschema" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/verifier" + jsonutil "github.com/hyperledger/aries-framework-go/pkg/doc/util/json" vdrapi "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" ) @@ -133,7 +134,7 @@ func (tid TypedID) MarshalJSON() ([]byte, error) { alias := Alias(tid) - data, err := marshalWithCustomFields(alias, tid.CustomFields) + data, err := jsonutil.MarshalWithCustomFields(alias, tid.CustomFields) if err != nil { return nil, fmt.Errorf("marshal TypedID: %w", err) } @@ -150,7 +151,7 @@ func (tid *TypedID) UnmarshalJSON(data []byte) error { tid.CustomFields = make(CustomFields) - err := unmarshalWithCustomFields(data, alias, tid.CustomFields) + err := jsonutil.UnmarshalWithCustomFields(data, alias, tid.CustomFields) if err != nil { return fmt.Errorf("unmarshal TypedID: %w", err) } diff --git a/pkg/doc/verifiable/credential.go b/pkg/doc/verifiable/credential.go index e4e35d8620..f4bdd885be 100644 --- a/pkg/doc/verifiable/credential.go +++ b/pkg/doc/verifiable/credential.go @@ -19,9 +19,11 @@ import ( "github.com/xeipuuv/gojsonschema" "github.com/hyperledger/aries-framework-go/pkg/common/log" + docjsonld "github.com/hyperledger/aries-framework-go/pkg/doc/jsonld" "github.com/hyperledger/aries-framework-go/pkg/doc/jwt" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/verifier" "github.com/hyperledger/aries-framework-go/pkg/doc/util" + jsonutil "github.com/hyperledger/aries-framework-go/pkg/doc/util/json" ) var logger = log.New("aries-framework/doc/verifiable") @@ -408,7 +410,7 @@ func (i *Issuer) MarshalJSON() ([]byte, error) { alias := Alias(*i) - data, err := marshalWithCustomFields(alias, i.CustomFields) + data, err := jsonutil.MarshalWithCustomFields(alias, i.CustomFields) if err != nil { return nil, fmt.Errorf("marshal Issuer: %w", err) } @@ -433,7 +435,7 @@ func (i *Issuer) UnmarshalJSON(bytes []byte) error { i.CustomFields = make(CustomFields) - err := unmarshalWithCustomFields(bytes, alias, i.CustomFields) + err := jsonutil.UnmarshalWithCustomFields(bytes, alias, i.CustomFields) if err != nil { return fmt.Errorf("unmarshal Issuer: %w", err) } @@ -463,7 +465,7 @@ func (s *Subject) MarshalJSON() ([]byte, error) { alias := Alias(*s) - data, err := marshalWithCustomFields(alias, s.CustomFields) + data, err := jsonutil.MarshalWithCustomFields(alias, s.CustomFields) if err != nil { return nil, fmt.Errorf("marshal Subject: %w", err) } @@ -487,7 +489,7 @@ func (s *Subject) UnmarshalJSON(bytes []byte) error { s.CustomFields = make(CustomFields) - err := unmarshalWithCustomFields(bytes, alias, s.CustomFields) + err := jsonutil.UnmarshalWithCustomFields(bytes, alias, s.CustomFields) if err != nil { return fmt.Errorf("unmarshal Subject: %w", err) } @@ -542,7 +544,7 @@ func (rc *rawCredential) MarshalJSON() ([]byte, error) { alias := (*Alias)(rc) - return marshalWithCustomFields(alias, rc.CustomFields) + return jsonutil.MarshalWithCustomFields(alias, rc.CustomFields) } // UnmarshalJSON defines custom unmarshalling of rawCredential from JSON. @@ -552,7 +554,7 @@ func (rc *rawCredential) UnmarshalJSON(data []byte) error { alias := (*Alias)(rc) rc.CustomFields = make(CustomFields) - err := unmarshalWithCustomFields(data, alias, rc.CustomFields) + err := jsonutil.UnmarshalWithCustomFields(data, alias, rc.CustomFields) if err != nil { return err } @@ -885,7 +887,11 @@ func (vc *Credential) validateBaseContextWithExtendedValidation(vcOpts *credenti } func (vc *Credential) validateJSONLD(vcBytes []byte, vcOpts *credentialOpts) error { - return compactJSONLD(string(vcBytes), &vcOpts.jsonldCredentialOpts, vcOpts.strictValidation) + return docjsonld.ValidateJSONLD(string(vcBytes), + docjsonld.WithDocumentLoader(vcOpts.jsonldCredentialOpts.jsonldDocumentLoader), + docjsonld.WithExternalContext(vcOpts.jsonldCredentialOpts.externalContext), + docjsonld.WithStrictValidation(vcOpts.strictValidation), + ) } // CustomCredentialProducer is a factory for Credentials with extended data model. @@ -1135,7 +1141,7 @@ func subjectStructToRaw(subject interface{}) (json.RawMessage, error) { subjects[i] = sValue.Index(i).Interface() } - sMaps, err := toMaps(subjects) + sMaps, err := jsonutil.ToMaps(subjects) if err != nil { return nil, errors.New("subject of unknown structure") } @@ -1144,7 +1150,7 @@ func subjectStructToRaw(subject interface{}) (json.RawMessage, error) { } // convert to map and try once again - sMap, err := toMap(subject) + sMap, err := jsonutil.ToMap(subject) if err != nil { return nil, errors.New("subject of unknown structure") } @@ -1367,7 +1373,7 @@ func SubjectID(subject interface{}) (string, error) { // nolint:gocyclo default: // convert to map and try once again - sMap, err := toMap(subject) + sMap, err := jsonutil.ToMap(subject) if err != nil { return "", errors.New("subject of unknown structure") } diff --git a/pkg/doc/verifiable/credential_bbs.go b/pkg/doc/verifiable/credential_bbs.go index 20e27365f3..be2201f8fa 100644 --- a/pkg/doc/verifiable/credential_bbs.go +++ b/pkg/doc/verifiable/credential_bbs.go @@ -11,6 +11,7 @@ import ( "fmt" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/bbsblssignatureproof2020" + jsonutil "github.com/hyperledger/aries-framework-go/pkg/doc/util/json" ) // GenerateBBSSelectiveDisclosure generate BBS+ selective disclosure from one BBS+ signature. @@ -29,7 +30,7 @@ func (vc *Credential) GenerateBBSSelectiveDisclosure(revealDoc map[string]interf suite := bbsblssignatureproof2020.New() - vcDoc, err := toMap(vc) + vcDoc, err := jsonutil.ToMap(vc) if err != nil { return nil, err } diff --git a/pkg/doc/verifiable/credential_bbs_test.go b/pkg/doc/verifiable/credential_bbs_test.go index ff4956bfaf..f3d43cf110 100644 --- a/pkg/doc/verifiable/credential_bbs_test.go +++ b/pkg/doc/verifiable/credential_bbs_test.go @@ -19,6 +19,7 @@ import ( "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/bbsblssignature2020" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/bbsblssignatureproof2020" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/ed25519signature2018" + jsonutil "github.com/hyperledger/aries-framework-go/pkg/doc/util/json" "github.com/hyperledger/aries-framework-go/pkg/kms" ) @@ -101,7 +102,7 @@ func TestCredential_GenerateBBSSelectiveDisclosure(t *testing.T) { } ` - revealDoc, err := toMap(revealJSON) + revealDoc, err := jsonutil.ToMap(revealJSON) require.NoError(t, err) nonce := []byte("nonce") @@ -178,7 +179,7 @@ func TestCredential_GenerateBBSSelectiveDisclosure(t *testing.T) { } ` - revealDoc, err = toMap(revealJSONWithMissingIssuer) + revealDoc, err = jsonutil.ToMap(revealJSONWithMissingIssuer) require.NoError(t, err) vcWithSelectiveDisclosure, err = vc.GenerateBBSSelectiveDisclosure(revealDoc, nonce, vcOptions...) diff --git a/pkg/doc/verifiable/credential_jwt.go b/pkg/doc/verifiable/credential_jwt.go index 1148dfc08a..97c1e56abe 100644 --- a/pkg/doc/verifiable/credential_jwt.go +++ b/pkg/doc/verifiable/credential_jwt.go @@ -14,6 +14,7 @@ import ( josejwt "github.com/square/go-jose/v3/jwt" "github.com/hyperledger/aries-framework-go/pkg/doc/jwt" + jsonutil "github.com/hyperledger/aries-framework-go/pkg/doc/util/json" ) const ( @@ -69,7 +70,7 @@ func newJWTCredClaims(vc *Credential, minimizeVC bool) (*JWTCredClaims, error) { return nil, err } - vcMap, err := mergeCustomFields(raw, raw.CustomFields) + vcMap, err := jsonutil.MergeCustomFields(raw, raw.CustomFields) if err != nil { return nil, err } diff --git a/pkg/doc/verifiable/credential_ldp_test.go b/pkg/doc/verifiable/credential_ldp_test.go index ec47b8b56b..6269928364 100644 --- a/pkg/doc/verifiable/credential_ldp_test.go +++ b/pkg/doc/verifiable/credential_ldp_test.go @@ -33,6 +33,7 @@ import ( "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/ed25519signature2020" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/jsonwebsignature2020" sigverifier "github.com/hyperledger/aries-framework-go/pkg/doc/signature/verifier" + jsonutil "github.com/hyperledger/aries-framework-go/pkg/doc/util/json" "github.com/hyperledger/aries-framework-go/pkg/kms" "github.com/hyperledger/aries-framework-go/pkg/kms/localkms" ) @@ -419,7 +420,7 @@ func TestExtraContextWithLDP(t *testing.T) { // Drop https://trustbloc.github.io/context/vc/examples-v1.jsonld context where // SupportingActivity and CredentialStatusList2017 are defined. - vcMap, err := toMap(vcBytes) + vcMap, err := jsonutil.ToMap(vcBytes) r.NoError(err) vcMap["@context"] = baseContext @@ -1217,7 +1218,7 @@ func TestCredential_AddLinkedDataProof(t *testing.T) { vc, err := parseTestCredential(t, []byte(validCredential)) r.NoError(err) - originalVCMap, err := toMap(vc) + originalVCMap, err := jsonutil.ToMap(vc) r.NoError(err) err = vc.AddLinkedDataProof(&LinkedDataProofContext{ @@ -1231,7 +1232,7 @@ func TestCredential_AddLinkedDataProof(t *testing.T) { }, jsonldsig.WithDocumentLoader(createTestDocumentLoader(t))) r.NoError(err) - vcMap, err := toMap(vc) + vcMap, err := jsonutil.ToMap(vc) r.NoError(err) r.Contains(vcMap, "proof") diff --git a/pkg/doc/verifiable/credential_test.go b/pkg/doc/verifiable/credential_test.go index 0642c443f9..5f5042c61c 100644 --- a/pkg/doc/verifiable/credential_test.go +++ b/pkg/doc/verifiable/credential_test.go @@ -21,6 +21,7 @@ import ( "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/ed25519signature2018" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/verifier" + jsonutil "github.com/hyperledger/aries-framework-go/pkg/doc/util/json" "github.com/hyperledger/aries-framework-go/pkg/kms" ) @@ -1622,7 +1623,7 @@ func TestParseCredentialFromRaw(t *testing.T) { } func TestParseCredentialFromRaw_PreserveDates(t *testing.T) { - vcMap, err := toMap(validCredential) + vcMap, err := jsonutil.ToMap(validCredential) require.NoError(t, err) vcMap["issuanceDate"] = "2020-01-01T00:00:00.000Z" @@ -1639,7 +1640,7 @@ func TestParseCredentialFromRaw_PreserveDates(t *testing.T) { require.NoError(t, err) // Check that the dates formatting is not corrupted. - rawMap, err := toMap(vcBytes) + rawMap, err := jsonutil.ToMap(vcBytes) require.NoError(t, err) require.Contains(t, rawMap, "issuanceDate") @@ -1960,7 +1961,7 @@ func TestMarshalCredential(t *testing.T) { require.NoError(t, err) require.NotNil(t, vc) - vcMap, err := toMap(vc) + vcMap, err := jsonutil.ToMap(vc) require.NoError(t, err) require.Empty(t, vcMap["credentialSchema"]) require.NotEmpty(t, vcMap["@context"]) @@ -1971,7 +1972,7 @@ func TestMarshalCredential(t *testing.T) { // now set schema and try again vc.Schemas = []TypedID{{ID: "test1"}, {ID: "test2"}} - vcMap, err = toMap(vc) + vcMap, err = jsonutil.ToMap(vc) require.NoError(t, err) require.NotEmpty(t, vcMap["credentialSchema"]) require.NotEmpty(t, vcMap["@context"]) diff --git a/pkg/doc/verifiable/jsonld.go b/pkg/doc/verifiable/jsonld.go index a327c6cd44..786cd2ea2d 100644 --- a/pkg/doc/verifiable/jsonld.go +++ b/pkg/doc/verifiable/jsonld.go @@ -5,14 +5,6 @@ SPDX-License-Identifier: Apache-2.0 package verifiable -import ( - "errors" - "fmt" - "reflect" - - "github.com/hyperledger/aries-framework-go/pkg/doc/signature/jsonld" -) - const ( // ContextURI is the required JSON-LD context for VCs and VPs. ContextURI = "https://www.w3.org/2018/credentials/v1" @@ -23,130 +15,3 @@ const ( // VPType is the required Type for Verifiable Credentials. VPType = "VerifiablePresentation" ) - -func compactJSONLD(doc string, opts *jsonldCredentialOpts, strict bool) error { - docMap, err := toMap(doc) - if err != nil { - return fmt.Errorf("convert JSON-LD doc to map: %w", err) - } - - jsonldProc := jsonld.Default() - - docCompactedMap, err := jsonldProc.Compact(docMap, - nil, jsonld.WithDocumentLoader(opts.jsonldDocumentLoader), - jsonld.WithExternalContext(opts.externalContext...)) - if err != nil { - return fmt.Errorf("compact JSON-LD document: %w", err) - } - - if strict && !mapsHaveSameStructure(docMap, docCompactedMap) { - return errors.New("JSON-LD doc has different structure after compaction") - } - - return nil -} - -func mapsHaveSameStructure(originalMap, compactedMap map[string]interface{}) bool { - original := compactMap(originalMap) - compacted := compactMap(compactedMap) - - if reflect.DeepEqual(original, compacted) { - return true - } - - if len(original) != len(compacted) { - return false - } - - for k, v1 := range original { - v1Map, isMap := v1.(map[string]interface{}) - if !isMap { - continue - } - - v2, present := compacted[k] - if !present { // special case - the name of the map was mapped, cannot guess what's a new name - continue - } - - v2Map, isMap := v2.(map[string]interface{}) - if !isMap { - return false - } - - if !mapsHaveSameStructure(v1Map, v2Map) { - return false - } - } - - return true -} - -func compactMap(m map[string]interface{}) map[string]interface{} { - mCopy := make(map[string]interface{}) - - for k, v := range m { - // ignore context - if k == "@context" { - continue - } - - vNorm := compactValue(v) - - switch kv := vNorm.(type) { - case []interface{}: - mCopy[k] = compactSlice(kv) - - case map[string]interface{}: - mCopy[k] = compactMap(kv) - - default: - mCopy[k] = vNorm - } - } - - return mCopy -} - -func compactSlice(s []interface{}) []interface{} { - sCopy := make([]interface{}, len(s)) - - for i := range s { - sItem := compactValue(s[i]) - - switch sItem := sItem.(type) { - case map[string]interface{}: - sCopy[i] = compactMap(sItem) - - default: - sCopy[i] = sItem - } - } - - return sCopy -} - -func compactValue(v interface{}) interface{} { - switch cv := v.(type) { - case []interface{}: - // consists of only one element - if len(cv) == 1 { - return compactValue(cv[0]) - } - - return cv - - case map[string]interface{}: - // contains "id" element only - if len(cv) == 1 { - if _, ok := cv["id"]; ok { - return cv["id"] - } - } - - return cv - - default: - return cv - } -} diff --git a/pkg/doc/verifiable/presentation.go b/pkg/doc/verifiable/presentation.go index c331147683..da2f2d6250 100644 --- a/pkg/doc/verifiable/presentation.go +++ b/pkg/doc/verifiable/presentation.go @@ -14,8 +14,10 @@ import ( "github.com/xeipuuv/gojsonschema" "github.com/hyperledger/aries-framework-go/pkg/doc/jose" + docjsonld "github.com/hyperledger/aries-framework-go/pkg/doc/jsonld" "github.com/hyperledger/aries-framework-go/pkg/doc/jwt" "github.com/hyperledger/aries-framework-go/pkg/doc/signature/verifier" + jsonutil "github.com/hyperledger/aries-framework-go/pkg/doc/util/json" ) const basePresentationSchema = ` @@ -315,7 +317,7 @@ func (rp *rawPresentation) MarshalJSON() ([]byte, error) { alias := (*Alias)(rp) - return marshalWithCustomFields(alias, rp.CustomFields) + return jsonutil.MarshalWithCustomFields(alias, rp.CustomFields) } // UnmarshalJSON defines custom unmarshalling of rawPresentation from JSON. @@ -325,7 +327,7 @@ func (rp *rawPresentation) UnmarshalJSON(data []byte) error { alias := (*Alias)(rp) rp.CustomFields = make(CustomFields) - err := unmarshalWithCustomFields(data, alias, rp.CustomFields) + err := jsonutil.UnmarshalWithCustomFields(data, alias, rp.CustomFields) if err != nil { return err } @@ -536,7 +538,11 @@ func validateVP(data []byte, opts *presentationOpts) error { } func validateVPJSONLD(vpBytes []byte, opts *presentationOpts) error { - return compactJSONLD(string(vpBytes), &opts.jsonldCredentialOpts, opts.strictValidation) + return docjsonld.ValidateJSONLD(string(vpBytes), + docjsonld.WithDocumentLoader(opts.jsonldCredentialOpts.jsonldDocumentLoader), + docjsonld.WithExternalContext(opts.jsonldCredentialOpts.externalContext), + docjsonld.WithStrictValidation(opts.strictValidation), + ) } func validateVPJSONSchema(data []byte) error { diff --git a/pkg/doc/verifiable/presentation_ldp_test.go b/pkg/doc/verifiable/presentation_ldp_test.go index 97681e8fe1..cb4eada2a3 100644 --- a/pkg/doc/verifiable/presentation_ldp_test.go +++ b/pkg/doc/verifiable/presentation_ldp_test.go @@ -14,6 +14,7 @@ import ( "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/ed25519signature2018" + jsonutil "github.com/hyperledger/aries-framework-go/pkg/doc/util/json" "github.com/hyperledger/aries-framework-go/pkg/kms" ) @@ -81,7 +82,7 @@ func TestPresentation_AddLinkedDataProof(t *testing.T) { vpJSON, err := vp.MarshalJSON() r.NoError(err) - vpMap, err := toMap(vpJSON) + vpMap, err := jsonutil.ToMap(vpJSON) r.NoError(err) r.Contains(vpMap, "proof") diff --git a/pkg/wallet/contents.go b/pkg/wallet/contents.go index 6cd6ea658f..8e9eb71a01 100644 --- a/pkg/wallet/contents.go +++ b/pkg/wallet/contents.go @@ -18,8 +18,10 @@ import ( "time" "github.com/bluele/gcache" + "github.com/piprate/json-gold/ld" "github.com/hyperledger/aries-framework-go/pkg/doc/did" + "github.com/hyperledger/aries-framework-go/pkg/doc/jsonld" "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" "github.com/hyperledger/aries-framework-go/pkg/kms" "github.com/hyperledger/aries-framework-go/spi/storage" @@ -104,17 +106,24 @@ var ( // contentStore is store for wallet contents for given user profile. type contentStore struct { - storeID string - provider *storageProvider - open storeOpenHandle - close storeCloseHandle - lock sync.RWMutex + storeID string + provider *storageProvider + open storeOpenHandle + close storeCloseHandle + lock sync.RWMutex + jsonldDocumentLoader ld.DocumentLoader } // newContentStore returns new wallet content store instance. // will use underlying storage provider as content storage if profile doesn't have edv settings. -func newContentStore(p storage.Provider, pr *profile) *contentStore { - contents := &contentStore{open: storeLocked, close: noOp, provider: newWalletStorageProvider(pr, p), storeID: pr.ID} +func newContentStore(p storage.Provider, jsonldDocumentLoader ld.DocumentLoader, pr *profile) *contentStore { + contents := &contentStore{ + open: storeLocked, + close: noOp, + provider: newWalletStorageProvider(pr, p), + storeID: pr.ID, + jsonldDocumentLoader: jsonldDocumentLoader, + } if store, err := storeManager().get(pr.ID); err == nil { contents.updateStoreHandles(store) @@ -178,13 +187,20 @@ func (cs *contentStore) Close() bool { // if content document id is missing from content, then system generated id will be used as key for storage. // returns error if content with same ID already exists in store. // For replacing already existing content, use 'Remove() + Add()'. -func (cs *contentStore) Save(auth string, ct ContentType, content []byte, options ...AddContentOptions) error { +func (cs *contentStore) Save(auth string, ct ContentType, content []byte, options ...AddContentOptions) error { //nolint:lll,gocyclo opts := &addContentOpts{} for _, option := range options { option(opts) } + if opts.validateDataModel && ct != DIDResolutionResponse { + err := jsonld.ValidateJSONLD(string(content), jsonld.WithDocumentLoader(cs.jsonldDocumentLoader)) + if err != nil { + return fmt.Errorf("incorrect document structure: %w", err) + } + } + switch ct { case Collection, Metadata, Connection, Credential: key, err := getContentID(content) diff --git a/pkg/wallet/contents_test.go b/pkg/wallet/contents_test.go index 70650cff2d..3f237db045 100644 --- a/pkg/wallet/contents_test.go +++ b/pkg/wallet/contents_test.go @@ -17,7 +17,9 @@ import ( "github.com/hyperledger/aries-framework-go/component/storage/edv" "github.com/hyperledger/aries-framework-go/internal/testdata" + "github.com/hyperledger/aries-framework-go/pkg/doc/ld" vdrapi "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" + "github.com/hyperledger/aries-framework-go/pkg/internal/ldtestutil" mockkms "github.com/hyperledger/aries-framework-go/pkg/mock/kms" mockstorage "github.com/hyperledger/aries-framework-go/pkg/mock/storage" "github.com/hyperledger/aries-framework-go/pkg/mock/vdr" @@ -120,16 +122,21 @@ const ( "didResolutionMetadata": {} }` sampleKeyContentBase58Valid = `{ + "@context": ["https://w3id.org/wallet/v1"], + "id": "did:example:123456789abcdefghi#key-1", + "type": "Ed25519VerificationKey2018", + "privateKeyBase58":"zJRjGFZydU5DBdS2p5qbiUzDFAxbXTkjiDuGPksMBbY5TNyEsGfK4a4WGKjBCh1zeNryeuKtPotp8W1ESnwP71y" + }` + sampleKeyContentBase58WithInvalidField = `{ "@context": ["https://w3id.org/wallet/v1"], "id": "did:example:123456789abcdefghi#key-1", - "controller": "did:example:123456789abcdefghi", + "controller": "did:example:123456789abcdefghi", "type": "Ed25519VerificationKey2018", "privateKeyBase58":"zJRjGFZydU5DBdS2p5qbiUzDFAxbXTkjiDuGPksMBbY5TNyEsGfK4a4WGKjBCh1zeNryeuKtPotp8W1ESnwP71y" }` sampleKeyContentJwkValid = `{ "@context": ["https://w3id.org/wallet/v1"], - "id": "did:example:123456789abcdefghi#z6MkiEh8RQL83nkPo8ehDeX7", - "controller": "did:example:123456789abcdefghi", + "id": "did:example:123456789abcdefghi#z6MkiEh8RQL83nkPo8ehDeX7", "type": "Ed25519VerificationKey2018", "privateKeyJwk": { "kty": "OKP", @@ -193,7 +200,7 @@ func TestContentStores(t *testing.T) { sp := getMockStorageProvider() // create new store - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.Empty(t, sp.config.TagNames) @@ -245,7 +252,7 @@ func TestContentStores(t *testing.T) { require.NoError(t, err) // create new store - contentStore := newContentStore(sp, profileInfo) + contentStore := newContentStore(sp, createTestDocumentLoader(t), profileInfo) require.NotEmpty(t, contentStore) require.Empty(t, sp.config.TagNames) @@ -267,7 +274,7 @@ func TestContentStores(t *testing.T) { t.Run("open store - failure", func(t *testing.T) { sp := getMockStorageProvider() - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) // open store error sp.ErrOpenStoreHandle = errors.New(sampleContenttErr) @@ -287,7 +294,7 @@ func TestContentStores(t *testing.T) { sp.ErrOpenStoreHandle = nil sp.failure = nil sp.Store.ErrClose = errors.New(sampleContenttErr) - contentStore = newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore = newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) require.True(t, contentStore.Close()) @@ -296,7 +303,7 @@ func TestContentStores(t *testing.T) { t.Run("save to store - success", func(t *testing.T) { sp := getMockStorageProvider() - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -316,7 +323,7 @@ func TestContentStores(t *testing.T) { t.Run("save content to store without ID - success", func(t *testing.T) { sp := getMockStorageProvider() - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -328,7 +335,7 @@ func TestContentStores(t *testing.T) { t.Run("save to doc resolution to store - success", func(t *testing.T) { sp := getMockStorageProvider() - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -354,7 +361,7 @@ func TestContentStores(t *testing.T) { sp := getMockStorageProvider() sampleUser := uuid.New().String() - contentStore := newContentStore(sp, &profile{ID: sampleUser}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: sampleUser}) require.NotEmpty(t, contentStore) // wallet locked @@ -388,11 +395,11 @@ func TestContentStores(t *testing.T) { require.NotEmpty(t, tkn) // import base58 private key - err = contentStore.Save(tkn, Key, []byte(sampleKeyContentBase58Valid)) + err = contentStore.Save(tkn, Key, []byte(sampleKeyContentBase58Valid), ValidateContent()) require.NoError(t, err) // import jwk private key - err = contentStore.Save(tkn, Key, []byte(sampleKeyContentJwkValid)) + err = contentStore.Save(tkn, Key, []byte(sampleKeyContentJwkValid), ValidateContent()) require.NoError(t, err) // import using invalid auth token @@ -400,11 +407,46 @@ func TestContentStores(t *testing.T) { require.True(t, errors.Is(err, ErrWalletLocked)) }) + t.Run("save key to store - invalid jsonld", func(t *testing.T) { + sp := getMockStorageProvider() + sampleUser := uuid.New().String() + + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: sampleUser}) + require.NotEmpty(t, contentStore) + + // unlock keymanager + masterLock, err := getDefaultSecretLock(samplePassPhrase) + require.NoError(t, err) + + masterLockCipherText, err := createMasterLock(masterLock) + require.NoError(t, err) + require.NotEmpty(t, masterLockCipherText) + + profileInfo := &profile{ + User: sampleUser, + MasterLockCipher: masterLockCipherText, + } + + kmgr, err := keyManager().createKeyManager(profileInfo, mockstorage.NewMockStoreProvider(), + &unlockOpts{passphrase: samplePassPhrase}) + require.NotEmpty(t, kmgr) + require.NoError(t, err) + + tkn, err := sessionManager().createSession(profileInfo.User, kmgr, 500*time.Millisecond) + + require.NoError(t, err) + require.NotEmpty(t, tkn) + + // import base58 private key + err = contentStore.Save(tkn, Key, []byte(sampleKeyContentBase58WithInvalidField), ValidateContent()) + require.Contains(t, err.Error(), "JSON-LD doc has different structure after compaction") + }) + t.Run("save key to store - failure", func(t *testing.T) { sp := getMockStorageProvider() sampleUser := uuid.New().String() - contentStore := newContentStore(sp, &profile{ID: sampleUser}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: sampleUser}) require.NotEmpty(t, contentStore) // wallet locked @@ -416,7 +458,7 @@ func TestContentStores(t *testing.T) { t.Run("save to doc resolution to store - failure", func(t *testing.T) { sp := getMockStorageProvider() - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) err := contentStore.Save(token, DIDResolutionResponse, []byte(sampleContentInvalid)) @@ -427,7 +469,7 @@ func TestContentStores(t *testing.T) { t.Run("save to store - failures", func(t *testing.T) { sp := getMockStorageProvider() - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) // invalid content type @@ -443,7 +485,7 @@ func TestContentStores(t *testing.T) { // store errors sp.Store.ErrPut = errors.New(sampleContenttErr) - contentStore = newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore = newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) // wallet locked @@ -465,7 +507,7 @@ func TestContentStores(t *testing.T) { t.Run("save to invalid content type - validation", func(t *testing.T) { sp := getMockStorageProvider() - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) err := contentStore.Save(token, "Test", []byte("{}")) @@ -476,7 +518,7 @@ func TestContentStores(t *testing.T) { t.Run("save duplicate items - validation", func(t *testing.T) { sp := getMockStorageProvider() - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -493,7 +535,7 @@ func TestContentStores(t *testing.T) { t.Run("get from store - success", func(t *testing.T) { sp := getMockStorageProvider() - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -512,7 +554,7 @@ func TestContentStores(t *testing.T) { sp := getMockStorageProvider() sp.Store.ErrGet = errors.New(sampleContenttErr) - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) content, err := contentStore.Get(token, "did:example:123456789abcdefghi", Collection) @@ -531,7 +573,7 @@ func TestContentStores(t *testing.T) { t.Run("remove from store - success", func(t *testing.T) { sp := getMockStorageProvider() - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -560,7 +602,7 @@ func TestContentStores(t *testing.T) { sp := getMockStorageProvider() sp.Store.ErrDelete = errors.New(sampleContenttErr) - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -620,7 +662,7 @@ func TestContentStore_GetAll(t *testing.T) { t.Run("get all content from store for credential type - success", func(t *testing.T) { sp := getMockStorageProvider() - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -657,7 +699,7 @@ func TestContentStore_GetAll(t *testing.T) { sp := getMockStorageProvider() // wallet locked - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) allVcs, err := contentStore.GetAll(token, Credential) require.True(t, errors.Is(err, ErrWalletLocked)) @@ -676,7 +718,7 @@ func TestContentStore_GetAll(t *testing.T) { // iterator value error sp.MockStoreProvider.Store.ErrKey = errors.New(sampleContenttErr + uuid.New().String()) - contentStore = newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore = newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -687,7 +729,7 @@ func TestContentStore_GetAll(t *testing.T) { // iterator next error sp.MockStoreProvider.Store.ErrNext = errors.New(sampleContenttErr + uuid.New().String()) - contentStore = newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore = newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -700,7 +742,7 @@ func TestContentStore_GetAll(t *testing.T) { // iterator next error sp.MockStoreProvider.Store.ErrQuery = errors.New(sampleContenttErr + uuid.New().String()) - contentStore = newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore = newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -719,7 +761,7 @@ func TestContentDIDResolver(t *testing.T) { t.Run("create new content store - success", func(t *testing.T) { sp := getMockStorageProvider() - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -745,7 +787,7 @@ func TestContentDIDResolver(t *testing.T) { t.Run("create new content store - errors", func(t *testing.T) { sp := getMockStorageProvider() - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) contentVDR := newContentBasedVDR(token, &vdr.MockVDRegistry{}, contentStore) @@ -843,7 +885,7 @@ func TestContentStore_Collections(t *testing.T) { t.Run("contents by collection - success", func(t *testing.T) { sp := getMockStorageProvider() - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -909,7 +951,7 @@ func TestContentStore_Collections(t *testing.T) { t.Run("contents by collection - failure", func(t *testing.T) { sp := getMockStorageProvider() - contentStore := newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore := newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -931,7 +973,7 @@ func TestContentStore_Collections(t *testing.T) { // get content error sp.MockStoreProvider.Store.ErrGet = errors.New(sampleContenttErr + uuid.New().String()) - contentStore = newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore = newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -942,7 +984,7 @@ func TestContentStore_Collections(t *testing.T) { // iterator value error sp.MockStoreProvider.Store.ErrValue = errors.New(sampleContenttErr + uuid.New().String()) - contentStore = newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore = newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -953,7 +995,7 @@ func TestContentStore_Collections(t *testing.T) { // iterator value error sp.MockStoreProvider.Store.ErrKey = errors.New(sampleContenttErr + uuid.New().String()) - contentStore = newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore = newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -964,7 +1006,7 @@ func TestContentStore_Collections(t *testing.T) { // iterator next error sp.MockStoreProvider.Store.ErrNext = errors.New(sampleContenttErr + uuid.New().String()) - contentStore = newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore = newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -975,7 +1017,7 @@ func TestContentStore_Collections(t *testing.T) { // iterator next error sp.MockStoreProvider.Store.ErrQuery = errors.New(sampleContenttErr + uuid.New().String()) - contentStore = newContentStore(sp, &profile{ID: uuid.New().String()}) + contentStore = newContentStore(sp, createTestDocumentLoader(t), &profile{ID: uuid.New().String()}) require.NotEmpty(t, contentStore) require.NoError(t, contentStore.Open(keyMgr, &unlockOpts{})) @@ -1010,3 +1052,12 @@ func (s *mockStorageProvider) GetStoreConfig(name string) (storage.StoreConfigur func getMockStorageProvider() *mockStorageProvider { return &mockStorageProvider{MockStoreProvider: mockstorage.NewMockStoreProvider()} } + +func createTestDocumentLoader(t *testing.T) *ld.DocumentLoader { + t.Helper() + + loader, err := ldtestutil.DocumentLoader() + require.NoError(t, err) + + return loader +} diff --git a/pkg/wallet/options.go b/pkg/wallet/options.go index 07623eeadd..052ad2e85f 100644 --- a/pkg/wallet/options.go +++ b/pkg/wallet/options.go @@ -272,6 +272,9 @@ type AddContentOptions func(opts *addContentOpts) type addContentOpts struct { // ID of the collection to which the content belongs. collectionID string + + // indicated if the model of data saved into the wallet should be validated. + validateDataModel bool } // AddByCollection option for grouping wallet contents by collection ID. @@ -281,6 +284,13 @@ func AddByCollection(collectionID string) AddContentOptions { } } +// ValidateContent enables data model validations of adding content. +func ValidateContent() AddContentOptions { + return func(opts *addContentOpts) { + opts.validateDataModel = true + } +} + // GetAllContentsOptions is option for getting all contents from wallet. type GetAllContentsOptions func(opts *getAllContentsOpts) diff --git a/pkg/wallet/wallet.go b/pkg/wallet/wallet.go index ee70c0a4a2..a50ec5dc48 100644 --- a/pkg/wallet/wallet.go +++ b/pkg/wallet/wallet.go @@ -209,7 +209,7 @@ func New(userID string, ctx provider) (*Wallet, error) { profile: profile, storeProvider: ctx.StorageProvider(), walletCrypto: ctx.Crypto(), - contents: newContentStore(ctx.StorageProvider(), profile), + contents: newContentStore(ctx.StorageProvider(), ctx.JSONLDDocumentLoader(), profile), vdr: ctx.VDRegistry(), jsonldDocumentLoader: ctx.JSONLDDocumentLoader(), presentProofClient: presentProofClient, diff --git a/pkg/wallet/wallet_test.go b/pkg/wallet/wallet_test.go index ecb29a5a97..4385b3e46b 100644 --- a/pkg/wallet/wallet_test.go +++ b/pkg/wallet/wallet_test.go @@ -721,7 +721,7 @@ func TestWallet_OpenClose(t *testing.T) { // corrupt content store wallet.contents = newContentStore(&mockstorage.MockStoreProvider{ ErrOpenStoreHandle: fmt.Errorf(sampleWalletErr), - }, wallet.profile) + }, createTestDocumentLoader(t), wallet.profile) // get token token, err := wallet.Open(WithUnlockByAuthorizationToken(sampleRemoteKMSAuth))