nimbus-eth1/nimbus/rpc/jwt_auth.nim
Jacek Sieka 5fc4c13ab1
Increase JSON-RPC limits, bump json-rpc (#2759)
* 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 :)
2024-10-23 10:26:56 +02:00

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
# ------------------------------------------------------------------------------