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

feat: validate data model of objects saved in a wallet. #3261

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion pkg/controller/command/vcwallet/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ type Config struct {
// Default token expiry for all wallet profiles created.
// Will be used only if wallet unlock request doesn't supply default timeout value.
DefaultTokenExpiry time.Duration
// Indicate if a data model of json-ld content stored in the wallet should be validated.
ValidateDataModel bool
}

// provider contains dependencies for the verifiable credential wallet command controller
Expand Down Expand Up @@ -415,7 +417,15 @@ func (o *Command) Add(rw io.Writer, req io.Reader) command.Error {
return command.NewExecuteError(AddToWalletErrorCode, err)
}

err = vcWallet.Add(request.Auth, request.ContentType, request.Content, wallet.AddByCollection(request.CollectionID))
addOpts := []wallet.AddContentOptions{
wallet.AddByCollection(request.CollectionID),
}

if o.config.ValidateDataModel {
addOpts = append(addOpts, wallet.ValidateContent())
}

err = vcWallet.Add(request.Auth, request.ContentType, request.Content, addOpts...)
if err != nil {
logutil.LogInfo(logger, CommandName, AddMethod, err.Error())

Expand Down
29 changes: 27 additions & 2 deletions pkg/controller/command/vcwallet/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -716,8 +716,8 @@ func TestCommand_AddRemoveGetGetAll(t *testing.T) {
require.NoError(t, cmdErr)
})

t.Run("add a metadata to wallet", func(t *testing.T) {
cmd := New(mockctx, &Config{})
t.Run("add a metadata to wallet with validation", func(t *testing.T) {
cmd := New(mockctx, &Config{ValidateDataModel: true})

var b bytes.Buffer

Expand Down Expand Up @@ -779,6 +779,31 @@ func TestCommand_AddRemoveGetGetAll(t *testing.T) {
require.Len(t, response.Contents, count)
})

t.Run("add a collection to wallet with validation failed", func(t *testing.T) {
const orgCollectionWithInvalidStructure = `{
"@context": ["https://w3id.org/wallet/v1"],
"id": "did:example:acme123456789abcdefghi",
"type": "Organization",
"name": "Acme Corp.",
"image": "https://via.placeholder.com/150",
"description" : "A software company.",
"tags": ["professional", "organization"],
"incorrectProp": "incorrectProp",
"correlation": ["4058a72a-9523-11ea-bb37-0242ac130002"]
}`

cmd := New(mockctx, &Config{ValidateDataModel: true})

var b bytes.Buffer

cmdErr := cmd.Add(&b, getReader(t, &AddContentRequest{
Content: []byte(orgCollectionWithInvalidStructure),
ContentType: wallet.Collection,
WalletAuth: WalletAuth{UserID: sampleUser1, Auth: token1},
}))
require.Contains(t, cmdErr.Error(), "JSON-LD doc has different structure after compaction")
})

t.Run("get all credentials from wallet by collection ID", func(t *testing.T) {
const orgCollection = `{
"@context": ["https://w3id.org/wallet/v1"],
Expand Down
58 changes: 58 additions & 0 deletions pkg/doc/jsonld/testdata/context/wallet_v1.jsonld
Original file line number Diff line number Diff line change
@@ -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"
}
}
]
}
189 changes: 189 additions & 0 deletions pkg/doc/jsonld/validate.go
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading