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
import (
"context"
"os"
"os/signal"
"strings"
@ -108,7 +109,8 @@ func localConsole(ctx *cli.Context) error {
// console to it.
func remoteConsole(ctx *cli.Context) error {
// 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 {
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.
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
// 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 == "" {
endpoint = node.DefaultIPCEndpoint(clientIdentifier)
} else if strings.HasPrefix(endpoint, "rpc:") || strings.HasPrefix(endpoint, "ipc:") {
@ -147,7 +154,7 @@ func dialRPC(endpoint string) (*rpc.Client, error) {
// these prefixes.
endpoint = endpoint[4:]
}
return rpc.Dial(endpoint)
return rpc.DialContext(ctx, endpoint)
}
// ephemeralConsole starts a new geth node, attaches an ephemeral JavaScript

View file

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

View file

@ -24,6 +24,7 @@ import (
"sort"
"strings"
"time"
"context"
"github.com/ethereum/go-ethereum/cmd/utils"
"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
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)
}
defer client.Close()

View file

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

View file

@ -19,6 +19,7 @@ package utils
import (
"crypto/ecdsa"
"crypto/tls"
"fmt"
"io/ioutil"
"math"
@ -274,6 +275,26 @@ var (
Usage: "API's offered over the HTTP-RPC interface",
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{
Name: "ipcdisable",
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),
WSOrigins: ctx.GlobalString(WSAllowedOriginsFlag.Name),
WSModules: MakeRPCModules(ctx.GlobalString(WSApiFlag.Name)),
TLSConfig: MakeServerTLSConfig(ctx),
}
if ctx.GlobalBool(DevModeFlag.Name) {
if !ctx.GlobalIsSet(DataDirFlag.Name) {
@ -703,6 +725,9 @@ func MakeNode(ctx *cli.Context, name, gitCommit string) *node.Node {
}
config.NetRestrict = list
}
if config.TLSConfig != nil {
config.TLSEnabled = true
}
stack, err := node.New(config)
if err != nil {
@ -958,3 +983,56 @@ func MakeConsolePreloads(ctx *cli.Context) []string {
}
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"
"runtime"
"strings"
"crypto/tls"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/common"
@ -157,6 +158,12 @@ type Config struct {
// exposed.
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
// this field is empty, no websocket API endpoint will be started.
WSHost string

View file

@ -17,6 +17,7 @@
package node
import (
"crypto/tls"
"errors"
"net"
"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 {
return err
}
if n.config.TLSEnabled {
listener = tls.NewListener(listener, n.config.TLSConfig)
}
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)
}
// All listeners booted successfully
n.httpEndpoint = endpoint
@ -424,8 +434,12 @@ func (n *Node) stopHTTP() {
n.httpListener.Close()
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)
}
}
if n.httpHandler != nil {
n.httpHandler.Stop()
n.httpHandler = nil

View file

@ -168,7 +168,7 @@ func DialContext(ctx context.Context, rawurl string) (*Client, error) {
}
switch u.Scheme {
case "http", "https":
return DialHTTP(rawurl)
return DialHTTP(ctx, rawurl)
case "ws", "wss":
return DialWebsocket(ctx, rawurl, "")
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.
func DialHTTP(endpoint string) (*Client, error) {
func DialHTTP(ctx context.Context, endpoint string) (*Client, error) {
req, err := http.NewRequest("POST", endpoint, nil)
if err != nil {
return nil, err
@ -74,7 +74,14 @@ func DialHTTP(endpoint string) (*Client, error) {
initctx := context.Background()
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
}