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 5 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
15 changes: 14 additions & 1 deletion packages/cli/src/options/beaconNodeOptions/execution.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
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[];
"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
jwtSecretHex = extractJwtHexSecret(fs.readFileSync(args["jwt-secret"], "utf-8").trim());
}
return {
urls: args["execution.urls"],
timeout: args["execution.timeout"],
jwtSecretHex,
};
}

Expand All @@ -29,4 +36,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",
},
};
1 change: 1 addition & 0 deletions packages/cli/src/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export * from "./sleep";
export * from "./stripOffNewlines";
export * from "./types";
export * from "./bls";
export * from "./jwt";
9 changes: 9 additions & 0 deletions packages/cli/src/util/jwt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function extractJwtHexSecret(jwtSecretContents: string): string {
const hexPattern = new RegExp(/^(0x|0X)?(?<jwtSecret>[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;
}
23 changes: 23 additions & 0 deletions packages/cli/test/unit/config/beaconNodeOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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));
});
}
});
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
20 changes: 18 additions & 2 deletions packages/lodestar/src/eth1/provider/jsonRpcHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +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 {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
*/
Expand All @@ -31,6 +32,7 @@ export interface IJsonRpcHttpClient {

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

constructor(
private readonly urls: string[],
Expand All @@ -39,6 +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;
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 +53,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 +127,20 @@ 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 = 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"};
}

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
28 changes: 28 additions & 0 deletions packages/lodestar/src/util/jwt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {encode, decode, TAlgorithm} from "jwt-simple";
dapplion marked this conversation as resolved.
Show resolved Hide resolved

/** 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<string, unknown> & 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;
}
17 changes: 12 additions & 5 deletions packages/lodestar/test/sim/merge-interop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,31 @@ 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";

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 +79,7 @@ describe("executionEngine / ExecutionEngineHttp", function () {
...process.env,
TTD,
DATA_DIR,
JWT_SECRET_HEX: jwtSecretHex,
},
});

Expand Down Expand Up @@ -149,7 +156,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 +320,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
Loading