567752ea9b
This option prevents the RPC server TLS key from ever being written to disk. This is performed by generating a new certificate pair each startup and writing (possibly overwriting) the certificate but not the key. Closes #359.
265 lines
8.5 KiB
Go
265 lines
8.5 KiB
Go
/*
|
|
* Copyright (c) 2013-2015 The btcsuite developers
|
|
*
|
|
* Permission to use, copy, modify, and distribute this software for any
|
|
* purpose with or without fee is hereby granted, provided that the above
|
|
* copyright notice and this permission notice appear in all copies.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/btcsuite/btcutil"
|
|
"github.com/btcsuite/btcwallet/rpc/legacyrpc"
|
|
"github.com/btcsuite/btcwallet/rpc/rpcserver"
|
|
"github.com/btcsuite/btcwallet/wallet"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
)
|
|
|
|
// openRPCKeyPair creates or loads the RPC TLS keypair specified by the
|
|
// application config. This function respects the cfg.OneTimeTLSKey setting.
|
|
func openRPCKeyPair() (tls.Certificate, error) {
|
|
// Check for existence of the TLS key file. If one time TLS keys are
|
|
// enabled but a key already exists, this function should error since
|
|
// it's possible that a persistent certificate was copied to a remote
|
|
// machine. Otherwise, generate a new keypair when the key is missing.
|
|
// When generating new persistent keys, overwriting an existing cert is
|
|
// acceptable if the previous execution used a one time TLS key.
|
|
// Otherwise, both the cert and key should be read from disk. If the
|
|
// cert is missing, the read error will occur in LoadX509KeyPair.
|
|
_, e := os.Stat(cfg.RPCKey)
|
|
keyExists := !os.IsNotExist(e)
|
|
switch {
|
|
case cfg.OneTimeTLSKey && keyExists:
|
|
err := fmt.Errorf("one time TLS keys are enabled, but TLS key "+
|
|
"`%s` already exists", cfg.RPCKey)
|
|
return tls.Certificate{}, err
|
|
case cfg.OneTimeTLSKey:
|
|
return generateRPCKeyPair(false)
|
|
case !keyExists:
|
|
return generateRPCKeyPair(true)
|
|
default:
|
|
return tls.LoadX509KeyPair(cfg.RPCCert, cfg.RPCKey)
|
|
}
|
|
}
|
|
|
|
// generateRPCKeyPair generates a new RPC TLS keypair and writes the cert and
|
|
// possibly also the key in PEM format to the paths specified by the config. If
|
|
// successful, the new keypair is returned.
|
|
func generateRPCKeyPair(writeKey bool) (tls.Certificate, error) {
|
|
log.Infof("Generating TLS certificates...")
|
|
|
|
// Create directories for cert and key files if they do not yet exist.
|
|
certDir, _ := filepath.Split(cfg.RPCCert)
|
|
keyDir, _ := filepath.Split(cfg.RPCKey)
|
|
err := os.MkdirAll(certDir, 0700)
|
|
if err != nil {
|
|
return tls.Certificate{}, err
|
|
}
|
|
err = os.MkdirAll(keyDir, 0700)
|
|
if err != nil {
|
|
return tls.Certificate{}, err
|
|
}
|
|
|
|
// Generate cert pair.
|
|
org := "btcwallet autogenerated cert"
|
|
validUntil := time.Now().Add(time.Hour * 24 * 365 * 10)
|
|
cert, key, err := btcutil.NewTLSCertPair(org, validUntil, nil)
|
|
if err != nil {
|
|
return tls.Certificate{}, err
|
|
}
|
|
keyPair, err := tls.X509KeyPair(cert, key)
|
|
if err != nil {
|
|
return tls.Certificate{}, err
|
|
}
|
|
|
|
// Write cert and (potentially) the key files.
|
|
err = ioutil.WriteFile(cfg.RPCCert, cert, 0600)
|
|
if err != nil {
|
|
return tls.Certificate{}, err
|
|
}
|
|
if writeKey {
|
|
err = ioutil.WriteFile(cfg.RPCKey, key, 0600)
|
|
if err != nil {
|
|
rmErr := os.Remove(cfg.RPCCert)
|
|
if rmErr != nil {
|
|
log.Warnf("Cannot remove written certificates: %v",
|
|
rmErr)
|
|
}
|
|
return tls.Certificate{}, err
|
|
}
|
|
}
|
|
|
|
log.Info("Done generating TLS certificates")
|
|
return keyPair, nil
|
|
}
|
|
|
|
func startRPCServers(walletLoader *wallet.Loader) (*grpc.Server, *legacyrpc.Server, error) {
|
|
var (
|
|
server *grpc.Server
|
|
legacyServer *legacyrpc.Server
|
|
legacyListen = net.Listen
|
|
keyPair tls.Certificate
|
|
err error
|
|
)
|
|
if cfg.DisableServerTLS {
|
|
log.Info("Server TLS is disabled. Only legacy RPC may be used")
|
|
} else {
|
|
keyPair, err = openRPCKeyPair()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Change the standard net.Listen function to the tls one.
|
|
tlsConfig := &tls.Config{
|
|
Certificates: []tls.Certificate{keyPair},
|
|
MinVersion: tls.VersionTLS12,
|
|
NextProtos: []string{"h2"}, // HTTP/2 over TLS
|
|
}
|
|
legacyListen = func(net string, laddr string) (net.Listener, error) {
|
|
return tls.Listen(net, laddr, tlsConfig)
|
|
}
|
|
|
|
if len(cfg.ExperimentalRPCListeners) != 0 {
|
|
listeners := makeListeners(cfg.ExperimentalRPCListeners, net.Listen)
|
|
if len(listeners) == 0 {
|
|
err := errors.New("failed to create listeners for RPC server")
|
|
return nil, nil, err
|
|
}
|
|
creds := credentials.NewServerTLSFromCert(&keyPair)
|
|
server = grpc.NewServer(grpc.Creds(creds))
|
|
rpcserver.StartWalletLoaderService(server, walletLoader, activeNet)
|
|
for _, lis := range listeners {
|
|
lis := lis
|
|
go func() {
|
|
log.Infof("Experimental RPC server listening on %s",
|
|
lis.Addr())
|
|
err := server.Serve(lis)
|
|
log.Tracef("Finished serving expimental RPC: %v",
|
|
err)
|
|
}()
|
|
}
|
|
}
|
|
}
|
|
|
|
if cfg.Username == "" || cfg.Password == "" {
|
|
log.Info("Legacy RPC server disabled (requires username and password)")
|
|
} else if len(cfg.LegacyRPCListeners) != 0 {
|
|
listeners := makeListeners(cfg.LegacyRPCListeners, legacyListen)
|
|
if len(listeners) == 0 {
|
|
err := errors.New("failed to create listeners for legacy RPC server")
|
|
return nil, nil, err
|
|
}
|
|
opts := legacyrpc.Options{
|
|
Username: cfg.Username,
|
|
Password: cfg.Password,
|
|
MaxPOSTClients: cfg.LegacyRPCMaxClients,
|
|
MaxWebsocketClients: cfg.LegacyRPCMaxWebsockets,
|
|
}
|
|
legacyServer = legacyrpc.NewServer(&opts, walletLoader, listeners)
|
|
}
|
|
|
|
// Error when neither the GRPC nor legacy RPC servers can be started.
|
|
if server == nil && legacyServer == nil {
|
|
return nil, nil, errors.New("no suitable RPC services can be started")
|
|
}
|
|
|
|
return server, legacyServer, nil
|
|
}
|
|
|
|
type listenFunc func(net string, laddr string) (net.Listener, error)
|
|
|
|
// makeListeners splits the normalized listen addresses into IPv4 and IPv6
|
|
// addresses and creates new net.Listeners for each with the passed listen func.
|
|
// Invalid addresses are logged and skipped.
|
|
func makeListeners(normalizedListenAddrs []string, listen listenFunc) []net.Listener {
|
|
ipv4Addrs := make([]string, 0, len(normalizedListenAddrs)*2)
|
|
ipv6Addrs := make([]string, 0, len(normalizedListenAddrs)*2)
|
|
for _, addr := range normalizedListenAddrs {
|
|
host, _, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
// Shouldn't happen due to already being normalized.
|
|
log.Errorf("`%s` is not a normalized "+
|
|
"listener address", addr)
|
|
continue
|
|
}
|
|
|
|
// Empty host or host of * on plan9 is both IPv4 and IPv6.
|
|
if host == "" || (host == "*" && runtime.GOOS == "plan9") {
|
|
ipv4Addrs = append(ipv4Addrs, addr)
|
|
ipv6Addrs = append(ipv6Addrs, addr)
|
|
continue
|
|
}
|
|
|
|
// Remove the IPv6 zone from the host, if present. The zone
|
|
// prevents ParseIP from correctly parsing the IP address.
|
|
// ResolveIPAddr is intentionally not used here due to the
|
|
// possibility of leaking a DNS query over Tor if the host is a
|
|
// hostname and not an IP address.
|
|
zoneIndex := strings.Index(host, "%")
|
|
if zoneIndex != -1 {
|
|
host = host[:zoneIndex]
|
|
}
|
|
|
|
ip := net.ParseIP(host)
|
|
switch {
|
|
case ip == nil:
|
|
log.Warnf("`%s` is not a valid IP address", host)
|
|
case ip.To4() == nil:
|
|
ipv6Addrs = append(ipv6Addrs, addr)
|
|
default:
|
|
ipv4Addrs = append(ipv4Addrs, addr)
|
|
}
|
|
}
|
|
listeners := make([]net.Listener, 0, len(ipv6Addrs)+len(ipv4Addrs))
|
|
for _, addr := range ipv4Addrs {
|
|
listener, err := listen("tcp4", addr)
|
|
if err != nil {
|
|
log.Warnf("Can't listen on %s: %v", addr, err)
|
|
continue
|
|
}
|
|
listeners = append(listeners, listener)
|
|
}
|
|
for _, addr := range ipv6Addrs {
|
|
listener, err := listen("tcp6", addr)
|
|
if err != nil {
|
|
log.Warnf("Can't listen on %s: %v", addr, err)
|
|
continue
|
|
}
|
|
listeners = append(listeners, listener)
|
|
}
|
|
return listeners
|
|
}
|
|
|
|
// startWalletRPCServices associates each of the (optionally-nil) RPC servers
|
|
// with a wallet to enable remote wallet access. For the GRPC server, this
|
|
// registers the WalletService service, and for the legacy JSON-RPC server it
|
|
// enables methods that require a loaded wallet.
|
|
func startWalletRPCServices(wallet *wallet.Wallet, server *grpc.Server, legacyServer *legacyrpc.Server) {
|
|
if server != nil {
|
|
rpcserver.StartWalletService(server, wallet)
|
|
}
|
|
if legacyServer != nil {
|
|
legacyServer.RegisterWallet(wallet)
|
|
}
|
|
}
|