nuts-foundation/nuts-node

View on GitHub
crypto/crypto.go

Summary

Maintainability
A
0 mins
Test Coverage
D
62%
/*
 * 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 crypto

import (
    "context"
    "crypto"
    "errors"
    "fmt"
    "github.com/lestrrat-go/jwx/v2/jwa"
    "github.com/lestrrat-go/jwx/v2/jwk"
    "github.com/nuts-foundation/nuts-node/crypto/storage/azure"
    "path"
    "time"

    "github.com/nuts-foundation/nuts-node/audit"
    "github.com/nuts-foundation/nuts-node/core"
    "github.com/nuts-foundation/nuts-node/crypto/log"
    "github.com/nuts-foundation/nuts-node/crypto/storage/external"
    "github.com/nuts-foundation/nuts-node/crypto/storage/fs"
    "github.com/nuts-foundation/nuts-node/crypto/storage/spi"
    "github.com/nuts-foundation/nuts-node/crypto/storage/vault"
)

const (
    // ModuleName contains the name of this module
    ModuleName = "Crypto"
)

// Config holds the values for the crypto engine
type Config struct {
    Storage       string          `koanf:"storage"`
    Vault         vault.Config    `koanf:"vault"`
    AzureKeyVault azure.Config    `koanf:"azurekv"`
    External      external.Config `koanf:"external"`
}

// DefaultCryptoConfig returns a Config with a fs backend storage
func DefaultCryptoConfig() Config {
    return Config{
        Vault:         vault.DefaultConfig(),
        AzureKeyVault: azure.DefaultConfig(),
        External: external.Config{
            Timeout: 100 * time.Millisecond,
        },
    }
}

var _ KeyStore = &Crypto{}

// Crypto holds references to storage and needed config
type Crypto struct {
    storage spi.Storage
    config  Config
}

func (client *Crypto) CheckHealth() map[string]core.Health {
    return client.storage.CheckHealth()
}

// NewCryptoInstance creates a new instance of the crypto engine.
func NewCryptoInstance() *Crypto {
    return &Crypto{
        config: DefaultCryptoConfig(),
    }
}

func (client *Crypto) Name() string {
    return ModuleName
}

func (client *Crypto) Config() interface{} {
    return &client.config
}

func (client *Crypto) setupFSBackend(config core.ServerConfig) error {
    log.Logger().Info("Setting up FileSystem backend for storage of private key material. " +
        "Discouraged for production use unless backups and encryption is properly set up. Consider using the Hashicorp Vault backend.")
    fsPath := path.Join(config.Datadir, "crypto")
    fsBackend, err := fs.NewFileSystemBackend(fsPath)
    if err != nil {
        return err
    }
    client.storage = spi.NewValidatedKIDBackendWrapper(fsBackend, spi.KidPattern)
    return nil
}

func (client *Crypto) setupStorageAPIBackend() error {
    log.Logger().Debug("Setting up StorageAPI backend for storage of private key material.")
    log.Logger().Warn("External key storage backend is deprecated and will be removed in the future.")
    apiBackend, err := external.NewAPIClient(client.config.External)
    if err != nil {
        return fmt.Errorf("unable to set up external crypto API client: %w", err)
    }
    client.storage = spi.NewValidatedKIDBackendWrapper(apiBackend, spi.KidPattern)
    return nil
}

func (client *Crypto) setupVaultBackend(_ core.ServerConfig) error {
    log.Logger().Debug("Setting up Vault backend for storage of private key material. " +
        "This feature is experimental and may change in the future.")
    vaultBackend, err := vault.NewVaultKVStorage(client.config.Vault)
    if err != nil {
        return err
    }

    client.storage = spi.NewValidatedKIDBackendWrapper(vaultBackend, spi.KidPattern)
    return nil
}

func (client *Crypto) setupAzureKeyVaultBackend(_ core.ServerConfig) error {
    log.Logger().Debug("Setting up Azure Key Vault backend for storage of private key material. ")
    azureBackend, err := azure.New(client.config.AzureKeyVault)
    if err != nil {
        return err
    }
    client.storage = spi.NewValidatedKIDBackendWrapper(azureBackend, spi.KidPattern)
    return nil
}

// List returns the KIDs of the private keys that are present in the key store.
func (client *Crypto) List(ctx context.Context) []string {
    return client.storage.ListPrivateKeys(ctx)
}

// Configure loads the given configurations in the engine. Any wrong combination will return an error
func (client *Crypto) Configure(config core.ServerConfig) error {
    switch client.config.Storage {
    case fs.StorageType:
        return client.setupFSBackend(config)
    case vault.StorageType:
        return client.setupVaultBackend(config)
    case azure.StorageType:
        return client.setupAzureKeyVaultBackend(config)
    case external.StorageType:
        return client.setupStorageAPIBackend()
    case "":
        if config.Strictmode {
            return errors.New("backend must be explicitly set in strict mode")
        }
        // default to file system and run this setup again
        return client.setupFSBackend(config)
    default:
        return fmt.Errorf("invalid config for crypto.storage. Available options are: vaultkv, fs, %s(experimental)", external.StorageType)
    }
}

// New generates a new key pair.
// Stores the private key, returns the public basicKey.
// It returns an error when a key with the resulting ID already exists.
func (client *Crypto) New(ctx context.Context, namingFunc KIDNamingFunc) (Key, error) {
    publicKey, kid, err := client.storage.NewPrivateKey(ctx, namingFunc)
    if err != nil {
        return nil, err
    }
    audit.Log(ctx, log.Logger(), audit.CryptoNewKeyEvent).Infof("Generated new key pair: %s", kid)
    return basicKey{
        publicKey: publicKey,
        kid:       kid,
    }, nil
}

// Delete removes the private key with the given KID from the KeyStore.
func (client *Crypto) Delete(ctx context.Context, kid string) error {
    audit.Log(ctx, log.Logger(), audit.CryptoDeleteKeyEvent).Infof("Deleting private key: %s", kid)
    return client.storage.DeletePrivateKey(ctx, kid)
}

// GenerateJWK a new in-memory key pair and returns it as JWK.
// It sets the alg field of the JWK.
func GenerateJWK() (jwk.Key, error) {
    keyPair, err := spi.GenerateKeyPair()
    if err != nil {
        return nil, nil
    }
    result, err := jwk.FromRaw(keyPair)
    if err != nil {
        return nil, err
    }
    return result, result.Set(jwk.AlgorithmKey, jwa.ES256)
}

// Exists checks storage for an entry for the given legal entity and returns true if it exists
func (client *Crypto) Exists(ctx context.Context, kid string) (bool, error) {
    exists, err := client.storage.PrivateKeyExists(ctx, kid)
    if err != nil {
        return false, fmt.Errorf("could not check if private key exists: %w", err)
    }
    return exists, nil
}

func (client *Crypto) Resolve(ctx context.Context, kid string) (Key, error) {
    keypair, err := client.storage.GetPrivateKey(ctx, kid)
    if err != nil {
        if errors.Is(err, spi.ErrNotFound) {
            return nil, ErrPrivateKeyNotFound
        }
        return nil, err
    }
    return basicKey{
        publicKey: keypair.Public(),
        kid:       kid,
    }, nil
}

// memoryKey is a Key that is only present in memory and not stored in the key store.
type memoryKey struct {
    basicKey
    privateKey crypto.Signer
}

func (m memoryKey) Signer() crypto.Signer {
    return m.privateKey
}

type basicKey struct {
    publicKey crypto.PublicKey
    kid       string
}

func (e basicKey) KID() string {
    return e.kid
}

func (e basicKey) Public() crypto.PublicKey {
    return e.publicKey
}