Added Engine Authentication [JWT] (#3531)

* jwt

* fuuuuture

* added auth

* merge

* merge

* Update jwt to the latest version

* ops

* comments

* cleanup

* bad

* gut

* maybe

* mod sum

Co-authored-by: yperbasis <andrey.ashikhmin@gmail.com>
This commit is contained in:
Giulio rebuffo 2022-02-28 12:07:09 +01:00 committed by GitHub
parent 0cac29d1d2
commit cb8aacae87
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 107 additions and 10 deletions

View File

@ -2,14 +2,18 @@ package cli
import (
"context"
"crypto/rand"
"encoding/binary"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/ledgerwatch/erigon-lib/direct"
"github.com/ledgerwatch/erigon-lib/gointerfaces"
"github.com/ledgerwatch/erigon-lib/gointerfaces/grpcutil"
@ -26,6 +30,7 @@ import (
"github.com/ledgerwatch/erigon/cmd/rpcdaemon/interfaces"
"github.com/ledgerwatch/erigon/cmd/rpcdaemon/services"
"github.com/ledgerwatch/erigon/cmd/utils"
"github.com/ledgerwatch/erigon/common"
"github.com/ledgerwatch/erigon/common/paths"
"github.com/ledgerwatch/erigon/core/rawdb"
"github.com/ledgerwatch/erigon/eth/ethconfig"
@ -46,6 +51,9 @@ var rootCmd = &cobra.Command{
Short: "rpcdaemon is JSON RPC server that connects to Erigon node for remote DB access",
}
const JwtTokenExpiry = 5 * time.Second
const JwtDefaultFile = "jwt.hex"
func RootCommand() (*cobra.Command, *httpcfg.HttpCfg) {
utils.CobraFlags(rootCmd, append(debug.Flags, utils.MetricFlags...))
@ -80,6 +88,7 @@ func RootCommand() (*cobra.Command, *httpcfg.HttpCfg) {
rootCmd.PersistentFlags().IntVar(&cfg.GRPCPort, "grpc.port", node.DefaultGRPCPort, "GRPC server listening port")
rootCmd.PersistentFlags().BoolVar(&cfg.GRPCHealthCheckEnabled, "grpc.healthcheck", false, "Enable GRPC health check")
rootCmd.PersistentFlags().StringVar(&cfg.StarknetGRPCAddress, "starknet.grpc.address", "127.0.0.1:6066", "Starknet GRPC address")
rootCmd.PersistentFlags().StringVar(&cfg.JWTSecretPath, "jwt-secret", "", "Token to ensure safe connection between CL and EL")
if err := rootCmd.MarkPersistentFlagFilename("rpc.accessList", "json"); err != nil {
panic(err)
@ -372,6 +381,7 @@ func RemoteServices(ctx context.Context, cfg httpcfg.HttpCfg, logger log.Logger,
func StartRpcServer(ctx context.Context, cfg httpcfg.HttpCfg, rpcAPI []rpc.API) error {
var engineListener *http.Server
var engineListenerAuth *http.Server
var enginesrv *rpc.Server
var engineHttpEndpoint string
@ -418,7 +428,10 @@ func StartRpcServer(ctx context.Context, cfg httpcfg.HttpCfg, rpcAPI []rpc.API)
wsHandler = srv.WebsocketHandler([]string{"*"}, cfg.WebsocketCompression)
}
apiHandler := createHandler(cfg, defaultAPIList, httpHandler, wsHandler)
apiHandler, err := createHandler(cfg, defaultAPIList, httpHandler, wsHandler, false)
if err != nil {
return err
}
listener, _, err := node.StartHTTPEndpoint(httpEndpoint, rpc.DefaultHTTPTimeouts, apiHandler)
if err != nil {
@ -428,7 +441,7 @@ func StartRpcServer(ctx context.Context, cfg httpcfg.HttpCfg, rpcAPI []rpc.API)
"ws.compression", cfg.WebsocketCompression, "grpc", cfg.GRPCServerEnabled}
if len(engineAPI) > 0 {
engineListener, enginesrv, engineHttpEndpoint, err = createEngineListener(cfg, engineAPI, engineFlag)
engineListener, engineListenerAuth, enginesrv, engineHttpEndpoint, err = createEngineListener(cfg, engineAPI, engineFlag)
if err != nil {
return fmt.Errorf("could not start RPC api for engine: %w", err)
}
@ -471,6 +484,11 @@ func StartRpcServer(ctx context.Context, cfg httpcfg.HttpCfg, rpcAPI []rpc.API)
log.Info("Engine HTTP endpoint close", "url", engineHttpEndpoint)
}
if engineListenerAuth != nil {
_ = engineListenerAuth.Shutdown(shutdownCtx)
log.Info("Engine HTTP endpoint close", "url", engineHttpEndpoint)
}
if cfg.GRPCServerEnabled {
if cfg.GRPCHealthCheckEnabled {
healthServer.Shutdown()
@ -491,7 +509,37 @@ func isWebsocket(r *http.Request) bool {
strings.Contains(strings.ToLower(r.Header.Get("Connection")), "upgrade")
}
func createHandler(cfg httpcfg.HttpCfg, apiList []rpc.API, httpHandler http.Handler, wsHandler http.Handler) http.Handler {
func createHandler(cfg httpcfg.HttpCfg, apiList []rpc.API, httpHandler http.Handler, wsHandler http.Handler, isAuth bool) (http.Handler, error) {
var jwtVerificationKey []byte
var err error
if isAuth {
// If no file is specified we generate a key in jwt.hex
if cfg.JWTSecretPath == "" {
jwtVerificationKey := make([]byte, 32)
rand.Read(jwtVerificationKey)
jwtVerificationKey = []byte(common.Bytes2Hex(jwtVerificationKey))
f, err := os.Create(JwtDefaultFile)
if err != nil {
return nil, err
}
defer f.Close()
_, err = f.Write(jwtVerificationKey)
if err != nil {
return nil, err
}
} else {
jwtVerificationKey, err = ioutil.ReadFile(cfg.JWTSecretPath)
if err != nil {
return nil, err
}
if len(jwtVerificationKey) != 64 {
return nil, fmt.Errorf("error: invalid size of verification key in %s", cfg.JWTSecretPath)
}
}
}
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// adding a healthcheck here
if health.ProcessHealthcheckIfNeeded(w, r, apiList) {
@ -501,37 +549,79 @@ func createHandler(cfg httpcfg.HttpCfg, apiList []rpc.API, httpHandler http.Hand
wsHandler.ServeHTTP(w, r)
return
}
if isAuth {
// Check if JWT signature is correct
tokenStr, ok := r.Header["Authorization"]
if !ok {
w.WriteHeader(http.StatusBadRequest)
return
}
claims := jwt.StandardClaims{}
tkn, err := jwt.ParseWithClaims(strings.Replace(tokenStr[0], "Bearer ", "", 1), &claims, func(token *jwt.Token) (interface{}, error) {
return jwtVerificationKey, nil
})
if err != nil || !tkn.Valid {
w.WriteHeader(http.StatusUnauthorized)
return
}
// Validate time of iat
now := time.Now().Unix()
if claims.IssuedAt > now+JwtTokenExpiry.Nanoseconds() && claims.IssuedAt < now-JwtTokenExpiry.Nanoseconds() {
w.WriteHeader(http.StatusUnauthorized)
return
}
}
httpHandler.ServeHTTP(w, r)
})
return handler
return handler, nil
}
func createEngineListener(cfg httpcfg.HttpCfg, engineApi []rpc.API, engineFlag []string) (*http.Server, *rpc.Server, string, error) {
func createEngineListener(cfg httpcfg.HttpCfg, engineApi []rpc.API, engineFlag []string) (*http.Server, *http.Server, *rpc.Server, string, error) {
engineHttpEndpoint := fmt.Sprintf("%s:%d", cfg.EngineHTTPListenAddress, cfg.EnginePort)
engineHttpEndpointAuth := fmt.Sprintf("%s:%d", cfg.EngineHTTPListenAddress, cfg.EnginePort+1)
enginesrv := rpc.NewServer(cfg.RpcBatchConcurrency)
allowListForRPC, err := parseAllowListForRPC(cfg.RpcAllowListFilePath)
if err != nil {
return nil, nil, "", err
return nil, nil, nil, "", err
}
enginesrv.SetAllowList(allowListForRPC)
if err := node.RegisterApisFromWhitelist(engineApi, engineFlag, enginesrv, false); err != nil {
return nil, nil, "", fmt.Errorf("could not start register RPC engine api: %w", err)
return nil, nil, nil, "", fmt.Errorf("could not start register RPC engine api: %w", err)
}
engineHttpHandler := node.NewHTTPHandlerStack(enginesrv, cfg.HttpCORSDomain, cfg.HttpVirtualHost, cfg.HttpCompression)
engineApiHandler := createHandler(cfg, engineApi, engineHttpHandler, nil)
engineApiHandler, err := createHandler(cfg, engineApi, engineHttpHandler, nil, false)
if err != nil {
return nil, nil, nil, "", err
}
engineApiHandlerAuth, err := createHandler(cfg, engineApi, engineHttpHandler, nil, true)
if err != nil {
return nil, nil, nil, "", err
}
engineListener, _, err := node.StartHTTPEndpoint(engineHttpEndpoint, rpc.DefaultHTTPTimeouts, engineApiHandler)
if err != nil {
return nil, nil, "", fmt.Errorf("could not start RPC api: %w", err)
return nil, nil, nil, "", fmt.Errorf("could not start RPC api: %w", err)
}
engineListenerAuth, _, err := node.StartHTTPEndpoint(engineHttpEndpointAuth, rpc.DefaultHTTPTimeouts, engineApiHandlerAuth)
if err != nil {
return nil, nil, nil, "", fmt.Errorf("could not start RPC api: %w", err)
}
engineInfo := []interface{}{"url", engineHttpEndpoint}
log.Info("HTTP endpoint opened for engine", engineInfo...)
engineInfoAuth := []interface{}{"url", engineHttpEndpointAuth}
log.Info("HTTP endpoint opened for auth engine", engineInfoAuth...)
return engineListener, enginesrv, engineHttpEndpoint, nil
return engineListener, engineListenerAuth, enginesrv, engineHttpEndpoint, nil
}

View File

@ -38,4 +38,5 @@ type HttpCfg struct {
GRPCPort int
GRPCHealthCheckEnabled bool
StarknetGRPCAddress string
JWTSecretPath string // Engine API Authentication
}

2
go.mod
View File

@ -26,6 +26,7 @@ require (
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/goccy/go-json v0.7.4
github.com/gofrs/flock v0.8.1
github.com/golang-jwt/jwt/v4 v4.3.0
github.com/golang/snappy v0.0.4
github.com/google/btree v1.0.1
github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa
@ -66,6 +67,7 @@ require (
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4 // indirect
google.golang.org/grpc v1.42.0

4
go.sum
View File

@ -421,6 +421,8 @@ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
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 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@ -1339,6 +1341,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=