diff --git a/pkg/client/vcwallet/client.go b/pkg/client/vcwallet/client.go index 8c1058c42..32dbfe7b8 100644 --- a/pkg/client/vcwallet/client.go +++ b/pkg/client/vcwallet/client.go @@ -10,51 +10,19 @@ import ( "encoding/json" "errors" "fmt" - "strings" - "time" - - "github.com/piprate/json-gold/ld" + "github.com/hyperledger/aries-framework-go/pkg/common/log" "github.com/hyperledger/aries-framework-go/pkg/crypto" - "github.com/hyperledger/aries-framework-go/pkg/doc/did" - jld "github.com/hyperledger/aries-framework-go/pkg/doc/jsonld" - "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/signer" - "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/signature/suite/ed25519signature2018" - "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/jsonwebsignature2020" "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" - "github.com/hyperledger/aries-framework-go/pkg/secretlock" + "github.com/hyperledger/aries-framework-go/pkg/wallet" "github.com/hyperledger/aries-framework-go/spi/storage" ) -// Proof types. -const ( - // Ed25519Signature2018 ed25519 signature suite. - Ed25519Signature2018 = "Ed25519Signature2018" - // JSONWebSignature2020 json web signature suite. - JSONWebSignature2020 = "JsonWebSignature2020" - // BbsBlsSignature2020 BBS signature suite. - BbsBlsSignature2020 = "BbsBlsSignature2020" -) +var logger = log.New("aries-framework/client/vcwallet") -// miscellaneous constants. -const ( - bbsContext = "https://w3id.org/security/bbs/v1" -) - -// proof options. -// nolint:gochecknoglobals -var ( - defaultSignatureRepresentation = verifiable.SignatureJWS - supportedRelationships = map[did.VerificationRelationship]string{ - did.Authentication: "authentication", - did.AssertionMethod: "assertionMethod", - } -) +// ErrWalletLocked when key manager related operation attempted on locked wallet. +var ErrWalletLocked = errors.New("wallet locked") // provider contains dependencies for the verifiable credential wallet client // and is typically created by using aries.Context(). @@ -64,154 +32,91 @@ type provider interface { Crypto() crypto.Crypto } -type provable interface { - AddLinkedDataProof(context *verifiable.LinkedDataProofContext, jsonldOpts ...jsonld.ProcessorOpts) error -} - -// kmsOpts contains options for creating verifiable credential wallet client. -type kmsOpts struct { - // local kms options - secretLockSvc secretlock.Service - passphrase string - - // remote(web) kms options - keyServerURL string -} - -// KeyManagerOptions is option for verifiable credential wallet client key manager. -type KeyManagerOptions func(opts *kmsOpts) - -// WithSecretLockService option, when provided then wallet client will use local kms for key operations. -func WithSecretLockService(svc secretlock.Service) KeyManagerOptions { - return func(opts *kmsOpts) { - opts.secretLockSvc = svc - } -} - -// WithPassphrase option to provide passphrase for local kms for key operations. -func WithPassphrase(passphrase string) KeyManagerOptions { - return func(opts *kmsOpts) { - opts.passphrase = passphrase - } -} +// walletAuth is auth function which returns wallet unlock token. +type walletAuth func() (string, error) -// WithKeyServerURL option, when provided then wallet client will use remote kms for key operations. -// This option will be ignore if provided with 'WithSecretLockService' option. -func WithKeyServerURL(url string) KeyManagerOptions { - return func(opts *kmsOpts) { - opts.keyServerURL = url - } -} +// noAuth default auth when wallet is still locked. +// nolint:gochecknoglobals +var noAuth walletAuth = func() (string, error) { return "", ErrWalletLocked } // Client enable access to verifiable credential wallet features. type Client struct { - // ID of wallet content owner - userID string - - // wallet profile - profile *profile - - // wallet content store - contents *contentStore - - // storage provider - ctx provider + wallet *wallet.Wallet + auth walletAuth } // New returns new verifiable credential wallet client for given user. +// +// Args: +// - userID : unique user identifier used for login. +// - provider: dependencies for the verifiable credential wallet client. +// - options : options for unlocking wallet. Any other existing wallet instance of same wallet user will be locked +// once this instance is unlocked. +// // returns error if wallet profile is not found. // To create a new wallet profile, use `CreateProfile()`. // To update an existing profile, use `UpdateProfile()`. -func New(userID string, ctx provider) (*Client, error) { - store, err := newProfileStore(ctx.StorageProvider()) +func New(userID string, ctx provider, options ...wallet.UnlockOptions) (*Client, error) { + w, err := wallet.New(userID, ctx) if err != nil { - return nil, fmt.Errorf("failed to get store to fetch VC wallet profile info: %w", err) + return nil, err } - profile, err := store.get(userID) - if err != nil { - return nil, fmt.Errorf("failed to get VC wallet profile: %w", err) - } + client := &Client{wallet: w, auth: noAuth} - contents, err := newContentStore(ctx.StorageProvider(), profile) - if err != nil { - return nil, fmt.Errorf("failed to get wallet content store: %w", err) + if len(options) > 0 { + if client.Close() { + logger.Debugf("wallet was already open, existing wallet instance key manager is now closed") + } + + err = client.Open(options...) + if err != nil { + return nil, err + } } - return &Client{userID: userID, profile: profile, ctx: ctx, contents: contents}, nil + return client, nil } // CreateProfile creates a new verifiable credential wallet profile for given user. // returns error if wallet profile is already created. // Use `UpdateProfile()` for replacing an already created verifiable credential wallet profile. -func CreateProfile(userID string, ctx provider, options ...KeyManagerOptions) error { - return createOrUpdate(userID, ctx, false, options...) +func CreateProfile(userID string, ctx provider, options ...wallet.ProfileKeyManagerOptions) error { + return wallet.CreateProfile(userID, ctx, options...) } // UpdateProfile updates existing verifiable credential wallet profile. // Will create new profile if no profile exists for given user. // Caution: you might lose your existing keys if you change kms options. -func UpdateProfile(userID string, ctx provider, options ...KeyManagerOptions) error { - return createOrUpdate(userID, ctx, true, options...) +func UpdateProfile(userID string, ctx provider, options ...wallet.ProfileKeyManagerOptions) error { + return wallet.UpdateProfile(userID, ctx, options...) } -func createOrUpdate(userID string, ctx provider, update bool, options ...KeyManagerOptions) error { - opts := &kmsOpts{} - - for _, opt := range options { - opt(opts) - } - - store, err := newProfileStore(ctx.StorageProvider()) +// Open unlocks wallet client's key manager instance and returns a token for subsequent use of wallet features. +// +// Args: +// - unlock options for opening wallet. +// +// Returns token with expiry that can be used for subsequent use of wallet features. +func (c *Client) Open(options ...wallet.UnlockOptions) error { + authToken, err := c.wallet.Open(options...) if err != nil { - return fmt.Errorf("failed to get store to save VC wallet profile: %w", err) + return err } - var profile *profile - - if update { - // find existing profile and update it. - profile, err = store.get(userID) - if err != nil { - return fmt.Errorf("failed to update wallet user profile: %w", err) - } - - err = profile.setKMSOptions(opts.passphrase, opts.secretLockSvc, opts.keyServerURL) - if err != nil { - return fmt.Errorf("failed to update wallet user profile KMS options: %w", err) - } - } else { - // create new profile. - profile, err = createProfile(userID, opts.passphrase, opts.secretLockSvc, opts.keyServerURL) - if err != nil { - return fmt.Errorf("failed to create new wallet user profile: %w", err) - } - } - - err = store.save(profile, update) - if err != nil { - return fmt.Errorf("failed to save VC wallet profile: %w", err) + c.auth = func() (s string, e error) { + return authToken, nil } return nil } -// Open unlocks wallet client's key manager instance and returns a token for subsequent use of wallet features. -// -// Args: -// - auth : auth token in case of remotekms or passphrase in case of localkms. -// - secretLockSvc: secret lock service for localkms if you choose not to provide passphrase. -// - tokenExpiry : (optional, default: 10 * time.minute) time duration after which issued token will expiry. -// -// Returns token with expiry that can be used for subsequent use of wallet features. -func (c *Client) Open(auth string, secretLockSvc secretlock.Service, tokenExpiry time.Duration) (string, error) { - return keyManager().createKeyManager(c.profile, c.ctx.StorageProvider(), auth, secretLockSvc, tokenExpiry) -} - // Close expires token issued to this VC wallet client. // returns false if token is not found or already expired for this wallet user. func (c *Client) Close() bool { - return keyManager().removeKeyManager(c.userID) + c.auth = noAuth + + return c.wallet.Close() } // Export produces a serialized exported wallet representation. @@ -260,8 +165,8 @@ func (c *Client) Import(auth string, contents json.RawMessage) error { // - https://w3c-ccg.github.io/universal-wallet-interop-spec/#connection // // TODO: (#2433) support for correlation between wallet contents (ex: credentials to a profile/collection). -func (c *Client) Add(contentType ContentType, content json.RawMessage) error { - return c.contents.Save(contentType, content) +func (c *Client) Add(contentType wallet.ContentType, content json.RawMessage) error { + return c.wallet.Add(contentType, content) } // Remove removes wallet content by content ID. @@ -273,8 +178,8 @@ func (c *Client) Add(contentType ContentType, content json.RawMessage) error { // - https://w3c-ccg.github.io/universal-wallet-interop-spec/#meta-data // - https://w3c-ccg.github.io/universal-wallet-interop-spec/#connection // -func (c *Client) Remove(contentType ContentType, contentID string) error { - return c.contents.Remove(contentType, contentID) +func (c *Client) Remove(contentType wallet.ContentType, contentID string) error { + return c.wallet.Remove(contentType, contentID) } // Get fetches a wallet content by content ID. @@ -286,8 +191,8 @@ func (c *Client) Remove(contentType ContentType, contentID string) error { // - https://w3c-ccg.github.io/universal-wallet-interop-spec/#meta-data // - https://w3c-ccg.github.io/universal-wallet-interop-spec/#connection // -func (c *Client) Get(contentType ContentType, contentID string) (json.RawMessage, error) { - return c.contents.Get(contentType, contentID) +func (c *Client) Get(contentType wallet.ContentType, contentID string) (json.RawMessage, error) { + return c.wallet.Get(contentType, contentID) } // Query returns a collection of results based on current wallet contents. @@ -296,7 +201,7 @@ func (c *Client) Get(contentType ContentType, contentID string) (json.RawMessage // - https://www.w3.org/TR/json-ld11-framing // - https://identity.foundation/presentation-exchange // -func (c *Client) Query(query *QueryParams) ([]json.RawMessage, error) { +func (c *Client) Query(query *wallet.QueryParams) ([]json.RawMessage, error) { // TODO to be added #2433 return nil, fmt.Errorf("to be implemented") } @@ -307,26 +212,14 @@ func (c *Client) Query(query *QueryParams) ([]json.RawMessage, error) { // - A verifiable credential with or without proof // - Proof options // -func (c *Client) Issue(authToken string, credential json.RawMessage, - options *ProofOptions) (*verifiable.Credential, error) { - vc, err := verifiable.ParseCredential(credential, verifiable.WithDisabledProofCheck()) +func (c *Client) Issue(credential json.RawMessage, + options *wallet.ProofOptions) (*verifiable.Credential, error) { + auth, err := c.auth() if err != nil { - return nil, fmt.Errorf("failed to parse credential: %w", err) - } - - purpose := did.AssertionMethod - - err = c.validateProofOption(options, purpose) - if err != nil { - return nil, fmt.Errorf("failed to prepare proof: %w", err) - } - - err = c.addLinkedDataProof(authToken, vc, options, purpose) - if err != nil { - return nil, fmt.Errorf("failed to issue credential: %w", err) + return nil, err } - return vc, nil + return c.wallet.Issue(auth, credential, options) } // Prove produces a Verifiable Presentation. @@ -335,7 +228,7 @@ func (c *Client) Issue(authToken string, credential json.RawMessage, // - List of verifiable credentials IDs. // - Proof options // -func (c *Client) Prove(credentialIDs []string, options *ProofOptions) (json.RawMessage, error) { +func (c *Client) Prove(credentialIDs []string, options *wallet.ProofOptions) (json.RawMessage, error) { // TODO to be added #2433 return nil, fmt.Errorf("to be implemented") } @@ -350,244 +243,3 @@ func (c *Client) Verify(raw json.RawMessage) (bool, error) { // TODO to be added #2433 return false, fmt.Errorf("to be implemented") } - -func (c *Client) addLinkedDataProof(authToken string, p provable, opts *ProofOptions, - relationship did.VerificationRelationship) error { - s, err := newKMSSigner(authToken, c.ctx.Crypto(), opts) - if err != nil { - return err - } - - var signatureSuite signer.SignatureSuite - - var processorOpts []jsonld.ProcessorOpts - - switch opts.ProofType { - case Ed25519Signature2018: - signatureSuite = ed25519signature2018.New(suite.WithSigner(s)) - case JSONWebSignature2020: - signatureSuite = jsonwebsignature2020.New(suite.WithSigner(s)) - case BbsBlsSignature2020: - // TODO document loader to be part of common API, to be removed - bbsLoader, e := bbsJSONLDDocumentLoader() - if e != nil { - return e - } - - processorOpts = append(processorOpts, jsonld.WithDocumentLoader(bbsLoader)) - - addContext(p, bbsContext) - - signatureSuite = bbsblssignature2020.New(suite.WithSigner(s)) - default: - return fmt.Errorf("unsupported signature type '%s'", opts.ProofType) - } - - signingCtx := &verifiable.LinkedDataProofContext{ - VerificationMethod: opts.VerificationMethod, - SignatureRepresentation: *opts.ProofRepresentation, - SignatureType: opts.ProofType, - Suite: signatureSuite, - Created: opts.Created, - Domain: opts.Domain, - Challenge: opts.Challenge, - Purpose: supportedRelationships[relationship], - } - - err = p.AddLinkedDataProof(signingCtx, processorOpts...) - if err != nil { - return fmt.Errorf("failed to add linked data proof: %w", err) - } - - return nil -} - -func (c *Client) validateProofOption(opts *ProofOptions, method did.VerificationRelationship) error { - if opts == nil || opts.Controller == "" { - return errors.New("invalid proof option, 'controller' is required") - } - - didDoc, err := c.getDIDDocument(opts.Controller) - if err != nil { - return err - } - - err = c.validateVerificationMethod(didDoc, opts, method) - if err != nil { - return err - } - - if opts.ProofRepresentation == nil { - opts.ProofRepresentation = &defaultSignatureRepresentation - } - - if opts.ProofType == "" { - opts.ProofType = Ed25519Signature2018 - } - - return nil -} - -// TODO stored DIDResolution response & DID Doc metadata should be read first before trying to resolve using VDR. -func (c *Client) getDIDDocument(didID string) (*did.Doc, error) { - doc, err := c.ctx.VDRegistry().Resolve(didID) - // if DID not found in VDR, look through in wallet content storage. - if err != nil { - docBytes, err := c.contents.Get(DIDResolutionResponse, didID) - if err != nil { - return nil, fmt.Errorf("failed to read DID document from wallet store or from VDR: %w", err) - } - - doc, err = did.ParseDocumentResolution(docBytes) - if err != nil { - return nil, fmt.Errorf("failed to parse stored DID: %w", err) - } - - return doc.DIDDocument, nil - } - - return doc.DIDDocument, nil -} - -func (c *Client) validateVerificationMethod(didDoc *did.Doc, opts *ProofOptions, - relationship did.VerificationRelationship) error { - vms := didDoc.VerificationMethods(relationship)[relationship] - - for _, vm := range vms { - if opts.VerificationMethod != "" { - // if verification method is provided as an option, then validate if it belongs to given method. - if opts.VerificationMethod == vm.VerificationMethod.ID { - return nil - } - - continue - } else { - // by default first public key matching relationship. - opts.VerificationMethod = vm.VerificationMethod.ID - - return nil - } - } - - return fmt.Errorf("unable to find '%s' for given verification method", supportedRelationships[relationship]) -} - -// addContext adds context if not found in given data model. -func addContext(v interface{}, context string) { - if vc, ok := v.(*verifiable.Credential); ok { - for _, ctx := range vc.Context { - if ctx == context { - return - } - } - - vc.Context = append(vc.Context, context) - } -} - -// TODO: context should not be loaded here, the loader should be defined once for the whole system. -func bbsJSONLDDocumentLoader() (*jld.CachingDocumentLoader, error) { - loader := presexch.CachingJSONLDLoader() - - reader, err := ld.DocumentFromReader(strings.NewReader(contextBBSContent)) - if err != nil { - return nil, err - } - - loader.AddDocument(bbsContext, reader) - - return loader, nil -} - -const contextBBSContent = `{ - "@context": { - "@version": 1.1, - "id": "@id", - "type": "@type", - "BbsBlsSignature2020": { - "@id": "https://w3id.org/security#BbsBlsSignature2020", - "@context": { - "@version": 1.1, - "@protected": true, - "id": "@id", - "type": "@type", - "challenge": "https://w3id.org/security#challenge", - "created": { - "@id": "http://purl.org/dc/terms/created", - "@type": "http://www.w3.org/2001/XMLSchema#dateTime" - }, - "domain": "https://w3id.org/security#domain", - "proofValue": "https://w3id.org/security#proofValue", - "nonce": "https://w3id.org/security#nonce", - "proofPurpose": { - "@id": "https://w3id.org/security#proofPurpose", - "@type": "@vocab", - "@context": { - "@version": 1.1, - "@protected": true, - "id": "@id", - "type": "@type", - "assertionMethod": { - "@id": "https://w3id.org/security#assertionMethod", - "@type": "@id", - "@container": "@set" - }, - "authentication": { - "@id": "https://w3id.org/security#authenticationMethod", - "@type": "@id", - "@container": "@set" - } - } - }, - "verificationMethod": { - "@id": "https://w3id.org/security#verificationMethod", - "@type": "@id" - } - } - }, - "BbsBlsSignatureProof2020": { - "@id": "https://w3id.org/security#BbsBlsSignatureProof2020", - "@context": { - "@version": 1.1, - "@protected": true, - "id": "@id", - "type": "@type", - - "challenge": "https://w3id.org/security#challenge", - "created": { - "@id": "http://purl.org/dc/terms/created", - "@type": "http://www.w3.org/2001/XMLSchema#dateTime" - }, - "domain": "https://w3id.org/security#domain", - "nonce": "https://w3id.org/security#nonce", - "proofPurpose": { - "@id": "https://w3id.org/security#proofPurpose", - "@type": "@vocab", - "@context": { - "@version": 1.1, - "@protected": true, - "id": "@id", - "type": "@type", - "sec": "https://w3id.org/security#", - "assertionMethod": { - "@id": "https://w3id.org/security#assertionMethod", - "@type": "@id", - "@container": "@set" - }, - "authentication": { - "@id": "https://w3id.org/security#authenticationMethod", - "@type": "@id", - "@container": "@set" - } - } - }, - "proofValue": "https://w3id.org/security#proofValue", - "verificationMethod": { - "@id": "https://w3id.org/security#verificationMethod", - "@type": "@id" - } - } - }, - "Bls12381G2Key2020": "https://w3id.org/security#Bls12381G2Key2020" - } -}` diff --git a/pkg/client/vcwallet/client_test.go b/pkg/client/vcwallet/client_test.go index f45059dda..031e7bca9 100644 --- a/pkg/client/vcwallet/client_test.go +++ b/pkg/client/vcwallet/client_test.go @@ -7,22 +7,17 @@ SPDX-License-Identifier: Apache-2.0 package vcwallet import ( - "crypto/ed25519" "crypto/sha256" "errors" "fmt" "strings" "testing" - "time" - "github.com/btcsuite/btcutil/base58" + "github.com/google/uuid" "github.com/stretchr/testify/require" - "github.com/hyperledger/aries-framework-go/pkg/crypto/primitive/bbs12381g2pub" "github.com/hyperledger/aries-framework-go/pkg/doc/did" - "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" vdrapi "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" - "github.com/hyperledger/aries-framework-go/pkg/kms" cryptomock "github.com/hyperledger/aries-framework-go/pkg/mock/crypto" mockprovider "github.com/hyperledger/aries-framework-go/pkg/mock/provider" "github.com/hyperledger/aries-framework-go/pkg/mock/secretlock" @@ -30,19 +25,29 @@ import ( mockvdr "github.com/hyperledger/aries-framework-go/pkg/mock/vdr" "github.com/hyperledger/aries-framework-go/pkg/secretlock/local/masterlock/pbkdf2" "github.com/hyperledger/aries-framework-go/pkg/vdr/key" + "github.com/hyperledger/aries-framework-go/pkg/wallet" "github.com/hyperledger/aries-framework-go/spi/storage" ) -// nolint: lll const ( - sampleUserID = "sample-user01" - sampleFakeTkn = "fake-auth-tkn" - toBeImplementedErr = "to be implemented" - sampleClientErr = "sample client err" - sampleCreatedDate = "2020-12-25" - sampleChallenge = "sample-challenge" - sampleDomain = "sample-domain" - sampleUDCVC = `{ + samplePassPhrase = "fakepassphrase" + sampleRemoteKMSAuth = "sample-auth-token" + sampleKeyServerURL = "sample/keyserver/test" + sampleUserID = "sample-user01" + toBeImplementedErr = "to be implemented" + sampleClientErr = "sample client err" + sampleDIDKey = "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5" + sampleContentValid = `{ + "@context": ["https://w3id.org/wallet/v1"], + "id": "did:example:123456789abcdefghi", + "type": "Person", + "name": "John Smith", + "image": "https://via.placeholder.com/150", + "description" : "Professional software developer for Acme Corp.", + "tags": ["professional", "person"], + "correlation": ["4058a72a-9523-11ea-bb37-0242ac130002"] + }` + sampleUDCVC = `{ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1", @@ -71,100 +76,37 @@ const ( "UniversityDegreeCredential" ] }` - sampleInvalidDIDID = "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHdI" - sampleInvalidDID = `{ - "@context": ["https://w3id.org/did/v1"], - "id": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHdI", - "verificationMethod": [{ - "controller": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - "id": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - "publicKeyBase58": "5yKdnU7ToTjAoRNDzfuzVTfWBH38qyhE1b9xh4v8JaWF", - "type": "Ed25519VerificationKey2018" - }], - "capabilityDelegation": ["did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd"], - "capabilityInvocation": ["did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd"], - "keyAgreement": [{ - "controller": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - "id": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6LShKMZ117txS1WuExddVM2rbJ2zy3AKFtZVY5WNi44aKzA", - "publicKeyBase58": "6eBPUhK2ryHmoras6qq5Y15Z9pW3ceiQcZMptFQXrxDQ", - "type": "X25519KeyAgreementKey2019" - }], - "created": "2021-03-23T16:23:39.682869-04:00", - "updated": "2021-03-23T16:23:39.682869-04:00" - }` - sampleInvalidDIDContent = `{ - "@context": ["https://w3id.org/did/v1"], - "id": "did:example:sampleInvalidDIDContent" - }` - - sampleDocResolutionResponse = `{ - "@context": [ - "https://w3id.org/wallet/v1", - "https://w3id.org/did-resolution/v1" - ], - "id": "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5", - "type": ["DIDResolutionResponse"], - "name": "Farming Sensor DID Document", - "image": "https://via.placeholder.com/150", - "description": "An IoT device in the middle of a corn field.", - "tags": ["professional"], - "correlation": ["4058a72a-9523-11ea-bb37-0242ac130002"], - "created": "2017-06-18T21:19:10Z", - "expires": "2026-06-18T21:19:10Z", - "didDocument": { - "@context": ["https://w3id.org/did/v1"], - "id": "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5", - "verificationMethod": [{ - "controller": "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5", - "id": "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5#z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5", - "publicKeyBase58": "8jkuMBqmu1TRA6is7TT5tKBksTZamrLhaXrg9NAczqeh", - "type": "Ed25519VerificationKey2018" - }], - "authentication": ["did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5#z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5"], - "assertionMethod": ["did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5#z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5"], - "capabilityDelegation": ["did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5#z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5"], - "capabilityInvocation": ["did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5#z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5"], - "keyAgreement": [{ - "controller": "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5", - "id": "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5#z6LSmjNfS5FC9W59JtPZq7fHgrjThxsidjEhZeMxCarbR998", - "publicKeyBase58": "B4CVumSL43MQDW1oJU9LNGWyrpLbw84YgfeGi8D4hmNN", - "type": "X25519KeyAgreementKey2019" - }], - "created": "2021-03-23T19:25:18.513655-04:00", - "updated": "2021-03-23T19:25:18.513655-04:00" - } - }` ) -func TestCreate(t *testing.T) { +func TestCreateProfile(t *testing.T) { t.Run("test create new wallet client using local kms passphrase", func(t *testing.T) { mockctx := newMockProvider() - err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + err := CreateProfile(sampleUserID, mockctx, wallet.WithPassphrase(samplePassPhrase)) require.NoError(t, err) - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUserID, mockctx) require.NoError(t, err) - require.NotEmpty(t, wallet) + require.NotEmpty(t, vcWallet) }) t.Run("test create new wallet client using local kms secret lock service", func(t *testing.T) { mockctx := newMockProvider() - err := CreateProfile(sampleUserID, mockctx, WithSecretLockService(&secretlock.MockSecretLock{})) + err := CreateProfile(sampleUserID, mockctx, wallet.WithSecretLockService(&secretlock.MockSecretLock{})) require.NoError(t, err) - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUserID, mockctx) require.NoError(t, err) - require.NotEmpty(t, wallet) + require.NotEmpty(t, vcWallet) }) t.Run("test create new wallet client using remote kms key server URL", func(t *testing.T) { mockctx := newMockProvider() - err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + err := CreateProfile(sampleUserID, mockctx, wallet.WithKeyServerURL(sampleKeyServerURL)) require.NoError(t, err) - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUserID, mockctx) require.NoError(t, err) - require.NotEmpty(t, wallet) + require.NotEmpty(t, vcWallet) }) t.Run("test create new wallet failure", func(t *testing.T) { @@ -173,9 +115,9 @@ func TestCreate(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "invalid create profile options") - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUserID, mockctx) require.Error(t, err) - require.Empty(t, wallet) + require.Empty(t, vcWallet) }) t.Run("test create new wallet failure - create store error", func(t *testing.T) { @@ -184,13 +126,13 @@ func TestCreate(t *testing.T) { ErrOpenStoreHandle: fmt.Errorf(sampleClientErr), } - err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + err := CreateProfile(sampleUserID, mockctx, wallet.WithKeyServerURL(sampleKeyServerURL)) require.Error(t, err) require.Contains(t, err.Error(), sampleClientErr) - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUserID, mockctx) require.Error(t, err) - require.Empty(t, wallet) + require.Empty(t, vcWallet) }) t.Run("test create new wallet failure - save profile error", func(t *testing.T) { @@ -201,13 +143,13 @@ func TestCreate(t *testing.T) { }, } - err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + err := CreateProfile(sampleUserID, mockctx, wallet.WithKeyServerURL(sampleKeyServerURL)) require.Error(t, err) require.Contains(t, err.Error(), sampleClientErr) - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUserID, mockctx) require.Error(t, err) - require.Empty(t, wallet) + require.Empty(t, vcWallet) }) t.Run("test create new wallet failure - create content store error", func(t *testing.T) { @@ -217,12 +159,12 @@ func TestCreate(t *testing.T) { failure: fmt.Errorf(sampleClientErr), } - err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + err := CreateProfile(sampleUserID, mockctx, wallet.WithKeyServerURL(sampleKeyServerURL)) require.NoError(t, err) - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUserID, mockctx) require.Error(t, err) - require.Empty(t, wallet) + require.Empty(t, vcWallet) require.Contains(t, err.Error(), "failed to get wallet content store:") }) } @@ -232,39 +174,36 @@ func TestUpdate(t *testing.T) { mockctx := newMockProvider() createSampleProfile(t, mockctx) - err := UpdateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + err := UpdateProfile(sampleUserID, mockctx, wallet.WithPassphrase(samplePassPhrase)) require.NoError(t, err) - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUserID, mockctx) require.NoError(t, err) - require.NotEmpty(t, wallet) - require.NotEmpty(t, wallet.profile.MasterLockCipher) + require.NotEmpty(t, vcWallet) }) t.Run("test update wallet client using local kms secret lock service", func(t *testing.T) { mockctx := newMockProvider() createSampleProfile(t, mockctx) - err := UpdateProfile(sampleUserID, mockctx, WithSecretLockService(&secretlock.MockSecretLock{})) + err := UpdateProfile(sampleUserID, mockctx, wallet.WithSecretLockService(&secretlock.MockSecretLock{})) require.NoError(t, err) - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUserID, mockctx) require.NoError(t, err) - require.NotEmpty(t, wallet) + require.NotEmpty(t, vcWallet) }) t.Run("test update wallet client using remote kms key server URL", func(t *testing.T) { mockctx := newMockProvider() createSampleProfile(t, mockctx) - err := UpdateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + err := UpdateProfile(sampleUserID, mockctx, wallet.WithKeyServerURL(sampleKeyServerURL)) require.NoError(t, err) - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUserID, mockctx) require.NoError(t, err) - require.NotEmpty(t, wallet) - require.Empty(t, wallet.profile.MasterLockCipher) - require.NotEmpty(t, wallet.profile.KeyServerURL) + require.NotEmpty(t, vcWallet) }) t.Run("test update wallet failure", func(t *testing.T) { @@ -275,10 +214,9 @@ func TestUpdate(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "invalid create profile options") - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUserID, mockctx) require.NoError(t, err) - require.NotEmpty(t, wallet) - require.NotEmpty(t, wallet.profile.MasterLockCipher) + require.NotEmpty(t, vcWallet) }) t.Run("test update wallet failure - profile doesn't exists", func(t *testing.T) { @@ -287,9 +225,9 @@ func TestUpdate(t *testing.T) { require.Error(t, err) require.Contains(t, err.Error(), "profile does not exist") - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUserID, mockctx) require.Error(t, err) - require.Empty(t, wallet) + require.Empty(t, vcWallet) }) t.Run("test update wallet failure - create store error", func(t *testing.T) { @@ -298,13 +236,13 @@ func TestUpdate(t *testing.T) { ErrOpenStoreHandle: fmt.Errorf(sampleClientErr), } - err := UpdateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + err := UpdateProfile(sampleUserID, mockctx, wallet.WithKeyServerURL(sampleKeyServerURL)) require.Error(t, err) require.Contains(t, err.Error(), sampleClientErr) - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUserID, mockctx) require.Error(t, err) - require.Empty(t, wallet) + require.Empty(t, vcWallet) }) t.Run("test update wallet failure - save profile error", func(t *testing.T) { @@ -313,37 +251,62 @@ func TestUpdate(t *testing.T) { mockctx.StorageProviderValue.(*mockstorage.MockStoreProvider).Store.ErrPut = fmt.Errorf(sampleClientErr) - err := UpdateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + err := UpdateProfile(sampleUserID, mockctx, wallet.WithKeyServerURL(sampleKeyServerURL)) require.Error(t, err) require.Contains(t, err.Error(), sampleClientErr) - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUserID, mockctx) require.NoError(t, err) - require.NotEmpty(t, wallet) - require.Empty(t, wallet.profile.KeyServerURL) - require.NotEmpty(t, wallet.profile.MasterLockCipher) + require.NotEmpty(t, vcWallet) }) } func TestNew(t *testing.T) { - t.Run("test get client by user", func(t *testing.T) { + t.Run("test get client", func(t *testing.T) { mockctx := newMockProvider() // create a wallet - err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + err := CreateProfile(sampleUserID, mockctx, wallet.WithPassphrase(samplePassPhrase)) require.NoError(t, err) - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUserID, mockctx) require.NoError(t, err) - require.NotEmpty(t, wallet) + require.NotEmpty(t, vcWallet) + }) + + t.Run("test get client unlocked", func(t *testing.T) { + mockctx := newMockProvider() + // create a wallet + err := CreateProfile(sampleUserID, mockctx, wallet.WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + vcWallet, err := New(sampleUserID, mockctx, wallet.WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, vcWallet) + + token, err := vcWallet.auth() + require.NoError(t, err) + require.NotEmpty(t, token) + }) + + t.Run("test get client unlock failure - wrong passphrase", func(t *testing.T) { + mockctx := newMockProvider() + // create a wallet + err := CreateProfile(sampleUserID, mockctx, wallet.WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + vcWallet, err := New(sampleUserID, mockctx, wallet.WithUnlockByPassphrase(samplePassPhrase+"ss")) + require.Error(t, err) + require.Contains(t, err.Error(), "message authentication failed") + require.Empty(t, vcWallet) }) t.Run("test get client by invalid userID", func(t *testing.T) { mockctx := newMockProvider() - err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + err := CreateProfile(sampleUserID, mockctx, wallet.WithPassphrase(samplePassPhrase)) require.NoError(t, err) - wallet, err := New(sampleUserID+"invalid", mockctx) - require.Empty(t, wallet) + vcWallet, err := New(sampleUserID+"invalid", mockctx) + require.Empty(t, vcWallet) require.Error(t, err) require.Contains(t, err.Error(), "profile does not exist") }) @@ -354,111 +317,145 @@ func TestNew(t *testing.T) { ErrOpenStoreHandle: fmt.Errorf(sampleClientErr), } - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUserID, mockctx) require.Error(t, err) - require.Empty(t, wallet) + require.Empty(t, vcWallet) require.Contains(t, err.Error(), sampleClientErr) }) } func TestClient_OpenClose(t *testing.T) { t.Run("test open & close wallet using local kms passphrase", func(t *testing.T) { + sampleUser := uuid.New().String() mockctx := newMockProvider() - err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + + err := CreateProfile(sampleUser, mockctx, wallet.WithPassphrase(samplePassPhrase)) require.NoError(t, err) - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUser, mockctx) require.NoError(t, err) - require.NotEmpty(t, wallet) + require.NotEmpty(t, vcWallet) // get token - token, err := wallet.Open(samplePassPhrase, nil, 0) + err = vcWallet.Open(wallet.WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + token, err := vcWallet.auth() require.NoError(t, err) require.NotEmpty(t, token) + defer vcWallet.Close() + // try again - token, err = wallet.Open(samplePassPhrase, nil, 0) - require.Empty(t, token) + err = vcWallet.Open(wallet.WithUnlockByPassphrase(samplePassPhrase)) require.Error(t, err) - require.Equal(t, err, ErrAlreadyUnlocked) + require.True(t, errors.Is(err, wallet.ErrAlreadyUnlocked)) + token, err = vcWallet.auth() + require.NoError(t, err) + require.NotEmpty(t, token) // close wallet - require.True(t, wallet.Close()) - require.False(t, wallet.Close()) + require.True(t, vcWallet.Close()) + require.False(t, vcWallet.Close()) // try to open with wrong passphrase - token, err = wallet.Open(samplePassPhrase+"wrong", nil, 0) - require.Empty(t, token) + err = vcWallet.Open(wallet.WithUnlockByPassphrase(samplePassPhrase + "wrong")) require.Error(t, err) require.Contains(t, err.Error(), "message authentication failed") + token, err = vcWallet.auth() + require.Empty(t, token) + require.Error(t, err) + require.True(t, errors.Is(err, ErrWalletLocked)) }) t.Run("test open & close wallet using secret lock service", func(t *testing.T) { + sampleUser := uuid.New().String() mockctx := newMockProvider() + masterLock, err := pbkdf2.NewMasterLock(samplePassPhrase, sha256.New, 0, nil) require.NoError(t, err) - err = CreateProfile(sampleUserID, mockctx, WithSecretLockService(masterLock)) + err = CreateProfile(sampleUser, mockctx, wallet.WithSecretLockService(masterLock)) require.NoError(t, err) - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUser, mockctx) require.NoError(t, err) - require.NotEmpty(t, wallet) + require.NotEmpty(t, vcWallet) // get token - token, err := wallet.Open("", masterLock, 0) + err = vcWallet.Open(wallet.WithUnlockBySecretLockService(masterLock)) + require.NoError(t, err) + token, err := vcWallet.auth() require.NoError(t, err) require.NotEmpty(t, token) + defer vcWallet.Close() + // try again - token, err = wallet.Open("", masterLock, 0) - require.Empty(t, token) + err = vcWallet.Open(wallet.WithUnlockBySecretLockService(masterLock)) require.Error(t, err) - require.Equal(t, err, ErrAlreadyUnlocked) + require.True(t, errors.Is(err, wallet.ErrAlreadyUnlocked)) + token, err = vcWallet.auth() + require.NoError(t, err) + require.NotEmpty(t, token) // close wallet - require.True(t, wallet.Close()) - require.False(t, wallet.Close()) + require.True(t, vcWallet.Close()) + require.False(t, vcWallet.Close()) // try to open with wrong secret lock service badLock, err := pbkdf2.NewMasterLock(samplePassPhrase+"wrong", sha256.New, 0, nil) require.NoError(t, err) - token, err = wallet.Open("", badLock, 0) - require.Empty(t, token) + err = vcWallet.Open(wallet.WithUnlockBySecretLockService(badLock)) require.Error(t, err) require.Contains(t, err.Error(), "message authentication failed") + token, err = vcWallet.auth() + require.Empty(t, token) + require.Error(t, err) + require.True(t, errors.Is(err, ErrWalletLocked)) }) t.Run("test open & close wallet using remote kms URL", func(t *testing.T) { + sampleUser := uuid.New().String() mockctx := newMockProvider() - err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + + err := CreateProfile(sampleUser, mockctx, wallet.WithKeyServerURL(sampleKeyServerURL)) require.NoError(t, err) - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUser, mockctx) require.NoError(t, err) - require.NotEmpty(t, wallet) + require.NotEmpty(t, vcWallet) // get token - token, err := wallet.Open(sampleRemoteKMSAuth, nil, 0) + err = vcWallet.Open(wallet.WithUnlockByAuthorizationToken(sampleRemoteKMSAuth)) + require.NoError(t, err) + token, err := vcWallet.auth() require.NoError(t, err) require.NotEmpty(t, token) + defer vcWallet.Close() + // try again - token, err = wallet.Open(sampleRemoteKMSAuth, nil, 0) - require.Empty(t, token) + err = vcWallet.Open(wallet.WithUnlockByAuthorizationToken(sampleRemoteKMSAuth)) require.Error(t, err) - require.Equal(t, err, ErrAlreadyUnlocked) + require.True(t, errors.Is(err, wallet.ErrAlreadyUnlocked)) + token, err = vcWallet.auth() + require.NoError(t, err) + require.NotEmpty(t, token) // close wallet - require.True(t, wallet.Close()) - require.False(t, wallet.Close()) + require.True(t, vcWallet.Close()) + require.False(t, vcWallet.Close()) + token, err = vcWallet.auth() + require.Empty(t, token) + require.Error(t, err) + require.True(t, errors.Is(err, ErrWalletLocked)) }) } func TestClient_Export(t *testing.T) { mockctx := newMockProvider() - err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + err := CreateProfile(sampleUserID, mockctx, wallet.WithKeyServerURL(sampleKeyServerURL)) require.NoError(t, err) vcWalletClient, err := New(sampleUserID, mockctx) @@ -473,7 +470,7 @@ func TestClient_Export(t *testing.T) { func TestClient_Import(t *testing.T) { mockctx := newMockProvider() - err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + err := CreateProfile(sampleUserID, mockctx, wallet.WithKeyServerURL(sampleKeyServerURL)) require.NoError(t, err) vcWalletClient, err := New(sampleUserID, mockctx) @@ -487,30 +484,30 @@ func TestClient_Import(t *testing.T) { func TestClient_Add(t *testing.T) { mockctx := newMockProvider() - err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + err := CreateProfile(sampleUserID, mockctx, wallet.WithKeyServerURL(sampleKeyServerURL)) require.NoError(t, err) vcWalletClient, err := New(sampleUserID, mockctx) require.NotEmpty(t, vcWalletClient) require.NoError(t, err) - err = vcWalletClient.Add(Metadata, []byte(sampleContentValid)) + err = vcWalletClient.Add(wallet.Metadata, []byte(sampleContentValid)) require.NoError(t, err) } func TestClient_Get(t *testing.T) { mockctx := newMockProvider() - err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + err := CreateProfile(sampleUserID, mockctx, wallet.WithKeyServerURL(sampleKeyServerURL)) require.NoError(t, err) vcWalletClient, err := New(sampleUserID, mockctx) require.NotEmpty(t, vcWalletClient) require.NoError(t, err) - err = vcWalletClient.Add(Metadata, []byte(sampleContentValid)) + err = vcWalletClient.Add(wallet.Metadata, []byte(sampleContentValid)) require.NoError(t, err) - content, err := vcWalletClient.Get(Metadata, "did:example:123456789abcdefghi") + content, err := vcWalletClient.Get(wallet.Metadata, "did:example:123456789abcdefghi") require.NoError(t, err) require.NotEmpty(t, content) require.Equal(t, sampleContentValid, string(content)) @@ -518,24 +515,24 @@ func TestClient_Get(t *testing.T) { func TestClient_Remove(t *testing.T) { mockctx := newMockProvider() - err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + err := CreateProfile(sampleUserID, mockctx, wallet.WithKeyServerURL(sampleKeyServerURL)) require.NoError(t, err) vcWalletClient, err := New(sampleUserID, mockctx) require.NotEmpty(t, vcWalletClient) require.NoError(t, err) - err = vcWalletClient.Add(Metadata, []byte(sampleContentValid)) + err = vcWalletClient.Add(wallet.Metadata, []byte(sampleContentValid)) require.NoError(t, err) - content, err := vcWalletClient.Get(Metadata, "did:example:123456789abcdefghi") + content, err := vcWalletClient.Get(wallet.Metadata, "did:example:123456789abcdefghi") require.NoError(t, err) require.NotEmpty(t, content) - err = vcWalletClient.Remove(Metadata, "did:example:123456789abcdefghi") + err = vcWalletClient.Remove(wallet.Metadata, "did:example:123456789abcdefghi") require.NoError(t, err) - content, err = vcWalletClient.Get(Metadata, "did:example:123456789abcdefghi") + content, err = vcWalletClient.Get(wallet.Metadata, "did:example:123456789abcdefghi") require.Empty(t, content) require.Error(t, err) require.True(t, errors.Is(err, storage.ErrDataNotFound)) @@ -543,32 +540,23 @@ func TestClient_Remove(t *testing.T) { func TestClient_Query(t *testing.T) { mockctx := newMockProvider() - err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + err := CreateProfile(sampleUserID, mockctx, wallet.WithKeyServerURL(sampleKeyServerURL)) require.NoError(t, err) vcWalletClient, err := New(sampleUserID, mockctx) require.NotEmpty(t, vcWalletClient) require.NoError(t, err) - results, err := vcWalletClient.Query(&QueryParams{}) + results, err := vcWalletClient.Query(&wallet.QueryParams{}) require.Empty(t, results) require.Error(t, err) require.EqualError(t, err, toBeImplementedErr) } func TestClient_Issue(t *testing.T) { - didKey := "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5" - pkBase58 := "2MP5gWCnf67jvW3E4Lz8PpVrDWAXMYY1sDxjnkEnKhkkbKD7yP2mkVeyVpu5nAtr3TeDgMNjBPirk2XcQacs3dvZ" - kid := "z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5" - customVDR := &mockvdr.MockVDRegistry{ ResolveFunc: func(didID string, opts ...vdrapi.ResolveOption) (*did.DocResolution, error) { - if didID == sampleInvalidDIDID { - d, e := did.ParseDocument([]byte(sampleInvalidDID)) - require.NoError(t, e) - - return &did.DocResolution{DIDDocument: d}, nil - } else if strings.HasPrefix(didID, "did:key:") { + if strings.HasPrefix(didID, "did:key:") { k := key.New() d, e := k.Read(didID) @@ -587,301 +575,52 @@ func TestClient_Issue(t *testing.T) { mockctx.VDRegistryValue = customVDR mockctx.CryptoValue = &cryptomock.Crypto{} - err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + err := CreateProfile(sampleUserID, mockctx, wallet.WithPassphrase(samplePassPhrase)) require.NoError(t, err) - t.Run("Test VC wallet client issue using controller - success", func(t *testing.T) { - vcWalletClient, err := New(sampleUserID, mockctx) + t.Run("Test VC wallet client issue using controller - failure", func(t *testing.T) { + vcWalletClient, err := New(sampleUserID, mockctx, wallet.WithUnlockByPassphrase(samplePassPhrase)) require.NotEmpty(t, vcWalletClient) require.NoError(t, err) - // unlock wallet - authToken, err := vcWalletClient.Open(samplePassPhrase, nil, 0) - require.NoError(t, err) - require.NotEmpty(t, authToken) - defer vcWalletClient.Close() - // import keys manually - kmgr, err := keyManager().getKeyManger(authToken) - require.NoError(t, err) - edPriv := ed25519.PrivateKey(base58.Decode(pkBase58)) - // nolint: errcheck, gosec - kmgr.ImportPrivateKey(edPriv, kms.ED25519, kms.WithKeyID(kid)) - // sign with just controller - result, err := vcWalletClient.Issue(authToken, []byte(sampleUDCVC), &ProofOptions{ - Controller: didKey, - }) - require.NoError(t, err) - require.NotEmpty(t, result) - require.Len(t, result.Proofs, 1) - }) - - t.Run("Test VC wallet client issue using verification method - success", func(t *testing.T) { - vcWalletClient, err := New(sampleUserID, mockctx) - require.NotEmpty(t, vcWalletClient) - require.NoError(t, err) - - // unlock wallet - authToken, err := vcWalletClient.Open(samplePassPhrase, nil, 0) - require.NoError(t, err) - require.NotEmpty(t, authToken) - - defer vcWalletClient.Close() - - // import keys manually - kmgr, err := keyManager().getKeyManger(authToken) - require.NoError(t, err) - edPriv := ed25519.PrivateKey(base58.Decode(pkBase58)) - // nolint: errcheck, gosec - kmgr.ImportPrivateKey(edPriv, kms.ED25519, kms.WithKeyID(kid)) - - // issue - result, err := vcWalletClient.Issue(authToken, []byte(sampleUDCVC), &ProofOptions{ - Controller: didKey, - VerificationMethod: "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5#z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5", //nolint:lll + result, err := vcWalletClient.Issue([]byte(sampleUDCVC), &wallet.ProofOptions{ + Controller: sampleDIDKey, }) - require.NoError(t, err) - require.NotEmpty(t, result) - require.Len(t, result.Proofs, 1) - }) - - t.Run("Test VC wallet client issue using all options - success", func(t *testing.T) { - vcWalletClient, err := New(sampleUserID, mockctx) - require.NotEmpty(t, vcWalletClient) - require.NoError(t, err) - - // unlock wallet - authToken, err := vcWalletClient.Open(samplePassPhrase, nil, 0) - require.NoError(t, err) - require.NotEmpty(t, authToken) - - defer vcWalletClient.Close() - - // import keys manually - kmgr, err := keyManager().getKeyManger(authToken) - require.NoError(t, err) - edPriv := ed25519.PrivateKey(base58.Decode(pkBase58)) - // nolint: errcheck, gosec - kmgr.ImportPrivateKey(edPriv, kms.ED25519, kms.WithKeyID(kid)) - - // issue credential - proofRepr := verifiable.SignatureJWS - vm := "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5#z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5" - created, err := time.Parse("2006-01-02", sampleCreatedDate) - require.NoError(t, err) - - result, err := vcWalletClient.Issue(authToken, []byte(sampleUDCVC), &ProofOptions{ - Controller: didKey, - VerificationMethod: vm, - ProofType: JSONWebSignature2020, - Challenge: sampleChallenge, - Domain: sampleDomain, - Created: &created, - ProofRepresentation: &proofRepr, - }) - require.NoError(t, err) - require.NotEmpty(t, result) - require.Len(t, result.Proofs, 1) - - require.Equal(t, result.Proofs[0]["challenge"], sampleChallenge) - require.Equal(t, result.Proofs[0]["created"], "2020-12-25T00:00:00Z") - require.Equal(t, result.Proofs[0]["domain"], sampleDomain) - require.NotEmpty(t, result.Proofs[0]["jws"]) - require.Equal(t, result.Proofs[0]["proofPurpose"], "assertionMethod") - require.Equal(t, result.Proofs[0]["type"], JSONWebSignature2020) - require.Equal(t, result.Proofs[0]["verificationMethod"], vm) - }) - - // nolint:lll - t.Run("Test VC wallet client issue using BBS - success", func(t *testing.T) { - didKeyBBS := "did:key:zUC72c7u4BYVmfYinDceXkNAwzPEyuEE23kUmJDjLy8495KH3pjLwFhae1Fww9qxxRdLnS2VNNwni6W3KbYZKsicDtiNNEp76fYWR6HCD8jAz6ihwmLRjcHH6kB294Xfg1SL1qQ" - pkBBSBase58 := "6gsgGpdx7p1nYoKJ4b5fKt1xEomWdnemg9nJFX6mqNCh" - keyIDBBS := "zUC72c7u4BYVmfYinDceXkNAwzPEyuEE23kUmJDjLy8495KH3pjLwFhae1Fww9qxxRdLnS2VNNwni6W3KbYZKsicDtiNNEp76fYWR6HCD8jAz6ihwmLRjcHH6kB294Xfg1SL1qQ" - - vcWalletClient, err := New(sampleUserID, mockctx) - require.NotEmpty(t, vcWalletClient) - require.NoError(t, err) - - // unlock wallet - authToken, err := vcWalletClient.Open(samplePassPhrase, nil, 0) - require.NoError(t, err) - require.NotEmpty(t, authToken) - - defer vcWalletClient.Close() - - // import keys manually - kmgr, err := keyManager().getKeyManger(authToken) - require.NoError(t, err) - privKeyBBS, err := bbs12381g2pub.UnmarshalPrivateKey(base58.Decode(pkBBSBase58)) - require.NoError(t, err) - // nolint: errcheck, gosec - kmgr.ImportPrivateKey(privKeyBBS, kms.BLS12381G2Type, kms.WithKeyID(keyIDBBS)) - - // sign with just controller - proofRepr := verifiable.SignatureProofValue - result, err := vcWalletClient.Issue(authToken, []byte(sampleUDCVC), &ProofOptions{ - Controller: didKeyBBS, - ProofType: BbsBlsSignature2020, - ProofRepresentation: &proofRepr, - }) - require.NoError(t, err) - require.NotEmpty(t, result) - require.Len(t, result.Proofs, 1) - }) - - t.Run("Test VC wallet client issue using stored DID - success", func(t *testing.T) { - mockctx1 := newMockProvider() - mockctx1.VDRegistryValue = &mockvdr.MockVDRegistry{} - mockctx1.CryptoValue = &cryptomock.Crypto{} - - err := CreateProfile(sampleUserID, mockctx1, WithPassphrase(samplePassPhrase)) - require.NoError(t, err) - - vcWalletClient, err := New(sampleUserID, mockctx1) - require.NotEmpty(t, vcWalletClient) - require.NoError(t, err) - - // unlock wallet - authToken, err := vcWalletClient.Open(samplePassPhrase, nil, 0) - require.NoError(t, err) - require.NotEmpty(t, authToken) - - defer vcWalletClient.Close() - - // import keys manually - kmgr, err := keyManager().getKeyManger(authToken) - require.NoError(t, err) - edPriv := ed25519.PrivateKey(base58.Decode(pkBase58)) - // nolint: errcheck, gosec - kmgr.ImportPrivateKey(edPriv, kms.ED25519, kms.WithKeyID(kid)) - - // save DID Resolution response - err = vcWalletClient.Add(DIDResolutionResponse, []byte(sampleDocResolutionResponse)) - require.NoError(t, err) - - // sign with just controller - result, err := vcWalletClient.Issue(authToken, []byte(sampleUDCVC), &ProofOptions{ - Controller: didKey, - }) - require.NoError(t, err) - require.NotEmpty(t, result) - require.Len(t, result.Proofs, 1) - }) - - t.Run("Test VC wallet client issue failure - invalid VC", func(t *testing.T) { - vcWalletClient, err := New(sampleUserID, mockctx) - require.NotEmpty(t, vcWalletClient) - require.NoError(t, err) - - result, err := vcWalletClient.Issue(sampleFakeTkn, []byte("--"), &ProofOptions{}) - require.Empty(t, result) require.Error(t, err) - require.Contains(t, err.Error(), "failed to parse credential") - }) - - t.Run("Test VC wallet client issue failure - proof option validation", func(t *testing.T) { - vcWalletClient, err := New(sampleUserID, mockctx) - require.NotEmpty(t, vcWalletClient) - require.NoError(t, err) - - // no controller - result, err := vcWalletClient.Issue(sampleFakeTkn, []byte(sampleUDCVC), &ProofOptions{}) - require.Empty(t, result) - require.Error(t, err) - require.Contains(t, err.Error(), "invalid proof option, 'controller' is required") - - // DID not found - result, err = vcWalletClient.Issue(sampleFakeTkn, []byte(sampleUDCVC), &ProofOptions{ - Controller: "did:example:1234", - }) - require.Empty(t, result) - require.Error(t, err) - require.Contains(t, err.Error(), "failed to read DID document from wallet store or from VDR") - - // no assertion method - result, err = vcWalletClient.Issue(sampleFakeTkn, []byte(sampleUDCVC), &ProofOptions{ - Controller: sampleInvalidDIDID, - }) + require.Contains(t, err.Error(), "failed to read json keyset from reader") require.Empty(t, result) - require.Error(t, err) - require.Contains(t, err.Error(), "unable to find 'assertionMethod' for given verification method") - - // invalid DID in store - err = vcWalletClient.Add(DIDResolutionResponse, []byte(sampleInvalidDIDContent)) - require.NoError(t, err) - - result, err = vcWalletClient.Issue(sampleFakeTkn, []byte(sampleUDCVC), &ProofOptions{ - Controller: "did:example:sampleInvalidDIDContent", - }) - require.Empty(t, result) - require.Error(t, err) - require.Contains(t, err.Error(), "failed to parse stored DID") }) - t.Run("Test VC wallet client issue failure - add proof errors", func(t *testing.T) { + t.Run("Test VC wallet client issue using controller - wallet locked", func(t *testing.T) { vcWalletClient, err := New(sampleUserID, mockctx) require.NotEmpty(t, vcWalletClient) require.NoError(t, err) - // wallet locked - result, err := vcWalletClient.Issue(sampleFakeTkn, []byte(sampleUDCVC), &ProofOptions{ - Controller: didKey, - }) - require.Empty(t, result) - require.Error(t, err) - require.Contains(t, err.Error(), "wallet locked") - - // get token - authToken, err := vcWalletClient.Open(samplePassPhrase, nil, 0) - require.NoError(t, err) - require.NotEmpty(t, authToken) - defer vcWalletClient.Close() - // key not found - result, err = vcWalletClient.Issue(authToken, []byte(sampleUDCVC), &ProofOptions{ - Controller: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - }) - require.Empty(t, result) - require.Contains(t, err.Error(), "cannot read data for keysetID") - - // import keys manually - kmgr, err := keyManager().getKeyManger(authToken) - require.NoError(t, err) - edPriv := ed25519.PrivateKey(base58.Decode(pkBase58)) - // nolint: errcheck, gosec - kmgr.ImportPrivateKey(edPriv, kms.ED25519, kms.WithKeyID(kid)) - - // invalid signature type - result, err = vcWalletClient.Issue(authToken, []byte(sampleUDCVC), &ProofOptions{ - Controller: didKey, - ProofType: "invalid", - }) - require.Empty(t, result) - require.Contains(t, err.Error(), " unsupported signature type 'invalid'") - - // wrong key type - result, err = vcWalletClient.Issue(authToken, []byte(sampleUDCVC), &ProofOptions{ - Controller: didKey, - ProofType: BbsBlsSignature2020, + // sign with just controller + result, err := vcWalletClient.Issue([]byte(sampleUDCVC), &wallet.ProofOptions{ + Controller: sampleDIDKey, }) + require.Error(t, err) + require.True(t, errors.Is(err, ErrWalletLocked)) require.Empty(t, result) - require.Contains(t, err.Error(), "failed to add linked data proof") }) } func TestClient_Prove(t *testing.T) { mockctx := newMockProvider() - err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + err := CreateProfile(sampleUserID, mockctx, wallet.WithKeyServerURL(sampleKeyServerURL)) require.NoError(t, err) vcWalletClient, err := New(sampleUserID, mockctx) require.NotEmpty(t, vcWalletClient) require.NoError(t, err) - result, err := vcWalletClient.Prove(nil, &ProofOptions{}) + result, err := vcWalletClient.Prove(nil, &wallet.ProofOptions{}) require.Empty(t, result) require.Error(t, err) require.EqualError(t, err, toBeImplementedErr) @@ -889,7 +628,7 @@ func TestClient_Prove(t *testing.T) { func TestClient_Verify(t *testing.T) { mockctx := newMockProvider() - err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + err := CreateProfile(sampleUserID, mockctx, wallet.WithKeyServerURL(sampleKeyServerURL)) require.NoError(t, err) vcWalletClient, err := New(sampleUserID, mockctx) @@ -907,11 +646,26 @@ func newMockProvider() *mockprovider.Provider { } func createSampleProfile(t *testing.T, mockctx *mockprovider.Provider) { - err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + err := CreateProfile(sampleUserID, mockctx, wallet.WithPassphrase(samplePassPhrase)) require.NoError(t, err) - wallet, err := New(sampleUserID, mockctx) + vcWallet, err := New(sampleUserID, mockctx) require.NoError(t, err) - require.NotEmpty(t, wallet) - require.NotEmpty(t, wallet.profile.MasterLockCipher) + require.NotEmpty(t, vcWallet) +} + +type mockStorageProvider struct { + *mockstorage.MockStoreProvider + config storage.StoreConfiguration + failure error +} + +func (s *mockStorageProvider) SetStoreConfig(name string, config storage.StoreConfiguration) error { + s.config = config + + return s.failure +} + +func (s *mockStorageProvider) GetStoreConfig(name string) (storage.StoreConfiguration, error) { + return s.config, nil } diff --git a/pkg/client/vcwallet/contents.go b/pkg/wallet/contents.go similarity index 99% rename from pkg/client/vcwallet/contents.go rename to pkg/wallet/contents.go index 001d0ce97..619618fcc 100644 --- a/pkg/client/vcwallet/contents.go +++ b/pkg/wallet/contents.go @@ -4,7 +4,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -package vcwallet +package wallet import ( "encoding/json" diff --git a/pkg/client/vcwallet/contents_test.go b/pkg/wallet/contents_test.go similarity index 99% rename from pkg/client/vcwallet/contents_test.go rename to pkg/wallet/contents_test.go index 31d56b16c..51d6e90e9 100644 --- a/pkg/client/vcwallet/contents_test.go +++ b/pkg/wallet/contents_test.go @@ -4,7 +4,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -package vcwallet +package wallet import ( "errors" diff --git a/pkg/client/vcwallet/kmsclient.go b/pkg/wallet/kmsclient.go similarity index 94% rename from pkg/client/vcwallet/kmsclient.go rename to pkg/wallet/kmsclient.go index 3d1e17025..7b924485b 100644 --- a/pkg/client/vcwallet/kmsclient.go +++ b/pkg/wallet/kmsclient.go @@ -4,7 +4,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -package vcwallet +package wallet import ( "bytes" @@ -74,8 +74,8 @@ type walletKeyManager struct { gstore gcache.Cache } -func (k *walletKeyManager) createKeyManager(profileInfo *profile, storeProvider storage.Provider, auth string, - secretLockSvc secretlock.Service, expiration time.Duration) (string, error) { +func (k *walletKeyManager) createKeyManager(profileInfo *profile, + storeProvider storage.Provider, opts *unlockOpts) (string, error) { if profileInfo.MasterLockCipher == "" && profileInfo.KeyServerURL == "" { return "", fmt.Errorf("invalid wallet profile") } @@ -93,21 +93,21 @@ func (k *walletKeyManager) createKeyManager(profileInfo *profile, storeProvider // create key manager if profileInfo.MasterLockCipher != "" { // local kms - keyManager, err = createLocalKeyManager(profileInfo.User, auth, - profileInfo.MasterLockCipher, secretLockSvc, storeProvider) + keyManager, err = createLocalKeyManager(profileInfo.User, opts.passphrase, + profileInfo.MasterLockCipher, opts.secretLockSvc, storeProvider) if err != nil { return "", fmt.Errorf("failed to create local key manager: %w", err) } } else { // remote kms - keyManager = createRemoteKeyManager(auth, profileInfo.KeyServerURL) + keyManager = createRemoteKeyManager(opts.authToken, profileInfo.KeyServerURL) } // generate token token = uuid.New().String() // save key manager - err = k.saveKeyManger(profileInfo.User, token, keyManager, expiration) + err = k.saveKeyManger(profileInfo.User, token, keyManager, opts.tokenExpiry) if err != nil { return "", fmt.Errorf("failed to persist local key manager: %w", err) } diff --git a/pkg/client/vcwallet/kmsclient_test.go b/pkg/wallet/kmsclient_test.go similarity index 92% rename from pkg/client/vcwallet/kmsclient_test.go rename to pkg/wallet/kmsclient_test.go index 1fd7b1e18..942db368e 100644 --- a/pkg/client/vcwallet/kmsclient_test.go +++ b/pkg/wallet/kmsclient_test.go @@ -4,7 +4,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -package vcwallet +package wallet import ( "crypto/sha256" @@ -47,7 +47,7 @@ func TestKeyManager(t *testing.T) { } tkn, err := keyManager().createKeyManager(profileInfo, mockstorage.NewMockStoreProvider(), - samplePassPhrase, nil, 0) + &unlockOpts{passphrase: samplePassPhrase}) require.NoError(t, err) require.NotEmpty(t, tkn) @@ -58,7 +58,7 @@ func TestKeyManager(t *testing.T) { // try to create again before expiry tkn, err = keyManager().createKeyManager(profileInfo, mockstorage.NewMockStoreProvider(), - samplePassPhrase, nil, 0) + &unlockOpts{passphrase: samplePassPhrase}) require.Error(t, err) require.Equal(t, err, ErrAlreadyUnlocked) require.Empty(t, tkn) @@ -79,7 +79,7 @@ func TestKeyManager(t *testing.T) { } tkn, err := keyManager().createKeyManager(profileInfo, mockstorage.NewMockStoreProvider(), - "", masterLock, 0) + &unlockOpts{secretLockSvc: masterLock}) require.NoError(t, err) require.NotEmpty(t, tkn) @@ -90,7 +90,7 @@ func TestKeyManager(t *testing.T) { // try to create again before expiry tkn, err = keyManager().createKeyManager(profileInfo, mockstorage.NewMockStoreProvider(), - "", masterLock, 0) + &unlockOpts{secretLockSvc: masterLock}) require.Error(t, err) require.Equal(t, err, ErrAlreadyUnlocked) require.Empty(t, tkn) @@ -112,7 +112,7 @@ func TestKeyManager(t *testing.T) { // use wrong passphrase tkn, err := keyManager().createKeyManager(profileInfo, mockstorage.NewMockStoreProvider(), - samplePassPhrase+"wrong", nil, 0) + &unlockOpts{passphrase: samplePassPhrase + "wrong"}) require.Empty(t, tkn) require.Error(t, err) require.Contains(t, err.Error(), "message authentication failed") @@ -143,7 +143,7 @@ func TestKeyManager(t *testing.T) { require.NoError(t, err) tkn, err := keyManager().createKeyManager(profileInfo, mockstorage.NewMockStoreProvider(), - "", masterLockBad, 0) + &unlockOpts{secretLockSvc: masterLockBad}) require.Empty(t, tkn) require.Error(t, err) require.Contains(t, err.Error(), "message authentication failed") @@ -163,7 +163,7 @@ func TestKeyManager(t *testing.T) { } tkn, err := keyManager().createKeyManager(profileInfo, mockstorage.NewMockStoreProvider(), - sampleRemoteKMSAuth, nil, 0) + &unlockOpts{authToken: sampleRemoteKMSAuth}) require.NoError(t, err) require.NotEmpty(t, tkn) @@ -177,7 +177,7 @@ func TestKeyManager(t *testing.T) { // try to create again before expiry tkn, err = keyManager().createKeyManager(profileInfo, mockstorage.NewMockStoreProvider(), - sampleRemoteKMSAuth, nil, 0) + &unlockOpts{authToken: sampleRemoteKMSAuth}) require.Error(t, err) require.Equal(t, err, ErrAlreadyUnlocked) require.Empty(t, tkn) @@ -189,7 +189,7 @@ func TestKeyManager(t *testing.T) { } tkn, err := keyManager().createKeyManager(profileInfo, mockstorage.NewMockStoreProvider(), - sampleRemoteKMSAuth, nil, 0) + &unlockOpts{authToken: sampleRemoteKMSAuth}) require.Empty(t, tkn) require.Error(t, err) require.Contains(t, err.Error(), "invalid wallet profile") @@ -209,7 +209,7 @@ func TestKeyManager(t *testing.T) { } tkn, err := keyManager().createKeyManager(profileInfo, mockstorage.NewMockStoreProvider(), - sampleRemoteKMSAuth, nil, 0) + &unlockOpts{authToken: sampleRemoteKMSAuth}) require.NoError(t, err) require.NotEmpty(t, tkn) @@ -220,7 +220,7 @@ func TestKeyManager(t *testing.T) { // try to create again before expiry tkn, err = keyManager().createKeyManager(profileInfo, mockstorage.NewMockStoreProvider(), - sampleRemoteKMSAuth, nil, 0) + &unlockOpts{authToken: sampleRemoteKMSAuth}) require.Error(t, err) require.Equal(t, err, ErrAlreadyUnlocked) require.Empty(t, tkn) @@ -237,7 +237,7 @@ func TestKeyManager(t *testing.T) { // try again to create tkn, err = keyManager().createKeyManager(profileInfo, mockstorage.NewMockStoreProvider(), - sampleRemoteKMSAuth, nil, 0) + &unlockOpts{authToken: sampleRemoteKMSAuth}) require.NoError(t, err) require.NotEmpty(t, tkn) diff --git a/pkg/client/vcwallet/models.go b/pkg/wallet/models.go similarity index 99% rename from pkg/client/vcwallet/models.go rename to pkg/wallet/models.go index a34d223bc..8fadf0cbc 100644 --- a/pkg/client/vcwallet/models.go +++ b/pkg/wallet/models.go @@ -4,7 +4,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -package vcwallet +package wallet import ( "encoding/json" diff --git a/pkg/client/vcwallet/profile.go b/pkg/wallet/profile.go similarity index 99% rename from pkg/client/vcwallet/profile.go rename to pkg/wallet/profile.go index 5274b6caa..995fc44a0 100644 --- a/pkg/client/vcwallet/profile.go +++ b/pkg/wallet/profile.go @@ -4,7 +4,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -package vcwallet +package wallet import ( "encoding/json" diff --git a/pkg/client/vcwallet/profile_test.go b/pkg/wallet/profile_test.go similarity index 99% rename from pkg/client/vcwallet/profile_test.go rename to pkg/wallet/profile_test.go index c1762a4ca..9b1072494 100644 --- a/pkg/client/vcwallet/profile_test.go +++ b/pkg/wallet/profile_test.go @@ -4,7 +4,7 @@ Copyright SecureKey Technologies Inc. All Rights Reserved. SPDX-License-Identifier: Apache-2.0 */ -package vcwallet +package wallet import ( "fmt" diff --git a/pkg/wallet/wallet.go b/pkg/wallet/wallet.go new file mode 100644 index 000000000..2ca469d09 --- /dev/null +++ b/pkg/wallet/wallet.go @@ -0,0 +1,643 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package wallet + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/piprate/json-gold/ld" + + "github.com/hyperledger/aries-framework-go/pkg/crypto" + "github.com/hyperledger/aries-framework-go/pkg/doc/did" + jld "github.com/hyperledger/aries-framework-go/pkg/doc/jsonld" + "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/signer" + "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/signature/suite/ed25519signature2018" + "github.com/hyperledger/aries-framework-go/pkg/doc/signature/suite/jsonwebsignature2020" + "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" + "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" + "github.com/hyperledger/aries-framework-go/pkg/secretlock" + "github.com/hyperledger/aries-framework-go/spi/storage" +) + +// Proof types. +const ( + // Ed25519Signature2018 ed25519 signature suite. + Ed25519Signature2018 = "Ed25519Signature2018" + // JSONWebSignature2020 json web signature suite. + JSONWebSignature2020 = "JsonWebSignature2020" + // BbsBlsSignature2020 BBS signature suite. + BbsBlsSignature2020 = "BbsBlsSignature2020" +) + +// miscellaneous constants. +const ( + bbsContext = "https://w3id.org/security/bbs/v1" +) + +// proof options. +// nolint:gochecknoglobals +var ( + defaultSignatureRepresentation = verifiable.SignatureJWS + supportedRelationships = map[did.VerificationRelationship]string{ + did.Authentication: "authentication", + did.AssertionMethod: "assertionMethod", + } +) + +// provider contains dependencies for the verifiable credential wallet +// and is typically created by using aries.Context(). +type provider interface { + StorageProvider() storage.Provider + VDRegistry() vdr.Registry + Crypto() crypto.Crypto +} + +type provable interface { + AddLinkedDataProof(context *verifiable.LinkedDataProofContext, jsonldOpts ...jsonld.ProcessorOpts) error +} + +// kmsOpts contains options for creating verifiable credential wallet. +type kmsOpts struct { + // local kms options + secretLockSvc secretlock.Service + passphrase string + + // remote(web) kms options + keyServerURL string +} + +// ProfileKeyManagerOptions is option for verifiable credential wallet key manager. +type ProfileKeyManagerOptions func(opts *kmsOpts) + +// WithSecretLockService option, when provided then wallet will use local kms for key operations. +func WithSecretLockService(svc secretlock.Service) ProfileKeyManagerOptions { + return func(opts *kmsOpts) { + opts.secretLockSvc = svc + } +} + +// WithPassphrase option to provide passphrase for local kms for key operations. +func WithPassphrase(passphrase string) ProfileKeyManagerOptions { + return func(opts *kmsOpts) { + opts.passphrase = passphrase + } +} + +// WithKeyServerURL option, when provided then wallet will use remote kms for key operations. +// This option will be ignore if provided with 'WithSecretLockService' option. +func WithKeyServerURL(url string) ProfileKeyManagerOptions { + return func(opts *kmsOpts) { + opts.keyServerURL = url + } +} + +// unlockOpts contains options for unlocking VC wallet client. +type unlockOpts struct { + // local kms options + passphrase string + secretLockSvc secretlock.Service + + // remote(web) kms options + authToken string + + // expiry + tokenExpiry time.Duration +} + +// UnlockOptions is option for unlocking verifiable credential wallet key manager. +// Wallet unlocking instantiates KMS instance for wallet operations. +// Type of key manager (local or remote) to be used will be decided based on options passed. +// Note: unlock options should match key manager options set for given wallet profile. +type UnlockOptions func(opts *unlockOpts) + +// WithUnlockByPassphrase option for supplying passphrase to open wallet. +// This option takes precedence when provided along with other options. +func WithUnlockByPassphrase(passphrase string) UnlockOptions { + return func(opts *unlockOpts) { + opts.passphrase = passphrase + } +} + +// WithUnlockBySecretLockService option for supplying secret lock service to open wallet. +// This option will be ignored when supplied with 'WithPassphrase' option. +func WithUnlockBySecretLockService(svc secretlock.Service) UnlockOptions { + return func(opts *unlockOpts) { + opts.secretLockSvc = svc + } +} + +// WithUnlockByAuthorizationToken option for supplying remote kms auth token to open wallet. +// This option will be ignore when supplied with localkms options. +func WithUnlockByAuthorizationToken(url string) UnlockOptions { + return func(opts *unlockOpts) { + opts.authToken = url + } +} + +// WithUnlockExpiry time duration after which wallet key manager will be expired. +// Wallet should be reopened by using 'client.Open()' once expired or a new instance needs to be created. +func WithUnlockExpiry(tokenExpiry time.Duration) UnlockOptions { + return func(opts *unlockOpts) { + opts.tokenExpiry = tokenExpiry + } +} + +// Wallet enables access to verifiable credential wallet features. +type Wallet struct { + // ID of wallet content owner + userID string + + // wallet profile + profile *profile + + // wallet content store + contents *contentStore + + // storage provider + ctx provider +} + +// New returns new verifiable credential wallet for given user. +// returns error if wallet profile is not found. +// To create a new wallet profile, use `CreateProfile()`. +// To update an existing profile, use `UpdateProfile()`. +func New(userID string, ctx provider) (*Wallet, error) { + store, err := newProfileStore(ctx.StorageProvider()) + if err != nil { + return nil, fmt.Errorf("failed to get store to fetch VC wallet profile info: %w", err) + } + + profile, err := store.get(userID) + if err != nil { + return nil, fmt.Errorf("failed to get VC wallet profile: %w", err) + } + + contents, err := newContentStore(ctx.StorageProvider(), profile) + if err != nil { + return nil, fmt.Errorf("failed to get wallet content store: %w", err) + } + + return &Wallet{userID: userID, profile: profile, ctx: ctx, contents: contents}, nil +} + +// CreateProfile creates a new verifiable credential wallet profile for given user. +// returns error if wallet profile is already created. +// Use `UpdateProfile()` for replacing an already created verifiable credential wallet profile. +func CreateProfile(userID string, ctx provider, options ...ProfileKeyManagerOptions) error { + return createOrUpdate(userID, ctx, false, options...) +} + +// UpdateProfile updates existing verifiable credential wallet profile. +// Will create new profile if no profile exists for given user. +// Caution: you might lose your existing keys if you change kms options. +func UpdateProfile(userID string, ctx provider, options ...ProfileKeyManagerOptions) error { + return createOrUpdate(userID, ctx, true, options...) +} + +func createOrUpdate(userID string, ctx provider, update bool, options ...ProfileKeyManagerOptions) error { + opts := &kmsOpts{} + + for _, opt := range options { + opt(opts) + } + + store, err := newProfileStore(ctx.StorageProvider()) + if err != nil { + return fmt.Errorf("failed to get store to save VC wallet profile: %w", err) + } + + var profile *profile + + if update { + // find existing profile and update it. + profile, err = store.get(userID) + if err != nil { + return fmt.Errorf("failed to update wallet user profile: %w", err) + } + + err = profile.setKMSOptions(opts.passphrase, opts.secretLockSvc, opts.keyServerURL) + if err != nil { + return fmt.Errorf("failed to update wallet user profile KMS options: %w", err) + } + } else { + // create new profile. + profile, err = createProfile(userID, opts.passphrase, opts.secretLockSvc, opts.keyServerURL) + if err != nil { + return fmt.Errorf("failed to create new wallet user profile: %w", err) + } + } + + err = store.save(profile, update) + if err != nil { + return fmt.Errorf("failed to save VC wallet profile: %w", err) + } + + return nil +} + +// Open unlocks wallet's key manager instance and returns a token for subsequent use of wallet features. +// +// Args: +// - unlock options for opening wallet. +// +// Returns token with expiry that can be used for subsequent use of wallet features. +func (c *Wallet) Open(options ...UnlockOptions) (string, error) { + opts := &unlockOpts{} + + for _, opt := range options { + opt(opts) + } + + return keyManager().createKeyManager(c.profile, c.ctx.StorageProvider(), opts) +} + +// Close expires token issued to this VC wallet. +// returns false if token is not found or already expired for this wallet user. +func (c *Wallet) Close() bool { + return keyManager().removeKeyManager(c.userID) +} + +// Export produces a serialized exported wallet representation. +// Only ciphertext wallet contents can be exported. +// +// Args: +// - auth: token to be used to lock the wallet before exporting. +// +// Returns exported locked wallet. +// +// Supported data models: +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Profile +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Credential +// +func (c *Wallet) Export(auth string) (json.RawMessage, error) { + // TODO to be added #2433 + return nil, fmt.Errorf("to be implemented") +} + +// Import Takes a serialized exported wallet representation as input +// and imports all contents into wallet. +// +// Args: +// - contents: wallet content to be imported. +// - auth: token used while exporting the wallet. +// +// Supported data models: +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Profile +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Credential +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#CachedDIDDocument +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#meta-data +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#connection +// +func (c *Wallet) Import(auth string, contents json.RawMessage) error { + // TODO to be added #2433 + return fmt.Errorf("to be implemented") +} + +// Add adds given data model to wallet contents store. +// +// Supported data models: +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Profile +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Credential +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#CachedDIDDocument +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#meta-data +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#connection +// +// TODO: (#2433) support for correlation between wallet contents (ex: credentials to a profile/collection). +func (c *Wallet) Add(contentType ContentType, content json.RawMessage) error { + return c.contents.Save(contentType, content) +} + +// Remove removes wallet content by content ID. +// +// Supported data models: +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Profile +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Credential +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#CachedDIDDocument +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#meta-data +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#connection +// +func (c *Wallet) Remove(contentType ContentType, contentID string) error { + return c.contents.Remove(contentType, contentID) +} + +// Get fetches a wallet content by content ID. +// +// Supported data models: +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Profile +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#Credential +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#CachedDIDDocument +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#meta-data +// - https://w3c-ccg.github.io/universal-wallet-interop-spec/#connection +// +func (c *Wallet) Get(contentType ContentType, contentID string) (json.RawMessage, error) { + return c.contents.Get(contentType, contentID) +} + +// Query returns a collection of results based on current wallet contents. +// +// Supported Query Types: +// - https://www.w3.org/TR/json-ld11-framing +// - https://identity.foundation/presentation-exchange +// +func (c *Wallet) Query(query *QueryParams) ([]json.RawMessage, error) { + // TODO to be added #2433 + return nil, fmt.Errorf("to be implemented") +} + +// Issue adds proof to a Verifiable Credential. +// +// Args: +// - A verifiable credential with or without proof +// - Proof options +// +func (c *Wallet) Issue(authToken string, credential json.RawMessage, + options *ProofOptions) (*verifiable.Credential, error) { + vc, err := verifiable.ParseCredential(credential, verifiable.WithDisabledProofCheck()) + if err != nil { + return nil, fmt.Errorf("failed to parse credential: %w", err) + } + + purpose := did.AssertionMethod + + err = c.validateProofOption(options, purpose) + if err != nil { + return nil, fmt.Errorf("failed to prepare proof: %w", err) + } + + err = c.addLinkedDataProof(authToken, vc, options, purpose) + if err != nil { + return nil, fmt.Errorf("failed to issue credential: %w", err) + } + + return vc, nil +} + +// Prove produces a Verifiable Presentation. +// +// Args: +// - List of verifiable credentials IDs. +// - Proof options +// +func (c *Wallet) Prove(credentialIDs []string, options *ProofOptions) (json.RawMessage, error) { + // TODO to be added #2433 + return nil, fmt.Errorf("to be implemented") +} + +// Verify takes Takes a Verifiable Credential or Verifiable Presentation as input,. +// +// Args: +// - a Verifiable Credential or Verifiable Presentation +// +// Returns: a boolean verified, and an error if verified is false. +func (c *Wallet) Verify(raw json.RawMessage) (bool, error) { + // TODO to be added #2433 + return false, fmt.Errorf("to be implemented") +} + +func (c *Wallet) addLinkedDataProof(authToken string, p provable, opts *ProofOptions, + relationship did.VerificationRelationship) error { + s, err := newKMSSigner(authToken, c.ctx.Crypto(), opts) + if err != nil { + return err + } + + var signatureSuite signer.SignatureSuite + + var processorOpts []jsonld.ProcessorOpts + + switch opts.ProofType { + case Ed25519Signature2018: + signatureSuite = ed25519signature2018.New(suite.WithSigner(s)) + case JSONWebSignature2020: + signatureSuite = jsonwebsignature2020.New(suite.WithSigner(s)) + case BbsBlsSignature2020: + // TODO document loader to be part of common API, to be removed + bbsLoader, e := bbsJSONLDDocumentLoader() + if e != nil { + return e + } + + processorOpts = append(processorOpts, jsonld.WithDocumentLoader(bbsLoader)) + + addContext(p, bbsContext) + + signatureSuite = bbsblssignature2020.New(suite.WithSigner(s)) + default: + return fmt.Errorf("unsupported signature type '%s'", opts.ProofType) + } + + signingCtx := &verifiable.LinkedDataProofContext{ + VerificationMethod: opts.VerificationMethod, + SignatureRepresentation: *opts.ProofRepresentation, + SignatureType: opts.ProofType, + Suite: signatureSuite, + Created: opts.Created, + Domain: opts.Domain, + Challenge: opts.Challenge, + Purpose: supportedRelationships[relationship], + } + + err = p.AddLinkedDataProof(signingCtx, processorOpts...) + if err != nil { + return fmt.Errorf("failed to add linked data proof: %w", err) + } + + return nil +} + +func (c *Wallet) validateProofOption(opts *ProofOptions, method did.VerificationRelationship) error { + if opts == nil || opts.Controller == "" { + return errors.New("invalid proof option, 'controller' is required") + } + + didDoc, err := c.getDIDDocument(opts.Controller) + if err != nil { + return err + } + + err = c.validateVerificationMethod(didDoc, opts, method) + if err != nil { + return err + } + + if opts.ProofRepresentation == nil { + opts.ProofRepresentation = &defaultSignatureRepresentation + } + + if opts.ProofType == "" { + opts.ProofType = Ed25519Signature2018 + } + + return nil +} + +// TODO stored DIDResolution response & DID Doc metadata should be read first before trying to resolve using VDR. +func (c *Wallet) getDIDDocument(didID string) (*did.Doc, error) { + doc, err := c.ctx.VDRegistry().Resolve(didID) + // if DID not found in VDR, look through in wallet content storage. + if err != nil { + docBytes, err := c.contents.Get(DIDResolutionResponse, didID) + if err != nil { + return nil, fmt.Errorf("failed to read DID document from wallet store or from VDR: %w", err) + } + + doc, err = did.ParseDocumentResolution(docBytes) + if err != nil { + return nil, fmt.Errorf("failed to parse stored DID: %w", err) + } + + return doc.DIDDocument, nil + } + + return doc.DIDDocument, nil +} + +func (c *Wallet) validateVerificationMethod(didDoc *did.Doc, opts *ProofOptions, + relationship did.VerificationRelationship) error { + vms := didDoc.VerificationMethods(relationship)[relationship] + + for _, vm := range vms { + if opts.VerificationMethod == "" { + opts.VerificationMethod = vm.VerificationMethod.ID + return nil + } + + if opts.VerificationMethod == vm.VerificationMethod.ID { + return nil + } + } + + return fmt.Errorf("unable to find '%s' for given verification method", supportedRelationships[relationship]) +} + +// addContext adds context if not found in given data model. +func addContext(v interface{}, context string) { + if vc, ok := v.(*verifiable.Credential); ok { + for _, ctx := range vc.Context { + if ctx == context { + return + } + } + + vc.Context = append(vc.Context, context) + } +} + +// TODO: context should not be loaded here, the loader should be defined once for the whole system. +func bbsJSONLDDocumentLoader() (*jld.CachingDocumentLoader, error) { + loader := presexch.CachingJSONLDLoader() + + reader, err := ld.DocumentFromReader(strings.NewReader(contextBBSContent)) + if err != nil { + return nil, err + } + + loader.AddDocument(bbsContext, reader) + + return loader, nil +} + +const contextBBSContent = `{ + "@context": { + "@version": 1.1, + "id": "@id", + "type": "@type", + "BbsBlsSignature2020": { + "@id": "https://w3id.org/security#BbsBlsSignature2020", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "proofValue": "https://w3id.org/security#proofValue", + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + }, + "BbsBlsSignatureProof2020": { + "@id": "https://w3id.org/security#BbsBlsSignatureProof2020", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "https://w3id.org/security#proofValue", + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + }, + "Bls12381G2Key2020": "https://w3id.org/security#Bls12381G2Key2020" + } +}` diff --git a/pkg/wallet/wallet_test.go b/pkg/wallet/wallet_test.go new file mode 100644 index 000000000..cecded909 --- /dev/null +++ b/pkg/wallet/wallet_test.go @@ -0,0 +1,929 @@ +/* +Copyright SecureKey Technologies Inc. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 +*/ + +package wallet + +import ( + "crypto/ed25519" + "crypto/sha256" + "errors" + "fmt" + "strings" + "testing" + "time" + + "github.com/btcsuite/btcutil/base58" + "github.com/stretchr/testify/require" + + "github.com/hyperledger/aries-framework-go/pkg/crypto/primitive/bbs12381g2pub" + "github.com/hyperledger/aries-framework-go/pkg/doc/did" + "github.com/hyperledger/aries-framework-go/pkg/doc/verifiable" + vdrapi "github.com/hyperledger/aries-framework-go/pkg/framework/aries/api/vdr" + "github.com/hyperledger/aries-framework-go/pkg/kms" + cryptomock "github.com/hyperledger/aries-framework-go/pkg/mock/crypto" + mockprovider "github.com/hyperledger/aries-framework-go/pkg/mock/provider" + "github.com/hyperledger/aries-framework-go/pkg/mock/secretlock" + mockstorage "github.com/hyperledger/aries-framework-go/pkg/mock/storage" + mockvdr "github.com/hyperledger/aries-framework-go/pkg/mock/vdr" + "github.com/hyperledger/aries-framework-go/pkg/secretlock/local/masterlock/pbkdf2" + "github.com/hyperledger/aries-framework-go/pkg/vdr/key" + "github.com/hyperledger/aries-framework-go/spi/storage" +) + +// nolint: lll +const ( + sampleUserID = "sample-user01" + sampleFakeTkn = "fake-auth-tkn" + toBeImplementedErr = "to be implemented" + sampleWalletErr = "sample wallet err" + sampleCreatedDate = "2020-12-25" + sampleChallenge = "sample-challenge" + sampleDomain = "sample-domain" + sampleUDCVC = `{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1", + "https://w3id.org/security/bbs/v1" + ], + "credentialSchema": [], + "credentialSubject": { + "degree": { + "type": "BachelorDegree", + "university": "MIT" + }, + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "name": "Jayden Doe", + "spouse": "did:example:c276e12ec21ebfeb1f712ebc6f1" + }, + "expirationDate": "2020-01-01T19:23:24Z", + "id": "http://example.edu/credentials/1872", + "issuanceDate": "2010-01-01T19:23:24Z", + "issuer": { + "id": "did:example:76e12ec712ebc6f1c221ebfeb1f", + "name": "Example University" + }, + "referenceNumber": 83294847, + "type": [ + "VerifiableCredential", + "UniversityDegreeCredential" + ] + }` + sampleInvalidDIDID = "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHdI" + sampleInvalidDID = `{ + "@context": ["https://w3id.org/did/v1"], + "id": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHdI", + "verificationMethod": [{ + "controller": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + "id": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + "publicKeyBase58": "5yKdnU7ToTjAoRNDzfuzVTfWBH38qyhE1b9xh4v8JaWF", + "type": "Ed25519VerificationKey2018" + }], + "capabilityDelegation": ["did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd"], + "capabilityInvocation": ["did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd"], + "keyAgreement": [{ + "controller": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + "id": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd#z6LShKMZ117txS1WuExddVM2rbJ2zy3AKFtZVY5WNi44aKzA", + "publicKeyBase58": "6eBPUhK2ryHmoras6qq5Y15Z9pW3ceiQcZMptFQXrxDQ", + "type": "X25519KeyAgreementKey2019" + }], + "created": "2021-03-23T16:23:39.682869-04:00", + "updated": "2021-03-23T16:23:39.682869-04:00" + }` + sampleInvalidDIDContent = `{ + "@context": ["https://w3id.org/did/v1"], + "id": "did:example:sampleInvalidDIDContent" + }` + + sampleDocResolutionResponse = `{ + "@context": [ + "https://w3id.org/wallet/v1", + "https://w3id.org/did-resolution/v1" + ], + "id": "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5", + "type": ["DIDResolutionResponse"], + "name": "Farming Sensor DID Document", + "image": "https://via.placeholder.com/150", + "description": "An IoT device in the middle of a corn field.", + "tags": ["professional"], + "correlation": ["4058a72a-9523-11ea-bb37-0242ac130002"], + "created": "2017-06-18T21:19:10Z", + "expires": "2026-06-18T21:19:10Z", + "didDocument": { + "@context": ["https://w3id.org/did/v1"], + "id": "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5", + "verificationMethod": [{ + "controller": "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5", + "id": "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5#z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5", + "publicKeyBase58": "8jkuMBqmu1TRA6is7TT5tKBksTZamrLhaXrg9NAczqeh", + "type": "Ed25519VerificationKey2018" + }], + "authentication": ["did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5#z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5"], + "assertionMethod": ["did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5#z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5"], + "capabilityDelegation": ["did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5#z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5"], + "capabilityInvocation": ["did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5#z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5"], + "keyAgreement": [{ + "controller": "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5", + "id": "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5#z6LSmjNfS5FC9W59JtPZq7fHgrjThxsidjEhZeMxCarbR998", + "publicKeyBase58": "B4CVumSL43MQDW1oJU9LNGWyrpLbw84YgfeGi8D4hmNN", + "type": "X25519KeyAgreementKey2019" + }], + "created": "2021-03-23T19:25:18.513655-04:00", + "updated": "2021-03-23T19:25:18.513655-04:00" + } + }` +) + +func TestCreate(t *testing.T) { + t.Run("test create new wallet using local kms passphrase", func(t *testing.T) { + mockctx := newMockProvider() + err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + }) + + t.Run("test create new wallet using local kms secret lock service", func(t *testing.T) { + mockctx := newMockProvider() + err := CreateProfile(sampleUserID, mockctx, WithSecretLockService(&secretlock.MockSecretLock{})) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + }) + + t.Run("test create new wallet using remote kms key server URL", func(t *testing.T) { + mockctx := newMockProvider() + err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + }) + + t.Run("test create new wallet failure", func(t *testing.T) { + mockctx := newMockProvider() + err := CreateProfile(sampleUserID, mockctx) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid create profile options") + + wallet, err := New(sampleUserID, mockctx) + require.Error(t, err) + require.Empty(t, wallet) + }) + + t.Run("test create new wallet failure - create store error", func(t *testing.T) { + mockctx := newMockProvider() + mockctx.StorageProviderValue = &mockstorage.MockStoreProvider{ + ErrOpenStoreHandle: fmt.Errorf(sampleWalletErr), + } + + err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + + wallet, err := New(sampleUserID, mockctx) + require.Error(t, err) + require.Empty(t, wallet) + }) + + t.Run("test create new wallet failure - save profile error", func(t *testing.T) { + mockctx := newMockProvider() + mockctx.StorageProviderValue = &mockstorage.MockStoreProvider{ + Store: &mockstorage.MockStore{ + ErrPut: fmt.Errorf(sampleWalletErr), + }, + } + + err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + + wallet, err := New(sampleUserID, mockctx) + require.Error(t, err) + require.Empty(t, wallet) + }) + + t.Run("test create new wallet failure - create content store error", func(t *testing.T) { + mockctx := newMockProvider() + mockctx.StorageProviderValue = &mockStorageProvider{ + MockStoreProvider: mockstorage.NewMockStoreProvider(), + failure: fmt.Errorf(sampleWalletErr), + } + + err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.Error(t, err) + require.Empty(t, wallet) + require.Contains(t, err.Error(), "failed to get wallet content store:") + }) +} + +func TestUpdate(t *testing.T) { + t.Run("test update wallet using local kms passphrase", func(t *testing.T) { + mockctx := newMockProvider() + createSampleProfile(t, mockctx) + + err := UpdateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + require.NotEmpty(t, wallet.profile.MasterLockCipher) + }) + + t.Run("test update wallet using local kms secret lock service", func(t *testing.T) { + mockctx := newMockProvider() + createSampleProfile(t, mockctx) + + err := UpdateProfile(sampleUserID, mockctx, WithSecretLockService(&secretlock.MockSecretLock{})) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + }) + + t.Run("test update wallet using remote kms key server URL", func(t *testing.T) { + mockctx := newMockProvider() + createSampleProfile(t, mockctx) + + err := UpdateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + require.Empty(t, wallet.profile.MasterLockCipher) + require.NotEmpty(t, wallet.profile.KeyServerURL) + }) + + t.Run("test update wallet failure", func(t *testing.T) { + mockctx := newMockProvider() + createSampleProfile(t, mockctx) + + err := UpdateProfile(sampleUserID, mockctx) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid create profile options") + + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + require.NotEmpty(t, wallet.profile.MasterLockCipher) + }) + + t.Run("test update wallet failure - profile doesn't exists", func(t *testing.T) { + mockctx := newMockProvider() + err := UpdateProfile(sampleUserID, mockctx) + require.Error(t, err) + require.Contains(t, err.Error(), "profile does not exist") + + wallet, err := New(sampleUserID, mockctx) + require.Error(t, err) + require.Empty(t, wallet) + }) + + t.Run("test update wallet failure - create store error", func(t *testing.T) { + mockctx := newMockProvider() + mockctx.StorageProviderValue = &mockstorage.MockStoreProvider{ + ErrOpenStoreHandle: fmt.Errorf(sampleWalletErr), + } + + err := UpdateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + + wallet, err := New(sampleUserID, mockctx) + require.Error(t, err) + require.Empty(t, wallet) + }) + + t.Run("test update wallet failure - save profile error", func(t *testing.T) { + mockctx := newMockProvider() + createSampleProfile(t, mockctx) + + mockctx.StorageProviderValue.(*mockstorage.MockStoreProvider).Store.ErrPut = fmt.Errorf(sampleWalletErr) + + err := UpdateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + require.Error(t, err) + require.Contains(t, err.Error(), sampleWalletErr) + + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + require.Empty(t, wallet.profile.KeyServerURL) + require.NotEmpty(t, wallet.profile.MasterLockCipher) + }) +} + +func TestNew(t *testing.T) { + t.Run("test get wallet by user", func(t *testing.T) { + mockctx := newMockProvider() + // create a wallet + err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + }) + + t.Run("test get wallet by invalid userID", func(t *testing.T) { + mockctx := newMockProvider() + err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + wallet, err := New(sampleUserID+"invalid", mockctx) + require.Empty(t, wallet) + require.Error(t, err) + require.Contains(t, err.Error(), "profile does not exist") + }) + + t.Run("test update wallet failure - save profile error", func(t *testing.T) { + mockctx := newMockProvider() + mockctx.StorageProviderValue = &mockstorage.MockStoreProvider{ + ErrOpenStoreHandle: fmt.Errorf(sampleWalletErr), + } + + wallet, err := New(sampleUserID, mockctx) + require.Error(t, err) + require.Empty(t, wallet) + require.Contains(t, err.Error(), sampleWalletErr) + }) +} + +func TestWallet_OpenClose(t *testing.T) { + t.Run("test open & close wallet using local kms passphrase", func(t *testing.T) { + mockctx := newMockProvider() + err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + // get token + token, err := wallet.Open(WithUnlockByPassphrase(samplePassPhrase), WithUnlockExpiry(500*time.Millisecond)) + require.NoError(t, err) + require.NotEmpty(t, token) + + // try again + token, err = wallet.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.Empty(t, token) + require.Error(t, err) + require.Equal(t, err, ErrAlreadyUnlocked) + + // close wallet + require.True(t, wallet.Close()) + require.False(t, wallet.Close()) + + // try to open with wrong passphrase + token, err = wallet.Open(WithUnlockByPassphrase(samplePassPhrase + "wrong")) + require.Empty(t, token) + require.Error(t, err) + require.Contains(t, err.Error(), "message authentication failed") + }) + + t.Run("test open & close wallet using secret lock service", func(t *testing.T) { + mockctx := newMockProvider() + masterLock, err := pbkdf2.NewMasterLock(samplePassPhrase, sha256.New, 0, nil) + require.NoError(t, err) + + err = CreateProfile(sampleUserID, mockctx, WithSecretLockService(masterLock)) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + // get token + token, err := wallet.Open(WithUnlockBySecretLockService(masterLock)) + require.NoError(t, err) + require.NotEmpty(t, token) + + // try again + token, err = wallet.Open(WithUnlockBySecretLockService(masterLock)) + require.Empty(t, token) + require.Error(t, err) + require.Equal(t, err, ErrAlreadyUnlocked) + + // close wallet + require.True(t, wallet.Close()) + require.False(t, wallet.Close()) + + // try to open with wrong secret lock service + badLock, err := pbkdf2.NewMasterLock(samplePassPhrase+"wrong", sha256.New, 0, nil) + require.NoError(t, err) + + token, err = wallet.Open(WithUnlockBySecretLockService(badLock)) + require.Empty(t, token) + require.Error(t, err) + require.Contains(t, err.Error(), "message authentication failed") + }) + + t.Run("test open & close wallet using remote kms URL", func(t *testing.T) { + mockctx := newMockProvider() + err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + + // get token + token, err := wallet.Open(WithUnlockByAuthorizationToken(sampleRemoteKMSAuth)) + require.NoError(t, err) + require.NotEmpty(t, token) + + // try again + token, err = wallet.Open(WithUnlockByAuthorizationToken(sampleRemoteKMSAuth)) + require.Empty(t, token) + require.Error(t, err) + require.Equal(t, err, ErrAlreadyUnlocked) + + // close wallet + require.True(t, wallet.Close()) + require.False(t, wallet.Close()) + }) +} + +func TestWallet_Export(t *testing.T) { + mockctx := newMockProvider() + err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + require.NoError(t, err) + + walletInstance, err := New(sampleUserID, mockctx) + require.NotEmpty(t, walletInstance) + require.NoError(t, err) + + result, err := walletInstance.Export("") + require.Empty(t, result) + require.Error(t, err) + require.EqualError(t, err, toBeImplementedErr) +} + +func TestWallet_Import(t *testing.T) { + mockctx := newMockProvider() + err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + require.NoError(t, err) + + walletInstance, err := New(sampleUserID, mockctx) + require.NotEmpty(t, walletInstance) + require.NoError(t, err) + + err = walletInstance.Import("", nil) + require.Error(t, err) + require.EqualError(t, err, toBeImplementedErr) +} + +func TestWallet_Add(t *testing.T) { + mockctx := newMockProvider() + err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + require.NoError(t, err) + + walletInstance, err := New(sampleUserID, mockctx) + require.NotEmpty(t, walletInstance) + require.NoError(t, err) + + err = walletInstance.Add(Metadata, []byte(sampleContentValid)) + require.NoError(t, err) +} + +func TestWallet_Get(t *testing.T) { + mockctx := newMockProvider() + err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + require.NoError(t, err) + + walletInstance, err := New(sampleUserID, mockctx) + require.NotEmpty(t, walletInstance) + require.NoError(t, err) + + err = walletInstance.Add(Metadata, []byte(sampleContentValid)) + require.NoError(t, err) + + content, err := walletInstance.Get(Metadata, "did:example:123456789abcdefghi") + require.NoError(t, err) + require.NotEmpty(t, content) + require.Equal(t, sampleContentValid, string(content)) +} + +func TestWallet_Remove(t *testing.T) { + mockctx := newMockProvider() + err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + require.NoError(t, err) + + walletInstance, err := New(sampleUserID, mockctx) + require.NotEmpty(t, walletInstance) + require.NoError(t, err) + + err = walletInstance.Add(Metadata, []byte(sampleContentValid)) + require.NoError(t, err) + + content, err := walletInstance.Get(Metadata, "did:example:123456789abcdefghi") + require.NoError(t, err) + require.NotEmpty(t, content) + + err = walletInstance.Remove(Metadata, "did:example:123456789abcdefghi") + require.NoError(t, err) + + content, err = walletInstance.Get(Metadata, "did:example:123456789abcdefghi") + require.Empty(t, content) + require.Error(t, err) + require.True(t, errors.Is(err, storage.ErrDataNotFound)) +} + +func TestWallet_Query(t *testing.T) { + mockctx := newMockProvider() + err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + require.NoError(t, err) + + walletInstance, err := New(sampleUserID, mockctx) + require.NotEmpty(t, walletInstance) + require.NoError(t, err) + + results, err := walletInstance.Query(&QueryParams{}) + require.Empty(t, results) + require.Error(t, err) + require.EqualError(t, err, toBeImplementedErr) +} + +func TestWallet_Issue(t *testing.T) { + didKey := "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5" + pkBase58 := "2MP5gWCnf67jvW3E4Lz8PpVrDWAXMYY1sDxjnkEnKhkkbKD7yP2mkVeyVpu5nAtr3TeDgMNjBPirk2XcQacs3dvZ" + kid := "z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5" + + customVDR := &mockvdr.MockVDRegistry{ + ResolveFunc: func(didID string, opts ...vdrapi.ResolveOption) (*did.DocResolution, error) { + if didID == sampleInvalidDIDID { + d, e := did.ParseDocument([]byte(sampleInvalidDID)) + require.NoError(t, e) + + return &did.DocResolution{DIDDocument: d}, nil + } else if strings.HasPrefix(didID, "did:key:") { + k := key.New() + + d, e := k.Read(didID) + if e != nil { + return nil, e + } + + return d, nil + } + + return nil, fmt.Errorf("did not found") + }, + } + + mockctx := newMockProvider() + mockctx.VDRegistryValue = customVDR + mockctx.CryptoValue = &cryptomock.Crypto{} + + err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + t.Run("Test VC wallet issue using controller - success", func(t *testing.T) { + walletInstance, err := New(sampleUserID, mockctx) + require.NotEmpty(t, walletInstance) + require.NoError(t, err) + + // unlock wallet + authToken, err := walletInstance.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, authToken) + + defer walletInstance.Close() + + // import keys manually + kmgr, err := keyManager().getKeyManger(authToken) + require.NoError(t, err) + edPriv := ed25519.PrivateKey(base58.Decode(pkBase58)) + // nolint: errcheck, gosec + kmgr.ImportPrivateKey(edPriv, kms.ED25519, kms.WithKeyID(kid)) + + // sign with just controller + result, err := walletInstance.Issue(authToken, []byte(sampleUDCVC), &ProofOptions{ + Controller: didKey, + }) + require.NoError(t, err) + require.NotEmpty(t, result) + require.Len(t, result.Proofs, 1) + }) + + t.Run("Test VC wallet issue using verification method - success", func(t *testing.T) { + walletInstance, err := New(sampleUserID, mockctx) + require.NotEmpty(t, walletInstance) + require.NoError(t, err) + + // unlock wallet + authToken, err := walletInstance.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, authToken) + + defer walletInstance.Close() + + // import keys manually + kmgr, err := keyManager().getKeyManger(authToken) + require.NoError(t, err) + edPriv := ed25519.PrivateKey(base58.Decode(pkBase58)) + // nolint: errcheck, gosec + kmgr.ImportPrivateKey(edPriv, kms.ED25519, kms.WithKeyID(kid)) + + // issue + result, err := walletInstance.Issue(authToken, []byte(sampleUDCVC), &ProofOptions{ + Controller: didKey, + VerificationMethod: "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5#z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5", //nolint:lll + }) + require.NoError(t, err) + require.NotEmpty(t, result) + require.Len(t, result.Proofs, 1) + }) + + t.Run("Test VC wallet issue using all options - success", func(t *testing.T) { + walletInstance, err := New(sampleUserID, mockctx) + require.NotEmpty(t, walletInstance) + require.NoError(t, err) + + // unlock wallet + authToken, err := walletInstance.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, authToken) + + defer walletInstance.Close() + + // import keys manually + kmgr, err := keyManager().getKeyManger(authToken) + require.NoError(t, err) + edPriv := ed25519.PrivateKey(base58.Decode(pkBase58)) + // nolint: errcheck, gosec + kmgr.ImportPrivateKey(edPriv, kms.ED25519, kms.WithKeyID(kid)) + + // issue credential + proofRepr := verifiable.SignatureJWS + vm := "did:key:z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5#z6MknC1wwS6DEYwtGbZZo2QvjQjkh2qSBjb4GYmbye8dv4S5" + created, err := time.Parse("2006-01-02", sampleCreatedDate) + require.NoError(t, err) + + result, err := walletInstance.Issue(authToken, []byte(sampleUDCVC), &ProofOptions{ + Controller: didKey, + VerificationMethod: vm, + ProofType: JSONWebSignature2020, + Challenge: sampleChallenge, + Domain: sampleDomain, + Created: &created, + ProofRepresentation: &proofRepr, + }) + require.NoError(t, err) + require.NotEmpty(t, result) + require.Len(t, result.Proofs, 1) + + require.Equal(t, result.Proofs[0]["challenge"], sampleChallenge) + require.Equal(t, result.Proofs[0]["created"], "2020-12-25T00:00:00Z") + require.Equal(t, result.Proofs[0]["domain"], sampleDomain) + require.NotEmpty(t, result.Proofs[0]["jws"]) + require.Equal(t, result.Proofs[0]["proofPurpose"], "assertionMethod") + require.Equal(t, result.Proofs[0]["type"], JSONWebSignature2020) + require.Equal(t, result.Proofs[0]["verificationMethod"], vm) + }) + + // nolint:lll + t.Run("Test VC wallet issue using BBS - success", func(t *testing.T) { + didKeyBBS := "did:key:zUC72c7u4BYVmfYinDceXkNAwzPEyuEE23kUmJDjLy8495KH3pjLwFhae1Fww9qxxRdLnS2VNNwni6W3KbYZKsicDtiNNEp76fYWR6HCD8jAz6ihwmLRjcHH6kB294Xfg1SL1qQ" + pkBBSBase58 := "6gsgGpdx7p1nYoKJ4b5fKt1xEomWdnemg9nJFX6mqNCh" + keyIDBBS := "zUC72c7u4BYVmfYinDceXkNAwzPEyuEE23kUmJDjLy8495KH3pjLwFhae1Fww9qxxRdLnS2VNNwni6W3KbYZKsicDtiNNEp76fYWR6HCD8jAz6ihwmLRjcHH6kB294Xfg1SL1qQ" + + walletInstance, err := New(sampleUserID, mockctx) + require.NotEmpty(t, walletInstance) + require.NoError(t, err) + + // unlock wallet + authToken, err := walletInstance.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, authToken) + + defer walletInstance.Close() + + // import keys manually + kmgr, err := keyManager().getKeyManger(authToken) + require.NoError(t, err) + privKeyBBS, err := bbs12381g2pub.UnmarshalPrivateKey(base58.Decode(pkBBSBase58)) + require.NoError(t, err) + // nolint: errcheck, gosec + kmgr.ImportPrivateKey(privKeyBBS, kms.BLS12381G2Type, kms.WithKeyID(keyIDBBS)) + + // sign with just controller + proofRepr := verifiable.SignatureProofValue + result, err := walletInstance.Issue(authToken, []byte(sampleUDCVC), &ProofOptions{ + Controller: didKeyBBS, + ProofType: BbsBlsSignature2020, + ProofRepresentation: &proofRepr, + }) + require.NoError(t, err) + require.NotEmpty(t, result) + require.Len(t, result.Proofs, 1) + }) + + t.Run("Test VC wallet issue using stored DID - success", func(t *testing.T) { + mockctx1 := newMockProvider() + mockctx1.VDRegistryValue = &mockvdr.MockVDRegistry{} + mockctx1.CryptoValue = &cryptomock.Crypto{} + + err := CreateProfile(sampleUserID, mockctx1, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + walletInstance, err := New(sampleUserID, mockctx1) + require.NotEmpty(t, walletInstance) + require.NoError(t, err) + + // unlock wallet + authToken, err := walletInstance.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, authToken) + + defer walletInstance.Close() + + // import keys manually + kmgr, err := keyManager().getKeyManger(authToken) + require.NoError(t, err) + edPriv := ed25519.PrivateKey(base58.Decode(pkBase58)) + // nolint: errcheck, gosec + kmgr.ImportPrivateKey(edPriv, kms.ED25519, kms.WithKeyID(kid)) + + // save DID Resolution response + err = walletInstance.Add(DIDResolutionResponse, []byte(sampleDocResolutionResponse)) + require.NoError(t, err) + + // sign with just controller + result, err := walletInstance.Issue(authToken, []byte(sampleUDCVC), &ProofOptions{ + Controller: didKey, + }) + require.NoError(t, err) + require.NotEmpty(t, result) + require.Len(t, result.Proofs, 1) + }) + + t.Run("Test VC wallet issue failure - invalid VC", func(t *testing.T) { + walletInstance, err := New(sampleUserID, mockctx) + require.NotEmpty(t, walletInstance) + require.NoError(t, err) + + result, err := walletInstance.Issue(sampleFakeTkn, []byte("--"), &ProofOptions{}) + require.Empty(t, result) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to parse credential") + }) + + t.Run("Test VC wallet issue failure - proof option validation", func(t *testing.T) { + walletInstance, err := New(sampleUserID, mockctx) + require.NotEmpty(t, walletInstance) + require.NoError(t, err) + + // no controller + result, err := walletInstance.Issue(sampleFakeTkn, []byte(sampleUDCVC), &ProofOptions{}) + require.Empty(t, result) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid proof option, 'controller' is required") + + // DID not found + result, err = walletInstance.Issue(sampleFakeTkn, []byte(sampleUDCVC), &ProofOptions{ + Controller: "did:example:1234", + }) + require.Empty(t, result) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to read DID document from wallet store or from VDR") + + // no assertion method + result, err = walletInstance.Issue(sampleFakeTkn, []byte(sampleUDCVC), &ProofOptions{ + Controller: sampleInvalidDIDID, + }) + require.Empty(t, result) + require.Error(t, err) + require.Contains(t, err.Error(), "unable to find 'assertionMethod' for given verification method") + + // invalid DID in store + err = walletInstance.Add(DIDResolutionResponse, []byte(sampleInvalidDIDContent)) + require.NoError(t, err) + + result, err = walletInstance.Issue(sampleFakeTkn, []byte(sampleUDCVC), &ProofOptions{ + Controller: "did:example:sampleInvalidDIDContent", + }) + require.Empty(t, result) + require.Error(t, err) + require.Contains(t, err.Error(), "failed to parse stored DID") + }) + + t.Run("Test VC wallet issue failure - add proof errors", func(t *testing.T) { + walletInstance, err := New(sampleUserID, mockctx) + require.NotEmpty(t, walletInstance) + require.NoError(t, err) + + // wallet locked + result, err := walletInstance.Issue(sampleFakeTkn, []byte(sampleUDCVC), &ProofOptions{ + Controller: didKey, + }) + require.Empty(t, result) + require.Error(t, err) + require.Contains(t, err.Error(), "wallet locked") + + // get token + authToken, err := walletInstance.Open(WithUnlockByPassphrase(samplePassPhrase)) + require.NoError(t, err) + require.NotEmpty(t, authToken) + + defer walletInstance.Close() + + // key not found + result, err = walletInstance.Issue(authToken, []byte(sampleUDCVC), &ProofOptions{ + Controller: "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", + }) + require.Empty(t, result) + require.Contains(t, err.Error(), "cannot read data for keysetID") + + // import keys manually + kmgr, err := keyManager().getKeyManger(authToken) + require.NoError(t, err) + edPriv := ed25519.PrivateKey(base58.Decode(pkBase58)) + // nolint: errcheck, gosec + kmgr.ImportPrivateKey(edPriv, kms.ED25519, kms.WithKeyID(kid)) + + // invalid signature type + result, err = walletInstance.Issue(authToken, []byte(sampleUDCVC), &ProofOptions{ + Controller: didKey, + ProofType: "invalid", + }) + require.Empty(t, result) + require.Contains(t, err.Error(), " unsupported signature type 'invalid'") + + // wrong key type + result, err = walletInstance.Issue(authToken, []byte(sampleUDCVC), &ProofOptions{ + Controller: didKey, + ProofType: BbsBlsSignature2020, + }) + require.Empty(t, result) + require.Contains(t, err.Error(), "failed to add linked data proof") + }) +} + +func Test_AddContext(t *testing.T) { + vc, err := verifiable.ParseCredential([]byte(sampleUDCVC)) + require.NoError(t, err) + require.NotEmpty(t, vc) + + require.Len(t, vc.Context, 3) + addContext(vc, bbsContext) + require.Len(t, vc.Context, 3) + addContext(vc, bbsContext+".01") + require.Len(t, vc.Context, 4) +} + +func TestWallet_Prove(t *testing.T) { + mockctx := newMockProvider() + err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + require.NoError(t, err) + + walletInstance, err := New(sampleUserID, mockctx) + require.NotEmpty(t, walletInstance) + require.NoError(t, err) + + result, err := walletInstance.Prove(nil, &ProofOptions{}) + require.Empty(t, result) + require.Error(t, err) + require.EqualError(t, err, toBeImplementedErr) +} + +func TestWallet_Verify(t *testing.T) { + mockctx := newMockProvider() + err := CreateProfile(sampleUserID, mockctx, WithKeyServerURL(sampleKeyServerURL)) + require.NoError(t, err) + + walletInstance, err := New(sampleUserID, mockctx) + require.NotEmpty(t, walletInstance) + require.NoError(t, err) + + result, err := walletInstance.Verify(nil) + require.Empty(t, result) + require.Error(t, err) + require.EqualError(t, err, toBeImplementedErr) +} + +func newMockProvider() *mockprovider.Provider { + return &mockprovider.Provider{StorageProviderValue: mockstorage.NewMockStoreProvider()} +} + +func createSampleProfile(t *testing.T, mockctx *mockprovider.Provider) { + err := CreateProfile(sampleUserID, mockctx, WithPassphrase(samplePassPhrase)) + require.NoError(t, err) + + wallet, err := New(sampleUserID, mockctx) + require.NoError(t, err) + require.NotEmpty(t, wallet) + require.NotEmpty(t, wallet.profile.MasterLockCipher) +}