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 }