mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 15:37:56 -05:00
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:
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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{}
|
||||
|
||||
@@ -108,6 +108,7 @@ var appHelpFlagGroups = []flagGroup{
|
||||
flags.GPRCGatewayCorsDomain,
|
||||
flags.HTTPWeb3ProviderFlag,
|
||||
flags.ExecutionProviderFlag,
|
||||
flags.ExecutionJWTSecretFlag,
|
||||
flags.FallbackWeb3ProviderFlag,
|
||||
flags.SetGCPercent,
|
||||
flags.HeadSync,
|
||||
|
||||
Reference in New Issue
Block a user