nuts-foundation/nuts-node

View on GitHub
vdr/vdr.go

Summary

Maintainability
A
1 hr
Test Coverage
C
76%
/*
 * Nuts node
 * Copyright (C) 2021 Nuts community
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

// Package vdr contains a verifiable data registry to the w3c specification
// and provides primitives for storing and working with Nuts DID based identities.
// It provides an easy to work with web api and a command line interface.
// It provides underlying storage back ends to store, update and search for Nuts identities.
package vdr

import (
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "github.com/nuts-foundation/nuts-node/audit"

    ssi "github.com/nuts-foundation/go-did"
    "github.com/nuts-foundation/go-did/did"
    "github.com/nuts-foundation/nuts-node/core"
    "github.com/nuts-foundation/nuts-node/crypto"
    "github.com/nuts-foundation/nuts-node/events"
    "github.com/nuts-foundation/nuts-node/network"
    "github.com/nuts-foundation/nuts-node/storage"
    "github.com/nuts-foundation/nuts-node/vdr/didjwk"
    "github.com/nuts-foundation/nuts-node/vdr/didkey"
    "github.com/nuts-foundation/nuts-node/vdr/didnuts"
    didnutsStore "github.com/nuts-foundation/nuts-node/vdr/didnuts/didstore"
    "github.com/nuts-foundation/nuts-node/vdr/didnuts/util"
    "github.com/nuts-foundation/nuts-node/vdr/didweb"
    "github.com/nuts-foundation/nuts-node/vdr/log"
    "github.com/nuts-foundation/nuts-node/vdr/management"
    "github.com/nuts-foundation/nuts-node/vdr/resolver"
)

// ModuleName is the name of the engine
const ModuleName = "VDR"

var _ VDR = (*Module)(nil)
var _ core.Named = (*Module)(nil)
var _ core.Configurable = (*Module)(nil)

// Module implements VDR, which stands for the Verifiable Data Registry. It is the public entrypoint to work with W3C DID documents.
// It connects the Resolve, Create and Update DID methods to the network, and receives events back from the network which are processed in the store.
// It is also a Runnable, Diagnosable and Configurable Nuts Engine.
type Module struct {
    store             didnutsStore.Store
    network           network.Transactions
    networkAmbassador didnuts.Ambassador
    documentManagers  map[string]management.DocumentManager
    didResolver       *resolver.DIDResolverRouter
    serviceResolver   resolver.ServiceResolver
    keyStore          crypto.KeyStore
    storageInstance   storage.Engine
    eventManager      events.Event
}

// ResolveManaged resolves a DID document that is managed by the local node.
func (r *Module) ResolveManaged(id did.DID) (*did.Document, error) {
    manager := r.documentManagers[id.Method]
    if manager == nil {
        return nil, fmt.Errorf("unsupported method: %s", id.Method)
    }
    document, _, err := manager.Resolve(id, nil)
    return document, err
}

// Resolve resolves any DID document which DID method is supported.
// To only resolve DID documents managed by the local node, use ResolveManaged().
func (r *Module) Resolve(id did.DID, metadata *resolver.ResolveMetadata) (*did.Document, *resolver.DocumentMetadata, error) {
    return r.didResolver.Resolve(id, metadata)
}

func (r *Module) Resolver() resolver.DIDResolver {
    return r.didResolver
}

// NewVDR creates a new Module with provided params
func NewVDR(cryptoClient crypto.KeyStore, networkClient network.Transactions,
    didStore didnutsStore.Store, eventManager events.Event, storageInstance storage.Engine) *Module {
    didResolver := &resolver.DIDResolverRouter{}
    return &Module{
        network:         networkClient,
        eventManager:    eventManager,
        didResolver:     didResolver,
        store:           didStore,
        serviceResolver: resolver.DIDServiceResolver{Resolver: didResolver},
        keyStore:        cryptoClient,
        storageInstance: storageInstance,
    }
}

func (r *Module) Name() string {
    return ModuleName
}

// Configure configures the Module engine.
func (r *Module) Configure(config core.ServerConfig) error {
    r.networkAmbassador = didnuts.NewAmbassador(r.network, r.store, r.eventManager)

    // Methods we can produce from the Nuts node
    // did:nuts
    r.documentManagers = map[string]management.DocumentManager{
        didnuts.MethodName: didnuts.NewManager(
            didnuts.Creator{
                KeyStore:      r.keyStore,
                NetworkClient: r.network,
                DIDResolver:   r.store,
            },
            newCachingDocumentOwner(privateKeyDocumentOwner{keyResolver: r.keyStore}, r.didResolver),
        ),
    }
    // did:web
    publicURL, err := config.ServerURL()
    if err != nil {
        return err
    }
    rootDID, err := didweb.URLToDID(*publicURL)
    if err != nil {
        return err
    }
    manager := didweb.NewManager(*rootDID, "iam", r.keyStore, r.storageInstance.GetSQLDatabase())
    r.documentManagers[didweb.MethodName] = manager
    // did:web resolver should first look in own database, then resolve over the web
    webResolver := resolver.ChainedDIDResolver{
        Resolvers: []resolver.DIDResolver{
            manager,
            didweb.NewResolver(),
        },
    }

    // Register DID methods we can resolve
    r.didResolver.Register(didnuts.MethodName, &didnuts.Resolver{Store: r.store})
    r.didResolver.Register(didweb.MethodName, webResolver)
    r.didResolver.Register(didjwk.MethodName, didjwk.NewResolver())
    r.didResolver.Register(didkey.MethodName, didkey.NewResolver())

    // Initiate the routines for auto-updating the data.
    return r.networkAmbassador.Configure()
}

func (r *Module) Start() error {
    err := r.networkAmbassador.Start()
    if err != nil {
        return err
    }

    // VDR migration needs to be started after ambassador has started!
    count, err := r.store.DocumentCount()
    if err != nil {
        return err
    }
    if count == 0 {
        // remove after v6 release
        _, err = r.network.Reprocess(context.Background(), "application/did+json")
    }

    return err
}

func (r *Module) Shutdown() error {
    return nil
}

func (r *Module) ConflictedDocuments() ([]did.Document, []resolver.DocumentMetadata, error) {
    conflictedDocs := make([]did.Document, 0)
    conflictedMeta := make([]resolver.DocumentMetadata, 0)

    err := r.store.Conflicted(func(doc did.Document, metadata resolver.DocumentMetadata) error {
        conflictedDocs = append(conflictedDocs, doc)
        conflictedMeta = append(conflictedMeta, metadata)
        return nil
    })
    return conflictedDocs, conflictedMeta, err
}

func (r *Module) IsOwner(ctx context.Context, id did.DID) (bool, error) {
    manager := r.documentManagers[id.Method]
    if manager == nil {
        return false, fmt.Errorf("unsupported method: %s", id.Method)
    }
    return manager.IsOwner(ctx, id)
}

func (r *Module) ListOwned(ctx context.Context) ([]did.DID, error) {
    results := make([]did.DID, 0)
    for _, owner := range r.documentManagers {
        owned, err := owner.ListOwned(ctx)
        if err != nil {
            return nil, err
        }
        results = append(results, owned...)
    }
    return results, nil
}

// newOwnConflictedDocIterator accepts two counters and returns a new DocIterator that counts the total number of
// conflicted documents, both total and owned by this node.
func (r *Module) newOwnConflictedDocIterator(totalCount, ownedCount *int) management.DocIterator {
    return func(doc did.Document, metadata resolver.DocumentMetadata) error {
        *totalCount++
        controllers, err := didnuts.ResolveControllers(r.store, doc, nil)
        if err != nil {
            log.Logger().
                WithField(core.LogFieldDID, doc.ID).
                WithError(err).
                Info("failed to resolve controller of conflicted DID document")
            return nil
        }
        for _, controller := range controllers {
            // TODO: Fix context.TODO() when we have a context in the Diagnostics() method
            isOwned, err := r.IsOwner(context.TODO(), controller.ID)
            if err != nil {
                log.Logger().
                    WithField(core.LogFieldDID, controller.ID).
                    WithError(err).
                    Info("failed to check ownership of conflicted DID document")
            }
            if isOwned {
                *ownedCount++
            }
        }
        return nil
    }
}

// Diagnostics returns the diagnostics for this engine
func (r *Module) Diagnostics() []core.DiagnosticResult {
    // return # conflicted docs
    totalCount := 0
    ownedCount := 0

    // uses dedicated storage shelf for conflicted docs, does not loop over all documents
    err := r.store.Conflicted(r.newOwnConflictedDocIterator(&totalCount, &ownedCount))
    if err != nil {
        log.Logger().Errorf("Failed to resolve conflicted documents diagnostics: %v", err)
    }

    docCount, _ := r.store.DocumentCount()

    // to go from int+error to interface{}
    countOrError := func(count int, err error) interface{} {
        if err != nil {
            return "error"
        }
        return count
    }

    return []core.DiagnosticResult{
        core.DiagnosticResultMap{
            Title: "conflicted_did_documents",
            Items: []core.DiagnosticResult{
                &core.GenericDiagnosticResult{
                    Title:   "total_count",
                    Outcome: countOrError(totalCount, err),
                },
                &core.GenericDiagnosticResult{
                    Title:   "owned_count",
                    Outcome: countOrError(ownedCount, err),
                },
            },
        },
        &core.GenericDiagnosticResult{
            Title:   "did_documents_count",
            Outcome: docCount,
        },
    }
}

func (r *Module) Migrate() error {
    // Find all documents that are managed by this node
    owned, err := r.ListOwned(context.Background())
    if err != nil {
        return err
    }
    auditContext := audit.Context(context.Background(), "system", ModuleName, "migrate")
    // resolve the DID Document if the did starts with did:nuts
    for _, did := range owned {
        if did.Method == didnuts.MethodName {
            doc, _, err := r.Resolve(did, nil)
            if err != nil {
                return fmt.Errorf("could not resolve owned DID document: %w", err)
            }
            if len(doc.Controller) > 0 {
                doc.Controller = nil

                if len(doc.VerificationMethod) == 0 {
                    log.Logger().Warnf("No verification method found in owned DID document (did=%s)", did.String())
                    continue
                }

                if len(doc.CapabilityInvocation) == 0 {
                    // add all keys as capabilityInvocation keys
                    for _, vm := range doc.VerificationMethod {
                        doc.CapabilityInvocation.Add(vm)
                    }
                }

                err = r.Update(auditContext, did, *doc)
                if err != nil {
                    return fmt.Errorf("could not update owned DID document: %w", err)
                }
            }
        }
    }
    return nil
}

// Create generates a new DID Document
func (r *Module) Create(ctx context.Context, options management.CreationOptions) (*did.Document, crypto.Key, error) {
    log.Logger().Debug("Creating new DID Document.")
    manager := r.documentManagers[options.Method()]
    if manager == nil {
        return nil, nil, fmt.Errorf("%w: %s", management.ErrUnsupportedDIDMethod, options.Method())
    }
    doc, key, err := manager.Create(ctx, options)
    if err != nil {
        return nil, nil, fmt.Errorf("could not create DID document (method %s): %w", options.Method(), err)
    }
    log.Logger().
        WithField(core.LogFieldDID, doc.ID).
        Info("New DID Document created")
    return doc, key, nil
}

func (r *Module) Deactivate(ctx context.Context, id did.DID) error {
    log.Logger().
        WithField(core.LogFieldDID, id).
        Debug("Deactivating DID Document")
    manager := r.documentManagers[id.Method]
    if manager == nil {
        return fmt.Errorf("%w: %s", management.ErrUnsupportedDIDMethod, id.Method)
    }
    err := manager.Deactivate(ctx, id)
    if err != nil {
        return fmt.Errorf("could not deactivate DID document: %w", err)
    }
    log.Logger().
        WithField(core.LogFieldDID, id).
        Info("DID Document deactivated")
    return nil
}

// Update updates a DID Document based on the DID.
// It only works on did:nuts, so is subject for removal in the future.
func (r *Module) Update(ctx context.Context, id did.DID, next did.Document) error {
    log.Logger().
        WithField(core.LogFieldDID, id).
        Debug("Updating DID Document")
    resolverMetadata := &resolver.ResolveMetadata{
        AllowDeactivated: true,
    }

    // Since the update mechanism is "did:nuts"-specific, we can't accidentally update a non-"did:nuts" document,
    // but check it defensively to avoid obscure errors later.
    if id.Method != didnuts.MethodName {
        return fmt.Errorf("can't update DID document of type: %s", id.Method)
    }

    currentDIDDocument, currentMeta, err := r.store.Resolve(id, resolverMetadata)
    if err != nil {
        return fmt.Errorf("update DID document: %w", err)
    }
    if resolver.IsDeactivated(*currentDIDDocument) {
        return fmt.Errorf("update DID document: %w", resolver.ErrDeactivated)
    }

    // #1530: add nuts and JWS context if not present
    next = withJSONLDContext(next, didnuts.NutsDIDContextV1URI())
    next = withJSONLDContext(next, didnuts.JWS2020ContextV1URI())

    // Validate document. No more changes should be made to the document after this point.
    if err = didnuts.ManagedDocumentValidator(r.serviceResolver).Validate(next); err != nil {
        return fmt.Errorf("update DID document: %w", err)
    }

    payload, err := json.Marshal(next)
    if err != nil {
        return fmt.Errorf("update DID document: %w", err)
    }

    controller, key, err := r.resolveControllerWithKey(ctx, *currentDIDDocument)
    if err != nil {
        return fmt.Errorf("update DID document: %w", err)
    }

    // for the metadata
    _, controllerMeta, err := r.didResolver.Resolve(controller.ID, nil)
    if err != nil {
        return fmt.Errorf("update DID document: %w", err)
    }

    // a DIDDocument update must point to its previous version, current heads and the controller TX (for signing key transaction ordering)
    previousTransactions := append(currentMeta.SourceTransactions, controllerMeta.SourceTransactions...)

    tx := network.TransactionTemplate(didnuts.DIDDocumentType, payload, key).WithAdditionalPrevs(previousTransactions)
    _, err = r.network.CreateTransaction(ctx, tx)
    if err != nil {
        log.Logger().WithError(err).Warn("Unable to update DID document")
        if errors.Is(err, crypto.ErrPrivateKeyNotFound) {
            err = resolver.ErrDIDNotManagedByThisNode
        }
        return fmt.Errorf("update DID document: %w", err)
    }

    log.Logger().
        WithField(core.LogFieldDID, id).
        Info("DID Document updated")

    return nil
}

// CreateService creates a new service in the DID document identified by subjectDID.
func (r *Module) CreateService(ctx context.Context, subjectDID did.DID, service did.Service) (*did.Service, error) {
    manager := r.documentManagers[subjectDID.Method]
    if manager == nil {
        return nil, fmt.Errorf("unsupported method: %s", subjectDID.Method)
    }
    return manager.CreateService(ctx, subjectDID, service)
}

// UpdateService updates a service in the DID document identified by subjectDID.
func (r *Module) UpdateService(ctx context.Context, subjectDID did.DID, serviceID ssi.URI, service did.Service) (*did.Service, error) {
    manager := r.documentManagers[subjectDID.Method]
    if manager == nil {
        return nil, fmt.Errorf("unsupported method: %s", subjectDID.Method)
    }
    return manager.UpdateService(ctx, subjectDID, serviceID, service)
}

// DeleteService removes a service from the DID document identified by subjectDID.
func (r *Module) DeleteService(ctx context.Context, subjectDID did.DID, serviceID ssi.URI) error {
    manager := r.documentManagers[subjectDID.Method]
    if manager == nil {
        return fmt.Errorf("unsupported method: %s", subjectDID.Method)
    }
    return manager.DeleteService(ctx, subjectDID, serviceID)
}

func (r *Module) resolveControllerWithKey(ctx context.Context, doc did.Document) (did.Document, crypto.Key, error) {
    controllers, err := didnuts.ResolveControllers(r.store, doc, nil)
    if err != nil {
        return did.Document{}, nil, fmt.Errorf("error while finding controllers for document: %w", err)
    }
    if len(controllers) == 0 {
        return did.Document{}, nil, fmt.Errorf("could not find any controllers for document")
    }

    var key crypto.Key
    for _, c := range controllers {
        for _, cik := range c.CapabilityInvocation {
            key, err = r.keyStore.Resolve(ctx, cik.ID.String())
            if err == nil {
                return c, key, nil
            }
        }
    }

    if errors.Is(err, crypto.ErrPrivateKeyNotFound) {
        return did.Document{}, nil, resolver.ErrDIDNotManagedByThisNode
    }

    return did.Document{}, nil, fmt.Errorf("could not find capabilityInvocation key for updating the DID document: %w", err)
}

func withJSONLDContext(document did.Document, ctx ssi.URI) did.Document {
    contextPresent := false

    for _, c := range document.Context {
        if util.LDContextToString(c) == ctx.String() {
            contextPresent = true
        }
    }

    if !contextPresent {
        document.Context = append(document.Context, ctx)
    }
    return document
}