diff --git a/cmd/rpcdaemon/cli/config.go b/cmd/rpcdaemon/cli/config.go index 4a819855c..7e0249433 100644 --- a/cmd/rpcdaemon/cli/config.go +++ b/cmd/rpcdaemon/cli/config.go @@ -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 } diff --git a/cmd/rpcdaemon/cli/httpcfg/http_cfg.go b/cmd/rpcdaemon/cli/httpcfg/http_cfg.go index 46c094d32..2ca515a9f 100644 --- a/cmd/rpcdaemon/cli/httpcfg/http_cfg.go +++ b/cmd/rpcdaemon/cli/httpcfg/http_cfg.go @@ -38,4 +38,5 @@ type HttpCfg struct { GRPCPort int GRPCHealthCheckEnabled bool StarknetGRPCAddress string + JWTSecretPath string // Engine API Authentication } diff --git a/go.mod b/go.mod index 71162f5c1..904da93f9 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index ddfd90186..d31381a20 100644 --- a/go.sum +++ b/go.sum @@ -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=