Skip to content

Commit

Permalink
plugin/security: protect JSON RPC APIs via a plugin interface (#1019)
Browse files Browse the repository at this point in the history
Co-authored-by: cucrisis <[email protected]>
  • Loading branch information
trung and cucrisis authored Jul 3, 2020
1 parent 40aad7c commit 21d0340
Show file tree
Hide file tree
Showing 38 changed files with 2,792 additions and 45 deletions.
2 changes: 1 addition & 1 deletion cmd/clef/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ func signer(c *cli.Context) error {

// start http server
httpEndpoint := fmt.Sprintf("%s:%d", c.GlobalString(utils.RPCListenAddrFlag.Name), c.Int(rpcPortFlag.Name))
listener, _, err := rpc.StartHTTPEndpoint(httpEndpoint, rpcAPI, []string{"account"}, cors, vhosts, rpc.DefaultHTTPTimeouts)
listener, _, _, err := rpc.StartHTTPEndpoint(httpEndpoint, rpcAPI, []string{"account"}, cors, vhosts, rpc.DefaultHTTPTimeouts, nil, nil)
if err != nil {
utils.Fatalf("Could not start RPC api: %v", err)
}
Expand Down
141 changes: 131 additions & 10 deletions cmd/geth/consolecmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
package main

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"os/signal"
"path/filepath"
Expand All @@ -26,13 +32,16 @@ import (

"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/console"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/plugin/security"
"github.com/ethereum/go-ethereum/rpc"
"gopkg.in/urfave/cli.v1"
)

var (
consoleFlags = []cli.Flag{utils.JSpathFlag, utils.ExecFlag, utils.PreloadJSFlag}
consoleFlags = []cli.Flag{utils.JSpathFlag, utils.ExecFlag, utils.PreloadJSFlag}
rpcClientFlags = []cli.Flag{utils.RPCClientToken, utils.RPCClientTLSCert, utils.RPCClientTLSCaCert, utils.RPCClientTLSCipherSuites, utils.RPCClientTLSInsecureSkipVerify}

consoleCommand = cli.Command{
Action: utils.MigrateFlags(localConsole),
Expand All @@ -51,7 +60,7 @@ See https://github.com/ethereum/go-ethereum/wiki/JavaScript-Console.`,
Name: "attach",
Usage: "Start an interactive JavaScript environment (connect to node)",
ArgsUsage: "[endpoint]",
Flags: append(consoleFlags, utils.DataDirFlag),
Flags: append(append(consoleFlags, utils.DataDirFlag), rpcClientFlags...),
Category: "CONSOLE COMMANDS",
Description: `
The Geth console is an interactive shell for the JavaScript runtime environment
Expand All @@ -73,6 +82,74 @@ JavaScript API. See https://github.com/ethereum/go-ethereum/wiki/JavaScript-Cons
}
)

// Quorum
//
// read tls client configuration from command line arguments
//
// only for HTTPS or WSS
func readTLSClientConfig(endpoint string, ctx *cli.Context) (*tls.Config, bool, error) {
if !strings.HasPrefix(endpoint, "https://") && !strings.HasPrefix(endpoint, "wss://") {
return nil, false, nil
}
hasCustomTls := false
insecureSkipVerify := ctx.Bool(utils.RPCClientTLSInsecureSkipVerify.Name)
tlsConfig := &tls.Config{
InsecureSkipVerify: insecureSkipVerify,
}
var certFile, caFile string
if !insecureSkipVerify {
var certPem, caPem []byte
certFile, caFile = ctx.String(utils.RPCClientTLSCert.Name), ctx.String(utils.RPCClientTLSCaCert.Name)
var err error
if certFile != "" {
if certPem, err = ioutil.ReadFile(certFile); err != nil {
return nil, true, err
}
}
if caFile != "" {
if caPem, err = ioutil.ReadFile(caFile); err != nil {
return nil, true, err
}
}
if len(certPem) != 0 || len(caPem) != 0 {
certPool, err := x509.SystemCertPool()
if err != nil {
certPool = x509.NewCertPool()
}
if len(certPem) != 0 {
certPool.AppendCertsFromPEM(certPem)
}
if len(caPem) != 0 {
certPool.AppendCertsFromPEM(caPem)
}
tlsConfig.RootCAs = certPool
hasCustomTls = true
}
} else {
hasCustomTls = true
}
cipherSuitesInput := ctx.String(utils.RPCClientTLSCipherSuites.Name)
cipherSuitesStrings := strings.FieldsFunc(cipherSuitesInput, func(r rune) bool {
return r == ','
})
if len(cipherSuitesStrings) > 0 {
cipherSuiteList := make(security.CipherSuiteList, len(cipherSuitesStrings))
for i, s := range cipherSuitesStrings {
cipherSuiteList[i] = security.CipherSuite(strings.TrimSpace(s))
}
cipherSuites, err := cipherSuiteList.ToUint16Array()
if err != nil {
return nil, true, err
}
tlsConfig.CipherSuites = cipherSuites
hasCustomTls = true
}
if !hasCustomTls {
return nil, false, nil
}
return tlsConfig, hasCustomTls, nil
}

// localConsole starts a new geth node, attaching a JavaScript console to it at the
// same time.
func localConsole(ctx *cli.Context) error {
Expand Down Expand Up @@ -131,7 +208,7 @@ func remoteConsole(ctx *cli.Context) error {
}
endpoint = fmt.Sprintf("%s/geth.ipc", path)
}
client, err := dialRPC(endpoint)
client, err := dialRPC(endpoint, ctx)
if err != nil {
utils.Fatalf("Unable to attach to remote geth: %v", err)
}
Expand All @@ -142,36 +219,80 @@ func remoteConsole(ctx *cli.Context) error {
Preload: utils.MakeConsolePreloads(ctx),
}

console, err := console.New(config)
consl, err := console.New(config)
if err != nil {
utils.Fatalf("Failed to start the JavaScript console: %v", err)
}
defer console.Stop(false)
defer consl.Stop(false)

if script := ctx.GlobalString(utils.ExecFlag.Name); script != "" {
console.Evaluate(script)
consl.Evaluate(script)
return nil
}

// Otherwise print the welcome screen and enter interactive mode
console.Welcome()
console.Interactive()
consl.Welcome()
consl.Interactive()

return nil
}

// dialRPC 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) {
//
// Quorum: passing the cli context to build security-aware client:
// 1. Custom TLS configuration
// 2. Access Token awareness via rpc.HttpCredentialsProviderFunc
func dialRPC(endpoint string, ctx *cli.Context) (*rpc.Client, error) {
if endpoint == "" {
endpoint = node.DefaultIPCEndpoint(clientIdentifier)
} else if strings.HasPrefix(endpoint, "rpc:") || strings.HasPrefix(endpoint, "ipc:") {
// Backwards compatibility with geth < 1.5 which required
// these prefixes.
endpoint = endpoint[4:]
}
return rpc.Dial(endpoint)
var (
client *rpc.Client
err error
dialCtx = context.Background()
)
tlsConfig, hasCustomTls, tlsReadErr := readTLSClientConfig(endpoint, ctx)
if tlsReadErr != nil {
return nil, tlsReadErr
}
if token := ctx.String(utils.RPCClientToken.Name); token != "" {
var f rpc.HttpCredentialsProviderFunc = func(ctx context.Context) (string, error) {
return token, nil
}
// it's important that f MUST BE OF TYPE rpc.HttpCredentialsProviderFunc
dialCtx = context.WithValue(dialCtx, rpc.CtxCredentialsProvider, f)
}
if hasCustomTls {
u, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
switch u.Scheme {
case "https":
customHttpClient := &http.Client{
Transport: http.DefaultTransport,
}
customHttpClient.Transport.(*http.Transport).TLSClientConfig = tlsConfig
client, err = rpc.DialHTTPWithClient(endpoint, customHttpClient)
case "wss":
client, err = rpc.DialWebsocketWithCustomTLS(dialCtx, endpoint, "", tlsConfig)
default:
log.Warn("unsupported scheme for custom TLS which is only for HTTPS/WSS", "scheme", u.Scheme)
client, err = rpc.DialContext(dialCtx, endpoint)
}
} else {
client, err = rpc.DialContext(dialCtx, endpoint)
}
if f, ok := dialCtx.Value(rpc.CtxCredentialsProvider).(rpc.HttpCredentialsProviderFunc); ok && err == nil {
client, err = client.WithHTTPCredentials(f)
}
return client, err
}

// ephemeralConsole starts a new geth node, attaches an ephemeral JavaScript
Expand Down
51 changes: 51 additions & 0 deletions cmd/geth/consolecmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ package main

import (
"crypto/rand"
"crypto/tls"
"flag"
"io/ioutil"
"math/big"
"os"
Expand All @@ -28,7 +30,10 @@ import (
"testing"
"time"

"github.com/ethereum/go-ethereum/cmd/utils"
"github.com/ethereum/go-ethereum/params"
testifyassert "github.com/stretchr/testify/assert"
"gopkg.in/urfave/cli.v1"
)

const (
Expand Down Expand Up @@ -238,6 +243,52 @@ func setupIstanbul(t *testing.T) string {
return datadir
}

func TestReadTLSClientConfig_whenCustomizeTLSCipherSuites(t *testing.T) {
assert := testifyassert.New(t)

flagSet := new(flag.FlagSet)
flagSet.Bool(utils.RPCClientTLSInsecureSkipVerify.Name, true, "")
flagSet.String(utils.RPCClientTLSCipherSuites.Name, "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", "")
ctx := cli.NewContext(nil, flagSet, nil)

tlsConf, ok, err := readTLSClientConfig("https://arbitraryendpoint", ctx)

assert.NoError(err)
assert.True(ok, "has custom TLS client configuration")
assert.True(tlsConf.InsecureSkipVerify)
assert.Len(tlsConf.CipherSuites, 2)
assert.Contains(tlsConf.CipherSuites, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384)
assert.Contains(tlsConf.CipherSuites, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384)
}

func TestReadTLSClientConfig_whenTypicalTLS(t *testing.T) {
assert := testifyassert.New(t)

flagSet := new(flag.FlagSet)
ctx := cli.NewContext(nil, flagSet, nil)

tlsConf, ok, err := readTLSClientConfig("https://arbitraryendpoint", ctx)

assert.NoError(err)
assert.False(ok, "no custom TLS client configuration")
assert.Nil(tlsConf, "no custom TLS config is set")
}

func TestReadTLSClientConfig_whenTLSInsecureFlagSet(t *testing.T) {
assert := testifyassert.New(t)

flagSet := new(flag.FlagSet)
flagSet.Bool(utils.RPCClientTLSInsecureSkipVerify.Name, true, "")
ctx := cli.NewContext(nil, flagSet, nil)

tlsConf, ok, err := readTLSClientConfig("https://arbitraryendpoint", ctx)

assert.NoError(err)
assert.True(ok, "has custom TLS client configuration")
assert.True(tlsConf.InsecureSkipVerify)
assert.Len(tlsConf.CipherSuites, 0)
}

func SetResetPrivateConfig(value string) func() {
existingValue := os.Getenv("PRIVATE_CONFIG")
os.Setenv("PRIVATE_CONFIG", value)
Expand Down
3 changes: 2 additions & 1 deletion cmd/geth/retesteth.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/plugin/security"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/rpc"
"github.com/ethereum/go-ethereum/trie"
Expand Down Expand Up @@ -881,7 +882,7 @@ func retesteth(ctx *cli.Context) error {

// start http server
httpEndpoint := fmt.Sprintf("%s:%d", ctx.GlobalString(utils.RPCListenAddrFlag.Name), ctx.Int(rpcPortFlag.Name))
listener, _, err := rpc.StartHTTPEndpoint(httpEndpoint, rpcAPI, []string{"test", "eth", "debug", "web3"}, cors, vhosts, rpc.DefaultHTTPTimeouts)
listener, _, _, err := rpc.StartHTTPEndpoint(httpEndpoint, rpcAPI, []string{"test", "eth", "debug", "web3"}, cors, vhosts, rpc.DefaultHTTPTimeouts, nil, &security.DisabledAuthenticationManager{})
if err != nil {
utils.Fatalf("Could not start RPC api: %v", err)
}
Expand Down
5 changes: 5 additions & 0 deletions cmd/geth/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ var AppHelpFlagGroups = []flagGroup{
utils.JSpathFlag,
utils.ExecFlag,
utils.PreloadJSFlag,
utils.RPCClientToken,
utils.RPCClientTLSInsecureSkipVerify,
utils.RPCClientTLSCert,
utils.RPCClientTLSCaCert,
utils.RPCClientTLSCipherSuites,
},
},
{
Expand Down
21 changes: 21 additions & 0 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,27 @@ var (
Name: "nocompaction",
Usage: "Disables db compaction after import",
}
// RPC Client Settings
RPCClientToken = cli.StringFlag{
Name: "rpcclitoken",
Usage: "RPC Client access token",
}
RPCClientTLSCert = cli.StringFlag{
Name: "rpcclitls.cert",
Usage: "Server's TLS certificate PEM file on connection by client",
}
RPCClientTLSCaCert = cli.StringFlag{
Name: "rpcclitls.cacert",
Usage: "CA certificate PEM file for provided server's TLS certificate on connection by client",
}
RPCClientTLSCipherSuites = cli.StringFlag{
Name: "rpcclitls.ciphersuites",
Usage: "Customize supported cipher suites when using TLS connection. Value is a comma-separated cipher suite string",
}
RPCClientTLSInsecureSkipVerify = cli.BoolFlag{
Name: "rpcclitls.insecureskipverify",
Usage: "Disable verification of server's TLS certificate on connection by client",
}
// RPC settings
IPCDisabledFlag = cli.BoolFlag{
Name: "ipcdisable",
Expand Down
Loading

0 comments on commit 21d0340

Please sign in to comment.