2022-02-27 16:55:02 +00:00
|
|
|
# beacon_chain
|
2024-01-06 14:26:56 +00:00
|
|
|
# Copyright (c) 2022-2024 Status Research & Development GmbH
|
2022-02-27 16:55:02 +00:00
|
|
|
# Licensed and distributed under either of
|
|
|
|
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
|
|
|
|
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
|
|
|
|
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
|
|
|
|
|
|
|
import
|
2023-03-05 01:40:21 +00:00
|
|
|
chronicles, confutils/defs,
|
2022-06-21 08:29:16 +00:00
|
|
|
bearssl/rand,
|
2022-02-27 16:55:02 +00:00
|
|
|
nimcrypto/[hmac, utils],
|
2024-01-16 22:37:14 +00:00
|
|
|
results,
|
|
|
|
stew/byteutils
|
2022-02-27 16:55:02 +00:00
|
|
|
|
2022-12-08 16:21:53 +00:00
|
|
|
from std/base64 import encode
|
|
|
|
from std/json import JsonNode, `$`, `%*`
|
|
|
|
from std/os import `/`
|
|
|
|
from std/strutils import replace
|
|
|
|
|
2022-06-21 08:29:16 +00:00
|
|
|
export rand, results
|
|
|
|
|
2023-01-20 14:14:37 +00:00
|
|
|
{.push raises: [].}
|
2022-02-27 16:55:02 +00:00
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
const
|
|
|
|
JWT_SECRET_LEN = 32
|
|
|
|
|
2024-01-08 21:12:58 +00:00
|
|
|
func base64urlEncode(x: auto): string =
|
2022-02-27 16:55:02 +00:00
|
|
|
# The only strings this gets are internally generated, and don't have
|
|
|
|
# encoding quirks.
|
|
|
|
base64.encode(x, safe = true).replace("=", "")
|
|
|
|
|
2022-03-31 14:43:05 +00:00
|
|
|
func getIatToken*(time: int64): JsonNode =
|
2023-04-17 20:11:28 +00:00
|
|
|
# https://github.com/ethereum/execution-apis/blob/v1.0.0-beta.3/src/engine/authentication.md#jwt-claims
|
2023-02-01 18:49:36 +00:00
|
|
|
# "Required: `iat` (issued-at) claim. The execution layer client **SHOULD**
|
|
|
|
# only accept `iat` timestamps which are within +-60 seconds from the current
|
|
|
|
# time."
|
2022-02-27 16:55:02 +00:00
|
|
|
#
|
|
|
|
# https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6 describes iat
|
|
|
|
# claims.
|
|
|
|
#
|
|
|
|
# https://pyjwt.readthedocs.io/en/stable/usage.html#issued-at-claim-iat shows
|
|
|
|
# an example of an iat claim: {"iat": 1371720939}
|
|
|
|
%* {"iat": time}
|
|
|
|
|
2024-01-08 21:12:58 +00:00
|
|
|
func getSignedToken*(key: openArray[byte], payload: string): string =
|
2023-04-17 20:11:28 +00:00
|
|
|
# https://github.com/ethereum/execution-apis/blob/v1.0.0-beta.3/src/engine/authentication.md#jwt-specifications
|
2023-02-01 18:49:36 +00:00
|
|
|
# "The execution layer client **MUST** support at least the following `alg`
|
|
|
|
# `HMAC + SHA256` (`HS256`)"
|
2022-02-27 16:55:02 +00:00
|
|
|
|
|
|
|
# https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.1.1
|
|
|
|
const jwsProtectedHeader =
|
2022-04-08 16:22:49 +00:00
|
|
|
base64urlEncode($ %* {"typ": "JWT", "alg": "HS256"}) & "."
|
2022-02-27 16:55:02 +00:00
|
|
|
# In theory, std/json might change how it encodes, and it doesn't per-se
|
|
|
|
# matter but can also simply specify the base64-encoded form directly if
|
|
|
|
# useful, since it's never checked here on its own.
|
|
|
|
static: doAssert jwsProtectedHeader == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9."
|
|
|
|
let signingInput = jwsProtectedHeader & base64urlEncode(payload)
|
|
|
|
|
2022-04-08 16:22:49 +00:00
|
|
|
signingInput & "." & base64urlEncode(sha256.hmac(key, signingInput).data)
|
2022-02-27 16:55:02 +00:00
|
|
|
|
2024-01-08 21:12:58 +00:00
|
|
|
func getSignedIatToken*(key: openArray[byte], time: int64): string =
|
2022-02-27 16:55:02 +00:00
|
|
|
getSignedToken(key, $getIatToken(time))
|
|
|
|
|
2024-01-08 21:12:58 +00:00
|
|
|
func parseJwtTokenValue*(input: string): Result[seq[byte], cstring] =
|
2023-03-05 01:40:21 +00:00
|
|
|
# Secret JWT key is parsed in constant time using nimcrypto:
|
|
|
|
# https://github.com/cheatfate/nimcrypto/pull/44
|
|
|
|
let secret = utils.fromHex(input)
|
|
|
|
if secret.len == JWT_SECRET_LEN:
|
|
|
|
ok(secret)
|
|
|
|
else:
|
|
|
|
err("The JWT secret should be 256 bits and hex-encoded")
|
|
|
|
|
|
|
|
proc loadJwtSecretFile*(jwtSecretFile: InputFile): Result[seq[byte], cstring] =
|
|
|
|
try:
|
|
|
|
let lines = readLines(string jwtSecretFile, 1)
|
|
|
|
if lines.len > 0:
|
|
|
|
parseJwtTokenValue(lines[0])
|
|
|
|
else:
|
|
|
|
err("The JWT token file should not be empty")
|
|
|
|
except IOError:
|
|
|
|
err("couldn't open specified JWT secret file")
|
|
|
|
except ValueError:
|
|
|
|
err("invalid JWT hex string")
|
|
|
|
|
2022-02-27 16:55:02 +00:00
|
|
|
proc checkJwtSecret*(
|
2023-11-27 14:48:29 +00:00
|
|
|
rng: var HmacDrbgContext, dataDir: string, jwtSecret: Opt[InputFile]):
|
2022-02-27 16:55:02 +00:00
|
|
|
Result[seq[byte], cstring] =
|
|
|
|
# If such a parameter is given, but the file cannot be read, or does not
|
2022-08-26 10:44:50 +00:00
|
|
|
# contain a hex-encoded key of 256 bits, the client should treat this as an
|
|
|
|
# error: either abort the startup, or show error and continue without
|
|
|
|
# exposing the authenticated port.
|
2022-02-27 16:55:02 +00:00
|
|
|
if jwtSecret.isNone:
|
|
|
|
# If such a parameter is not given, the client SHOULD generate such a
|
2023-02-01 18:49:36 +00:00
|
|
|
# token, valid for the duration of the execution, and store the
|
2022-02-27 16:55:02 +00:00
|
|
|
# hex-encoded secret as a jwt.hex file on the filesystem. This file can
|
|
|
|
# then be used to provision the counterpart client.
|
|
|
|
#
|
2023-04-17 20:11:28 +00:00
|
|
|
# https://github.com/ethereum/execution-apis/blob/v1.0.0-beta.3/src/engine/authentication.md#key-distribution
|
2022-02-27 16:55:02 +00:00
|
|
|
const jwtSecretFilename = "jwt.hex"
|
|
|
|
let jwtSecretPath = dataDir / jwtSecretFilename
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
let newSecret = rng.generateBytes(JWT_SECRET_LEN)
|
2022-02-27 16:55:02 +00:00
|
|
|
try:
|
|
|
|
writeFile(jwtSecretPath, newSecret.to0xHex())
|
2022-03-31 14:43:05 +00:00
|
|
|
except IOError as exc:
|
2022-02-27 16:55:02 +00:00
|
|
|
# Allow continuing to run, though this is effectively fatal for a merge
|
|
|
|
# client using authentication. This keeps it lower-risk initially.
|
|
|
|
warn "Could not write JWT secret to data directory",
|
2022-03-31 14:43:05 +00:00
|
|
|
jwtSecretPath,
|
|
|
|
err = exc.msg
|
2022-02-27 16:55:02 +00:00
|
|
|
return ok(newSecret)
|
|
|
|
|
2023-03-05 01:40:21 +00:00
|
|
|
loadJwtSecretFile(jwtSecret.get)
|