node,rpc: adds TLS support

This commit is contained in:
Victor Farazdagi 2016-12-27 20:32:32 +03:00
parent 891fcd8ce1
commit d8ee45a0aa
10 changed files with 332 additions and 9 deletions

View file

@ -17,6 +17,7 @@
package main package main
import ( import (
"context"
"os" "os"
"os/signal" "os/signal"
"strings" "strings"
@ -108,7 +109,8 @@ func localConsole(ctx *cli.Context) error {
// console to it. // console to it.
func remoteConsole(ctx *cli.Context) error { func remoteConsole(ctx *cli.Context) error {
// Attach to a remotely running geth instance and start the JavaScript console // Attach to a remotely running geth instance and start the JavaScript console
client, err := dialRPC(ctx.Args().First()) dialctx := rpc.MakeTLSConfigContext(utils.MakeClientTLSConfig(ctx))
client, err := dialRPCContext(dialctx, ctx.Args().First())
if err != nil { if err != nil {
utils.Fatalf("Unable to attach to remote geth: %v", err) utils.Fatalf("Unable to attach to remote geth: %v", err)
} }
@ -137,9 +139,14 @@ func remoteConsole(ctx *cli.Context) error {
} }
// dialRPC returns a RPC client which connects to the given endpoint. // dialRPC returns a RPC client which connects to the given endpoint.
func dialRPC(endpoint string) (*rpc.Client, error) {
return dialRPCContext(context.Background(), endpoint)
}
// dialRPCContext returns a RPC client which connects to the given endpoint.
// The check for empty endpoint implements the defaulting logic // The check for empty endpoint implements the defaulting logic
// for "geth attach" and "geth monitor" with no argument. // for "geth attach" and "geth monitor" with no argument.
func dialRPC(endpoint string) (*rpc.Client, error) { func dialRPCContext(ctx context.Context, endpoint string) (*rpc.Client, error) {
if endpoint == "" { if endpoint == "" {
endpoint = node.DefaultIPCEndpoint(clientIdentifier) endpoint = node.DefaultIPCEndpoint(clientIdentifier)
} else if strings.HasPrefix(endpoint, "rpc:") || strings.HasPrefix(endpoint, "ipc:") { } else if strings.HasPrefix(endpoint, "rpc:") || strings.HasPrefix(endpoint, "ipc:") {
@ -147,7 +154,7 @@ func dialRPC(endpoint string) (*rpc.Client, error) {
// these prefixes. // these prefixes.
endpoint = endpoint[4:] endpoint = endpoint[4:]
} }
return rpc.Dial(endpoint) return rpc.DialContext(ctx, endpoint)
} }
// ephemeralConsole starts a new geth node, attaches an ephemeral JavaScript // ephemeralConsole starts a new geth node, attaches an ephemeral JavaScript

View file

@ -117,6 +117,11 @@ func init() {
utils.RPCListenAddrFlag, utils.RPCListenAddrFlag,
utils.RPCPortFlag, utils.RPCPortFlag,
utils.RPCApiFlag, utils.RPCApiFlag,
utils.TLSEnabledFlag,
utils.TLSCertFileFlag,
utils.TLSCertCAFlag,
utils.TLSKeyFileFlag,
utils.TLSNoVerifyFlag,
utils.WSEnabledFlag, utils.WSEnabledFlag,
utils.WSListenAddrFlag, utils.WSListenAddrFlag,
utils.WSPortFlag, utils.WSPortFlag,

View file

@ -24,6 +24,7 @@ import (
"sort" "sort"
"strings" "strings"
"time" "time"
"context"
"github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/node" "github.com/ethereum/go-ethereum/node"
@ -75,7 +76,7 @@ func monitor(ctx *cli.Context) error {
) )
// Attach to an Ethereum node over IPC or RPC // Attach to an Ethereum node over IPC or RPC
endpoint := ctx.String(monitorCommandAttachFlag.Name) endpoint := ctx.String(monitorCommandAttachFlag.Name)
if client, err = dialRPC(endpoint); err != nil { if client, err = dialRPCContext(context.Background(), endpoint); err != nil {
utils.Fatalf("Unable to attach to geth node: %v", err) utils.Fatalf("Unable to attach to geth node: %v", err)
} }
defer client.Close() defer client.Close()

View file

@ -99,6 +99,11 @@ var AppHelpFlagGroups = []flagGroup{
utils.RPCListenAddrFlag, utils.RPCListenAddrFlag,
utils.RPCPortFlag, utils.RPCPortFlag,
utils.RPCApiFlag, utils.RPCApiFlag,
utils.TLSEnabledFlag,
utils.TLSCertFileFlag,
utils.TLSCertCAFlag,
utils.TLSKeyFileFlag,
utils.TLSNoVerifyFlag,
utils.WSEnabledFlag, utils.WSEnabledFlag,
utils.WSListenAddrFlag, utils.WSListenAddrFlag,
utils.WSPortFlag, utils.WSPortFlag,

View file

@ -19,6 +19,7 @@ package utils
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/tls"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"math" "math"
@ -274,6 +275,26 @@ var (
Usage: "API's offered over the HTTP-RPC interface", Usage: "API's offered over the HTTP-RPC interface",
Value: rpc.DefaultHTTPApis, Value: rpc.DefaultHTTPApis,
} }
TLSEnabledFlag = cli.BoolFlag{
Name: "tls",
Usage: "Activate TLS support on HTTP-RPC server",
}
TLSCertFileFlag = cli.StringFlag{
Name: "tlscert",
Usage: "TLS certificate file (auto-generated if empty, and TLS support is enabled)",
}
TLSCertCAFlag = cli.BoolFlag{
Name: "tlscertca",
Usage: "Whether provided certificate is serves as its own CA",
}
TLSKeyFileFlag = cli.StringFlag{
Name: "tlskey",
Usage: "TLS key file (auto-generated if empty, and TLS support is enabled)",
}
TLSNoVerifyFlag = cli.BoolFlag{
Name: "tlsnoverify",
Usage: "Whether server's TSL certificate must be verified or not (on connection by client)",
}
IPCDisabledFlag = cli.BoolFlag{ IPCDisabledFlag = cli.BoolFlag{
Name: "ipcdisable", Name: "ipcdisable",
Usage: "Disable the IPC-RPC server", Usage: "Disable the IPC-RPC server",
@ -687,6 +708,7 @@ func MakeNode(ctx *cli.Context, name, gitCommit string) *node.Node {
WSPort: ctx.GlobalInt(WSPortFlag.Name), WSPort: ctx.GlobalInt(WSPortFlag.Name),
WSOrigins: ctx.GlobalString(WSAllowedOriginsFlag.Name), WSOrigins: ctx.GlobalString(WSAllowedOriginsFlag.Name),
WSModules: MakeRPCModules(ctx.GlobalString(WSApiFlag.Name)), WSModules: MakeRPCModules(ctx.GlobalString(WSApiFlag.Name)),
TLSConfig: MakeServerTLSConfig(ctx),
} }
if ctx.GlobalBool(DevModeFlag.Name) { if ctx.GlobalBool(DevModeFlag.Name) {
if !ctx.GlobalIsSet(DataDirFlag.Name) { if !ctx.GlobalIsSet(DataDirFlag.Name) {
@ -703,6 +725,9 @@ func MakeNode(ctx *cli.Context, name, gitCommit string) *node.Node {
} }
config.NetRestrict = list config.NetRestrict = list
} }
if config.TLSConfig != nil {
config.TLSEnabled = true
}
stack, err := node.New(config) stack, err := node.New(config)
if err != nil { if err != nil {
@ -958,3 +983,56 @@ func MakeConsolePreloads(ctx *cli.Context) []string {
} }
return preloads return preloads
} }
// MakeServerTLSConfig parses incoming TLS-related options, and produces configuration
// ready to be injected into server connection's transport
func MakeServerTLSConfig(ctx *cli.Context) *tls.Config {
if !ctx.GlobalBool(RPCEnabledFlag.Name) {
return nil
}
if !ctx.GlobalBool(TLSEnabledFlag.Name) {
return nil
}
config, err := rpc.MakeServerTLSConfig(MakeHTTPRpcHost(ctx), MakeTLSCertPath(ctx), MakeTLSKeyPath(ctx))
if err != nil {
return nil
}
return config
}
// MakeClientTLSConfig parses incoming TLS-related options, and produces configuration
// ready to be injected into client connection's transport
func MakeClientTLSConfig(ctx *cli.Context) *tls.Config {
return rpc.MakeClientTLSConfig(
MakeTLSCertPath(ctx), MakeTLSKeyPath(ctx),
ctx.GlobalBool(TLSCertCAFlag.Name),
ctx.GlobalBool(TLSNoVerifyFlag.Name))
}
// MakeTLSCertPath returns TLS certificate file path from CLI options
// (or default one, if nothing is provided in options)
func MakeTLSCertPath(ctx *cli.Context) string {
basePath := MakeDataDir(ctx)
certPath := ctx.GlobalString(TLSCertFileFlag.Name)
if certPath == "" {
certPath = filepath.Join(basePath, rpc.DefaultTLSCertFile)
}
return certPath
}
// MakeTLSKeyPath returns TLS key file path from CLI options
// (or default one, if nothing is provided in options)
func MakeTLSKeyPath(ctx *cli.Context) string {
basePath := MakeDataDir(ctx)
keyPath := ctx.GlobalString(TLSKeyFileFlag.Name)
if keyPath == "" {
keyPath = filepath.Join(basePath, rpc.DefaultTLSKeyFile)
}
return keyPath
}

View file

@ -25,6 +25,7 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"crypto/tls"
"github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -157,6 +158,12 @@ type Config struct {
// exposed. // exposed.
HTTPModules []string HTTPModules []string
// TLSEnabled whether TLSConfig was correctly populated, and server is TLS-enabled node.
TLSEnabled bool
// TLSConfig is used to configure TLS capability of a server
TLSConfig *tls.Config
// WSHost is the host interface on which to start the websocket RPC server. If // WSHost is the host interface on which to start the websocket RPC server. If
// this field is empty, no websocket API endpoint will be started. // this field is empty, no websocket API endpoint will be started.
WSHost string WSHost string

View file

@ -17,6 +17,7 @@
package node package node
import ( import (
"crypto/tls"
"errors" "errors"
"net" "net"
"os" "os"
@ -407,8 +408,17 @@ func (n *Node) startHTTP(endpoint string, apis []rpc.API, modules []string, cors
if listener, err = net.Listen("tcp", endpoint); err != nil { if listener, err = net.Listen("tcp", endpoint); err != nil {
return err return err
} }
if n.config.TLSEnabled {
listener = tls.NewListener(listener, n.config.TLSConfig)
}
go rpc.NewHTTPServer(cors, handler).Serve(listener) go rpc.NewHTTPServer(cors, handler).Serve(listener)
if n.config.TLSEnabled {
glog.V(logger.Info).Infof("HTTPS endpoint opened: https://%s", endpoint)
} else {
glog.V(logger.Info).Infof("HTTP endpoint opened: http://%s", endpoint) glog.V(logger.Info).Infof("HTTP endpoint opened: http://%s", endpoint)
}
// All listeners booted successfully // All listeners booted successfully
n.httpEndpoint = endpoint n.httpEndpoint = endpoint
@ -424,8 +434,12 @@ func (n *Node) stopHTTP() {
n.httpListener.Close() n.httpListener.Close()
n.httpListener = nil n.httpListener = nil
if n.config.TLSEnabled {
glog.V(logger.Info).Infof("HTTPS endpoint closed: https://%s", n.httpEndpoint)
} else {
glog.V(logger.Info).Infof("HTTP endpoint closed: http://%s", n.httpEndpoint) glog.V(logger.Info).Infof("HTTP endpoint closed: http://%s", n.httpEndpoint)
} }
}
if n.httpHandler != nil { if n.httpHandler != nil {
n.httpHandler.Stop() n.httpHandler.Stop()
n.httpHandler = nil n.httpHandler = nil

View file

@ -168,7 +168,7 @@ func DialContext(ctx context.Context, rawurl string) (*Client, error) {
} }
switch u.Scheme { switch u.Scheme {
case "http", "https": case "http", "https":
return DialHTTP(rawurl) return DialHTTP(ctx, rawurl)
case "ws", "wss": case "ws", "wss":
return DialWebsocket(ctx, rawurl, "") return DialWebsocket(ctx, rawurl, "")
case "": case "":

View file

@ -64,7 +64,7 @@ func (hc *httpConn) Close() error {
} }
// DialHTTP creates a new RPC clients that connection to an RPC server over HTTP. // DialHTTP creates a new RPC clients that connection to an RPC server over HTTP.
func DialHTTP(endpoint string) (*Client, error) { func DialHTTP(ctx context.Context, endpoint string) (*Client, error) {
req, err := http.NewRequest("POST", endpoint, nil) req, err := http.NewRequest("POST", endpoint, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@ -74,7 +74,14 @@ func DialHTTP(endpoint string) (*Client, error) {
initctx := context.Background() initctx := context.Background()
return newClient(initctx, func(context.Context) (net.Conn, error) { return newClient(initctx, func(context.Context) (net.Conn, error) {
return &httpConn{client: new(http.Client), req: req, closed: make(chan struct{})}, nil // configure client capable of TLS handling
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: TLSConfigFromContext(ctx),
},
}
return &httpConn{client: client, req: req, closed: make(chan struct{})}, nil
}) })
} }

199
rpc/tls.go Normal file
View file

@ -0,0 +1,199 @@
package rpc
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"io/ioutil"
"math/big"
"net"
"os"
"time"
"github.com/ethereum/go-ethereum/logger"
"github.com/ethereum/go-ethereum/logger/glog"
)
// TLSConfigKey is used to store a TLS configuration within the connection context
type TLSConfigKey struct{}
const (
DefaultTLSCertFile = "tlscert.pem"
DefaultTLSKeyFile = "tlskey.pem"
)
var (
validFor = 365 * 24 * time.Hour
rsaBits = 2048
)
// TLSConfigFromContext fetches TLS configuration from context.
// Fetched config is to be normally used to configure client connection's transport.
func TLSConfigFromContext(ctx context.Context) *tls.Config {
config, ok := ctx.Value(TLSConfigKey{}).(*tls.Config)
if !ok {
return &tls.Config{}
}
return config
}
// MakeTLSContext packs TLS configuration into context.
func MakeTLSConfigContext(config *tls.Config) context.Context {
return context.WithValue(context.Background(), TLSConfigKey{}, config)
}
func MakeServerTLSConfig(host string, certPath string, keyPath string) (*tls.Config, error) {
config := &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
},
}
// Make sure certificate and key files do exist
if err := EnsureCerts(certPath, keyPath, host); err != nil {
return config, err
}
// Load certificate/key
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err == nil {
config.Certificates = []tls.Certificate{cert}
}
return config, nil
}
// MakeClientTLSConfig is used to configure connection context. Particularly,
// setting up TLS support for connection transport.
//
// For auto-generated certificates, isCA is always true, that's generated
// certificate is used as its own parent when signing.
func MakeClientTLSConfig(certPath string, keyPath string, isCA bool, certNoVerify bool) *tls.Config {
config := &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
},
}
// Load certificate/key
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err == nil {
config.Certificates = []tls.Certificate{cert}
// Provide CA certs
if isCA {
if rootCert, err := ioutil.ReadFile(certPath); err == nil {
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(rootCert)
config.RootCAs = certPool
config.ClientCAs = certPool
}
}
}
// Allow to skip TLS certificate verification
if certNoVerify {
config.InsecureSkipVerify = true
}
return config
}
// EnsureCerts check certificate/key files, and auto-generates both if necessary
func EnsureCerts(certPath string, keyPath string, host string) error {
var certFileExists, keyFileExists bool
if _, err := os.Stat(certPath); os.IsNotExist(err) {
certFileExists = false
}
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
keyFileExists = false
}
// generate cert and private key (if they are not already available)
if !certFileExists || !keyFileExists {
if err := Generate(certPath, keyPath, host); err != nil {
return err
}
}
return nil
}
func Generate(certPath string, keyPath string, host string) error {
priv, err := rsa.GenerateKey(rand.Reader, rsaBits)
if err != nil {
return fmt.Errorf("failed to generate private key: %s", err)
}
notBefore := time.Now()
notAfter := notBefore.Add(validFor)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
return fmt.Errorf("failed to generate serial number: %s", err)
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Acme Co"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
if ip := net.ParseIP(host); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, host)
}
template.IsCA = true
template.KeyUsage |= x509.KeyUsageCertSign
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return fmt.Errorf("Failed to create certificate: %s", err)
}
certOut, err := os.Create(certPath)
if err != nil {
return fmt.Errorf("failed to open "+certPath+" for writing: %s", err)
}
pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
certOut.Close()
glog.V(logger.Info).Infof("TLS: %s written", certPath)
keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return fmt.Errorf("failed to open "+keyPath+" for writing: %s", err)
}
pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
keyOut.Close()
glog.V(logger.Info).Infof("TLS: %s written", keyPath)
return nil
}