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

feat: add optional id and clv to JWT claims #6052

Merged
merged 27 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
15f1906
Add id and clv to jwt claim and cli
ensi321 Oct 19, 2023
156bd02
Update doc
ensi321 Oct 19, 2023
de7e876
Add unit test
ensi321 Oct 19, 2023
70f2f72
Update comments and lint
ensi321 Oct 19, 2023
ef35d9f
Update unit test
ensi321 Oct 19, 2023
0d4dfff
Update unit test
ensi321 Oct 19, 2023
6233200
Revert "Update unit test"
ensi321 Oct 19, 2023
a63cd22
Add id and clv to eth1
ensi321 Oct 19, 2023
445cd00
Lint
ensi321 Oct 19, 2023
74dde4a
Address comment
ensi321 Oct 19, 2023
0d5f268
Update doc to remove jwt version
ensi321 Oct 23, 2023
b66e1b7
Remove jwt version from cli arg
ensi321 Oct 23, 2023
1377617
Populate jwtVersion from beaconHandlerinit
ensi321 Oct 23, 2023
485da7a
Rename jwt-id to jwtId
ensi321 Oct 23, 2023
d49c063
Rename jwt-secret to jwtSecret
ensi321 Oct 23, 2023
baa0ae4
Use Date.now() for iat
ensi321 Oct 24, 2023
4b8098c
Update packages/beacon-node/src/eth1/provider/jwt.ts
ensi321 Oct 24, 2023
2aa08fe
Update packages/beacon-node/src/execution/engine/http.ts
ensi321 Oct 24, 2023
40e61e1
Update packages/beacon-node/src/execution/engine/http.ts
ensi321 Oct 24, 2023
20dcfec
Update packages/cli/src/options/beaconNodeOptions/execution.ts
ensi321 Oct 24, 2023
fef1859
Address comments
ensi321 Oct 24, 2023
262afb2
Fix jsdoc
nflaig Oct 24, 2023
36844c3
id is not used for authentication but jwt tokens are, id is just incl…
nflaig Oct 24, 2023
10c6a05
Remove jwt claim section from doc
ensi321 Oct 24, 2023
cc5ffe4
Update private description and lint
ensi321 Oct 24, 2023
8eb36c6
Remove extra white space from jsdoc
nflaig Oct 24, 2023
f4996d2
Fix lint issues
nflaig Oct 24, 2023
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
11 changes: 8 additions & 3 deletions docs/usage/beacon-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ You must generate a secret 32-byte (64 characters) hexadecimal string that will

### Configure Lodestar to locate the JWT secret

When starting up a Lodestar beacon node in any configuration, ensure you add the `--jwt-secret $JWT_SECRET_PATH` flag to point to the saved secret key file.
When starting up a Lodestar beacon node in any configuration, ensure you add the `--jwtSecret $JWT_SECRET_PATH` flag to point to the saved secret key file.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why change this flag? I think yargs supports both notations and the --jwt-secret flag is kind of standarized across clients

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kind of standarized across clients

Kinda, but not really

  • Teku: --ee-jwt-secret-file (ref)
  • Lighthouse: --execution-jwt (ref)
  • Prsym: --jwt-secret (ref)
  • Nimbus: --jwt-secret (ref)

For Lodestar, --jwt-secret is currently the only commonly used flag which uses different casing. I'd recommend we use the same standard (camelCase) for this flag as well.


### Set up and include identifiers in JWT tokens

Lodestar auto-populates `clv` field in the claims of JWT authentication tokens with a non-configurable value `Lodestar/$CLIENT_VERSION` eg. `Lodestar/v1.3.0/2d0938e` to communicate the client's version. Lodestar also optionally includes `id` field in the claims with value `$JWT_ID` if the appropriate flag `--jwtId $JWT_ID` is added.
`id` and `clv` are particularly useful when running multiple consensus-layer clients with the different JWT secrets which makes the execution-layer client difficult to choose which JWT secret to verify against due to the inability to distinguish between the different consensus-layer clients.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be confusing, as far as I am aware execution clients do not natively support multiplexing of consensus clients and I don't think this will ever be something that will be implemented. Without a multiplexer middleware like eleel it is not recommended to connect multiple CLs to a single EL.

Copy link
Contributor Author

@ensi321 ensi321 Oct 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might be confusing, as far as I am aware execution clients do not natively support multiplexing of consensus clients and I don't think this will ever be something that will be implemented. Without a multiplexer middleware like eleel it is not recommended to connect multiple CLs to a single EL.

I thought about the possible use case of configuring --jwtId for normal users and I can't think of any. The multiplexer case is nice but users who read this doc probably won't ever need it.

I am inclined to just remove this entirely because the point of this doc is to set up a beacon node. Explaining things in jwt claim and the option to add id because of multiplexer seem really out of place in the doc.

Man writing user doc is hard

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The multiplexer case is nice but users who read this doc probably won't ever need it.

Yeah, I rather see people using eleel come to the CLI reference to see if Lodestar has the option to set the jwt id

I am inclined to just remove this entirely because the point of this doc is to set up a beacon node.

+1, I think should keep it simple here

Man writing user doc is hard

Defintiely hard thing because it is quite subjective and user feedback is required in a lot of cases, @matthewkeil is working on a docs restructuring which should make it clearer on what details to include where.

In my opinion

  • step-by-step instructions should mostly omit unnecessary details
  • while sections explaining different concepts can go more into detail

ensi321 marked this conversation as resolved.
Show resolved Hide resolved

### Ensure JWT is configured with your execution node

Expand All @@ -54,7 +59,7 @@ Use the `--authrpc.jwtsecret` flag to configure the secret. Use their documentat
To start a Lodestar beacon run the command:

```bash
./lodestar beacon --network $NETWORK_NAME --jwt-secret $JWT_SECRET_PATH
./lodestar beacon --network $NETWORK_NAME --jwtSecret $JWT_SECRET_PATH
```

This will assume an execution-layer client is available at the default
Expand All @@ -63,7 +68,7 @@ location of `https://localhost:8545`.
In case execution-layer clients are available at different locations, use `--execution.urls` to specify these locations in the command:

```bash
./lodestar beacon --network $NETWORK_NAME --jwt-secret $JWT_SECRET_PATH --execution.urls $EL_URL1 $EL_URL2
./lodestar beacon --network $NETWORK_NAME --jwtSecret $JWT_SECRET_PATH --execution.urls $EL_URL1 $EL_URL2
```

Immediately you should see confirmation that the node has started
Expand Down
2 changes: 2 additions & 0 deletions packages/beacon-node/src/eth1/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ export type Eth1Options = {
* protected engine endpoints.
*/
jwtSecretHex?: string;
jwtId?: string;
jwtVersion?: string;
depositContractDeployBlock?: number;
unsafeAllowDepositDataOverwrite?: boolean;
/**
Expand Down
6 changes: 5 additions & 1 deletion packages/beacon-node/src/eth1/provider/eth1Provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ export class Eth1Provider implements IEth1Provider {

constructor(
config: Pick<ChainConfig, "DEPOSIT_CONTRACT_ADDRESS">,
opts: Pick<Eth1Options, "depositContractDeployBlock" | "providerUrls" | "jwtSecretHex"> & {logger?: Logger},
opts: Pick<Eth1Options, "depositContractDeployBlock" | "providerUrls" | "jwtSecretHex" | "jwtId" | "jwtVersion"> & {
logger?: Logger;
},
signal?: AbortSignal,
metrics?: JsonRpcHttpClientMetrics | null
) {
Expand All @@ -76,6 +78,8 @@ export class Eth1Provider implements IEth1Provider {
// Don't fallback with is truncated error. Throw early and let the retry on this class handle it
shouldNotFallback: isJsonRpcTruncatedError,
jwtSecret: opts.jwtSecretHex ? fromHex(opts.jwtSecretHex) : undefined,
jwtId: opts.jwtId,
jwtVersion: opts.jwtVersion,
metrics: metrics,
});

Expand Down
18 changes: 16 additions & 2 deletions packages/beacon-node/src/eth1/provider/jsonRpcHttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {fetch} from "@lodestar/api";
import {ErrorAborted, TimeoutError, isValidHttpUrl, retry} from "@lodestar/utils";
import {IGauge, IHistogram} from "../../metrics/interface.js";
import {IJson, RpcPayload} from "../interface.js";
import {encodeJwtToken} from "./jwt.js";
import {JwtClaim, encodeJwtToken} from "./jwt.js";

export enum JsonRpcHttpClientEvent {
/**
Expand Down Expand Up @@ -83,6 +83,8 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient {
* the token freshness +-5 seconds (via `iat` property of the token claim)
*/
private readonly jwtSecret?: Uint8Array;
private readonly jwtId?: string;
private readonly jwtVersion?: string;
private readonly metrics: JsonRpcHttpClientMetrics | null;
readonly emitter = new JsonRpcHttpClientEventEmitter();

Expand All @@ -103,6 +105,10 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient {
* and it might deny responses to the RPC requests.
*/
jwtSecret?: Uint8Array;
/** If jwtSecret and jwtId are provided, jwtId will be included in JwtClaim.id */
jwtId?: string;
/** If jwtSecret and jwtVersion are provided, jwtVersion will be included in JwtClaim.clv. */
jwtVersion?: string;
/** Retry attempts */
retryAttempts?: number;
/** Retry delay, only relevant with retry attempts */
Expand All @@ -125,6 +131,8 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient {
}

this.jwtSecret = opts?.jwtSecret;
this.jwtId = opts?.jwtId;
this.jwtVersion = opts?.jwtVersion;
this.metrics = opts?.metrics ?? null;

this.metrics?.configUrlsCount.set(urls.length);
Expand Down Expand Up @@ -255,7 +263,13 @@ 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);
const jwtClaim: JwtClaim = {
iat: Math.floor(Date.now() / 1000),
id: this.jwtId,
clv: this.jwtVersion,
};

const token = encodeJwtToken(jwtClaim, this.jwtSecret);
headers["Authorization"] = `Bearer ${token}`;
}

Expand Down
7 changes: 5 additions & 2 deletions packages/beacon-node/src/eth1/provider/jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import jwt from "jwt-simple";

const {encode, decode} = jwt;

/** jwt token has iat which is issued at unix timestamp, and an optional exp for expiry */
type JwtClaim = {iat: number; exp?: number};
/**
* jwt token has iat which is issued at unix timestamp, an optional exp for expiry,
* an optional id as unique identifier, and an optional clv for client type/version
nflaig marked this conversation as resolved.
Show resolved Hide resolved
*/
export type JwtClaim = {iat: number; exp?: number; id?: string; clv?: string};

export function encodeJwtToken(
claim: JwtClaim,
Expand Down
8 changes: 8 additions & 0 deletions packages/beacon-node/src/execution/engine/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ export type ExecutionEngineHttpOpts = {
* +-5 seconds interval.
*/
jwtSecretHex?: string;
/**
* An identifier string passed as CLI arg that will be set in `id` field of jwt claims
*/
jwtId?: string;
/**
* A version string that will be set in `clv` field of jwt claims
*/
jwtVersion?: string;
};

export const defaultExecutionEngineHttpOpts: ExecutionEngineHttpOpts = {
Expand Down
2 changes: 2 additions & 0 deletions packages/beacon-node/src/execution/engine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export function getExecutionEngineHttp(
signal: modules.signal,
metrics: modules.metrics?.executionEnginerHttpClient,
jwtSecret: opts.jwtSecretHex ? fromHex(opts.jwtSecretHex) : undefined,
jwtId: opts.jwtId,
jwtVersion: opts.jwtVersion,
});
return new ExecutionEngineHttp(rpc, modules);
}
Expand Down
33 changes: 33 additions & 0 deletions packages/beacon-node/test/unit/eth1/jwt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ describe("ExecutionEngine / jwt", () => {
expect(decoded).toEqual(claim);
});

it("encode/decode correctly with id and clv", () => {
const jwtSecret = Buffer.from(Array.from({length: 32}, () => Math.round(Math.random() * 255)));
const claim = {iat: Math.floor(new Date().getTime() / 1000), id: "4ac0", clv: "Lodestar/v0.36.0/80c248bb"};
const token = encodeJwtToken(claim, jwtSecret);
const decoded = decodeJwtToken(token, jwtSecret);
expect(decoded).toEqual(claim);
});

it("encode a claim correctly from a hex key", () => {
const jwtSecretHex = "7e2d709fb01382352aaf830e755d33ca48cb34ba1c21d999e45c1a7a6f88b193";
const jwtSecret = Buffer.from(jwtSecretHex, "hex");
Expand All @@ -19,4 +27,29 @@ describe("ExecutionEngine / jwt", () => {
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDU1NTE0NTJ9.nUDaIyGPgRX76tQ_kDlcIGj4uyFA4lFJGKsD_GHIEzM"
);
});

it("encode a claim with id and clv correctly from a hex key", () => {
const jwtSecretHex = "7e2d709fb01382352aaf830e755d33ca48cb34ba1c21d999e45c1a7a6f88b193";
const jwtSecret = Buffer.from(jwtSecretHex, "hex");
const id = "4ac0";
const clv = "v1.11.3";
const claimWithId = {iat: 1645551452, id: id};
const claimWithVersion = {iat: 1645551452, clv: clv};
const claimWithIdAndVersion = {iat: 1645551452, id: id, clv: clv};

const tokenWithId = encodeJwtToken(claimWithId, jwtSecret);
expect(tokenWithId).toBe(
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDU1NTE0NTIsImlkIjoiNGFjMCJ9.g3iKsQk9Q1PSYaGldo9MM0Mds8E59t24K6rHdQ9HXg0"
);

const tokenWithVersion = encodeJwtToken(claimWithVersion, jwtSecret);
expect(tokenWithVersion).toBe(
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDU1NTE0NTIsImNsdiI6InYxLjExLjMifQ.s5iLRa04o_rtATWubz6LZc27rVXhE2n9BPpXpnmnR5o"
);

const tokenWithIdAndVersion = encodeJwtToken(claimWithIdAndVersion, jwtSecret);
expect(tokenWithIdAndVersion).toBe(
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE2NDU1NTE0NTIsImlkIjoiNGFjMCIsImNsdiI6InYxLjExLjMifQ.QahIO3hcnMV385ES5jedRudsdECMVDembkoQv4BnSTs"
);
});
});
8 changes: 6 additions & 2 deletions packages/cli/src/cmds/beacon/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,14 @@ export async function beaconHandlerInit(args: BeaconArgs & GlobalArgs) {
if (args.private) {
beaconNodeOptions.set({network: {private: true}});
} else {
const versionStr = `Lodestar/${version}`;
const simpleVersionStr = version.split("/")[0];
// Add simple version string for libp2p agent version
beaconNodeOptions.set({network: {version: version.split("/")[0]}});
beaconNodeOptions.set({network: {version: simpleVersionStr}});
// Add User-Agent header to all builder requests
beaconNodeOptions.set({executionBuilder: {userAgent: `Lodestar/${version}`}});
beaconNodeOptions.set({executionBuilder: {userAgent: versionStr}});
// Set jwt version with version string
beaconNodeOptions.set({executionEngine: {jwtVersion: versionStr}, eth1: {jwtVersion: versionStr}});
ensi321 marked this conversation as resolved.
Show resolved Hide resolved
}

// Render final options
Expand Down
8 changes: 6 additions & 2 deletions packages/cli/src/options/beaconNodeOptions/eth1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export type Eth1Args = {

export function parseArgs(args: Eth1Args & Partial<ExecutionEngineArgs>): IBeaconNodeOptions["eth1"] {
let jwtSecretHex: string | undefined;
let jwtId: string | undefined;

let providerUrls = args["eth1.providerUrls"];

// If no providerUrls are explicitly provided, we should pick the execution endpoint
Expand All @@ -22,15 +24,17 @@ export function parseArgs(args: Eth1Args & Partial<ExecutionEngineArgs>): IBeaco
// jwt auth mechanism.
if (providerUrls === undefined && args["execution.urls"]) {
providerUrls = args["execution.urls"];
jwtSecretHex = args["jwt-secret"]
? extractJwtHexSecret(fs.readFileSync(args["jwt-secret"], "utf-8").trim())
jwtSecretHex = args["jwtSecret"]
? extractJwtHexSecret(fs.readFileSync(args["jwtSecret"], "utf-8").trim())
: undefined;
jwtId = args["jwtId"];
}

return {
enabled: args["eth1"],
providerUrls,
jwtSecretHex,
jwtId,
depositContractDeployBlock: args["eth1.depositContractDeployBlock"],
disableEth1DepositDataTracker: args["eth1.disableEth1DepositDataTracker"],
unsafeAllowDepositDataOverwrite: args["eth1.unsafeAllowDepositDataOverwrite"],
Expand Down
17 changes: 13 additions & 4 deletions packages/cli/src/options/beaconNodeOptions/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export type ExecutionEngineArgs = {
"execution.retryAttempts": number;
"execution.retryDelay": number;
"execution.engineMock"?: boolean;
"jwt-secret"?: string;
"jwtSecret"?: string;
"jwtId"?: string;
};

export function parseArgs(args: ExecutionEngineArgs): IBeaconNodeOptions["executionEngine"] {
Expand All @@ -28,9 +29,10 @@ export function parseArgs(args: ExecutionEngineArgs): IBeaconNodeOptions["execut
* jwtSecret is parsed as hex instead of bytes because the merge with defaults
* in beaconOptions messes up the bytes array as as index => value object
*/
jwtSecretHex: args["jwt-secret"]
? extractJwtHexSecret(fs.readFileSync(args["jwt-secret"], "utf-8").trim())
jwtSecretHex: args["jwtSecret"]
? extractJwtHexSecret(fs.readFileSync(args["jwtSecret"], "utf-8").trim())
: undefined,
jwtId: args["jwtId"],
};
}

Expand Down Expand Up @@ -74,10 +76,17 @@ export const options: CliCommandOptions<ExecutionEngineArgs> = {
group: "execution",
},

"jwt-secret": {
"jwtSecret": {
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",
},

"jwtId": {
description:
"An optional identifier to be set in the id field of the claims included in jwt tokens for authentication with EL client's rpc server hosting engine apis",
nflaig marked this conversation as resolved.
Show resolved Hide resolved
type: "string",
group: "execution",
},
};
2 changes: 1 addition & 1 deletion packages/cli/test/unit/options/beaconNodeOptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ describe("options / beaconNodeOptions", () => {
const beaconNodeArgsPartial = {
eth1: true,
"execution.urls": ["http://my.node:8551"],
"jwt-secret": jwtSecretFile,
"jwtSecret": jwtSecretFile,
} as BeaconNodeArgs;

const expectedOptions: RecursivePartial<IBeaconNodeOptions> = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export const generateLodestarBeaconNode: BeaconNodeGenerator<BeaconClient.Lodest
logLevel: LogLevel.debug,
logFileDailyRotate: 0,
logFile: "none",
"jwt-secret": jwtsecretFilePath,
"jwtSecret": jwtsecretFilePath,
paramsFile: paramsPath,
...clientOptions,
} as unknown as BeaconArgs & GlobalArgs;
Expand Down
Loading