Enable JWT authentication for websockets (#1039)

* Enable JWT authentication for websockets

details:
  Currently, this is optional and only enabled when the jwtsecret option
  is set.

  There is a default mechanism to generate a JWT secret if it is not
  explicitly stated. This mechanism is currently unused.

* Make JWT authentication compulsory for websockets

* Fix unit test entry point + cosmetics

* Update JSON-RPC link

* Improvements as suggested by Mamy
This commit is contained in:
Jordan Hrycaj 2022-04-06 15:11:13 +01:00 committed by GitHub
parent 203d8e8b70
commit 737236fd6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 687 additions and 20 deletions

View File

@ -403,6 +403,15 @@ type
defaultValueDesc: $RpcFlag.Eth
name: "ws-api" }: seq[string]
# github.com/ethereum/execution-apis/
# /blob/v1.0.0-alpha.8/src/engine/authentication.md#key-distribution
jwtSecret* {.
desc: "Path to a file containing a 32 byte hex-encoded shared secret" &
" needed for websocket authentication. By default, the secret key" &
" is auto-generated."
defaultValueDesc: "\"jwt.hex\" in the data directory (see --data-dir)"
name: "jwt-secret" .}: Option[InputFile]
graphqlEnabled* {.
desc: "Enable the GraphQL HTTP server"
defaultValue: false

View File

@ -11,19 +11,26 @@ import
../nimbus/vm_compile_info
import
os, strutils, net, options,
std/[os, strutils, net, options],
chronicles,
chronos,
eth/[keys, net/nat, trie/db],
eth/common as eth_common,
eth/p2p as eth_p2p,
eth/p2p/[peer_pool, rlpx_protocols/les_protocol],
json_rpc/rpcserver,
metrics,
metrics/[chronos_httpserver, chronicles_support],
stew/shims/net as stewNet,
eth/keys, db/[storage_types, db_chain, select_backend],
eth/common as eth_common, eth/p2p as eth_p2p,
chronos, json_rpc/rpcserver, chronicles,
eth/p2p/rlpx_protocols/les_protocol,
./sync/protocol_ethxx,
./p2p/blockchain_sync, eth/net/nat, eth/p2p/peer_pool,
websock/types as ws,
"."/[conf_utils, config, constants, context, genesis, sealer, utils, version],
./db/[storage_types, db_chain, select_backend],
./graphql/ethapi,
./p2p/[chain, blockchain_sync],
./p2p/clique/[clique_desc, clique_sealer],
config, genesis, rpc/[common, p2p, debug, engine_api], p2p/chain,
eth/trie/db, metrics, metrics/[chronos_httpserver, chronicles_support],
graphql/ethapi, context, utils/tx_pool,
"."/[conf_utils, sealer, constants, utils, version]
./rpc/[common, debug, engine_api, jwt_auth, p2p],
./sync/protocol_ethxx,
./utils/tx_pool
when defined(evmc_enabled):
import transaction/evmc_dynamic_loader
@ -154,7 +161,6 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
info "metrics", registry
discard setTimer(Moment.fromNow(conf.logMetricsInterval.seconds), logMetrics)
discard setTimer(Moment.fromNow(conf.logMetricsInterval.seconds), logMetrics)
# Creating RPC Server
if conf.rpcEnabled:
nimbus.rpcServer = newRpcHttpServer([initTAddress(conf.rpcAddress, conf.rpcPort)])
@ -175,9 +181,23 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
nimbus.rpcServer.start()
# Provide JWT authentication handler for websockets
let jwtHook = block:
# Create or load shared secret
let rc = nimbus.ctx.rng.jwtSharedSecret(conf)
if rc.isErr:
error "Failed create or load shared secret",
msg = $(rc.unsafeError) # avoid side effects
quit(QuitFailure)
# Authentcation handler constructor
@[rc.value.jwtAuthAsyHook]
# Creating Websocket RPC Server
if conf.wsEnabled:
nimbus.wsRpcServer = newRpcWebSocketServer(initTAddress(conf.wsAddress, conf.wsPort))
# Construct server object
nimbus.wsRpcServer = newRpcWebSocketServer(
initTAddress(conf.wsAddress, conf.wsPort),
authHandler = jwtHook)
setupCommonRpc(nimbus.ethNode, conf, nimbus.wsRpcServer)
# Enable Websocket RPC APIs based on RPC flags and protocol flags
@ -242,7 +262,8 @@ proc localServices(nimbus: NimbusNode, conf: NimbusConf,
if conf.engineApiWsEnabled:
if conf.engineApiWsPort != conf.wsPort:
nimbus.engineApiWsServer = newRpcWebSocketServer(
initTAddress(conf.engineApiWsAddress, conf.engineApiWsPort))
initTAddress(conf.engineApiWsAddress, conf.engineApiWsPort),
authHandler = jwtHook)
setupEngineAPI(nimbus.sealingEngine, nimbus.engineApiWsServer)
setupEthRpc(nimbus.ethNode, nimbus.ctx, chainDB, nimbus.txPool, nimbus.engineApiWsServer)
nimbus.engineApiWsServer.start()

337
nimbus/rpc/jwt_auth.nim Normal file
View File

@ -0,0 +1,337 @@
# Nimbus
# Copyright (c) 2022 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
import
std/[base64, json, options, os, strutils, times],
bearssl,
chronicles,
chronos,
chronos/apps/http/httptable,
httputils,
websock/types as ws,
nimcrypto/[hmac, utils],
stew/[byteutils, objects, results],
../config
{.push raises: [Defect].}
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
# -- currently unused --
#
#JwtAuthHandler* = ##\
# ## JSW authenticator prototype
# proc(req: HttpTable): Result[void,(HttpCode,string)]
# {.gcsafe, raises: [Defect].}
#
JwtAuthAsyHandler* = ##\
## Asynchroneous JSW authenticator prototype. This is the definition
## appicable for the `verify` entry of a `ws.Hook`.
proc(req: HttpTable): Future[Result[void,string]]
{.closure, gcsafe, raises: [Defect].}
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\
## `BrHmacDrbgContext`.
proc(): JwtSharedKey {.gcsafe.}
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"
JwtHeader = object ##\
## Template used for JSON unmarshalling
typ, alg: string
JwtIatPayload = object ##\
## Template used for JSON unmarshalling
iat: uint64
# ------------------------------------------------------------------------------
# Private functions
# ------------------------------------------------------------------------------
template safeExecutor(info: string; code: untyped) =
try:
code
except Exception as e:
raise newException(JwtExcept, info & "(): " & $e.name & " -- " & e.msg)
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: [Defect, 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.parseJson.to(JwtHeader)
# 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.parseJson.to(JwtIatPayload)
time = jwtPayload.iat.int64
except:
debug "JWT token decoding error",
protectedHeader = p[0],
payload = p[1],
error
return err(error)
# github.com/ethereum/
# /execution-apis/blob/v1.0.0-alpha.8/src/engine/authentication.md#jwt-claims
#
# "Required: iat (issued-at) claim. The EL SHOULD only accept iat timestamps
# which are within +-5 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 < -5 or 5 < 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
let secret = utils.fromHex(src)
if secret.len < jwtMinSecretLen:
return err(jwtKeyTooSmall)
key = toArray(JwtSharedKeyRaw.len, secret).JwtSharedKey
ok()
proc jwtGenSecret*(rng: ref BrHmacDrbgContext): 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[].brHmacDrbgGenerate(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
newSecret = rndSecret()
try:
jwtSecretPath.writeFile(newSecret.JwtSharedKeyRaw.to0xHex)
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
return ok(newSecret)
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)
return ok(key.JwtSharedKey)
except IOError:
return err(jwtKeyFileCannotOpen)
except ValueError:
return err(jwtKeyInvalidHexString)
proc jwtSharedSecret*(rng: ref BrHmacDrbgContext; config: NimbusConf):
Result[JwtSharedKey, JwtError]
{.gcsafe, raises: [Defect,JwtExcept].} =
## Variant of `jwtSharedSecret()` with explicit random generator argument.
safeExecutor("jwtSharedSecret"):
result = rng.jwtGenSecret.jwtSharedSecret(config)
# -- currently unused --
#
#proc jwtAuthHandler*(key: JwtSharedKey): JwtAuthHandler =
# ## Returns a JWT authentication handler that can be used with an HTTP header
# ## based call back system.
# ##
# ## The argument `key` is captured by the session handler for JWT
# ## authentication. The function `jwtSharedSecret()` provides such a key.
# result = proc(req: HttpTable): Result[void,(HttpCode,string)] =
# let auth = req.getString("Authorization","?")
# if auth.len < 9 or auth[0..6].cmpIgnoreCase("Bearer ") != 0:
# return err((Http403, "Missing Token"))
#
# let rc = auth[7..^1].strip.verifyTokenHS256(key)
# if rc.isOk:
# return ok()
#
# debug "Could not authenticate",
# error = rc.error
#
# case rc.error:
# of jwtTokenValidationError, jwtMethodUnsupported:
# return err((Http401, "Unauthorized"))
# else:
# return err((Http403, "Malformed Token"))
#
proc jwtAuthAsyHandler*(key: JwtSharedKey): JwtAuthAsyHandler =
## Returns an asynchroneous JWT authentication handler that can be used with
## an HTTP header based call back system.
##
## The argument `key` is captured by the session handler for JWT
## authentication. The function `jwtSharedSecret()` provides such a key.
result = proc(req: HttpTable): Future[Result[void,string]] {.async.} =
let auth = req.getString("Authorization","?")
if auth.len < 9 or auth[0..6].cmpIgnoreCase("Bearer ") != 0:
return err("Missing Token")
let rc = auth[7..^1].strip.verifyTokenHS256(key)
if rc.isOk:
return ok()
debug "Could not authenticate",
error = rc.error
case rc.error:
of jwtTokenValidationError, jwtMethodUnsupported:
return err("Unauthorized")
else:
return err("Malformed Token")
proc jwtAuthAsyHook*(key: JwtSharedKey): ws.Hook =
## Variant of `jwtAuthHandler()` (e.g. directly suitable for Json WebSockets.)
##
## Note that currently there is no meaningful way to send a http 401/403 in
## case of an authentication problem.
let handler = key.jwtAuthAsyHandler
ws.Hook(
append: proc(ctx: ws.Hook, req: var HttpTable): Result[void,string] =
ok(),
verify: proc(ctx: ws.Hook, req: HttpTable): Future[Result[void,string]] =
req.handler)
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------

View File

@ -13,6 +13,7 @@ cliBuilder:
import ./test_code_stream,
./test_accounts_cache,
./test_custom_network,
./test_jwt_auth,
./test_gas_meter,
./test_memory,
./test_stack,

View File

@ -128,13 +128,13 @@ const
# Helpers
# ------------------------------------------------------------------------------
proc findFilePath(file: string): string =
result = "?unknown?" / file
proc findFilePath(file: string): Result[string,void] =
for dir in baseDir:
for repo in repoDir:
let path = dir / repo / file
if path.fileExists:
return path
return ok(path)
err()
proc flushDbDir(s: string) =
if s != "":
@ -211,7 +211,7 @@ proc genesisLoadRunner(noisy = true;
let
gFileInfo = sSpcs.genesisFile.splitFile.name.split(".")[0]
gFilePath = sSpcs.genesisFile.findFilePath
gFilePath = sSpcs.genesisFile.findFilePath.value
tmpDir = if disablePersistentDB: "*notused*"
else: gFilePath.splitFile.dir / "tmp"
@ -288,7 +288,7 @@ proc testnetChainRunner(noisy = true;
stopAfterBlock = 999999999) =
let
cFileInfo = sSpcs.captures[0].splitFile.name.split(".")[0]
cFilePath = sSpcs.captures.mapIt(it.findFilePath)
cFilePath = sSpcs.captures.mapIt(it.findFilePath.value)
dbInfo = if memoryDB: "in-memory" else: "persistent"
pivotBlockNumber = sSpcs.failBlockAt.u256

297
tests/test_jwt_auth.nim Normal file
View File

@ -0,0 +1,297 @@
# Nimbus
# Copyright (c) 2018-2019 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
# http://www.apache.org/licenses/LICENSE-2.0)
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
# http://opensource.org/licenses/MIT)
# at your option. This file may not be copied, modified, or distributed except
# according to those terms.
## Test Jwt Authorisation Functionality
## ====================================
import
std/[base64, json, options, os, strutils, tables, times],
../nimbus/config,
../nimbus/rpc/jwt_auth,
./replay/pp,
confutils/defs,
chronicles,
chronos/apps/http/httpserver,
eth/[common, keys, p2p],
json_rpc/rpcserver,
nimcrypto/[hmac, utils],
stew/results,
stint,
unittest2
type
UnGuardedKey =
array[jwtMinSecretLen,byte]
const
jwtKeyFile ="jwtsecret.txt" # external shared secret file
jwtKeyStripped ="jwtstripped.txt" # without leading 0x
jwtKeyCopy = jwtSecretFile # file containing effective data key
baseDir = [".", "..", ".."/"..", $DirSep]
repoDir = [".", "tests" / "test_jwt_auth"]
let
fakeKey = block:
var rc: JwtSharedKey
discard rc.fromHex((0..31).mapIt(15 - (it mod 16)).mapIt(it.byte).toHex)
rc
# ------------------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------------------
proc findFilePath(file: string): Result[string,void] =
for dir in baseDir:
for repo in repoDir:
let path = dir / repo / file
if path.fileExists:
return ok(path)
err()
proc say*(noisy = false; pfx = "***"; args: varargs[string, `$`]) =
if noisy:
if args.len == 0:
echo "*** ", pfx
elif 0 < pfx.len and pfx[^1] != ' ':
echo pfx, " ", args.toSeq.join
else:
echo pfx, args.toSeq.join
proc setTraceLevel =
discard
when defined(chronicles_runtime_filtering) and loggingEnabled:
setLogLevel(LogLevel.TRACE)
proc setErrorLevel =
discard
when defined(chronicles_runtime_filtering) and loggingEnabled:
setLogLevel(LogLevel.ERROR)
# ------------------------------------------------------------------------------
# Private Functions
# ------------------------------------------------------------------------------
proc fakeGenSecret(fake: JwtSharedKey): JwtGenSecret =
## Key random generator, fake version
result = proc: JwtSharedKey =
fake
proc base64urlEncode(x: auto): string =
## from nimbus-eth2, engine_authentication.nim
base64.encode(x, safe = true).replace("=", "")
func getIatToken*(time: uint64): JsonNode =
## from nimbus-eth2, engine_authentication.nim
%* {"iat": time}
proc getSignedToken*(key: openArray[byte], payload: string): string =
## from nimbus-eth2, engine_authentication.nim
# Using hard coded string for """{"typ": "JWT", "alg": "HS256"}"""
let sData = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9." & base64urlEncode(payload)
sData & "." & sha256.hmac(key, sData).data.base64UrlEncode
proc getSignedToken2*(key: openArray[byte], payload: string): string =
## Variant of `getSignedToken()`: different algorithm encoding
let
jNode = %* {"alg": "HS256", "typ": "JWT" }
sData = base64urlEncode($jNode) & "." & base64urlEncode(payload)
sData & "." & sha256.hmac(key, sData).data.base64UrlEncode
proc getHttpAuthReqHeader(secret: JwtSharedKey; time: uint64): HttpTable =
let bearer = secret.UnGuardedKey.getSignedToken($getIatToken(time))
result.add("aUtHoRiZaTiOn", "Bearer " & bearer)
proc getHttpAuthReqHeader2(secret: JwtSharedKey; time: uint64): HttpTable =
let bearer = secret.UnGuardedKey.getSignedToken2($getIatToken(time))
result.add("aUtHoRiZaTiOn", "Bearer " & bearer)
# ------------------------------------------------------------------------------
# Test Runners
# ------------------------------------------------------------------------------
proc runKeyLoader(noisy = true;
keyFile = jwtKeyFile; strippedFile = jwtKeyStripped) =
let
filePath = keyFile.findFilePath.value
fileInfo = keyFile.splitFile.name.split(".")[0]
strippedPath = strippedFile.findFilePath.value
strippedInfo = strippedFile.splitFile.name.split(".")[0]
dataDir = filePath.splitPath.head
localKeyFile = dataDir / jwtKeyCopy
dataDirCmdOpt = &"--data-dir={dataDir}"
jwtSecretCmdOpt = &"--jwt-secret={filePath}"
jwtStrippedCmdOpt = &"--jwt-secret={strippedPath}"
suite "EngineAuth: Load or generate shared secrets":
test &"Load shared key file {fileInfo}":
let
config = @[dataDirCmdOpt,jwtSecretCmdOpt].makeConfig
secret = fakeKey.fakeGenSecret.jwtSharedSecret(config)
lines = config.jwtSecret.get.string.readLines(1)
check secret.isOk
check 0 < lines.len
let
hexKey = "0x" & secret.value.UnGuardedKey.toHex
hexFake = "0x" & fakeKey.UnGuardedKey.toSeq.toHex
hexLine = lines[0].strip
noisy.say "***", "key=", hexKey
noisy.say " ", "text=", hexLine
noisy.say " ", "fake=", hexFake
# Compare key against tcontents of shared key file
check hexKey.cmpIgnoreCase(hexLine) == 0
# Just to make sure that there was no random generator used
check hexKey.cmpIgnoreCase(hexFake) != 0
test &"Load shared key file {strippedInfo}, missing 0x prefix":
let
config = @[dataDirCmdOpt,jwtStrippedCmdOpt].makeConfig
secret = fakeKey.fakeGenSecret.jwtSharedSecret(config)
lines = config.jwtSecret.get.string.readLines(1)
check secret.isOk
check 0 < lines.len
let
hexKey = secret.value.UnGuardedKey.toHex
hexFake = fakeKey.UnGuardedKey.toSeq.toHex
hexLine = lines[0].strip
noisy.say "***", "key=", hexKey
noisy.say " ", "text=", hexLine
noisy.say " ", "fake=", hexFake
# Compare key against tcontents of shared key file
check hexKey.cmpIgnoreCase(hexLine) == 0
# Just to make sure that there was no random generator used
check hexKey.cmpIgnoreCase(hexFake) != 0
test &"Generate shared key file, store it in {jwtKeyCopy}":
# Clean up after file generation
defer: localKeyFile.removeFile
# Maybe a stale left over
localKeyFile.removeFile
let
config = @[dataDirCmdOpt].makeConfig
secret = fakeKey.fakeGenSecret.jwtSharedSecret(config)
lines = localKeyFile.readLines(1)
check secret.isOk
let
hexKey = "0x" & secret.value.UnGuardedKey.toHex
hexLine = lines[0].strip
noisy.say "***", "key=", hexKey
noisy.say " ", "text=", hexLine
# Compare key against tcontents of shared key file
check hexKey.cmpIgnoreCase(hexLine) == 0
proc runJwtAuth(noisy = true; keyFile = jwtKeyFile) =
let
filePath = keyFile.findFilePath.value
fileInfo = keyFile.splitFile.name.split(".")[0]
dataDir = filePath.splitPath.head
dataDirCmdOpt = &"--data-dir={dataDir}"
jwtSecretCmdOpt = &"--jwt-secret={filePath}"
config = @[dataDirCmdOpt,jwtSecretCmdOpt].makeConfig
# The secret is just used for extracting the key, it would otherwise
# be hidden in the closure of the handler function
secret = fakeKey.fakeGenSecret.jwtSharedSecret(config)
# The wrapper contains the handler function with the captured shared key
asyHandler = secret.value.jwtAuthAsyHandler
suite "EngineAuth: Http/rpc authentication mechanics":
test &"JSW/HS256 authentication using shared secret file {fileInfo}":
# Just to make sure that we made a proper choice. Typically, all
# ingredients shoud have been tested, already in the preceeding test
# suite.
let
lines = config.jwtSecret.get.string.readLines(1)
hexKey = "0x" & secret.value.UnGuardedKey.toHex
hexLine = lines[0].strip
noisy.say "***", "key=", hexKey
noisy.say " ", "text=", hexLine
check hexKey.cmpIgnoreCase(hexLine) == 0
let
time = getTime().toUnix.uint64
req = secret.value.getHttpAuthReqHeader(time)
noisy.say "***", "request",
" Authorization=", req.getString("Authorization")
setTraceLevel()
# Run http authorisation request
let htCode = waitFor req.asyHandler
noisy.say "***", "result",
" htCode=", htCode
check htCode.isOk
setErrorLevel()
test &"JSW/HS256, ditto with protected header variant":
let
time = getTime().toUnix.uint64
req = secret.value.getHttpAuthReqHeader2(time)
# Assemble request header
noisy.say "***", "request",
" Authorization=", req.getString("Authorization")
setTraceLevel()
# Run http authorisation request
let htCode = waitFor req.asyHandler
noisy.say "***", "result",
" htCode=", htCode
check htCode.isOk
setErrorLevel()
# ------------------------------------------------------------------------------
# Main function(s)
# ------------------------------------------------------------------------------
proc jwtAuthMain*(noisy = defined(debug)) =
noisy.runKeyLoader
noisy.runJwtAuth
when isMainModule:
const
noisy = defined(debug) or true
setErrorLevel()
noisy.runKeyLoader
noisy.runJwtAuth
# ------------------------------------------------------------------------------
# End
# ------------------------------------------------------------------------------

View File

@ -0,0 +1 @@
0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

View File

@ -0,0 +1 @@
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

2
vendor/nim-json-rpc vendored

@ -1 +1 @@
Subproject commit b4bab89abdde3653939abe36ab9f6cae4aa1cbd1
Subproject commit d4ae2328d4247c59cefd8d5e0fbc3f178a0eb4ef