nuts-foundation/nuts-node

View on GitHub
network/transport/grpc/config.go

Summary

Maintainability
A
0 mins
Test Coverage
B
86%
/*
 * 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 grpc

import (
    "crypto/tls"
    "crypto/x509"
    "errors"
    "github.com/nuts-foundation/nuts-node/core"
    networkTypes "github.com/nuts-foundation/nuts-node/network/transport"
    "github.com/nuts-foundation/nuts-node/pki"
    "google.golang.org/grpc"
    "net"
    "time"
)

// tcpListenerCreator starts a TCP listener for the inbound gRPC server on the given address.
// It's used by default when running the Nuts node, but unit tests can use an alternative listener creator (e.g. bufconn for in-memory channels).
func tcpListenerCreator(addr string) (net.Listener, error) {
    return net.Listen("tcp", addr)
}

// ConfigOption is used to build Config.
type ConfigOption func(config *Config) error

// NewConfig creates a new Config, used for configuring a gRPC ConnectionManager.
func NewConfig(grpcAddress string, peerID networkTypes.PeerID, options ...ConfigOption) (Config, error) {
    cfg := Config{
        listenAddress:     grpcAddress,
        peerID:            peerID,
        dialer:            grpc.DialContext,
        listener:          tcpListenerCreator,
        connectionTimeout: 5 * time.Second,
        backoffCreator: func() Backoff {
            return BoundedBackoff(time.Second, time.Hour)
        },
    }
    for _, opt := range options {
        if err := opt(&cfg); err != nil {
            return Config{}, err
        }
    }
    return cfg, nil
}

// WithTLS enables TLS for gRPC ConnectionManager.
func WithTLS(clientCertificate tls.Certificate, trustStore *core.TrustStore, pkiValidator pki.Validator) ConfigOption {
    return func(config *Config) error {
        config.clientCert = &clientCertificate
        config.trustStore = trustStore.CertPool
        config.pkiValidator = pkiValidator
        if err := pkiValidator.AddTruststore(trustStore.Certificates()); err != nil {
            return err
        }
        // Load TLS server certificate if the gRPC server should be started.
        if config.listenAddress != "" {
            config.serverCert = config.clientCert
        }
        return nil
    }
}

// WithTLSOffloading enables TLS for outgoing connections, but is offloaded for incoming connections.
// It MUST be used in conjunction, but after with WithTLS.
func WithTLSOffloading(clientCertHeaderName string) ConfigOption {
    return func(config *Config) error {
        if clientCertHeaderName == "" {
            return errors.New("tls.certheader must be configured to enable TLS offloading ")
        }
        config.clientCertHeaderName = clientCertHeaderName
        config.serverCert = nil
        return nil
    }
}

// WithConnectionTimeout specifies the connection timeout for outbound connection attempts.
func WithConnectionTimeout(value time.Duration) ConfigOption {
    return func(config *Config) error {
        config.connectionTimeout = value
        return nil
    }
}

func WithBackoff(value func() Backoff) ConfigOption {
    return func(config *Config) error {
        config.backoffCreator = value
        return nil
    }
}

// Config holds values for configuring the gRPC ConnectionManager.
type Config struct {
    // PeerID contains the ID of the local node.
    peerID networkTypes.PeerID
    // listenAddress specifies the socket address the gRPC server should listen on.
    // If not set, the node will not accept incoming connectionList (but outbound connectionList can still be made).
    listenAddress string
    // clientCert specifies the TLS client certificate. If set the client should open a TLS socket, otherwise plain TCP.
    clientCert *tls.Certificate
    // serverCert specifies the TLS server certificate. If set the server should open a TLS socket, otherwise plain TCP.
    serverCert *tls.Certificate
    // trustStore contains the trust anchors used when verifying remote a peer's TLS certificate.
    trustStore *x509.CertPool
    // pkiValidator contains the database for revoked certificates
    pkiValidator pki.Validator
    // clientCertHeaderName specifies the name of the HTTP header that contains the client certificate, if TLS is offloaded.
    clientCertHeaderName string
    // connectionTimeout specifies the time before an outbound connection attempt times out.
    connectionTimeout time.Duration
    // listener holds a function to create the net.Listener that is used for inbound connections.
    listener func(string) (net.Listener, error)
    // dialer holds a function to open connections to remote gRPC services.
    dialer dialer
    // backoffCreator holds a function that creates a Backoff.
    backoffCreator func() Backoff
}

func (cfg Config) tlsEnabled() bool {
    return cfg.trustStore != nil
}

func newServerTLSConfig(config Config) (*tls.Config, error) {
    tlsConfig, err := baseTLSConfig(config)
    if err != nil {
        return nil, err
    }
    tlsConfig.ClientCAs = config.trustStore
    tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
    tlsConfig.Certificates = []tls.Certificate{
        *config.serverCert,
    }
    return tlsConfig, nil
}

// NewClientTLSConfig returns the network grpc client tls.Config
func NewClientTLSConfig(clientCert *tls.Certificate, trustStore *x509.CertPool, pkiValidator pki.Validator) (*tls.Config, error) {
    return newClientTLSConfig(Config{
        clientCert:   clientCert,
        trustStore:   trustStore,
        pkiValidator: pkiValidator,
    })
}

func newClientTLSConfig(config Config) (*tls.Config, error) {
    tlsConfig, err := baseTLSConfig(config)
    if err != nil {
        return nil, err
    }
    tlsConfig.RootCAs = config.trustStore
    tlsConfig.Certificates = []tls.Certificate{
        *config.clientCert,
    }
    return tlsConfig, nil
}

func baseTLSConfig(config Config) (*tls.Config, error) {
    tlsConfig := &tls.Config{
        MinVersion: core.MinTLSVersion,
    }

    if err := config.pkiValidator.SetVerifyPeerCertificateFunc(tlsConfig); err != nil {
        // cannot fail
        return nil, err
    }

    return tlsConfig, nil
}