Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Engine Authentication [JWT] #3531

Merged
merged 16 commits into from
Feb 28, 2022
79 changes: 75 additions & 4 deletions cmd/rpcdaemon/cli/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ package cli

import (
"context"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"io"
"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"
Expand All @@ -28,6 +32,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"
Expand All @@ -50,6 +55,9 @@ var rootCmd = &cobra.Command{
Short: "rpcdaemon is JSON RPC server that connects to Erigon node for remote DB access",
}

const JwtTokenExpiry = 1000 * time.Hour
const JwtDefaultFile = "jwt.hex"

func RootCommand() (*cobra.Command, *httpcfg.HttpCfg) {
utils.CobraFlags(rootCmd, append(debug.Flags, utils.MetricFlags...))

Expand Down Expand Up @@ -84,6 +92,8 @@ 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, "engine.authsecret", "", "Token to ensure safe connection beetwen CL and EL")
rootCmd.PersistentFlags().BoolVar(&cfg.EngineAuthentication, "engine.auth", false, "Enable Authentication accordingly to Klin Specs")

if err := rootCmd.MarkPersistentFlagFilename("rpc.accessList", "json"); err != nil {
panic(err)
Expand Down Expand Up @@ -426,7 +436,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 {
Expand Down Expand Up @@ -499,7 +512,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, isEngine bool) (http.Handler, error) {
var jwtVerificationKey []byte
var err error

if isEngine && cfg.EngineAuthentication {
// 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
}

_, err = f.Write(jwtVerificationKey)
if err != nil {
return nil, err
}
f.Close()
} 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) {
Expand All @@ -509,10 +552,35 @@ func createHandler(cfg httpcfg.HttpCfg, apiList []rpc.API, httpHandler http.Hand
wsHandler.ServeHTTP(w, r)
return
}

if isEngine && cfg.EngineAuthentication {
// 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) {
Expand All @@ -531,7 +599,10 @@ func createEngineListener(cfg httpcfg.HttpCfg, engineApi []rpc.API, engineFlag [
}

engineHttpHandler := node.NewHTTPHandlerStack(enginesrv, cfg.HttpCORSDomain, cfg.HttpVirtualHost, cfg.HttpCompression)
engineApiHandler := createHandler(cfg, engineApi, engineHttpHandler, nil)
engineApiHandler, err := createHandler(cfg, engineApi, engineHttpHandler, nil, true)
if err != nil {
return nil, nil, "", err
}

engineListener, _, err := node.StartHTTPEndpoint(engineHttpEndpoint, rpc.DefaultHTTPTimeouts, engineApiHandler)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions cmd/rpcdaemon/cli/httpcfg/http_cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,6 @@ type HttpCfg struct {
GRPCPort int
GRPCHealthCheckEnabled bool
StarknetGRPCAddress string
JWTSecretPath string // Engine API Authentication
EngineAuthentication bool
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,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=
Expand Down