Skip to content

Commit

Permalink
VCR: Introduce wallet containing the node's own credentials (#2446)
Browse files Browse the repository at this point in the history
  • Loading branch information
reinkrul authored Sep 8, 2023
1 parent e3304a1 commit 11f516e
Show file tree
Hide file tree
Showing 11 changed files with 678 additions and 60 deletions.
9 changes: 8 additions & 1 deletion docs/pages/deployment/backup-restore.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,11 @@ To restore a backup, follow the following steps:
BBolt
=====

In step 3, copy ``network/data.db``, ``vcr/backup-credentials.db``, ``vcr/backup-issued-credentials.db``, ``vcr/backup-revoked-credentials.db`` and ``vdr/didstore.db`` from your backup to the ``datadir`` (keep the directory structure).
In step 3, copy the following files from your backup to the ``datadir`` (keep the directory structure)

- ``network/data.db``
- ``vcr/wallet.db``
- ``vcr/backup-credentials.db``
- ``vcr/backup-issued-credentials.db``
- ``vcr/backup-revoked-credentials.db``
- ``vdr/didstore.db``
1 change: 1 addition & 0 deletions vcr/ambassador_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ func TestAmbassador_handleReprocessEvent(t *testing.T) {
// mocks
publicKey := signer.Public()

ctx.vdr.EXPECT().IsOwner(gomock.Any(), gomock.Any()).Return(false, nil)
ctx.didResolver.EXPECT().Resolve(gomock.Any(), gomock.Any()).Return(documentWithPublicKey(t, publicKey), nil, nil)

// Publish a VC
Expand Down
13 changes: 13 additions & 0 deletions vcr/holder/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/vcr/signature/proof"
)

Expand All @@ -34,10 +35,22 @@ var VerifiablePresentationLDType = ssi.MustParseURI("VerifiablePresentation")

// Wallet holds Verifiable Credentials and can present them.
type Wallet interface {
core.Diagnosable

// BuildPresentation builds and signs a Verifiable Presentation using the given Verifiable Credentials.
// The assertion key used for signing it is taken from signerDID's DID document.
// If signerDID is not provided, it will be derived from the credentials credentialSubject.id fields. But only if all provided credentials have the same (singular) credentialSubject.id field.
BuildPresentation(ctx context.Context, credentials []vc.VerifiableCredential, options PresentationOptions, signerDID *did.DID, validateVC bool) (*vc.VerifiablePresentation, error)

// List returns all credentials in the wallet for the given holder.
List(ctx context.Context, holderDID did.DID) ([]vc.VerifiableCredential, error)

// Put adds the given credentials to the wallet. It is an all-or-nothing operation:
// if one of them fails, none of the credentials are added.
Put(ctx context.Context, credentials ...vc.VerifiableCredential) error

// IsEmpty returns true if the wallet contains no credentials at all (for all holder DIDs).
IsEmpty() (bool, error)
}

// PresentationOptions contains parameters used to create the right VerifiablePresentation
Expand Down
64 changes: 64 additions & 0 deletions vcr/holder/mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

115 changes: 113 additions & 2 deletions vcr/holder/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,36 +20,46 @@ package holder

import (
"context"
"encoding/binary"
"encoding/json"
"errors"
"fmt"

ssi "github.com/nuts-foundation/go-did"
"github.com/nuts-foundation/go-did/did"
"github.com/nuts-foundation/go-did/vc"
"github.com/nuts-foundation/go-stoabs"
"github.com/nuts-foundation/nuts-node/core"
"github.com/nuts-foundation/nuts-node/crypto"
"github.com/nuts-foundation/nuts-node/jsonld"
"github.com/nuts-foundation/nuts-node/vcr/log"
"github.com/nuts-foundation/nuts-node/vcr/signature"
"github.com/nuts-foundation/nuts-node/vcr/signature/proof"
"github.com/nuts-foundation/nuts-node/vcr/verifier"
vdr "github.com/nuts-foundation/nuts-node/vdr/types"
)

const statsShelf = "stats"

var credentialCountStatsKey = stoabs.BytesKey("credential_count")

type wallet struct {
keyResolver vdr.KeyResolver
keyStore crypto.KeyStore
verifier verifier.Verifier
jsonldManager jsonld.JSONLD
walletStore stoabs.KVStore
}

// New creates a new Wallet.
func New(keyResolver vdr.KeyResolver, keyStore crypto.KeyStore, verifier verifier.Verifier, jsonldManager jsonld.JSONLD) Wallet {
func New(
keyResolver vdr.KeyResolver, keyStore crypto.KeyStore, verifier verifier.Verifier, jsonldManager jsonld.JSONLD,
walletStore stoabs.KVStore) Wallet {
return &wallet{
keyResolver: keyResolver,
keyStore: keyStore,
verifier: verifier,
jsonldManager: jsonldManager,
walletStore: walletStore,
}
}

Expand Down Expand Up @@ -120,6 +130,96 @@ func (h wallet) BuildPresentation(ctx context.Context, credentials []vc.Verifiab
return &signedVP, nil
}

func (h wallet) Put(ctx context.Context, credentials ...vc.VerifiableCredential) error {
err := h.walletStore.Write(ctx, func(tx stoabs.WriteTx) error {
stats := tx.GetShelfWriter(statsShelf)
var newCredentials uint32
for _, credential := range credentials {
subjectDID, err := h.resolveSubjectDID([]vc.VerifiableCredential{credential})
if err != nil {
return fmt.Errorf("unable to resolve subject DID from VC %s: %w", credential.ID, err)
}
walletKey := stoabs.BytesKey(credential.ID.String())
// First check if the VC doesn't already exist; otherwise stats will be incorrect
walletShelf := tx.GetShelfWriter(subjectDID.String())
_, err = walletShelf.Get(walletKey)
if err == nil {
// Already exists
continue
} else if !errors.Is(err, stoabs.ErrKeyNotFound) {
// Other error
return fmt.Errorf("unable to check if credential %s already exists: %w", credential.ID, err)
}
// Write credential
data, _ := credential.MarshalJSON()
err = walletShelf.Put(walletKey, data)
if err != nil {
return fmt.Errorf("unable to store credential %s: %w", credential.ID, err)
}
newCredentials++
}
// Update stats
currentCount, err := h.readCredentialCount(stats)
if err != nil {
return fmt.Errorf("unable to read wallet credential count: %w", err)
}
return stats.Put(credentialCountStatsKey, binary.BigEndian.AppendUint32([]byte{}, currentCount+newCredentials))
}, stoabs.WithWriteLock()) // lock required for stats consistency
if err != nil {
return fmt.Errorf("unable to store credential(s): %w", err)
}
return nil
}

func (h wallet) List(ctx context.Context, holderDID did.DID) ([]vc.VerifiableCredential, error) {
var result []vc.VerifiableCredential
err := h.walletStore.ReadShelf(ctx, holderDID.String(), func(reader stoabs.Reader) error {
return reader.Iterate(func(key stoabs.Key, value []byte) error {
var cred vc.VerifiableCredential
err := json.Unmarshal(value, &cred)
if err != nil {
return fmt.Errorf("unable to unmarshal credential %s: %w", string(key.Bytes()), err)
}
result = append(result, cred)
return nil
}, stoabs.BytesKey{})
})
if err != nil {
return nil, fmt.Errorf("unable to list credentials: %w", err)
}
return result, nil
}

func (h wallet) Diagnostics() []core.DiagnosticResult {
ctx := context.Background()
var count uint32
var err error
err = h.walletStore.Read(ctx, func(tx stoabs.ReadTx) error {
count, err = h.readCredentialCount(tx.GetShelfReader(statsShelf))
return err
})
if err != nil {
log.Logger().WithError(err).Warn("unable to read credential count in wallet")
}
return []core.DiagnosticResult{
core.GenericDiagnosticResult{
Title: "credential_count",
Outcome: int(count),
},
}
}

func (h wallet) IsEmpty() (bool, error) {
ctx := context.Background()
var count uint32
var err error
err = h.walletStore.Read(ctx, func(tx stoabs.ReadTx) error {
count, err = h.readCredentialCount(tx.GetShelfReader(statsShelf))
return err
})
return count == 0, err
}

func (h wallet) resolveSubjectDID(credentials []vc.VerifiableCredential) (*did.DID, error) {
var subjectID did.DID
for _, credential := range credentials {
Expand All @@ -139,3 +239,14 @@ func (h wallet) resolveSubjectDID(credentials []vc.VerifiableCredential) (*did.D

return &subjectID, nil
}

func (h wallet) readCredentialCount(statsShelf stoabs.Reader) (uint32, error) {
countBytes, err := statsShelf.Get(credentialCountStatsKey)
if errors.Is(err, stoabs.ErrKeyNotFound) {
// No stats yet
countBytes = make([]byte, 4)
} else if err != nil {
return 0, fmt.Errorf("error reading credential count for wallet: %w", err)
}
return binary.BigEndian.Uint32(countBytes), nil
}
Loading

0 comments on commit 11f516e

Please sign in to comment.