nuts-foundation/nuts-node

View on GitHub
vdr/did_owner.go

Summary

Maintainability
A
0 mins
Test Coverage
A
97%
/*
 * 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 vdr

import (
    "context"
    "fmt"
    "github.com/nuts-foundation/go-did/did"
    "github.com/nuts-foundation/nuts-node/crypto"
    "github.com/nuts-foundation/nuts-node/vdr/management"
    "github.com/nuts-foundation/nuts-node/vdr/resolver"
    "strings"
    "sync"
)

var _ management.DocumentOwner = (*cachingDocumentOwner)(nil)
var _ management.DocumentOwner = (*privateKeyDocumentOwner)(nil)

// cachingDocumentOwner is a types.DocumentOwner that caches the result, minimizing expensive lookups.
// It assumes:
//   - Positive matches never change until restart (would indicate a DID deactivation, and it being used afterwards).
//   - Negative matches never change until restart (would indicate an attacker trying DIDs, or a bug/misconfiguration).
//
// Before calling the more expensive, underlying types.DocumentOwner, it checks whether the DID actually exists.
// The ListOwned call is not cached.
type cachingDocumentOwner struct {
    underlying   management.DocumentOwner
    ownedDIDs    *sync.Map
    notOwnedDIDs *sync.Map
    didResolver  resolver.DIDResolver
}

func (t *cachingDocumentOwner) ListOwned(ctx context.Context) ([]did.DID, error) {
    return t.underlying.ListOwned(ctx)
}

func newCachingDocumentOwner(underlying management.DocumentOwner, didResolver resolver.DIDResolver) *cachingDocumentOwner {
    return &cachingDocumentOwner{
        didResolver:  didResolver,
        underlying:   underlying,
        ownedDIDs:    new(sync.Map),
        notOwnedDIDs: new(sync.Map),
    }
}

func (t *cachingDocumentOwner) IsOwner(ctx context.Context, id did.DID) (bool, error) {
    // Check if the DID is in the negative matches
    isAsString := id.String()
    _, isNotATenant := t.notOwnedDIDs.Load(isAsString)
    if isNotATenant {
        return false, nil
    }

    // Check if the DID is in the positive matches
    _, isATenant := t.ownedDIDs.Load(isAsString)
    if isATenant {
        return true, nil
    }

    // First perform a cheap DID existence check (subsequent checks are more expensive),
    // without caching it as negative match (would allow unbound number of negative matches).
    _, _, err := t.didResolver.Resolve(id, nil)
    if resolver.IsFunctionalResolveError(err) {
        return false, nil
    } else if err != nil {
        return false, fmt.Errorf("unable to check ownership of DID: %w", err)
    }

    result, err := t.underlying.IsOwner(ctx, id)
    if err != nil {
        return false, err
    }

    // Cache result for future use
    if result {
        t.ownedDIDs.Store(isAsString, true)
    } else {
        t.notOwnedDIDs.Store(isAsString, true)
    }

    return result, nil
}

// privateKeyDocumentOwner checks if the DID is managed by the local node by checking if there are private keys of the DID in the node's key store.
// It does not check whether the DID is active or not, or whether the private keys are still (if ever) were registered on the DID document.
type privateKeyDocumentOwner struct {
    keyResolver crypto.KeyResolver
}

func (p privateKeyDocumentOwner) ListOwned(ctx context.Context) ([]did.DID, error) {
    return p.listDIDs(ctx), nil
}

func (p privateKeyDocumentOwner) IsOwner(ctx context.Context, id did.DID) (bool, error) {
    didList := p.listDIDs(ctx)
    for _, curr := range didList {
        if id.Equals(curr) {
            return true, nil
        }
    }
    return false, nil
}

func (p privateKeyDocumentOwner) listDIDs(ctx context.Context) []did.DID {
    keyList := p.keyResolver.List(ctx)
    var didList []did.DID
    didMap := make(map[string]bool, 0)
    for _, key := range keyList {
        // Assume format <did>#<keyID>
        idx := strings.Index(key, "#")
        if idx == -1 {
            // Not a valid key ID
            continue
        }
        curr := key[:idx]
        if didMap[curr] {
            // Already in list
            continue
        }
        parsedDID, _ := did.ParseDID(curr)
        if parsedDID != nil {
            didList = append(didList, *parsedDID)
        }
        didMap[curr] = true
    }
    return didList
}