Skip to content

Commit

Permalink
cmd/geth, node, rpc: implement jwt tokens (#24364)
Browse files Browse the repository at this point in the history
* rpc, node: refactor request validation and add jwt validation

* node, rpc: fix error message, ignore engine api in RegisterAPIs

* node: make authenticated port configurable

* eth/catalyst: enable unauthenticated version of engine api

* node: rework obtainjwtsecret (backport later)

* cmd/geth: added auth port flag

* node: happy lint, happy life

* node: refactor authenticated api

Modifies the authentication mechanism to use default values

* node: trim spaces and newline away from secret

Co-authored-by: Marius van der Wijden <m.vanderwijden@live.de>
  • Loading branch information
holiman and MariusVanDerWijden authored Mar 7, 2022
1 parent 37f9d25 commit 4860e50
Show file tree
Hide file tree
Showing 21 changed files with 417 additions and 57 deletions.
2 changes: 1 addition & 1 deletion cmd/clef/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ func signer(c *cli.Context) error {
if err != nil {
utils.Fatalf("Could not register API: %w", err)
}
handler := node.NewHTTPHandlerStack(srv, cors, vhosts)
handler := node.NewHTTPHandlerStack(srv, cors, vhosts, nil)

// set port
port := c.Int(rpcPortFlag.Name)
Expand Down
2 changes: 2 additions & 0 deletions cmd/geth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ var (
utils.HTTPListenAddrFlag,
utils.HTTPPortFlag,
utils.HTTPCORSDomainFlag,
utils.AuthPortFlag,
utils.JWTSecretFlag,
utils.HTTPVirtualHostsFlag,
utils.GraphQLEnabledFlag,
utils.GraphQLCORSDomainFlag,
Expand Down
1 change: 1 addition & 0 deletions cmd/geth/usage.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ var AppHelpFlagGroups = []flags.FlagGroup{
Flags: []cli.Flag{
utils.IPCDisabledFlag,
utils.IPCPathFlag,
utils.JWTSecretFlag,
utils.HTTPEnabledFlag,
utils.HTTPListenAddrFlag,
utils.HTTPPortFlag,
Expand Down
18 changes: 18 additions & 0 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,16 @@ var (
Usage: "Sets a cap on transaction fee (in ether) that can be sent via the RPC APIs (0 = no cap)",
Value: ethconfig.Defaults.RPCTxFeeCap,
}
// Authenticated port settings
AuthPortFlag = cli.IntFlag{
Name: "authrpc.port",
Usage: "Listening port for authenticated APIs",
Value: node.DefaultAuthPort,
}
JWTSecretFlag = cli.StringFlag{
Name: "authrpc.jwtsecret",
Usage: "JWT secret (or path to a jwt secret) to use for authenticated RPC endpoints",
}
// Logging and debug settings
EthStatsURLFlag = cli.StringFlag{
Name: "ethstats",
Expand Down Expand Up @@ -951,6 +961,10 @@ func setHTTP(ctx *cli.Context, cfg *node.Config) {
cfg.HTTPPort = ctx.GlobalInt(HTTPPortFlag.Name)
}

if ctx.GlobalIsSet(AuthPortFlag.Name) {
cfg.AuthPort = ctx.GlobalInt(AuthPortFlag.Name)
}

if ctx.GlobalIsSet(HTTPCORSDomainFlag.Name) {
cfg.HTTPCors = SplitAndTrim(ctx.GlobalString(HTTPCORSDomainFlag.Name))
}
Expand Down Expand Up @@ -1218,6 +1232,10 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) {
setDataDir(ctx, cfg)
setSmartCard(ctx, cfg)

if ctx.GlobalIsSet(JWTSecretFlag.Name) {
cfg.JWTSecret = ctx.GlobalString(JWTSecretFlag.Name)
}

if ctx.GlobalIsSet(ExternalSignerFlag.Name) {
cfg.ExternalSigner = ctx.GlobalString(ExternalSignerFlag.Name)
}
Expand Down
16 changes: 12 additions & 4 deletions eth/catalyst/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,18 @@ func Register(stack *node.Node, backend *eth.Ethereum) error {
log.Warn("Catalyst mode enabled", "protocol", "eth")
stack.RegisterAPIs([]rpc.API{
{
Namespace: "engine",
Version: "1.0",
Service: NewConsensusAPI(backend),
Public: true,
Namespace: "engine",
Version: "1.0",
Service: NewConsensusAPI(backend),
Public: true,
Authenticated: true,
},
{
Namespace: "engine",
Version: "1.0",
Service: NewConsensusAPI(backend),
Public: true,
Authenticated: false,
},
})
return nil
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ require (
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff
github.com/go-ole/go-ole v1.2.1 // indirect
github.com/go-stack/stack v1.8.0
github.com/golang-jwt/jwt/v4 v4.3.0 // indirect
github.com/golang/protobuf v1.4.3
github.com/golang/snappy v0.0.4
github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog=
github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
Expand Down
2 changes: 1 addition & 1 deletion graphql/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func newHandler(stack *node.Node, backend ethapi.Backend, cors, vhosts []string)
return err
}
h := handler{Schema: s}
handler := node.NewHTTPHandlerStack(h, cors, vhosts)
handler := node.NewHTTPHandlerStack(h, cors, vhosts, nil)

stack.RegisterHandler("GraphQL UI", "/graphql/ui", GraphiQL{})
stack.RegisterHandler("GraphQL", "/graphql", handler)
Expand Down
9 changes: 5 additions & 4 deletions les/catalyst/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,11 @@ func Register(stack *node.Node, backend *les.LightEthereum) error {
log.Warn("Catalyst mode enabled", "protocol", "les")
stack.RegisterAPIs([]rpc.API{
{
Namespace: "engine",
Version: "1.0",
Service: NewConsensusAPI(backend),
Public: true,
Namespace: "engine",
Version: "1.0",
Service: NewConsensusAPI(backend),
Public: true,
Authenticated: true,
},
})
return nil
Expand Down
5 changes: 3 additions & 2 deletions node/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,11 +274,12 @@ func (api *privateAdminAPI) StartWS(host *string, port *int, allowedOrigins *str
}

// Enable WebSocket on the server.
server := api.node.wsServerForPort(*port)
server := api.node.wsServerForPort(*port, false)
if err := server.setListenAddr(*host, *port); err != nil {
return false, err
}
if err := server.enableWS(api.node.rpcAPIs, config); err != nil {
openApis, _ := api.node.GetAPIs()
if err := server.enableWS(openApis, config); err != nil {
return false, err
}
if err := server.start(); err != nil {
Expand Down
9 changes: 8 additions & 1 deletion node/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (

const (
datadirPrivateKey = "nodekey" // Path within the datadir to the node's private key
datadirJWTKey = "jwtsecret" // Path within the datadir to the node's jwt secret
datadirDefaultKeyStore = "keystore" // Path within the datadir to the keystore
datadirStaticNodes = "static-nodes.json" // Path within the datadir to the static node list
datadirTrustedNodes = "trusted-nodes.json" // Path within the datadir to the trusted node list
Expand Down Expand Up @@ -112,6 +113,9 @@ type Config struct {
// for ephemeral nodes).
HTTPPort int `toml:",omitempty"`

// Authport is the port number on which the authenticated API is provided.
AuthPort int `toml:",omitempty"`

// HTTPCors is the Cross-Origin Resource Sharing header to send to requesting
// clients. Please be aware that CORS is a browser enforced security, it's fully
// useless for custom HTTP clients.
Expand Down Expand Up @@ -190,6 +194,9 @@ type Config struct {

// AllowUnprotectedTxs allows non EIP-155 protected transactions to be send over RPC.
AllowUnprotectedTxs bool `toml:",omitempty"`

// JWTSecret is the hex-encoded jwt secret.
JWTSecret string `toml:",omitempty"`
}

// IPCEndpoint resolves an IPC endpoint based on a configured value, taking into
Expand Down Expand Up @@ -248,7 +255,7 @@ func (c *Config) HTTPEndpoint() string {

// DefaultHTTPEndpoint returns the HTTP endpoint used by default.
func DefaultHTTPEndpoint() string {
config := &Config{HTTPHost: DefaultHTTPHost, HTTPPort: DefaultHTTPPort}
config := &Config{HTTPHost: DefaultHTTPHost, HTTPPort: DefaultHTTPPort, AuthPort: DefaultAuthPort}
return config.HTTPEndpoint()
}

Expand Down
11 changes: 11 additions & 0 deletions node/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,23 @@ const (
DefaultWSPort = 8546 // Default TCP port for the websocket RPC server
DefaultGraphQLHost = "localhost" // Default host interface for the GraphQL server
DefaultGraphQLPort = 8547 // Default TCP port for the GraphQL server
DefaultAuthHost = "localhost" // Default host interface for the authenticated apis
DefaultAuthPort = 8551 // Default port for the authenticated apis
)

var (
DefaultAuthCors = []string{"localhost"} // Default cors domain for the authenticated apis
DefaultAuthVhosts = []string{"localhost"} // Default virtual hosts for the authenticated apis
DefaultAuthOrigins = []string{"localhost"} // Default origins for the authenticated apis
DefaultAuthPrefix = "" // Default prefix for the authenticated apis
DefaultAuthModules = []string{"eth", "engine"}
)

// DefaultConfig contains reasonable default settings.
var DefaultConfig = Config{
DataDir: DefaultDataDir(),
HTTPPort: DefaultHTTPPort,
AuthPort: DefaultAuthPort,
HTTPModules: []string{"net", "web3"},
HTTPVirtualHosts: []string{"localhost"},
HTTPTimeouts: rpc.DefaultHTTPTimeouts,
Expand Down
6 changes: 4 additions & 2 deletions node/endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,10 @@ func checkModuleAvailability(modules []string, apis []rpc.API) (bad, available [
}
}
for _, name := range modules {
if _, ok := availableSet[name]; !ok && name != rpc.MetadataApi {
bad = append(bad, name)
if _, ok := availableSet[name]; !ok {
if name != rpc.MetadataApi && name != rpc.EngineApi {
bad = append(bad, name)
}
}
}
return bad, available
Expand Down
78 changes: 78 additions & 0 deletions node/jwt_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2022 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package node

import (
"net/http"
"strings"
"time"

"github.com/golang-jwt/jwt/v4"
)

type jwtHandler struct {
keyFunc func(token *jwt.Token) (interface{}, error)
next http.Handler
}

// newJWTHandler creates a http.Handler with jwt authentication support.
func newJWTHandler(secret []byte, next http.Handler) http.Handler {
return &jwtHandler{
keyFunc: func(token *jwt.Token) (interface{}, error) {
return secret, nil
},
next: next,
}
}

// ServeHTTP implements http.Handler
func (handler *jwtHandler) ServeHTTP(out http.ResponseWriter, r *http.Request) {
var (
strToken string
claims jwt.RegisteredClaims
)
if auth := r.Header.Get("Authorization"); strings.HasPrefix(auth, "Bearer ") {
strToken = strings.TrimPrefix(auth, "Bearer ")
}
if len(strToken) == 0 {
http.Error(out, "missing token", http.StatusForbidden)
return
}
// We explicitly set only HS256 allowed, and also disables the
// claim-check: the RegisteredClaims internally requires 'iat' to
// be no later than 'now', but we allow for a bit of drift.
token, err := jwt.ParseWithClaims(strToken, &claims, handler.keyFunc,
jwt.WithValidMethods([]string{"HS256"}),
jwt.WithoutClaimsValidation())

switch {
case err != nil:
http.Error(out, err.Error(), http.StatusForbidden)
case !token.Valid:
http.Error(out, "invalid token", http.StatusForbidden)
case !claims.VerifyExpiresAt(time.Now(), false): // optional
http.Error(out, "token is expired", http.StatusForbidden)
case claims.IssuedAt == nil:
http.Error(out, "missing issued-at", http.StatusForbidden)
case time.Since(claims.IssuedAt.Time) > 5*time.Second:
http.Error(out, "stale token", http.StatusForbidden)
case time.Until(claims.IssuedAt.Time) > 5*time.Second:
http.Error(out, "future token", http.StatusForbidden)
default:
handler.next.ServeHTTP(out, r)
}
}
Loading

0 comments on commit 4860e50

Please sign in to comment.