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

Add jwt based token auth to the engine api calls #3777

Merged
merged 10 commits into from
Feb 28, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/test-sim-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Sim merge tests
on: [pull_request, push]

env:
GETH_COMMIT: 0569213dc4032da83abed44fab7f65794a526f21
GETH_COMMIT: 815a414312db9a922c5a34ac034fb7aa8861f2e7
NETHERMIND_COMMIT: 78ae2353be5d05a285d6aaa1826910489d381a3e

jobs:
Expand Down Expand Up @@ -37,8 +37,10 @@ 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 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

Expand All @@ -49,6 +51,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
Expand Down
9 changes: 8 additions & 1 deletion kiln/geth/common-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 3 additions & 1 deletion kiln/geth/post-merge.sh
Original file line number Diff line number Diff line change
@@ -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
4 changes: 3 additions & 1 deletion kiln/geth/pre-merge.sh
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions kiln/gethdocker/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 3 additions & 3 deletions kiln/gethdocker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
3 changes: 3 additions & 0 deletions kiln/gethdocker/common-setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
echo $TTD
echo $DATA_DIR
echo $EL_BINARY_DIR
echo $JWT_SECRET_HEX

echo $scriptDir
echo $currentDir
Expand All @@ -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
2 changes: 1 addition & 1 deletion kiln/gethdocker/post-merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion kiln/gethdocker/pre-merge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 18 additions & 0 deletions packages/cli/src/options/beaconNodeOptions/execution.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import fs from "node:fs";
import {defaultOptions, IBeaconNodeOptions} from "@chainsafe/lodestar";
import {ICliCommandOptions} from "../../util";

export type ExecutionEngineArgs = {
"execution.urls": string[];
"execution.timeout": number;
"jwt-secret"?: string;
};

export function parseArgs(args: ExecutionEngineArgs): IBeaconNodeOptions["executionEngine"] {
let jwtSecretHex;
if (args["jwt-secret"]) {
twoeths marked this conversation as resolved.
Show resolved Hide resolved
const jwtSecretContents = fs.readFileSync(args["jwt-secret"], "utf-8").trim();
const hexPattern = new RegExp(/^(0x|0X)?(?<jwtSecret>[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");
}
}
return {
urls: args["execution.urls"],
timeout: args["execution.timeout"],
jwtSecretHex,
};
}

Expand All @@ -29,4 +41,10 @@ export const options: ICliCommandOptions<ExecutionEngineArgs> = {
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",
},
};
2 changes: 2 additions & 0 deletions packages/lodestar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
"interface-datastore": "^5.1.2",
"it-all": "^1.0.2",
"it-pipe": "^1.1.0",
"jwt-simple": "^0.5.6",
dapplion marked this conversation as resolved.
Show resolved Hide resolved
"libp2p": "^0.32.4",
"libp2p-bootstrap": "^0.13.0",
"libp2p-gossipsub": "^0.11.1",
Expand All @@ -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",
Expand Down
26 changes: 25 additions & 1 deletion packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -31,6 +34,7 @@ export interface IJsonRpcHttpClient {

export class JsonRpcHttpClient implements IJsonRpcHttpClient {
private id = 1;
private jwtSecret?: Uint8Array;

constructor(
private readonly urls: string[],
Expand All @@ -39,6 +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;
jwtSecretHex?: string;
dapplion marked this conversation as resolved.
Show resolved Hide resolved
}
) {
// Sanity check for all URLs to be properly defined. Otherwise it will error in loop on fetch
Expand All @@ -50,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");
}
}

/**
Expand Down Expand Up @@ -121,10 +129,26 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient {
}

try {
let headers;
dapplion marked this conversation as resolved.
Show resolved Hide resolved
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
);
// 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);
Expand Down
3 changes: 2 additions & 1 deletion packages/lodestar/src/executionEngine/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
export type ExecutionEngineHttpOpts = {
urls: string[];
timeout?: number;
jwtSecretHex?: string;
};

export const defaultExecutionEngineHttpOpts: ExecutionEngineHttpOpts = {
Expand All @@ -50,7 +51,7 @@ export class ExecutionEngineHttp implements IExecutionEngine {
rpc ??
new JsonRpcHttpClient(opts.urls, {
signal,
timeout: opts.timeout,
...opts,
dapplion marked this conversation as resolved.
Show resolved Hide resolved
});
}

Expand Down
8 changes: 5 additions & 3 deletions packages/lodestar/test/sim/merge-interop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;

Expand All @@ -73,6 +74,7 @@ describe("executionEngine / ExecutionEngineHttp", function () {
...process.env,
TTD,
DATA_DIR,
JWT_SECRET_HEX: jwtSecretHex,
},
});

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down