mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-01-15 14:54:10 +00:00
5fc4c13ab1
* blocks can be bigger than the default 1mb when json-rpc-encoded - this happens on sepolia for example * json-rpc bump improves debug logging and fixes a number of bugs * json-serialization bump fixes a crash on invalid arrays in json data At some point, it would probably be better to compute the maximum block size from actual block constraints, though this is somewhat tricky and depends on gas limits etc. Until then, 16mb should be plenty. With this, sepolia can be synced :)
283 lines
9.7 KiB
Nim
283 lines
9.7 KiB
Nim
# Nimbus
|
|
# Copyright (c) 2022-2024 Status Research & Development GmbH
|
|
# 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.
|
|
#
|
|
# Ackn:
|
|
# nimbus-eth2/beacon_chain/spec/engine_authentication.nim
|
|
# go-ethereum/node/jwt_handler.go
|
|
|
|
{.push gcsafe, raises: [].}
|
|
|
|
import
|
|
std/[base64, options, strutils, times],
|
|
bearssl/rand,
|
|
chronicles,
|
|
chronos,
|
|
chronos/apps/http/httptable,
|
|
chronos/apps/http/httpserver,
|
|
httputils,
|
|
nimcrypto/[hmac, sha2, utils],
|
|
stew/[byteutils, objects],
|
|
results,
|
|
../config,
|
|
./jwt_auth_helper,
|
|
./rpc_server
|
|
|
|
logScope:
|
|
topics = "Jwt/HS256 auth"
|
|
|
|
const
|
|
jwtSecretFile* = ##\
|
|
## A copy on the secret key in the `dataDir` directory
|
|
"jwt.hex"
|
|
|
|
jwtMinSecretLen* = ##\
|
|
## Number of bytes needed with the shared key
|
|
32
|
|
|
|
type
|
|
JwtSharedKey* = ##\
|
|
## Convenience type, needed quite often
|
|
distinct array[jwtMinSecretLen,byte]
|
|
|
|
JwtSharedKeyRaw =
|
|
array[jwtMinSecretLen,byte]
|
|
|
|
JwtGenSecret* = ##\
|
|
## Random generator function producing a shared key. Typically, this\
|
|
## will be a wrapper around a random generator type, such as\
|
|
## `HmacDrbgContext`.
|
|
proc(): JwtSharedKey {.gcsafe, raises: [CatchableError].}
|
|
|
|
JwtExcept* = object of CatchableError
|
|
## Catch and relay exception error
|
|
|
|
JwtError* = enum
|
|
jwtKeyTooSmall = "JWT secret not at least 256 bits"
|
|
jwtKeyEmptyFile = "no 0x-prefixed hex string found"
|
|
jwtKeyFileCannotOpen = "couldn't open specified JWT secret file"
|
|
jwtKeyInvalidHexString = "invalid JWT hex string"
|
|
|
|
jwtTokenInvNumSegments = "token contains an invalid number of segments"
|
|
jwtProtHeaderInvBase64 = "token protected header invalid base64 encoding"
|
|
jwtProtHeaderInvJson = "token protected header invalid JSON data"
|
|
jwtIatPayloadInvBase64 = "iat payload time invalid base64 encoding"
|
|
jwtIatPayloadInvJson = "iat payload time invalid JSON data"
|
|
jwtMethodUnsupported = "token protected header provides unsupported method"
|
|
jwtTimeValidationError = "token time validation failed"
|
|
jwtTokenValidationError = "token signature validation failed"
|
|
jwtCreationError = "Cannot create jwt secret"
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Private functions
|
|
# ------------------------------------------------------------------------------
|
|
|
|
proc base64urlEncode(x: auto): string =
|
|
# The only strings this gets are internally generated, and don't have
|
|
# encoding quirks.
|
|
base64.encode(x, safe = true).replace("=", "")
|
|
|
|
proc base64urlDecode(data: string): string
|
|
{.gcsafe, raises: [CatchableError].} =
|
|
## Decodes a JWT specific base64url, optionally encoding with stripped
|
|
## padding.
|
|
let l = data.len mod 4
|
|
if 0 < l:
|
|
return base64.decode(data & "=".repeat(4-l))
|
|
base64.decode(data)
|
|
|
|
proc verifyTokenHS256(token: string; key: JwtSharedKey): Result[void,JwtError] =
|
|
let p = token.split('.')
|
|
if p.len != 3:
|
|
return err(jwtTokenInvNumSegments)
|
|
|
|
var
|
|
time: int64
|
|
error: JwtError
|
|
try:
|
|
# Parse/verify protected header, try first the most common encoding
|
|
# of """{"typ": "JWT", "alg": "HS256"}"""
|
|
if p[0] != "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9":
|
|
error = jwtProtHeaderInvBase64
|
|
let jsonHeader = p[0].base64urlDecode
|
|
|
|
error = jwtProtHeaderInvJson
|
|
let jwtHeader = jsonHeader.decodeJwtHeader()
|
|
|
|
# The following JSON decoded object is required
|
|
if jwtHeader.typ != "JWT" and jwtHeader.alg != "HS256":
|
|
return err(jwtMethodUnsupported)
|
|
|
|
# Get the time payload
|
|
error = jwtIatPayloadInvBase64
|
|
let jsonPayload = p[1].base64urlDecode
|
|
|
|
error = jwtIatPayloadInvJson
|
|
let jwtPayload = jsonPayload.decodeJwtIatPayload()
|
|
time = jwtPayload.iat.int64
|
|
except CatchableError as e:
|
|
discard e
|
|
debug "JWT token decoding error",
|
|
protectedHeader = p[0],
|
|
payload = p[1],
|
|
msg = e.msg,
|
|
error
|
|
return err(error)
|
|
|
|
# github.com/ethereum/
|
|
# /execution-apis/blob/v1.0.0-beta.3/src/engine/authentication.md#jwt-claims
|
|
#
|
|
# "Required: iat (issued-at) claim. The EL SHOULD only accept iat timestamps
|
|
# which are within +-60 seconds from the current time."
|
|
#
|
|
# https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6 describes iat
|
|
# claims.
|
|
let delta = getTime().toUnix - time
|
|
if delta < -60 or 60 < delta:
|
|
debug "Iat timestamp problem, accepted |delta| <= 5",
|
|
delta
|
|
return err(jwtTimeValidationError)
|
|
|
|
let
|
|
keyArray = cast[array[jwtMinSecretLen,byte]](key)
|
|
b64sig = base64urlEncode(sha256.hmac(keyArray, p[0] & "." & p[1]).data)
|
|
if b64sig != p[2]:
|
|
return err(jwtTokenValidationError)
|
|
|
|
ok()
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public functions
|
|
# ------------------------------------------------------------------------------
|
|
|
|
proc fromHex*(key: var JwtSharedKey, src: string): Result[void,JwtError] =
|
|
## Parse argument `src` from hex-string and fill it into the argument `key`.
|
|
## This function is supposed to read and convert data in constant-time
|
|
## fashion, guarding against side channel attacks.
|
|
# utils.fromHex() does the constant-time job
|
|
try:
|
|
let secret = utils.fromHex(src)
|
|
if secret.len < jwtMinSecretLen:
|
|
return err(jwtKeyTooSmall)
|
|
key = toArray(JwtSharedKeyRaw.len, secret).JwtSharedKey
|
|
ok()
|
|
except ValueError:
|
|
err(jwtKeyInvalidHexString)
|
|
|
|
proc jwtGenSecret*(rng: ref rand.HmacDrbgContext): JwtGenSecret =
|
|
## Standard shared key random generator. If a fixed key is needed, a
|
|
## function like
|
|
## ::
|
|
## proc preCompiledGenSecret(key: JwtSharedKey): JwtGenSecret =
|
|
## result = proc: JwtSharedKey =
|
|
## key
|
|
##
|
|
## might do. Not that in most cases, this function is internally used,
|
|
## only.
|
|
result = proc: JwtSharedKey =
|
|
var data: array[jwtMinSecretLen,byte]
|
|
rng[].generate(data)
|
|
data.JwtSharedKey
|
|
|
|
proc jwtSharedSecret*(
|
|
rndSecret: JwtGenSecret;
|
|
config: NimbusConf;
|
|
): Result[JwtSharedKey, JwtError] =
|
|
## Return a key for jwt authentication preferable from the argument file
|
|
## `config.jwtSecret` (which contains at least 32 bytes hex encoded random
|
|
## data.) Otherwise it creates a key and stores it in the `config.dataDir`.
|
|
##
|
|
## The resulting `JwtSharedKey` is supposed to be usewd as argument for
|
|
## the function `jwtHandlerHS256()`, below.
|
|
##
|
|
## Note that this function variant is mainly used for debugging and testing.
|
|
## For a more common interface prototype with explicit random generator
|
|
## object see the variant below this one.
|
|
##
|
|
## Ackn nimbus-eth2:
|
|
## beacon_chain/spec/engine_authentication.nim.`checkJwtSecret()`
|
|
#
|
|
# If such a parameter is given, but the file cannot be read, or does not
|
|
# contain a hex-encoded key of at least 256 bits (aka ``jwtMinSecretLen`
|
|
# bytes.), the client should treat this as an error: either abort the
|
|
# startup, or show error and continue without exposing the authenticated
|
|
# port.
|
|
#
|
|
if config.jwtSecret.isNone:
|
|
# If such a parameter is not given, the client SHOULD generate such a
|
|
# token, valid for the duration of the execution, and store it the
|
|
# hex-encoded secret as a jwt.hex file on the filesystem. This file can
|
|
# then be used to provision the counterpart client.
|
|
#
|
|
# github.com/ethereum/
|
|
# /execution-apis/blob/v1.0.0-alpha.8/src/engine/
|
|
# /authentication.md#key-distribution
|
|
let jwtSecretPath = config.dataDir.string & "/" & jwtSecretFile
|
|
try:
|
|
let newSecret = rndSecret()
|
|
jwtSecretPath.writeFile(newSecret.JwtSharedKeyRaw.to0xHex)
|
|
notice "JWT secret generated", jwtSecretPath
|
|
return ok(newSecret)
|
|
except IOError as e:
|
|
# 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",
|
|
jwtSecretPath
|
|
discard e
|
|
except CatchableError:
|
|
return err(jwtCreationError)
|
|
|
|
try:
|
|
let lines = config.jwtSecret.get.string.readLines(1)
|
|
if lines.len == 0:
|
|
return err(jwtKeyEmptyFile)
|
|
var key: JwtSharedKey
|
|
let rc = key.fromHex(lines[0])
|
|
if rc.isErr:
|
|
return err(rc.error)
|
|
info "JWT secret loaded", jwtSecretPath = config.jwtSecret.get.string
|
|
return ok(key)
|
|
except IOError:
|
|
return err(jwtKeyFileCannotOpen)
|
|
except ValueError:
|
|
return err(jwtKeyInvalidHexString)
|
|
|
|
proc jwtSharedSecret*(rng: ref rand.HmacDrbgContext; config: NimbusConf):
|
|
Result[JwtSharedKey, JwtError] =
|
|
## Variant of `jwtSharedSecret()` with explicit random generator argument.
|
|
try:
|
|
rng.jwtGenSecret.jwtSharedSecret(config)
|
|
except CatchableError:
|
|
return err(jwtCreationError)
|
|
|
|
proc httpJwtAuth*(key: JwtSharedKey): RpcAuthHook =
|
|
proc handler(req: HttpRequestRef): Future[HttpResponseRef] {.async: (raises: [CatchableError]).} =
|
|
let auth = req.headers.getString("Authorization", "?")
|
|
if auth.len < 9 or auth[0..6].cmpIgnoreCase("Bearer ") != 0:
|
|
return await req.respond(Http403, "Missing authorization token")
|
|
|
|
let rc = auth[7..^1].strip.verifyTokenHS256(key)
|
|
if rc.isOk:
|
|
return HttpResponseRef(nil)
|
|
|
|
debug "Could not authenticate",
|
|
error = rc.error
|
|
|
|
case rc.error:
|
|
of jwtTokenValidationError, jwtMethodUnsupported:
|
|
return await req.respond(Http401, "Unauthorized access")
|
|
else:
|
|
return await req.respond(Http403, "Malformed token")
|
|
|
|
result = handler
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# End
|
|
# ------------------------------------------------------------------------------
|