diff --git a/cmd/geth/consolecmd.go b/cmd/geth/consolecmd.go index b1c435e00f..a664ad66ac 100644 --- a/cmd/geth/consolecmd.go +++ b/cmd/geth/consolecmd.go @@ -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 diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 332e1ae8d5..cf833e4ef5 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -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, diff --git a/cmd/geth/monitorcmd.go b/cmd/geth/monitorcmd.go index 03a1244945..718b55f193 100644 --- a/cmd/geth/monitorcmd.go +++ b/cmd/geth/monitorcmd.go @@ -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() diff --git a/cmd/geth/usage.go b/cmd/geth/usage.go index 50c742e7c5..bebdcb0210 100644 --- a/cmd/geth/usage.go +++ b/cmd/geth/usage.go @@ -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, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 9df891f788..085cf78cca 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -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 +} diff --git a/node/config.go b/node/config.go index 8d75e441b7..eb0904498c 100644 --- a/node/config.go +++ b/node/config.go @@ -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 diff --git a/node/node.go b/node/node.go index 4b56fba4c5..9df48ba3cc 100644 --- a/node/node.go +++ b/node/node.go @@ -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) - glog.V(logger.Info).Infof("HTTP endpoint opened: http://%s", endpoint) + 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,7 +434,11 @@ func (n *Node) stopHTTP() { n.httpListener.Close() n.httpListener = nil - glog.V(logger.Info).Infof("HTTP endpoint closed: http://%s", n.httpEndpoint) + 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() diff --git a/rpc/client.go b/rpc/client.go index 34a3b78317..c5513cddba 100644 --- a/rpc/client.go +++ b/rpc/client.go @@ -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 "": diff --git a/rpc/http.go b/rpc/http.go index 7d4fe5d474..14d71f50b4 100644 --- a/rpc/http.go +++ b/rpc/http.go @@ -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 }) } diff --git a/rpc/tls.go b/rpc/tls.go new file mode 100644 index 0000000000..af7c515cbe --- /dev/null +++ b/rpc/tls.go @@ -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 +}