From 75e577c82e53e9e3664cf104a82ee8759eed59c3 Mon Sep 17 00:00:00 2001 From: "Owain G. Ainsworth" Date: Thu, 7 Nov 2013 11:25:11 -0500 Subject: [PATCH] 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. --- config.go | 9 ++++ rpcserver.go | 121 ++++++++++++++++++++++++++++++++++++++++-- util/btcctl/btcctl.go | 21 +++++++- 3 files changed, 147 insertions(+), 4 deletions(-) diff --git a/config.go b/config.go index 665683a8..186c6496 100644 --- a/config.go +++ b/config.go @@ -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, diff --git a/rpcserver.go b/rpcserver.go index 5f67da9e..d1e7bde8 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -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) diff --git a/util/btcctl/btcctl.go b/util/btcctl/btcctl.go index e7234bad..6276a802 100644 --- a/util/btcctl/btcctl.go +++ b/util/btcctl/btcctl.go @@ -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 }