Engine API Client Authentication for the Merge via HTTP (#10236)

* round tripper with claims

* auth

* edit auth

* test out jwt

* passing

* jwt flag

* comment

* passing

* commentary

* fix up jwt parsing

* gaz

* update jwt libs

* tidy

* gaz

* lint

* tidy up

* comment too long

Co-authored-by: james-prysm <90280386+james-prysm@users.noreply.github.com>
This commit is contained in:
Raul Jordan
2022-02-25 19:08:43 +00:00
committed by GitHub
parent 6163e091a7
commit f3a7f399c0
20 changed files with 314 additions and 37 deletions

View File

@@ -23,6 +23,18 @@ var (
Usage: "An http endpoint for an Ethereum execution node",
Value: "",
}
// ExecutionJWTSecretFlag provides a path to a file containing a hex-encoded string representing a 32 byte secret
// used to authenticate with an execution node via HTTP. This is required if using an HTTP connection, otherwise all requests
// to execution nodes for consensus-related calls will fail. This is not required if using an IPC connection.
ExecutionJWTSecretFlag = &cli.StringFlag{
Name: "jwt-secret",
Usage: "REQUIRED if connecting to an execution node via HTTP. Provides a path to a file containing " +
"a hex-encoded string representing a 32 byte secret used for authentication with an execution node via " +
"HTTP. If this is not set, all requests to execution nodes via HTTP for consensus-related calls will " +
"fail, which will prevent your validators from performing their duties. " +
"This is not required if using an IPC connection.",
Value: "",
}
// FallbackWeb3ProviderFlag provides a fallback endpoint to an ETH 1.0 RPC.
FallbackWeb3ProviderFlag = &cli.StringSliceFlag{
Name: "fallback-web3provider",

View File

@@ -34,6 +34,7 @@ var appFlags = []cli.Flag{
flags.DepositContractFlag,
flags.HTTPWeb3ProviderFlag,
flags.ExecutionProviderFlag,
flags.ExecutionJWTSecretFlag,
flags.FallbackWeb3ProviderFlag,
flags.RPCHost,
flags.RPCPort,
@@ -232,11 +233,11 @@ func startNode(ctx *cli.Context) error {
blockchainFlagOpts, err := blockchaincmd.FlagOptions(ctx)
if err != nil {
return nil
return err
}
powchainFlagOpts, err := powchaincmd.FlagOptions(ctx)
if err != nil {
return nil
return err
}
opts := []node.Option{
node.WithBlockchainFlagOptions(blockchainFlagOpts),

View File

@@ -11,6 +11,8 @@ go_library(
deps = [
"//beacon-chain/powchain:go_default_library",
"//cmd/beacon-chain/flags:go_default_library",
"//io/file:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",
],
@@ -22,6 +24,8 @@ go_test(
embed = [":go_default_library"],
deps = [
"//cmd/beacon-chain/flags:go_default_library",
"//encoding/bytesutil:go_default_library",
"//io/file:go_default_library",
"//testing/assert:go_default_library",
"//testing/require:go_default_library",
"@com_github_sirupsen_logrus//hooks/test:go_default_library",

View File

@@ -1,8 +1,14 @@
package powchaincmd
import (
"encoding/hex"
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/beacon-chain/powchain"
"github.com/prysmaticlabs/prysm/cmd/beacon-chain/flags"
"github.com/prysmaticlabs/prysm/io/file"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
@@ -13,6 +19,10 @@ var log = logrus.WithField("prefix", "cmd-powchain")
func FlagOptions(c *cli.Context) ([]powchain.Option, error) {
endpoints := parsePowchainEndpoints(c)
executionEndpoint := parseExecutionEndpoint(c)
jwtSecret, err := parseJWTSecretFromFile(c)
if err != nil {
return nil, errors.Wrap(err, "could not read JWT secret file for authenticating execution API")
}
opts := []powchain.Option{
powchain.WithHttpEndpoints(endpoints),
powchain.WithEth1HeaderRequestLimit(c.Uint64(flags.Eth1HeaderReqLimit.Name)),
@@ -20,9 +30,43 @@ func FlagOptions(c *cli.Context) ([]powchain.Option, error) {
if executionEndpoint != "" {
opts = append(opts, powchain.WithExecutionEndpoint(executionEndpoint))
}
if len(jwtSecret) > 0 {
opts = append(opts, powchain.WithExecutionClientJWTSecret(jwtSecret))
}
return opts, nil
}
// Parses a JWT secret from a file path. This secret is required when connecting to execution nodes
// over HTTP, and must be the same one used in Prysm and the execution node server Prysm is connecting to.
// The engine API specification here https://github.com/ethereum/execution-apis/blob/main/src/engine/authentication.md
// Explains how we should validate this secret and the format of the file a user can specify.
//
// The secret must be stored as a hex-encoded string within a file in the filesystem.
// If the --jwt-secret flag is provided to Prysm, but the file cannot be read, or does not contain a hex-encoded
// key of at least 256 bits, the client should treat this as an error and abort the startup.
func parseJWTSecretFromFile(c *cli.Context) ([]byte, error) {
jwtSecretFile := c.String(flags.ExecutionJWTSecretFlag.Name)
if jwtSecretFile == "" {
return nil, nil
}
enc, err := file.ReadFileAsBytes(jwtSecretFile)
if err != nil {
return nil, err
}
strData := strings.TrimSpace(string(enc))
if len(strData) == 0 {
return nil, fmt.Errorf("provided JWT secret in file %s cannot be empty", jwtSecretFile)
}
secret, err := hex.DecodeString(strings.TrimPrefix(strData, "0x"))
if err != nil {
return nil, err
}
if len(secret) < 32 {
return nil, errors.New("provided JWT secret should be a hex string of at least 32 bytes")
}
return secret, nil
}
func parsePowchainEndpoints(c *cli.Context) []string {
if c.String(flags.HTTPWeb3ProviderFlag.Name) == "" && len(c.StringSlice(flags.FallbackWeb3ProviderFlag.Name)) == 0 {
log.Error(

View File

@@ -2,9 +2,14 @@ package powchaincmd
import (
"flag"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/prysmaticlabs/prysm/cmd/beacon-chain/flags"
"github.com/prysmaticlabs/prysm/encoding/bytesutil"
"github.com/prysmaticlabs/prysm/io/file"
"github.com/prysmaticlabs/prysm/testing/assert"
"github.com/prysmaticlabs/prysm/testing/require"
logTest "github.com/sirupsen/logrus/hooks/test"
@@ -27,6 +32,92 @@ func TestPowchainCmd(t *testing.T) {
assert.DeepEqual(t, []string{"primary", "fallback1", "fallback2"}, endpoints)
}
func Test_parseJWTSecretFromFile(t *testing.T) {
t.Run("no flag value specified leads to nil secret", func(t *testing.T) {
app := cli.App{}
set := flag.NewFlagSet("test", 0)
set.String(flags.ExecutionJWTSecretFlag.Name, "", "")
ctx := cli.NewContext(&app, set, nil)
secret, err := parseJWTSecretFromFile(ctx)
require.NoError(t, err)
require.Equal(t, true, secret == nil)
})
t.Run("flag specified but no file found", func(t *testing.T) {
app := cli.App{}
set := flag.NewFlagSet("test", 0)
set.String(flags.ExecutionJWTSecretFlag.Name, "/tmp/askdjkajsd", "")
ctx := cli.NewContext(&app, set, nil)
_, err := parseJWTSecretFromFile(ctx)
require.ErrorContains(t, "no such file", err)
})
t.Run("empty string in file", func(t *testing.T) {
app := cli.App{}
set := flag.NewFlagSet("test", 0)
fullPath := filepath.Join(os.TempDir(), "foohex")
require.NoError(t, file.WriteFile(fullPath, []byte{}))
t.Cleanup(func() {
if err := os.RemoveAll(fullPath); err != nil {
t.Fatalf("Could not delete temp dir: %v", err)
}
})
set.String(flags.ExecutionJWTSecretFlag.Name, fullPath, "")
ctx := cli.NewContext(&app, set, nil)
_, err := parseJWTSecretFromFile(ctx)
require.ErrorContains(t, "cannot be empty", err)
})
t.Run("less than 32 bytes", func(t *testing.T) {
app := cli.App{}
set := flag.NewFlagSet("test", 0)
fullPath := filepath.Join(os.TempDir(), "foohex")
secret := bytesutil.PadTo([]byte("foo"), 31)
hexData := fmt.Sprintf("%#x", secret)
require.NoError(t, file.WriteFile(fullPath, []byte(hexData)))
t.Cleanup(func() {
if err := os.RemoveAll(fullPath); err != nil {
t.Fatalf("Could not delete temp dir: %v", err)
}
})
set.String(flags.ExecutionJWTSecretFlag.Name, fullPath, "")
ctx := cli.NewContext(&app, set, nil)
_, err := parseJWTSecretFromFile(ctx)
require.ErrorContains(t, "should be a hex string of at least 32 bytes", err)
})
t.Run("bad data", func(t *testing.T) {
app := cli.App{}
set := flag.NewFlagSet("test", 0)
fullPath := filepath.Join(os.TempDir(), "foohex")
secret := []byte("foo")
require.NoError(t, file.WriteFile(fullPath, secret))
t.Cleanup(func() {
if err := os.RemoveAll(fullPath); err != nil {
t.Fatalf("Could not delete temp dir: %v", err)
}
})
set.String(flags.ExecutionJWTSecretFlag.Name, fullPath, "")
ctx := cli.NewContext(&app, set, nil)
_, err := parseJWTSecretFromFile(ctx)
require.ErrorContains(t, "invalid byte", err)
})
t.Run("correct format", func(t *testing.T) {
app := cli.App{}
set := flag.NewFlagSet("test", 0)
fullPath := filepath.Join(os.TempDir(), "foohex")
secret := bytesutil.ToBytes32([]byte("foo"))
secretHex := fmt.Sprintf("%#x", secret)
require.NoError(t, file.WriteFile(fullPath, []byte(secretHex)))
t.Cleanup(func() {
if err := os.RemoveAll(fullPath); err != nil {
t.Fatalf("Could not delete temp dir: %v", err)
}
})
set.String(flags.ExecutionJWTSecretFlag.Name, fullPath, "")
ctx := cli.NewContext(&app, set, nil)
got, err := parseJWTSecretFromFile(ctx)
require.NoError(t, err)
require.DeepEqual(t, secret[:], got)
})
}
func TestPowchainPreregistration_EmptyWeb3Provider(t *testing.T) {
hook := logTest.NewGlobal()
app := cli.App{}

View File

@@ -108,6 +108,7 @@ var appHelpFlagGroups = []flagGroup{
flags.GPRCGatewayCorsDomain,
flags.HTTPWeb3ProviderFlag,
flags.ExecutionProviderFlag,
flags.ExecutionJWTSecretFlag,
flags.FallbackWeb3ProviderFlag,
flags.SetGCPercent,
flags.HeadSync,