RPC TLS Support.

All rpc sockets now listen using TLS by default, and this can not be
turned off. The keys (defauling to the datadirectory) may be provided by
--rpccert and --rpckey. If the keys do not exist we will generate a new
self-signed keypair with some sane defaults (hostname and all current
interface addresses).

Additionally add tls capability to btcctl so that it can still be used.
The certificate to use for verify can be provided on the commandline or
verification can be turned off (this leaves you susceptible to MITM
attacks)

Initial code from dhill (rpc tls support) and jrick (key generation),
cleanup, debugging and polishing from me.
This commit is contained in:
Owain G. Ainsworth 2013-11-07 11:25:11 -05:00
parent 5da5dfe1c4
commit 75e577c82e
3 changed files with 147 additions and 4 deletions

View file

@ -54,6 +54,8 @@ type config struct {
RPCUser string `short:"u" long:"rpcuser" description:"Username for RPC connections"`
RPCPass string `short:"P" long:"rpcpass" default-mask:"-" description:"Password for RPC connections"`
RPCListeners []string `long:"rpclisten" description:"Listen for RPC connections on this interface/port (default no listening. default port: 8334, testnet: 18334)"`
RPCCert string `long:"rpccert" description:"File containing the certificate file."`
RPCKey string `long:"rpckey" description:"File containing the certificate key."`
DisableRPC bool `long:"norpc" description:"Disable built-in RPC server -- NOTE: The RPC server is disabled by default if no rpcuser/rpcpass is specified"`
DisableDNSSeed bool `long:"nodnsseed" description:"Disable DNS seeding for peers"`
Proxy string `long:"proxy" description:"Connect via SOCKS5 proxy (eg. 127.0.0.1:9050)"`
@ -340,6 +342,13 @@ func loadConfig() (*config, []string, error) {
}
}
if cfg.RPCKey == "" {
cfg.RPCKey = filepath.Join(cfg.DataDir, "rpc.key")
}
if cfg.RPCCert == "" {
cfg.RPCCert = filepath.Join(cfg.DataDir, "rpc.cert")
}
// Add default port to all listener addresses if needed and remove
// duplicate addresses.
cfg.Listeners = normalizeAddresses(cfg.Listeners,

View file

@ -8,9 +8,17 @@ import (
"bytes"
"code.google.com/p/go.net/websocket"
"container/list"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
_ "crypto/sha512" // for cert generation
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"github.com/conformal/btcchain"
@ -23,10 +31,12 @@ import (
"math/big"
"net"
"net/http"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
)
// Errors
@ -311,6 +321,89 @@ func (s *rpcServer) Stop() error {
return nil
}
// genkey generates a key/cert pair to the paths provided.
// TODO(oga) wrap errors with fmt.Errorf for more context?
func genKey(key, cert string) error {
log.Infof("Generating tls certificates...")
priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
if err != nil {
return err
}
notBefore := time.Now()
notAfter := notBefore.Add(10 * 365 * 24 * time.Hour)
// end of ASN.1 time
endOfTime := time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC)
if notAfter.After(endOfTime) {
notAfter = endOfTime
}
template := x509.Certificate{
SerialNumber: new(big.Int).SetInt64(0),
Subject: pkix.Name{
Organization: []string{"btcd autogenerated cert"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
IsCA: true, // so can sign self.
BasicConstraintsValid: true,
}
host, err := os.Hostname()
if err != nil {
return err
}
template.DNSNames = append(template.DNSNames, host)
addrs, err := net.InterfaceAddrs()
if err != nil {
return err
}
for _, a := range addrs {
ip, _, err := net.ParseCIDR(a.String())
if err == nil {
template.IPAddresses = append(template.IPAddresses, ip)
}
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template,
&template, &priv.PublicKey, priv)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create certificate: %v\n", err)
os.Exit(-1)
}
certOut, err := os.Create(cert)
if err != nil {
return err
}
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
certOut.Close()
keyOut, err := os.OpenFile(key, os.O_WRONLY|os.O_CREATE|os.O_TRUNC,
0600)
if err != nil {
os.Remove(cert)
return err
}
keybytes, err := x509.MarshalECPrivateKey(priv)
if err != nil {
os.Remove(key)
os.Remove(cert)
return err
}
pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keybytes})
keyOut.Close()
log.Infof("Done generating tls certificates")
return nil
}
// newRPCServer returns a new instance of the rpcServer struct.
func newRPCServer(listenAddrs []string, s *server) (*rpcServer, error) {
rpc := rpcServer{
@ -328,13 +421,34 @@ func newRPCServer(listenAddrs []string, s *server) (*rpcServer, error) {
rpc.ws.spentNotifications = make(map[btcwire.OutPoint]*list.List)
rpc.ws.minedTxNotifications = make(map[btcwire.ShaHash]*list.List)
// TODO(oga) this code is identical to that in server, should be
// check for existance of cert file and key file
if !fileExists(cfg.RPCKey) && !fileExists(cfg.RPCCert) {
// if both files do not exist, we generate them.
err := genKey(cfg.RPCKey, cfg.RPCCert)
if err != nil {
return nil, err
}
}
keypair, err := tls.LoadX509KeyPair(cfg.RPCCert, cfg.RPCKey)
if err != nil {
return nil, err
}
tlsConfig := tls.Config{
Certificates: []tls.Certificate{keypair},
}
// TODO(oga) this code is similar to that in server, should be
// factored into something shared.
ipv4ListenAddrs, ipv6ListenAddrs, err := parseListeners(listenAddrs)
if err != nil {
return nil, err
}
listeners := make([]net.Listener, 0,
len(ipv6ListenAddrs)+len(ipv4ListenAddrs))
for _, addr := range ipv4ListenAddrs {
listener, err := net.Listen("tcp4", addr)
var listener net.Listener
listener, err = tls.Listen("tcp4", addr, &tlsConfig)
if err != nil {
log.Warnf("RPCS: Can't listen on %s: %v", addr,
err)
@ -344,7 +458,8 @@ func newRPCServer(listenAddrs []string, s *server) (*rpcServer, error) {
}
for _, addr := range ipv6ListenAddrs {
listener, err := net.Listen("tcp6", addr)
var listener net.Listener
listener, err = tls.Listen("tcp6", addr, &tlsConfig)
if err != nil {
log.Warnf("RPCS: Can't listen on %s: %v", addr,
err)

View file

@ -7,6 +7,7 @@ import (
"github.com/conformal/btcjson"
"github.com/conformal/go-flags"
"github.com/davecgh/go-spew/spew"
"io/ioutil"
"os"
"sort"
"strconv"
@ -17,6 +18,8 @@ type config struct {
RpcUser string `short:"u" long:"rpcuser" description:"RPC username"`
RpcPassword string `short:"P" long:"rpcpass" description:"RPC password"`
RpcServer string `short:"s" long:"rpcserver" description:"RPC server to connect to"`
RpcCert string `short:"c" long:"rpccert" description:"RPC server certificate chain for validation"`
TlsSkipVerify bool `long:"skipverify" description:"Do not verify tls certificates (not recommended!)"`
}
// conversionHandler is a handler that is used to convert parameters from the
@ -233,7 +236,23 @@ func makeVerifyChain(args []interface{}) (btcjson.Cmd, error) {
// results for various error conditions. It either returns a valid result or
// an appropriate error.
func send(cfg *config, msg []byte) (interface{}, error) {
reply, err := btcjson.RpcCommand(cfg.RpcUser, cfg.RpcPassword, cfg.RpcServer, msg)
var reply btcjson.Reply
var err error
if cfg.RpcCert != "" || cfg.TlsSkipVerify {
var pem []byte
if cfg.RpcCert != "" {
pem, err = ioutil.ReadFile(cfg.RpcCert)
if err != nil {
return nil, err
}
}
reply, err = btcjson.TlsRpcCommand(cfg.RpcUser,
cfg.RpcPassword, cfg.RpcServer, msg, pem,
cfg.TlsSkipVerify)
} else {
reply, err = btcjson.RpcCommand(cfg.RpcUser, cfg.RpcPassword,
cfg.RpcServer, msg)
}
if err != nil {
return nil, err
}