nuts-foundation/nuts-node

View on GitHub
vdr/didweb/manager.go

Summary

Maintainability
A
1 hr
Test Coverage
B
81%
/*
 * Copyright (C) 2023 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 didweb

import (
    "context"
    crypt "crypto"
    "encoding/json"
    "errors"
    "fmt"
    "github.com/google/uuid"
    ssi "github.com/nuts-foundation/go-did"
    "github.com/nuts-foundation/go-did/did"
    "github.com/nuts-foundation/nuts-node/crypto"
    "github.com/nuts-foundation/nuts-node/jsonld"
    "github.com/nuts-foundation/nuts-node/vdr/management"
    "github.com/nuts-foundation/nuts-node/vdr/resolver"
    "github.com/nuts-foundation/nuts-node/vdr/sql"
    "gorm.io/gorm"
)

func DefaultCreationOptions() management.CreationOptions {
    return management.Create(MethodName)
}

type rootDIDOption struct{}

// RootDID is an option to set the DID for the did:web document.
func RootDID() management.CreationOption {
    return rootDIDOption{}
}

var _ management.DocumentManager = (*Manager)(nil)

// NewManager creates a new Manager to create and update did:web DID documents.
func NewManager(rootDID did.DID, tenantPath string, keyStore crypto.KeyStore, db *gorm.DB) *Manager {
    return &Manager{
        db:         db,
        rootDID:    rootDID,
        tenantPath: tenantPath,
        keyStore:   keyStore,
    }
}

// Manager creates and updates did:web documents
type Manager struct {
    db         *gorm.DB
    rootDID    did.DID
    keyStore   crypto.KeyStore
    tenantPath string
}

func (m Manager) Deactivate(ctx context.Context, subjectDID did.DID) error {
    var err error
    var sqlDocument *sql.DIDDocument
    err = m.db.Transaction(func(tx *gorm.DB) error {
        didStore := sql.NewDIDManager(tx)
        documentStore := sql.NewDIDDocumentManager(tx)
        sqlDocument, err = documentStore.Latest(subjectDID)
        if err != nil {
            if errors.Is(err, gorm.ErrRecordNotFound) {
                return resolver.ErrNotFound
            }
            return err
        }
        return didStore.Delete(subjectDID)
    })
    if err != nil {
        return err
    }
    var deleteErrors []error
    for _, verificationMethod := range sqlDocument.VerificationMethods {
        if err := m.keyStore.Delete(ctx, verificationMethod.ID); err != nil {
            deleteErrors = append(deleteErrors, fmt.Errorf("verification method '%s': %w", verificationMethod.ID, err))
        }
    }
    if len(deleteErrors) == 0 {
        return nil
    }
    return errors.Join(append([]error{errors.New("did:web DID deleted, but could not remove one or more private keys")}, deleteErrors...)...)
}

func (m Manager) RemoveVerificationMethod(ctx context.Context, id did.DID, keyID did.DIDURL) error {
    return errors.New("RemoveVerificationMethod() is not yet supported for did:web")
}

func (m Manager) AddVerificationMethod(_ context.Context, _ did.DID, _ management.DIDKeyFlags) (*did.VerificationMethod, error) {
    return nil, errors.New("AddVerificationMethod() is not yet supported for did:web")
}

// Create creates a new did:web document.
func (m Manager) Create(ctx context.Context, opts management.CreationOptions) (*did.Document, crypto.Key, error) {
    var newDID *did.DID
    var err error
    for _, opt := range opts.All() {
        switch opt.(type) {
        case rootDIDOption:
            newDID = &m.rootDID
        default:
            return nil, nil, fmt.Errorf("unknown option: %T", opt)
        }
    }
    if newDID == nil {
        newDID, err = did.ParseDID(fmt.Sprintf("%s:iam:%s", m.rootDID.String(), uuid.NewString()))
    }
    if err != nil {
        return nil, nil, fmt.Errorf("parse new DID: %w", err)
    }
    var document did.Document
    var verificationMethodKey crypto.Key
    err = m.db.Transaction(func(tx *gorm.DB) error {
        var verificationMethod *did.VerificationMethod
        documentStore := sql.NewDIDDocumentManager(tx)

        _, err = documentStore.Latest(*newDID)
        if err == nil {
            return management.ErrDIDAlreadyExists
        }
        if !errors.Is(err, gorm.ErrRecordNotFound) {
            return err
        }

        verificationMethodKey, verificationMethod, err = m.createVerificationMethod(ctx, *newDID)
        if err != nil {
            return err
        }
        vmAsJson, err := json.Marshal(verificationMethod)
        if err != nil {
            return err
        }

        sqlDid := sql.DID{
            ID:      newDID.String(),
            Subject: newDID.String(), // todo pass through options
        }
        var doc *sql.DIDDocument
        if doc, err = documentStore.CreateOrUpdate(sqlDid, []sql.SqlVerificationMethod{{
            ID:            verificationMethod.ID.String(),
            DIDDocumentID: sqlDid.ID,
            KeyTypes:      sql.VerificationMethodKeyType(management.AssertionMethodUsage | management.AuthenticationUsage | management.CapabilityDelegationUsage | management.CapabilityInvocationUsage), // todo pass via options
            Data:          vmAsJson,
        }}, nil); err != nil {
            return fmt.Errorf("store new DID document: %w", err)
        }

        document, err = buildDocument(*newDID, *doc)
        return err
    })

    return &document, verificationMethodKey, err
}

func (m Manager) createVerificationMethod(ctx context.Context, ownerDID did.DID) (crypto.Key, *did.VerificationMethod, error) {
    verificationMethodID := did.DIDURL{
        DID:      ownerDID,
        Fragment: "0", // TODO: Which fragment should we use? Thumbprint, UUID, index, etc...
    }
    verificationMethodKey, err := m.keyStore.New(ctx, func(key crypt.PublicKey) (string, error) {
        return verificationMethodID.String(), nil
    })
    if err != nil {
        return nil, nil, err
    }
    verificationMethod, err := did.NewVerificationMethod(verificationMethodID, ssi.JsonWebKey2020, ownerDID, verificationMethodKey.Public())
    if err != nil {
        return nil, nil, err
    }
    return verificationMethodKey, verificationMethod, nil
}

// Resolve returns the did:web document for the given DID, if it is managed by this node.
func (m Manager) Resolve(id did.DID, _ *resolver.ResolveMetadata) (*did.Document, *resolver.DocumentMetadata, error) {
    didDocumentMananager := sql.NewDIDDocumentManager(m.db)

    doc, err := didDocumentMananager.Latest(id)
    if err != nil {
        if errors.Is(err, gorm.ErrRecordNotFound) {
            return nil, nil, resolver.ErrNotFound
        }
        return nil, nil, err
    }
    document, err := buildDocument(id, *doc)
    return &document, &resolver.DocumentMetadata{}, err
}

func (m Manager) IsOwner(_ context.Context, id did.DID) (bool, error) {
    didManager := sql.NewDIDManager(m.db)

    did, err := didManager.Find(id)
    return did != nil, err
}

func (m Manager) ListOwned(_ context.Context) ([]did.DID, error) {
    didManager := sql.NewDIDManager(m.db)
    stored, err := didManager.All()
    if err != nil {
        return nil, err
    }
    dids := make([]did.DID, len(stored))
    for i, d := range stored {
        parsed, err := did.ParseDID(d.ID)
        if err != nil {
            return nil, err
        }
        dids[i] = *parsed
    }

    return dids, nil
}

func (m Manager) CreateService(_ context.Context, subjectDID did.DID, service did.Service) (*did.Service, error) {
    var err error
    var added *did.Service
    err = m.db.Transaction(func(tx *gorm.DB) error {
        added, err = m.createService(tx, subjectDID, service)
        return err
    })

    return added, err
}

func (m Manager) createService(tx *gorm.DB, subjectDID did.DID, service did.Service) (*did.Service, error) {
    didDocumentManager := sql.NewDIDDocumentManager(tx)

    current, err := didDocumentManager.Latest(subjectDID)
    if err != nil {
        return nil, err
    }

    if service.ID.String() == "" {
        // Generate random service ID
        serviceID := did.DIDURL{
            DID:      subjectDID,
            Fragment: uuid.NewString(),
        }
        service.ID = serviceID.URI()
    }
    asJson, err := json.Marshal(service)
    if err != nil {
        return nil, err
    }
    sqlService := sql.SqlService{
        ID:            service.ID.String(),
        DIDDocumentID: current.DidID,
        Data:          asJson,
    }

    _, err = didDocumentManager.CreateOrUpdate(current.DID, current.VerificationMethods, append(current.Services, sqlService))

    return &service, nil
}

func (m Manager) UpdateService(_ context.Context, subjectDID did.DID, serviceID ssi.URI, service did.Service) (*did.Service, error) {
    if service.ID.String() == "" {
        // ID not set in new version of the service, use the provided serviceID
        service.ID = serviceID
    }
    var added *did.Service
    var err error
    err = m.db.Transaction(func(tx *gorm.DB) error {
        // first delete
        err := m.deleteService(tx, subjectDID, serviceID)
        if err != nil {
            return err
        }
        // then add
        added, err = m.createService(tx, subjectDID, service)
        return err
    })

    //commit and return
    return added, err
}

func (m Manager) DeleteService(_ context.Context, subjectDID did.DID, serviceID ssi.URI) error {
    return m.db.Transaction(func(tx *gorm.DB) error {
        return m.deleteService(tx, subjectDID, serviceID)
    })
}

func (m Manager) deleteService(tx *gorm.DB, subjectDID did.DID, serviceID ssi.URI) error {
    didDocumentManager := sql.NewDIDDocumentManager(tx)

    current, err := didDocumentManager.Latest(subjectDID)
    if err != nil {
        return err
    }

    services := current.Services
    j := 0
    for i, s := range services {
        if s.ID == serviceID.String() {
            continue
        }
        services[j] = services[i]
        j++
    }
    services = services[:j]
    _, err = didDocumentManager.CreateOrUpdate(current.DID, current.VerificationMethods, services)

    return err
}

func buildDocument(newDID did.DID, doc sql.DIDDocument) (did.Document, error) {
    document := did.Document{
        Context: []interface{}{
            ssi.MustParseURI(jsonld.Jws2020Context),
            did.DIDContextV1URI(),
        },
        ID: newDID,
    }
    for _, sqlVM := range doc.VerificationMethods {
        verificationMethod := did.VerificationMethod{}
        err := json.Unmarshal(sqlVM.Data, &verificationMethod)
        if err != nil {
            return document, err
        }

        if sqlVM.KeyTypes&sql.VerificationMethodKeyType(management.AssertionMethodUsage) != 0 {
            document.AddAssertionMethod(&verificationMethod)
        }
        if sqlVM.KeyTypes&sql.VerificationMethodKeyType(management.AuthenticationUsage) != 0 {
            document.AddAuthenticationMethod(&verificationMethod)
        }
        if sqlVM.KeyTypes&sql.VerificationMethodKeyType(management.KeyAgreementUsage) != 0 {
            document.AddKeyAgreement(&verificationMethod)
        }
        if sqlVM.KeyTypes&sql.VerificationMethodKeyType(management.CapabilityDelegationUsage) != 0 {
            document.AddCapabilityDelegation(&verificationMethod)
        }
        if sqlVM.KeyTypes&sql.VerificationMethodKeyType(management.CapabilityInvocationUsage) != 0 {
            document.AddCapabilityInvocation(&verificationMethod)
        }
    }
    for _, sqlService := range doc.Services {
        service := did.Service{}
        err := json.Unmarshal(sqlService.Data, &service)
        if err != nil {
            return document, err
        }
        document.Service = append(document.Service, service)
    }

    return document, nil
}