From 198197a3b4bd8935d020011db5020cdffb36e2c7 Mon Sep 17 00:00:00 2001 From: harkamal Date: Sun, 20 Feb 2022 01:33:03 +0530 Subject: [PATCH 01/10] Add jwt based token auth to the engine api calls --- .../src/options/beaconNodeOptions/execution.ts | 8 ++++++++ packages/lodestar/package.json | 2 ++ .../src/eth1/provider/jsonRpcHttpClient.ts | 16 +++++++++++++++- packages/lodestar/src/executionEngine/http.ts | 3 ++- yarn.lock | 10 ++++++++++ 5 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/options/beaconNodeOptions/execution.ts b/packages/cli/src/options/beaconNodeOptions/execution.ts index c0b9cb0d69a0..00bb2f161d42 100644 --- a/packages/cli/src/options/beaconNodeOptions/execution.ts +++ b/packages/cli/src/options/beaconNodeOptions/execution.ts @@ -4,12 +4,14 @@ import {ICliCommandOptions} from "../../util"; export type ExecutionEngineArgs = { "execution.urls": string[]; "execution.timeout": number; + "jwt-secret"?: string; }; export function parseArgs(args: ExecutionEngineArgs): IBeaconNodeOptions["executionEngine"] { return { urls: args["execution.urls"], timeout: args["execution.timeout"], + jwtSecret: args["jwt-secret"], }; } @@ -29,4 +31,10 @@ export const options: ICliCommandOptions = { defaultOptions.executionEngine.mode === "http" ? String(defaultOptions.executionEngine.timeout) : "", group: "execution", }, + + "jwt-secret": { + description: "Shared jwt secret which EL will use to authenticate engine api calls", + type: "string", + group: "execution", + }, }; diff --git a/packages/lodestar/package.json b/packages/lodestar/package.json index c49257daad2a..d082f54190a9 100644 --- a/packages/lodestar/package.json +++ b/packages/lodestar/package.json @@ -91,6 +91,7 @@ "interface-datastore": "^5.1.2", "it-all": "^1.0.2", "it-pipe": "^1.1.0", + "jwt-simple": "^0.5.6", "libp2p": "^0.32.4", "libp2p-bootstrap": "^0.13.0", "libp2p-gossipsub": "^0.11.1", @@ -114,6 +115,7 @@ "@types/eventsource": "^1.1.5", "@types/http-terminator": "^2.0.1", "@types/it-all": "^1.0.0", + "@types/jwt-simple": "^0.5.33", "@types/leveldown": "^4.0.2", "@types/prometheus-gc-stats": "^0.6.1", "@types/supertest": "^2.0.8", diff --git a/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts b/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts index db48677b1cea..787ffda8aa0a 100644 --- a/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts +++ b/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts @@ -2,6 +2,9 @@ // Note: isomorphic-fetch is not well mantained and does not support abort signals import fetch from "cross-fetch"; import {AbortController, AbortSignal} from "@chainsafe/abort-controller"; +import {encode, TAlgorithm} from "jwt-simple"; +const algorithm: TAlgorithm = "HS256"; + import {ErrorAborted, TimeoutError} from "@chainsafe/lodestar-utils"; import {IJson, IRpcPayload, ReqOpts} from "../interface"; @@ -39,6 +42,7 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { timeout?: number; /** If returns true, do not fallback to other urls and throw early */ shouldNotFallback?: (error: Error) => boolean; + jwtSecret?: string; } ) { // Sanity check for all URLs to be properly defined. Otherwise it will error in loop on fetch @@ -121,10 +125,20 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { } try { + let headers; + if (this.opts?.jwtSecret) { + /** ELs have a tight +-5 second freshness check on token's iat i.e. issued at */ + const token = encode({iat: Math.floor(new Date().getTime() / 1000)}, this.opts?.jwtSecret, algorithm); + // eslint-disable-next-line @typescript-eslint/naming-convention + headers = {"Content-Type": "application/json", Authorization: `Bearer ${token}`}; + } else { + headers = {"Content-Type": "application/json"}; + } + const res = await fetch(url, { method: "post", body: JSON.stringify(json), - headers: {"Content-Type": "application/json"}, + headers, signal: controller.signal, }).finally(() => { clearTimeout(timeout); diff --git a/packages/lodestar/src/executionEngine/http.ts b/packages/lodestar/src/executionEngine/http.ts index f6b47cd9b829..4b52e06434e7 100644 --- a/packages/lodestar/src/executionEngine/http.ts +++ b/packages/lodestar/src/executionEngine/http.ts @@ -26,6 +26,7 @@ import { export type ExecutionEngineHttpOpts = { urls: string[]; timeout?: number; + jwtSecret?: string; }; export const defaultExecutionEngineHttpOpts: ExecutionEngineHttpOpts = { @@ -50,7 +51,7 @@ export class ExecutionEngineHttp implements IExecutionEngine { rpc ?? new JsonRpcHttpClient(opts.urls, { signal, - timeout: opts.timeout, + ...opts, }); } diff --git a/yarn.lock b/yarn.lock index a239582afae6..1f05577e8972 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2363,6 +2363,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= +"@types/jwt-simple@^0.5.33": + version "0.5.33" + resolved "https://registry.yarnpkg.com/@types/jwt-simple/-/jwt-simple-0.5.33.tgz#fb839cabe81437954f7d0cd01760ad8096ec526e" + integrity sha1-+4Ocq+gUN5VPfQzQF2CtgJbsUm4= + "@types/keyv@*": version "3.1.1" resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.1.tgz#e45a45324fca9dab716ab1230ee249c9fb52cfa7" @@ -6779,6 +6784,11 @@ just-extend@^4.0.2: resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== +jwt-simple@^0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/jwt-simple/-/jwt-simple-0.5.6.tgz#3357adec55b26547114157be66748995b75b333a" + integrity sha512-40aUybvhH9t2h71ncA1/1SbtTNCVZHgsTsTgqPUxGWDmUDrXyDf2wMNQKEbdBjbf4AI+fQhbECNTV6lWxQKUzg== + keccak@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/keccak/-/keccak-3.0.1.tgz#ae30a0e94dbe43414f741375cff6d64c8bea0bff" From e751f301b8688c6d66da536731480b440b67699d Mon Sep 17 00:00:00 2001 From: harkamal Date: Sun, 20 Feb 2022 18:52:16 +0530 Subject: [PATCH 02/10] read and validate from a file with hex encoded 256 bit secret key --- .../cli/src/options/beaconNodeOptions/execution.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/options/beaconNodeOptions/execution.ts b/packages/cli/src/options/beaconNodeOptions/execution.ts index 00bb2f161d42..2785b1b36e3e 100644 --- a/packages/cli/src/options/beaconNodeOptions/execution.ts +++ b/packages/cli/src/options/beaconNodeOptions/execution.ts @@ -1,3 +1,4 @@ +import fs from "node:fs"; import {defaultOptions, IBeaconNodeOptions} from "@chainsafe/lodestar"; import {ICliCommandOptions} from "../../util"; @@ -8,10 +9,19 @@ export type ExecutionEngineArgs = { }; export function parseArgs(args: ExecutionEngineArgs): IBeaconNodeOptions["executionEngine"] { + let jwtSecret; + if (args["jwt-secret"]) { + const jwtSecretContents = fs.readFileSync(args["jwt-secret"], "utf-8").trim(); + const hexPattern = new RegExp(/^(0x|0X)?(?[a-fA-F0-9]+)$/, "g"); + jwtSecret = hexPattern.exec(jwtSecretContents)?.groups?.jwtSecret; + if (!jwtSecret || jwtSecret.length != 64) { + throw Error("Need a valid 256 bit hex encoded secret"); + } + } return { urls: args["execution.urls"], timeout: args["execution.timeout"], - jwtSecret: args["jwt-secret"], + jwtSecret, }; } From bb9ff825568dac261aea85c501d7ab4a754c4f53 Mon Sep 17 00:00:00 2001 From: harkamal Date: Mon, 21 Feb 2022 12:45:11 +0530 Subject: [PATCH 03/10] convert hex to bytes secret and interop with geth's jwt --- .github/workflows/test-sim-merge.yml | 5 +++-- kiln/geth/common-setup.sh | 9 ++++++++- kiln/geth/post-merge.sh | 4 +++- kiln/geth/pre-merge.sh | 4 +++- kiln/gethdocker/Dockerfile | 4 ++-- kiln/gethdocker/README.md | 6 +++--- kiln/gethdocker/common-setup.sh | 3 +++ kiln/gethdocker/post-merge.sh | 2 +- kiln/gethdocker/pre-merge.sh | 2 +- .../src/options/beaconNodeOptions/execution.ts | 8 ++++---- .../src/eth1/provider/jsonRpcHttpClient.ts | 16 +++++++++++++--- packages/lodestar/src/executionEngine/http.ts | 2 +- packages/lodestar/test/sim/merge-interop.test.ts | 8 +++++--- 13 files changed, 50 insertions(+), 23 deletions(-) diff --git a/.github/workflows/test-sim-merge.yml b/.github/workflows/test-sim-merge.yml index 077a24a310c0..8dff774d9dcf 100644 --- a/.github/workflows/test-sim-merge.yml +++ b/.github/workflows/test-sim-merge.yml @@ -3,7 +3,7 @@ name: Sim merge tests on: [pull_request, push] env: - GETH_COMMIT: 0569213dc4032da83abed44fab7f65794a526f21 + GETH_COMMIT: 815a414312db9a922c5a34ac034fb7aa8861f2e7 NETHERMIND_COMMIT: 78ae2353be5d05a285d6aaa1826910489d381a3e jobs: @@ -38,7 +38,7 @@ jobs: # Install Geth merge interop - uses: actions/setup-go@v2 - name: Clone Geth merge interop branch - run: git clone -b merge-kiln https://github.com/g11tech/go-ethereum.git && cd go-ethereum && git reset --hard $GETH_COMMIT && git submodule update --init --recursive + run: git clone -b merge-kiln-jwt https://github.com/g11tech/go-ethereum.git && cd go-ethereum && git reset --hard $GETH_COMMIT && git submodule update --init --recursive - name: Build Geth run: cd go-ethereum && make @@ -49,6 +49,7 @@ jobs: EL_BINARY_DIR: ../../go-ethereum/build/bin EL_SCRIPT_DIR: kiln/geth EL_PORT: 8545 + ENGINE_PORT: 8551 TX_SCENARIOS: simple # Install Nethermind merge interop diff --git a/kiln/geth/common-setup.sh b/kiln/geth/common-setup.sh index 029f34be21e7..c979928ce6b7 100755 --- a/kiln/geth/common-setup.sh +++ b/kiln/geth/common-setup.sh @@ -2,13 +2,20 @@ echo $TTD echo $DATA_DIR -echo $scriptDir echo $EL_BINARY_DIR +echo $JWT_SECRET_HEX + +echo $scriptDir +echo $currentDir + env TTD=$TTD envsubst < $scriptDir/genesisPre.tmpl > $DATA_DIR/genesis.json echo "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" > $DATA_DIR/sk.json echo "12345678" > $DATA_DIR/password.txt pubKey="0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" +# echo a hex encoded 256 bit secret into a file +echo $JWT_SECRET_HEX> $DATA_DIR/jwtsecret + $EL_BINARY_DIR/geth --datadir $DATA_DIR init $DATA_DIR/genesis.json $EL_BINARY_DIR/geth --datadir $DATA_DIR account import $DATA_DIR/sk.json --password $DATA_DIR/password.txt diff --git a/kiln/geth/post-merge.sh b/kiln/geth/post-merge.sh index 3a402321e1a0..2c87450c69b0 100755 --- a/kiln/geth/post-merge.sh +++ b/kiln/geth/post-merge.sh @@ -1,6 +1,8 @@ #!/bin/bash -x scriptDir=$(dirname $0) +currentDir=$(pwd) + . $scriptDir/common-setup.sh -$EL_BINARY_DIR/geth --http --ws -http.api "engine,net,eth" --datadir $DATA_DIR --allow-insecure-unlock --unlock $pubKey --password $DATA_DIR/password.txt +$EL_BINARY_DIR/geth --http --ws -http.api "engine,net,eth" --datadir $DATA_DIR --allow-insecure-unlock --unlock $pubKey --password $DATA_DIR/password.txt --jwt-secret $JWT_SECRET_HEX diff --git a/kiln/geth/pre-merge.sh b/kiln/geth/pre-merge.sh index 492fab66497c..b4d80ef03591 100755 --- a/kiln/geth/pre-merge.sh +++ b/kiln/geth/pre-merge.sh @@ -1,6 +1,8 @@ #!/bin/bash -x scriptDir=$(dirname $0) +currentDir=$(pwd) + . $scriptDir/common-setup.sh -$EL_BINARY_DIR/geth --http --ws -http.api "engine,net,eth,miner" --datadir $DATA_DIR --allow-insecure-unlock --unlock $pubKey --password $DATA_DIR/password.txt --nodiscover --mine +$EL_BINARY_DIR/geth --http --ws -http.api "engine,net,eth,miner" --datadir $DATA_DIR --allow-insecure-unlock --unlock $pubKey --password $DATA_DIR/password.txt --nodiscover --mine --jwt-secret $JWT_SECRET_HEX diff --git a/kiln/gethdocker/Dockerfile b/kiln/gethdocker/Dockerfile index 8ae825f087c6..eb87fca8814c 100644 --- a/kiln/gethdocker/Dockerfile +++ b/kiln/gethdocker/Dockerfile @@ -1,9 +1,9 @@ # Build Geth in a stock Go builder container FROM golang:1.17-alpine as builder -RUN apk add --no-cache gcc musl-dev linux-headers git +RUN apk add --no-cache gcc musl-dev linux-headers git bash -RUN git clone --depth 1 -b merge-kiln https://github.com/MariusVanDerWijden/go-ethereum.git /go-ethereum +RUN git clone --depth 1 -b merge-kiln-jwt https://github.com/MariusVanDerWijden/go-ethereum.git /go-ethereum RUN cd /go-ethereum && go run build/ci.go install ./cmd/geth FROM alpine:latest diff --git a/kiln/gethdocker/README.md b/kiln/gethdocker/README.md index 117e9007b730..92643769bdc9 100644 --- a/kiln/gethdocker/README.md +++ b/kiln/gethdocker/README.md @@ -3,13 +3,13 @@ ###### Build geth docker image ```bash -cd kintsugi/gethdocker -docker build . --tag geth:kintsugi +cd kiln/gethdocker +docker build . --tag geth:kiln ``` ###### Run test scripts ```bash cd packages/lodestar -EL_BINARY_DIR=geth:kintsugi EL_SCRIPT_DIR=kiln/gethdocker EL_PORT=8545 TX_SCENARIOS=simple yarn mocha test/sim/merge-interop.test.ts +EL_BINARY_DIR=geth:kiln EL_SCRIPT_DIR=kiln/gethdocker EL_PORT=8545 ENGINE_PORT=8551 TX_SCENARIOS=simple yarn mocha test/sim/merge-interop.test.ts ``` diff --git a/kiln/gethdocker/common-setup.sh b/kiln/gethdocker/common-setup.sh index f0f5bb922130..159faa05962b 100755 --- a/kiln/gethdocker/common-setup.sh +++ b/kiln/gethdocker/common-setup.sh @@ -3,6 +3,7 @@ echo $TTD echo $DATA_DIR echo $EL_BINARY_DIR +echo $JWT_SECRET_HEX echo $scriptDir echo $currentDir @@ -13,6 +14,8 @@ echo "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8" > $DATA_ echo "12345678" > $DATA_DIR/password.txt pubKey="0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" +# echo a hex encoded 256 bit secret into a file +echo $JWT_SECRET_HEX> $DATA_DIR/jwtsecret docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR geth --datadir /data init /data/genesis.json docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR geth --datadir /data account import /data/sk.json --password /data/password.txt diff --git a/kiln/gethdocker/post-merge.sh b/kiln/gethdocker/post-merge.sh index cd16cc8f59e3..d615759361ce 100755 --- a/kiln/gethdocker/post-merge.sh +++ b/kiln/gethdocker/post-merge.sh @@ -5,4 +5,4 @@ currentDir=$(pwd) . $scriptDir/common-setup.sh -docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) --network host -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR geth --http --ws -http.api "engine,net,eth" --allow-insecure-unlock --unlock $pubKey --password /data/password.txt --datadir /data +docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) --network host -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR geth --http --ws -http.api "engine,net,eth" --allow-insecure-unlock --unlock $pubKey --password /data/password.txt --datadir /data --jwt-secret $JWT_SECRET_HEX diff --git a/kiln/gethdocker/pre-merge.sh b/kiln/gethdocker/pre-merge.sh index 2818b9098186..dc498388ba2e 100755 --- a/kiln/gethdocker/pre-merge.sh +++ b/kiln/gethdocker/pre-merge.sh @@ -6,4 +6,4 @@ currentDir=$(pwd) . $scriptDir/common-setup.sh # EL_BINARY_DIR refers to the local docker image build from kintsugi/gethdocker folder -docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) --network host -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR geth --http --ws -http.api "engine,net,eth,miner" --allow-insecure-unlock --unlock $pubKey --password /data/password.txt --datadir /data --nodiscover --mine +docker run --rm -u $(id -u ${USER}):$(id -g ${USER}) --network host -v $currentDir/$DATA_DIR:/data $EL_BINARY_DIR geth --http --ws -http.api "engine,net,eth,miner" --allow-insecure-unlock --unlock $pubKey --password /data/password.txt --datadir /data --nodiscover --mine --jwt-secret $JWT_SECRET_HEX diff --git a/packages/cli/src/options/beaconNodeOptions/execution.ts b/packages/cli/src/options/beaconNodeOptions/execution.ts index 2785b1b36e3e..1867b3ec106f 100644 --- a/packages/cli/src/options/beaconNodeOptions/execution.ts +++ b/packages/cli/src/options/beaconNodeOptions/execution.ts @@ -9,19 +9,19 @@ export type ExecutionEngineArgs = { }; export function parseArgs(args: ExecutionEngineArgs): IBeaconNodeOptions["executionEngine"] { - let jwtSecret; + let jwtSecretHex; if (args["jwt-secret"]) { const jwtSecretContents = fs.readFileSync(args["jwt-secret"], "utf-8").trim(); const hexPattern = new RegExp(/^(0x|0X)?(?[a-fA-F0-9]+)$/, "g"); - jwtSecret = hexPattern.exec(jwtSecretContents)?.groups?.jwtSecret; - if (!jwtSecret || jwtSecret.length != 64) { + jwtSecretHex = hexPattern.exec(jwtSecretContents)?.groups?.jwtSecret; + if (!jwtSecretHex || jwtSecretHex.length != 64) { throw Error("Need a valid 256 bit hex encoded secret"); } } return { urls: args["execution.urls"], timeout: args["execution.timeout"], - jwtSecret, + jwtSecretHex, }; } diff --git a/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts b/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts index 787ffda8aa0a..4c5ec4de0ede 100644 --- a/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts +++ b/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts @@ -34,6 +34,7 @@ export interface IJsonRpcHttpClient { export class JsonRpcHttpClient implements IJsonRpcHttpClient { private id = 1; + private jwtSecret?: Uint8Array; constructor( private readonly urls: string[], @@ -42,7 +43,7 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { timeout?: number; /** If returns true, do not fallback to other urls and throw early */ shouldNotFallback?: (error: Error) => boolean; - jwtSecret?: string; + jwtSecretHex?: string; } ) { // Sanity check for all URLs to be properly defined. Otherwise it will error in loop on fetch @@ -54,6 +55,9 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { throw Error(`JsonRpcHttpClient.urls[${i}] is empty or undefined: ${url}`); } } + if (this.opts?.jwtSecretHex) { + this.jwtSecret = Buffer.from(this.opts.jwtSecretHex, "hex"); + } } /** @@ -126,9 +130,15 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { try { let headers; - if (this.opts?.jwtSecret) { + if (this.jwtSecret) { /** ELs have a tight +-5 second freshness check on token's iat i.e. issued at */ - const token = encode({iat: Math.floor(new Date().getTime() / 1000)}, this.opts?.jwtSecret, algorithm); + const token = encode( + {iat: Math.floor(new Date().getTime() / 1000)}, + // Note: This type casting is required as even though jwt-simple accepts a buffer as a + // secret types definitions exposed by @types/jwt-simple only takes a string + (this.jwtSecret as unknown) as string, + algorithm + ); // eslint-disable-next-line @typescript-eslint/naming-convention headers = {"Content-Type": "application/json", Authorization: `Bearer ${token}`}; } else { diff --git a/packages/lodestar/src/executionEngine/http.ts b/packages/lodestar/src/executionEngine/http.ts index 4b52e06434e7..cfd399d17b3f 100644 --- a/packages/lodestar/src/executionEngine/http.ts +++ b/packages/lodestar/src/executionEngine/http.ts @@ -26,7 +26,7 @@ import { export type ExecutionEngineHttpOpts = { urls: string[]; timeout?: number; - jwtSecret?: string; + jwtSecretHex?: string; }; export const defaultExecutionEngineHttpOpts: ExecutionEngineHttpOpts = { diff --git a/packages/lodestar/test/sim/merge-interop.test.ts b/packages/lodestar/test/sim/merge-interop.test.ts index 31f247971fc4..5b8473ff374f 100644 --- a/packages/lodestar/test/sim/merge-interop.test.ts +++ b/packages/lodestar/test/sim/merge-interop.test.ts @@ -41,13 +41,14 @@ import {bytesToData, dataToBytes, quantityToNum} from "../../src/eth1/provider/u // 10 ttd / 2 difficulty per block = 5 blocks * 5 sec = 25 sec const terminalTotalDifficultyPreMerge = 20; const TX_SCENARIOS = process.env.TX_SCENARIOS?.split(",") || []; +const jwtSecretHex = "dc6457099f127cf0bac78de8b297df04951281909db4f58b43def7c7151e765d"; describe("executionEngine / ExecutionEngineHttp", function () { this.timeout("10min"); const dataPath = fs.mkdtempSync("lodestar-test-merge-interop"); const jsonRpcPort = process.env.EL_PORT; - const enginePort = process.env.EL_PORT; + const enginePort = process.env.ENGINE_PORT ?? jsonRpcPort; const jsonRpcUrl = `http://localhost:${jsonRpcPort}`; const engineApiUrl = `http://localhost:${enginePort}`; @@ -73,6 +74,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { ...process.env, TTD, DATA_DIR, + JWT_SECRET_HEX: jwtSecretHex, }, }); @@ -149,7 +151,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { } const controller = new AbortController(); - const executionEngine = new ExecutionEngineHttp({urls: [engineApiUrl]}, controller.signal); + const executionEngine = new ExecutionEngineHttp({urls: [engineApiUrl], jwtSecretHex}, controller.signal); // 1. Prepare a payload @@ -313,7 +315,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { sync: {isSingleNode: true}, network: {discv5: null}, eth1: {enabled: true, providerUrls: [jsonRpcUrl]}, - executionEngine: {urls: [engineApiUrl]}, + executionEngine: {urls: [engineApiUrl], jwtSecretHex}, }, validatorCount: validatorClientCount * validatorsPerClient, logger: loggerNodeA, From eec825b73e74610a09248cd0a637a5f48d891baf Mon Sep 17 00:00:00 2001 From: harkamal Date: Mon, 21 Feb 2022 23:27:33 +0530 Subject: [PATCH 04/10] specify go version --- .github/workflows/test-sim-merge.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test-sim-merge.yml b/.github/workflows/test-sim-merge.yml index 8dff774d9dcf..d7399bd30aad 100644 --- a/.github/workflows/test-sim-merge.yml +++ b/.github/workflows/test-sim-merge.yml @@ -37,6 +37,8 @@ jobs: # Install Geth merge interop - uses: actions/setup-go@v2 + with: + go-version: '1.17' - name: Clone Geth merge interop branch run: git clone -b merge-kiln-jwt https://github.com/g11tech/go-ethereum.git && cd go-ethereum && git reset --hard $GETH_COMMIT && git submodule update --init --recursive - name: Build Geth From 53a3f16eb905ded7b98addf6004974a52934a579 Mon Sep 17 00:00:00 2001 From: harkamal Date: Tue, 22 Feb 2022 23:23:46 +0530 Subject: [PATCH 05/10] tests for cli parsing of secret as well as jwt encoding --- .../options/beaconNodeOptions/execution.ts | 9 ++---- packages/cli/src/util/index.ts | 1 + packages/cli/src/util/jwt.ts | 9 ++++++ .../unit/config/beaconNodeOptions.test.ts | 23 +++++++++++++++ .../src/eth1/provider/jsonRpcHttpClient.ts | 12 ++------ packages/lodestar/src/util/jwt.ts | 28 +++++++++++++++++++ .../lodestar/test/sim/merge-interop.test.ts | 9 ++++-- .../test/unit/executionEngine/jwt.test.ts | 22 +++++++++++++++ 8 files changed, 94 insertions(+), 19 deletions(-) create mode 100644 packages/cli/src/util/jwt.ts create mode 100644 packages/lodestar/src/util/jwt.ts create mode 100644 packages/lodestar/test/unit/executionEngine/jwt.test.ts diff --git a/packages/cli/src/options/beaconNodeOptions/execution.ts b/packages/cli/src/options/beaconNodeOptions/execution.ts index 1867b3ec106f..287919150596 100644 --- a/packages/cli/src/options/beaconNodeOptions/execution.ts +++ b/packages/cli/src/options/beaconNodeOptions/execution.ts @@ -1,6 +1,6 @@ import fs from "node:fs"; import {defaultOptions, IBeaconNodeOptions} from "@chainsafe/lodestar"; -import {ICliCommandOptions} from "../../util"; +import {ICliCommandOptions, extractJwtHexSecret} from "../../util"; export type ExecutionEngineArgs = { "execution.urls": string[]; @@ -11,12 +11,7 @@ export type ExecutionEngineArgs = { export function parseArgs(args: ExecutionEngineArgs): IBeaconNodeOptions["executionEngine"] { let jwtSecretHex; if (args["jwt-secret"]) { - const jwtSecretContents = fs.readFileSync(args["jwt-secret"], "utf-8").trim(); - const hexPattern = new RegExp(/^(0x|0X)?(?[a-fA-F0-9]+)$/, "g"); - jwtSecretHex = hexPattern.exec(jwtSecretContents)?.groups?.jwtSecret; - if (!jwtSecretHex || jwtSecretHex.length != 64) { - throw Error("Need a valid 256 bit hex encoded secret"); - } + jwtSecretHex = extractJwtHexSecret(fs.readFileSync(args["jwt-secret"], "utf-8").trim()); } return { urls: args["execution.urls"], diff --git a/packages/cli/src/util/index.ts b/packages/cli/src/util/index.ts index 82359892ff2a..e7d6d32b62d8 100644 --- a/packages/cli/src/util/index.ts +++ b/packages/cli/src/util/index.ts @@ -16,3 +16,4 @@ export * from "./sleep"; export * from "./stripOffNewlines"; export * from "./types"; export * from "./bls"; +export * from "./jwt"; diff --git a/packages/cli/src/util/jwt.ts b/packages/cli/src/util/jwt.ts new file mode 100644 index 000000000000..00fe079c2bdc --- /dev/null +++ b/packages/cli/src/util/jwt.ts @@ -0,0 +1,9 @@ +export function extractJwtHexSecret(jwtSecretContents: string): string { + const hexPattern = new RegExp(/^(0x|0X)?(?[a-fA-F0-9]+)$/, "g"); + const jwtSecretHexMatch = hexPattern.exec(jwtSecretContents); + const jwtSecretHex = jwtSecretHexMatch?.groups?.jwtSecret; + if (!jwtSecretHex || jwtSecretHex.length != 64) { + throw Error(`Need a valid 256 bit hex encoded secret ${jwtSecretHex} ${jwtSecretContents}`); + } + return jwtSecretHex; +} diff --git a/packages/cli/test/unit/config/beaconNodeOptions.test.ts b/packages/cli/test/unit/config/beaconNodeOptions.test.ts index a673d94e9af5..285171e316ee 100644 --- a/packages/cli/test/unit/config/beaconNodeOptions.test.ts +++ b/packages/cli/test/unit/config/beaconNodeOptions.test.ts @@ -7,6 +7,7 @@ import {BeaconNodeOptions, mergeBeaconNodeOptions} from "../../../src/config"; import {enrsToNetworkConfig, parseBootnodesFile} from "../../../src/networks"; import {bootEnrs as praterBootEnrs} from "../../../src/networks/prater"; import {testFilesDir} from "../../utils"; +import {extractJwtHexSecret} from "../../../src/util"; describe("config / beaconNodeOptions", () => { it("Should return prater options", () => { @@ -210,3 +211,25 @@ describe("mergeBeaconNodeOptions", () => { }); } }); + +describe("parseJwtHexSecret", () => { + const testCases: {raw: string; parsed: string}[] = [ + { + raw: "c58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", + parsed: "c58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", + }, + { + raw: "0xc58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", + parsed: "c58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", + }, + { + raw: "0Xc58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", + parsed: "c58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", + }, + ]; + for (const {raw, parsed} of testCases) { + it(`parse ${raw}`, () => { + expect(parsed).to.be.equal(extractJwtHexSecret(raw)); + }); + } +}); diff --git a/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts b/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts index 4c5ec4de0ede..6adfe2558050 100644 --- a/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts +++ b/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts @@ -2,12 +2,10 @@ // Note: isomorphic-fetch is not well mantained and does not support abort signals import fetch from "cross-fetch"; import {AbortController, AbortSignal} from "@chainsafe/abort-controller"; -import {encode, TAlgorithm} from "jwt-simple"; -const algorithm: TAlgorithm = "HS256"; import {ErrorAborted, TimeoutError} from "@chainsafe/lodestar-utils"; import {IJson, IRpcPayload, ReqOpts} from "../interface"; - +import {encodeJwtToken} from "../../util/jwt"; /** * Limits the amount of response text printed with RPC or parsing errors */ @@ -132,13 +130,7 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { let headers; if (this.jwtSecret) { /** ELs have a tight +-5 second freshness check on token's iat i.e. issued at */ - const token = encode( - {iat: Math.floor(new Date().getTime() / 1000)}, - // Note: This type casting is required as even though jwt-simple accepts a buffer as a - // secret types definitions exposed by @types/jwt-simple only takes a string - (this.jwtSecret as unknown) as string, - algorithm - ); + const token = encodeJwtToken({iat: Math.floor(new Date().getTime() / 1000)}, this.jwtSecret); // eslint-disable-next-line @typescript-eslint/naming-convention headers = {"Content-Type": "application/json", Authorization: `Bearer ${token}`}; } else { diff --git a/packages/lodestar/src/util/jwt.ts b/packages/lodestar/src/util/jwt.ts new file mode 100644 index 000000000000..551009b3c31d --- /dev/null +++ b/packages/lodestar/src/util/jwt.ts @@ -0,0 +1,28 @@ +import {encode, decode, TAlgorithm} from "jwt-simple"; + +/** jwt token has iat which is issued at unix timestamp, and an optional exp for expiry */ +type JwtClaim = {iat: number; exp?: number}; + +export function encodeJwtToken( + claim: Record & JwtClaim, + jwtSecret: Buffer | Uint8Array | string, + algorithm: TAlgorithm = "HS256" +): string { + const token = encode( + claim, + // Note: This type casting is required as even though jwt-simple accepts a buffer as a + // secret types definitions exposed by @types/jwt-simple only takes a string + (jwtSecret as unknown) as string, + algorithm + ); + return token; +} + +export function decodeJwtToken( + token: string, + jwtSecret: Buffer | Uint8Array | string, + algorithm: TAlgorithm = "HS256" +): JwtClaim { + const claim = decode(token, (jwtSecret as never) as string, false, algorithm) as JwtClaim; + return claim; +} diff --git a/packages/lodestar/test/sim/merge-interop.test.ts b/packages/lodestar/test/sim/merge-interop.test.ts index 5b8473ff374f..ba22ba2089c0 100644 --- a/packages/lodestar/test/sim/merge-interop.test.ts +++ b/packages/lodestar/test/sim/merge-interop.test.ts @@ -29,17 +29,22 @@ import {bytesToData, dataToBytes, quantityToNum} from "../../src/eth1/provider/u // EL_SCRIPT_DIR: Directory in packages/lodestar for the EL client, from where to // execute post-merge/pre-merge EL scenario scripts // EL_PORT: EL port on localhost for hosting both engine & json rpc endpoints +// ENGINE_PORT: Specify the port on which an jwt auth protected engine api is being hosted, +// typically by default at 8551 for geth. Some ELs could host it as same port as eth_ apis, +// but just with the engine_ methods protected. In that case this param can be skipped // TX_SCENARIOS: comma seprated transaction scenarios this EL client build supports // Example: // ``` -// $ EL_BINARY_DIR=/home/lion/Code/eth2.0/merge-interop/go-ethereum/build/bin EL_SCRIPT_DIR=kiln/geth EL_PORT=8545 TX_SCENARIOS=simple ../../node_modules/.bin/mocha test/sim/merge.test.ts +// $ EL_BINARY_DIR=/home/lion/Code/eth2.0/merge-interop/go-ethereum/build/bin \ +// EL_SCRIPT_DIR=kiln/geth EL_PORT=8545 ENGINE_PORT=8551 TX_SCENARIOS=simple \ +// ../../node_modules/.bin/mocha test/sim/merge.test.ts // ``` /* eslint-disable no-console, @typescript-eslint/naming-convention, quotes */ // BELLATRIX_EPOCH will happen at 2 sec * 8 slots = 16 sec // 10 ttd / 2 difficulty per block = 5 blocks * 5 sec = 25 sec -const terminalTotalDifficultyPreMerge = 20; +const terminalTotalDifficultyPreMerge = 10; const TX_SCENARIOS = process.env.TX_SCENARIOS?.split(",") || []; const jwtSecretHex = "dc6457099f127cf0bac78de8b297df04951281909db4f58b43def7c7151e765d"; diff --git a/packages/lodestar/test/unit/executionEngine/jwt.test.ts b/packages/lodestar/test/unit/executionEngine/jwt.test.ts new file mode 100644 index 000000000000..316d8997479c --- /dev/null +++ b/packages/lodestar/test/unit/executionEngine/jwt.test.ts @@ -0,0 +1,22 @@ +import {expect} from "chai"; +import {encodeJwtToken, decodeJwtToken} from "../../../src/util/jwt"; +describe("ExecutionEngine / jwt", () => { + it("encode/decode correctly", () => { + const jwtSecret = Buffer.from(Array.from({length: 32}, () => Math.round(Math.random() * 255))); + const claim = {iat: Math.floor(new Date().getTime() / 1000)}; + const token = encodeJwtToken(claim, jwtSecret); + const decoded = decodeJwtToken(token, jwtSecret); + expect(decoded).to.be.deep.equal(claim, "Invalid encoding/decoding of claim"); + }); + + it("encode a claim correctly from a hex key", () => { + const jwtSecretHex = "7e2d709fb01382352aaf830e755d33ca48cb34ba1c21d999e45c1a7a6f88b193"; + const jwtSecret = Buffer.from(jwtSecretHex, "hex"); + const claim = {iat: 1645551452}; + const token = encodeJwtToken(claim, jwtSecret); + expect(token).to.be.equal( + "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDU1NTE0NTJ9.nUDaIyGPgRX76tQ_kDlcIGj4uyFA4lFJGKsD_GHIEzM", + "Invalid encoding of claim" + ); + }); +}); From 8f8197d55d2021f04fa1927cd6bd76c9b4db1c5d Mon Sep 17 00:00:00 2001 From: harkamal Date: Thu, 24 Feb 2022 22:41:18 +0530 Subject: [PATCH 06/10] refac for better code readability --- .../cli/src/options/beaconNodeOptions/execution.ts | 6 +++--- packages/cli/src/util/jwt.ts | 8 ++++---- packages/lodestar/package.json | 2 +- .../src/eth1/provider/jsonRpcHttpClient.ts | 14 +++++--------- .../lodestar/src/{util => eth1/provider}/jwt.ts | 0 packages/lodestar/src/executionEngine/http.ts | 6 ++++-- packages/lodestar/test/sim/merge-interop.test.ts | 8 ++++---- .../unit/{executionEngine => eth1}/jwt.test.ts | 3 ++- yarn.lock | 2 +- 9 files changed, 24 insertions(+), 25 deletions(-) rename packages/lodestar/src/{util => eth1/provider}/jwt.ts (100%) rename packages/lodestar/test/unit/{executionEngine => eth1}/jwt.test.ts (92%) diff --git a/packages/cli/src/options/beaconNodeOptions/execution.ts b/packages/cli/src/options/beaconNodeOptions/execution.ts index 287919150596..1570f1e30e66 100644 --- a/packages/cli/src/options/beaconNodeOptions/execution.ts +++ b/packages/cli/src/options/beaconNodeOptions/execution.ts @@ -9,14 +9,14 @@ export type ExecutionEngineArgs = { }; export function parseArgs(args: ExecutionEngineArgs): IBeaconNodeOptions["executionEngine"] { - let jwtSecretHex; + let jwtSecret; if (args["jwt-secret"]) { - jwtSecretHex = extractJwtHexSecret(fs.readFileSync(args["jwt-secret"], "utf-8").trim()); + jwtSecret = extractJwtHexSecret(fs.readFileSync(args["jwt-secret"], "utf-8").trim()); } return { urls: args["execution.urls"], timeout: args["execution.timeout"], - jwtSecretHex, + jwtSecret, }; } diff --git a/packages/cli/src/util/jwt.ts b/packages/cli/src/util/jwt.ts index 00fe079c2bdc..f94f71f1117f 100644 --- a/packages/cli/src/util/jwt.ts +++ b/packages/cli/src/util/jwt.ts @@ -1,9 +1,9 @@ export function extractJwtHexSecret(jwtSecretContents: string): string { const hexPattern = new RegExp(/^(0x|0X)?(?[a-fA-F0-9]+)$/, "g"); const jwtSecretHexMatch = hexPattern.exec(jwtSecretContents); - const jwtSecretHex = jwtSecretHexMatch?.groups?.jwtSecret; - if (!jwtSecretHex || jwtSecretHex.length != 64) { - throw Error(`Need a valid 256 bit hex encoded secret ${jwtSecretHex} ${jwtSecretContents}`); + const jwtSecret = jwtSecretHexMatch?.groups?.jwtSecret; + if (!jwtSecret || jwtSecret.length != 64) { + throw Error(`Need a valid 256 bit hex encoded secret ${jwtSecret} ${jwtSecretContents}`); } - return jwtSecretHex; + return jwtSecret; } diff --git a/packages/lodestar/package.json b/packages/lodestar/package.json index d082f54190a9..bbb3083c4fe2 100644 --- a/packages/lodestar/package.json +++ b/packages/lodestar/package.json @@ -91,7 +91,7 @@ "interface-datastore": "^5.1.2", "it-all": "^1.0.2", "it-pipe": "^1.1.0", - "jwt-simple": "^0.5.6", + "jwt-simple": "0.5.6", "libp2p": "^0.32.4", "libp2p-bootstrap": "^0.13.0", "libp2p-gossipsub": "^0.11.1", diff --git a/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts b/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts index 6adfe2558050..41928d376611 100644 --- a/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts +++ b/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts @@ -5,7 +5,7 @@ import {AbortController, AbortSignal} from "@chainsafe/abort-controller"; import {ErrorAborted, TimeoutError} from "@chainsafe/lodestar-utils"; import {IJson, IRpcPayload, ReqOpts} from "../interface"; -import {encodeJwtToken} from "../../util/jwt"; +import {encodeJwtToken} from "./jwt"; /** * Limits the amount of response text printed with RPC or parsing errors */ @@ -41,7 +41,7 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { timeout?: number; /** If returns true, do not fallback to other urls and throw early */ shouldNotFallback?: (error: Error) => boolean; - jwtSecretHex?: string; + jwtSecret?: Uint8Array; } ) { // Sanity check for all URLs to be properly defined. Otherwise it will error in loop on fetch @@ -53,9 +53,7 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { throw Error(`JsonRpcHttpClient.urls[${i}] is empty or undefined: ${url}`); } } - if (this.opts?.jwtSecretHex) { - this.jwtSecret = Buffer.from(this.opts.jwtSecretHex, "hex"); - } + this.jwtSecret = opts?.jwtSecret; } /** @@ -127,14 +125,12 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { } try { - let headers; + const headers = {"Content-Type": "application/json"}; if (this.jwtSecret) { /** ELs have a tight +-5 second freshness check on token's iat i.e. issued at */ const token = encodeJwtToken({iat: Math.floor(new Date().getTime() / 1000)}, this.jwtSecret); // eslint-disable-next-line @typescript-eslint/naming-convention - headers = {"Content-Type": "application/json", Authorization: `Bearer ${token}`}; - } else { - headers = {"Content-Type": "application/json"}; + Object.assign(headers, {Authorization: `Bearer ${token}`}); } const res = await fetch(url, { diff --git a/packages/lodestar/src/util/jwt.ts b/packages/lodestar/src/eth1/provider/jwt.ts similarity index 100% rename from packages/lodestar/src/util/jwt.ts rename to packages/lodestar/src/eth1/provider/jwt.ts diff --git a/packages/lodestar/src/executionEngine/http.ts b/packages/lodestar/src/executionEngine/http.ts index cfd399d17b3f..e817b020b3f0 100644 --- a/packages/lodestar/src/executionEngine/http.ts +++ b/packages/lodestar/src/executionEngine/http.ts @@ -26,7 +26,8 @@ import { export type ExecutionEngineHttpOpts = { urls: string[]; timeout?: number; - jwtSecretHex?: string; + /** 256 bit jwt secret in hex format without the leading 0x */ + jwtSecret?: string; }; export const defaultExecutionEngineHttpOpts: ExecutionEngineHttpOpts = { @@ -51,7 +52,8 @@ export class ExecutionEngineHttp implements IExecutionEngine { rpc ?? new JsonRpcHttpClient(opts.urls, { signal, - ...opts, + timeout: opts.timeout, + jwtSecret: opts.jwtSecret ? Buffer.from(opts.jwtSecret, "hex") : undefined, }); } diff --git a/packages/lodestar/test/sim/merge-interop.test.ts b/packages/lodestar/test/sim/merge-interop.test.ts index ba22ba2089c0..ea2f2e08ecf4 100644 --- a/packages/lodestar/test/sim/merge-interop.test.ts +++ b/packages/lodestar/test/sim/merge-interop.test.ts @@ -46,7 +46,7 @@ import {bytesToData, dataToBytes, quantityToNum} from "../../src/eth1/provider/u // 10 ttd / 2 difficulty per block = 5 blocks * 5 sec = 25 sec const terminalTotalDifficultyPreMerge = 10; const TX_SCENARIOS = process.env.TX_SCENARIOS?.split(",") || []; -const jwtSecretHex = "dc6457099f127cf0bac78de8b297df04951281909db4f58b43def7c7151e765d"; +const jwtSecret = "dc6457099f127cf0bac78de8b297df04951281909db4f58b43def7c7151e765d"; describe("executionEngine / ExecutionEngineHttp", function () { this.timeout("10min"); @@ -79,7 +79,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { ...process.env, TTD, DATA_DIR, - JWT_SECRET_HEX: jwtSecretHex, + JWT_SECRET_HEX: `0x${jwtSecret}`, }, }); @@ -156,7 +156,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { } const controller = new AbortController(); - const executionEngine = new ExecutionEngineHttp({urls: [engineApiUrl], jwtSecretHex}, controller.signal); + const executionEngine = new ExecutionEngineHttp({urls: [engineApiUrl], jwtSecret}, controller.signal); // 1. Prepare a payload @@ -320,7 +320,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { sync: {isSingleNode: true}, network: {discv5: null}, eth1: {enabled: true, providerUrls: [jsonRpcUrl]}, - executionEngine: {urls: [engineApiUrl], jwtSecretHex}, + executionEngine: {urls: [engineApiUrl], jwtSecret}, }, validatorCount: validatorClientCount * validatorsPerClient, logger: loggerNodeA, diff --git a/packages/lodestar/test/unit/executionEngine/jwt.test.ts b/packages/lodestar/test/unit/eth1/jwt.test.ts similarity index 92% rename from packages/lodestar/test/unit/executionEngine/jwt.test.ts rename to packages/lodestar/test/unit/eth1/jwt.test.ts index 316d8997479c..0fefbc4ee7c2 100644 --- a/packages/lodestar/test/unit/executionEngine/jwt.test.ts +++ b/packages/lodestar/test/unit/eth1/jwt.test.ts @@ -1,5 +1,6 @@ import {expect} from "chai"; -import {encodeJwtToken, decodeJwtToken} from "../../../src/util/jwt"; +import {encodeJwtToken, decodeJwtToken} from "../../../src/eth1/provider/jwt"; + describe("ExecutionEngine / jwt", () => { it("encode/decode correctly", () => { const jwtSecret = Buffer.from(Array.from({length: 32}, () => Math.round(Math.random() * 255))); diff --git a/yarn.lock b/yarn.lock index 1f05577e8972..4021ef224afd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6784,7 +6784,7 @@ just-extend@^4.0.2: resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.2.1.tgz#ef5e589afb61e5d66b24eca749409a8939a8c744" integrity sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg== -jwt-simple@^0.5.6: +jwt-simple@0.5.6: version "0.5.6" resolved "https://registry.yarnpkg.com/jwt-simple/-/jwt-simple-0.5.6.tgz#3357adec55b26547114157be66748995b75b333a" integrity sha512-40aUybvhH9t2h71ncA1/1SbtTNCVZHgsTsTgqPUxGWDmUDrXyDf2wMNQKEbdBjbf4AI+fQhbECNTV6lWxQKUzg== From d8c2868629cf501563100df6dad0f73cfd0fac24 Mon Sep 17 00:00:00 2001 From: harkamal Date: Thu, 24 Feb 2022 22:45:58 +0530 Subject: [PATCH 07/10] freeze jwt-simple types package as well --- packages/lodestar/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lodestar/package.json b/packages/lodestar/package.json index bbb3083c4fe2..5606345efa69 100644 --- a/packages/lodestar/package.json +++ b/packages/lodestar/package.json @@ -115,7 +115,7 @@ "@types/eventsource": "^1.1.5", "@types/http-terminator": "^2.0.1", "@types/it-all": "^1.0.0", - "@types/jwt-simple": "^0.5.33", + "@types/jwt-simple": "0.5.33", "@types/leveldown": "^4.0.2", "@types/prometheus-gc-stats": "^0.6.1", "@types/supertest": "^2.0.8", From ed9868bcd7d3e42f1d8db89e9f138aa5bf0bc5a7 Mon Sep 17 00:00:00 2001 From: harkamal Date: Fri, 25 Feb 2022 00:02:52 +0530 Subject: [PATCH 08/10] js-docs for better clarity on the jwt secret --- .../options/beaconNodeOptions/execution.ts | 3 ++- .../src/eth1/provider/jsonRpcHttpClient.ts | 20 ++++++++++++++++++- packages/lodestar/src/executionEngine/http.ts | 7 ++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/options/beaconNodeOptions/execution.ts b/packages/cli/src/options/beaconNodeOptions/execution.ts index 1570f1e30e66..a6cf30ae89d8 100644 --- a/packages/cli/src/options/beaconNodeOptions/execution.ts +++ b/packages/cli/src/options/beaconNodeOptions/execution.ts @@ -38,7 +38,8 @@ export const options: ICliCommandOptions = { }, "jwt-secret": { - description: "Shared jwt secret which EL will use to authenticate engine api calls", + description: + "File path to a shared hex-encoded jwt secret which will be used to generate and bundle HS256 encoded jwt tokens for authentication with the EL client's rpc server hosting engine apis. Secret to be exactly same as the one used by the corresponding EL client.", type: "string", group: "execution", }, diff --git a/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts b/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts index 41928d376611..1b36fac8b7a8 100644 --- a/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts +++ b/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts @@ -32,6 +32,12 @@ export interface IJsonRpcHttpClient { export class JsonRpcHttpClient implements IJsonRpcHttpClient { private id = 1; + /** + * Optional: If provided, use this jwt secret to HS256 encode and add a jwt token in the + * request header which can be authenticated by the RPC server to provide access. + * A fresh token is generated on each requests as EL spec mandates the ELs to check + * the token freshness +-5 seconds (via `iat` property of the token claim) + */ private jwtSecret?: Uint8Array; constructor( @@ -41,6 +47,11 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { timeout?: number; /** If returns true, do not fallback to other urls and throw early */ shouldNotFallback?: (error: Error) => boolean; + /** + * If provided, the requests to the RPC server will be bundled with a HS256 encoded + * token using this secret. Otherwise the requests to the RPC server will be unauthorized + * and it might deny responses to the RPC requests. + */ jwtSecret?: Uint8Array; } ) { @@ -127,7 +138,14 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { try { const headers = {"Content-Type": "application/json"}; if (this.jwtSecret) { - /** ELs have a tight +-5 second freshness check on token's iat i.e. issued at */ + /** + * ELs have a tight +-5 second freshness check on token's iat i.e. issued at + * so its better to generate a new token each time. Currently iat is the only claim + * we are encoding but potentially we can encode more claims. + * Also currently the algorithm for the token generation is mandated to HS256 + * + * Jwt auth spec: https://github.com/ethereum/execution-apis/pull/167 + */ const token = encodeJwtToken({iat: Math.floor(new Date().getTime() / 1000)}, this.jwtSecret); // eslint-disable-next-line @typescript-eslint/naming-convention Object.assign(headers, {Authorization: `Bearer ${token}`}); diff --git a/packages/lodestar/src/executionEngine/http.ts b/packages/lodestar/src/executionEngine/http.ts index e817b020b3f0..5afa746704df 100644 --- a/packages/lodestar/src/executionEngine/http.ts +++ b/packages/lodestar/src/executionEngine/http.ts @@ -26,7 +26,12 @@ import { export type ExecutionEngineHttpOpts = { urls: string[]; timeout?: number; - /** 256 bit jwt secret in hex format without the leading 0x */ + /** + * 256 bit jwt secret in hex format without the leading 0x. If provided, the execution engine + * rpc requests will be bundled by an authorization header having a fresh jwt token on each + * request, as the EL auth specs mandate the fresh of the token (iat) to be checked within + * +-5 seconds interval. + */ jwtSecret?: string; }; From d50cd8df0329e2a823b0041026c0924dc86a5463 Mon Sep 17 00:00:00 2001 From: harkamal Date: Fri, 25 Feb 2022 00:10:26 +0530 Subject: [PATCH 09/10] making lint happy --- .../src/eth1/provider/jsonRpcHttpClient.ts | 16 ++++++++-------- packages/lodestar/src/executionEngine/http.ts | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts b/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts index 1b36fac8b7a8..2ef5bca2f5e5 100644 --- a/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts +++ b/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts @@ -32,8 +32,8 @@ export interface IJsonRpcHttpClient { export class JsonRpcHttpClient implements IJsonRpcHttpClient { private id = 1; - /** - * Optional: If provided, use this jwt secret to HS256 encode and add a jwt token in the + /** + * Optional: If provided, use this jwt secret to HS256 encode and add a jwt token in the * request header which can be authenticated by the RPC server to provide access. * A fresh token is generated on each requests as EL spec mandates the ELs to check * the token freshness +-5 seconds (via `iat` property of the token claim) @@ -47,9 +47,9 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { timeout?: number; /** If returns true, do not fallback to other urls and throw early */ shouldNotFallback?: (error: Error) => boolean; - /** - * If provided, the requests to the RPC server will be bundled with a HS256 encoded - * token using this secret. Otherwise the requests to the RPC server will be unauthorized + /** + * If provided, the requests to the RPC server will be bundled with a HS256 encoded + * token using this secret. Otherwise the requests to the RPC server will be unauthorized * and it might deny responses to the RPC requests. */ jwtSecret?: Uint8Array; @@ -138,12 +138,12 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { try { const headers = {"Content-Type": "application/json"}; if (this.jwtSecret) { - /** + /** * ELs have a tight +-5 second freshness check on token's iat i.e. issued at - * so its better to generate a new token each time. Currently iat is the only claim + * so its better to generate a new token each time. Currently iat is the only claim * we are encoding but potentially we can encode more claims. * Also currently the algorithm for the token generation is mandated to HS256 - * + * * Jwt auth spec: https://github.com/ethereum/execution-apis/pull/167 */ const token = encodeJwtToken({iat: Math.floor(new Date().getTime() / 1000)}, this.jwtSecret); diff --git a/packages/lodestar/src/executionEngine/http.ts b/packages/lodestar/src/executionEngine/http.ts index 5afa746704df..1a7ceefe8d75 100644 --- a/packages/lodestar/src/executionEngine/http.ts +++ b/packages/lodestar/src/executionEngine/http.ts @@ -26,7 +26,7 @@ import { export type ExecutionEngineHttpOpts = { urls: string[]; timeout?: number; - /** + /** * 256 bit jwt secret in hex format without the leading 0x. If provided, the execution engine * rpc requests will be bundled by an authorization header having a fresh jwt token on each * request, as the EL auth specs mandate the fresh of the token (iat) to be checked within From 46e723b0912b42392276d384059d5aadbcb0cc43 Mon Sep 17 00:00:00 2001 From: harkamal Date: Mon, 28 Feb 2022 12:14:51 +0530 Subject: [PATCH 10/10] hex format changes --- .../src/options/beaconNodeOptions/execution.ts | 8 +++----- packages/cli/src/util/jwt.ts | 3 ++- .../test/unit/config/beaconNodeOptions.test.ts | 18 +++++++++++++++--- .../src/eth1/provider/jsonRpcHttpClient.ts | 5 ++--- packages/lodestar/src/executionEngine/http.ts | 5 +++-- .../lodestar/test/sim/merge-interop.test.ts | 8 ++++---- 6 files changed, 29 insertions(+), 18 deletions(-) diff --git a/packages/cli/src/options/beaconNodeOptions/execution.ts b/packages/cli/src/options/beaconNodeOptions/execution.ts index a6cf30ae89d8..5c94c248f60b 100644 --- a/packages/cli/src/options/beaconNodeOptions/execution.ts +++ b/packages/cli/src/options/beaconNodeOptions/execution.ts @@ -9,14 +9,12 @@ export type ExecutionEngineArgs = { }; export function parseArgs(args: ExecutionEngineArgs): IBeaconNodeOptions["executionEngine"] { - let jwtSecret; - if (args["jwt-secret"]) { - jwtSecret = extractJwtHexSecret(fs.readFileSync(args["jwt-secret"], "utf-8").trim()); - } return { urls: args["execution.urls"], timeout: args["execution.timeout"], - jwtSecret, + jwtSecretHex: args["jwt-secret"] + ? extractJwtHexSecret(fs.readFileSync(args["jwt-secret"], "utf-8").trim()) + : undefined, }; } diff --git a/packages/cli/src/util/jwt.ts b/packages/cli/src/util/jwt.ts index f94f71f1117f..e77e9e692cd6 100644 --- a/packages/cli/src/util/jwt.ts +++ b/packages/cli/src/util/jwt.ts @@ -5,5 +5,6 @@ export function extractJwtHexSecret(jwtSecretContents: string): string { if (!jwtSecret || jwtSecret.length != 64) { throw Error(`Need a valid 256 bit hex encoded secret ${jwtSecret} ${jwtSecretContents}`); } - return jwtSecret; + // Return the secret in proper hex format + return `0x${jwtSecret}`; } diff --git a/packages/cli/test/unit/config/beaconNodeOptions.test.ts b/packages/cli/test/unit/config/beaconNodeOptions.test.ts index 285171e316ee..3d14016cae43 100644 --- a/packages/cli/test/unit/config/beaconNodeOptions.test.ts +++ b/packages/cli/test/unit/config/beaconNodeOptions.test.ts @@ -216,15 +216,15 @@ describe("parseJwtHexSecret", () => { const testCases: {raw: string; parsed: string}[] = [ { raw: "c58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", - parsed: "c58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", + parsed: "0xc58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", }, { raw: "0xc58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", - parsed: "c58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", + parsed: "0xc58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", }, { raw: "0Xc58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", - parsed: "c58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", + parsed: "0xc58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", }, ]; for (const {raw, parsed} of testCases) { @@ -233,3 +233,15 @@ describe("parseJwtHexSecret", () => { }); } }); + +describe("invalid jwtHexSecret", () => { + const testCases: {raw: string; error: string}[] = [ + {raw: "c58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b23", error: "invalid length"}, + {raw: "X58e5dddf552f9f35e24466cc0c3cc479f82b1d09626c4217ff28220629d306b", error: "invalid hex"}, + ]; + for (const {raw, error} of testCases) { + it(`should error on ${error}: ${raw}`, () => { + expect(() => extractJwtHexSecret(raw)).to.throw(); + }); + } +}); diff --git a/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts b/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts index 2ef5bca2f5e5..a8de849d0772 100644 --- a/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts +++ b/packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts @@ -136,7 +136,7 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { } try { - const headers = {"Content-Type": "application/json"}; + const headers: Record = {"Content-Type": "application/json"}; if (this.jwtSecret) { /** * ELs have a tight +-5 second freshness check on token's iat i.e. issued at @@ -147,8 +147,7 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient { * Jwt auth spec: https://github.com/ethereum/execution-apis/pull/167 */ const token = encodeJwtToken({iat: Math.floor(new Date().getTime() / 1000)}, this.jwtSecret); - // eslint-disable-next-line @typescript-eslint/naming-convention - Object.assign(headers, {Authorization: `Bearer ${token}`}); + headers["Authorization"] = `Bearer ${token}`; } const res = await fetch(url, { diff --git a/packages/lodestar/src/executionEngine/http.ts b/packages/lodestar/src/executionEngine/http.ts index 1a7ceefe8d75..52a672cda8d0 100644 --- a/packages/lodestar/src/executionEngine/http.ts +++ b/packages/lodestar/src/executionEngine/http.ts @@ -1,6 +1,7 @@ import {AbortSignal} from "@chainsafe/abort-controller"; import {bellatrix, RootHex, Root} from "@chainsafe/lodestar-types"; import {BYTES_PER_LOGS_BLOOM} from "@chainsafe/lodestar-params"; +import {fromHex} from "@chainsafe/lodestar-utils"; import {ErrorJsonRpcResponse, HttpRpcError, JsonRpcHttpClient} from "../eth1/provider/jsonRpcHttpClient"; import { @@ -32,7 +33,7 @@ export type ExecutionEngineHttpOpts = { * request, as the EL auth specs mandate the fresh of the token (iat) to be checked within * +-5 seconds interval. */ - jwtSecret?: string; + jwtSecretHex?: string; }; export const defaultExecutionEngineHttpOpts: ExecutionEngineHttpOpts = { @@ -58,7 +59,7 @@ export class ExecutionEngineHttp implements IExecutionEngine { new JsonRpcHttpClient(opts.urls, { signal, timeout: opts.timeout, - jwtSecret: opts.jwtSecret ? Buffer.from(opts.jwtSecret, "hex") : undefined, + jwtSecret: opts.jwtSecretHex ? fromHex(opts.jwtSecretHex) : undefined, }); } diff --git a/packages/lodestar/test/sim/merge-interop.test.ts b/packages/lodestar/test/sim/merge-interop.test.ts index ea2f2e08ecf4..d685b25c2781 100644 --- a/packages/lodestar/test/sim/merge-interop.test.ts +++ b/packages/lodestar/test/sim/merge-interop.test.ts @@ -46,7 +46,7 @@ import {bytesToData, dataToBytes, quantityToNum} from "../../src/eth1/provider/u // 10 ttd / 2 difficulty per block = 5 blocks * 5 sec = 25 sec const terminalTotalDifficultyPreMerge = 10; const TX_SCENARIOS = process.env.TX_SCENARIOS?.split(",") || []; -const jwtSecret = "dc6457099f127cf0bac78de8b297df04951281909db4f58b43def7c7151e765d"; +const jwtSecretHex = "0xdc6457099f127cf0bac78de8b297df04951281909db4f58b43def7c7151e765d"; describe("executionEngine / ExecutionEngineHttp", function () { this.timeout("10min"); @@ -79,7 +79,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { ...process.env, TTD, DATA_DIR, - JWT_SECRET_HEX: `0x${jwtSecret}`, + JWT_SECRET_HEX: `${jwtSecretHex}`, }, }); @@ -156,7 +156,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { } const controller = new AbortController(); - const executionEngine = new ExecutionEngineHttp({urls: [engineApiUrl], jwtSecret}, controller.signal); + const executionEngine = new ExecutionEngineHttp({urls: [engineApiUrl], jwtSecretHex}, controller.signal); // 1. Prepare a payload @@ -320,7 +320,7 @@ describe("executionEngine / ExecutionEngineHttp", function () { sync: {isSingleNode: true}, network: {discv5: null}, eth1: {enabled: true, providerUrls: [jsonRpcUrl]}, - executionEngine: {urls: [engineApiUrl], jwtSecret}, + executionEngine: {urls: [engineApiUrl], jwtSecretHex}, }, validatorCount: validatorClientCount * validatorsPerClient, logger: loggerNodeA,