2022-04-06 14:11:13 +00:00
|
|
|
# Nimbus
|
Core db and aristo updates for destructor and tx logic (#1894)
* Disable `TransactionID` related functions from `state_db.nim`
why:
Functions `getCommittedStorage()` and `updateOriginalRoot()` from
the `state_db` module are nowhere used. The emulation of a legacy
`TransactionID` type functionality is administratively expensive to
provide by `Aristo` (the legacy DB version is only partially
implemented, anyway).
As there is no other place where `TransactionID`s are used, they will
not be provided by the `Aristo` variant of the `CoreDb`. For the
legacy DB API, nothing will change.
* Fix copyright headers in source code
* Get rid of compiler warning
* Update Aristo code, remove unused `merge()` variant, export `hashify()`
why:
Adapt to upcoming `CoreDb` wrapper
* Remove synced tx feature from `Aristo`
why:
+ This feature allowed to synchronise transaction methods like begin,
commit, and rollback for a group of descriptors.
+ The feature is over engineered and not needed for `CoreDb`, neither
is it complete (some convergence features missing.)
* Add debugging helpers to `Kvt`
also:
Update database iterator, add count variable yield argument similar
to `Aristo`.
* Provide optional destructors for `CoreDb` API
why;
For the upcoming Aristo wrapper, this allows to control when certain
smart destruction and update can take place. The auto destructor works
fine in general when the storage/cache strategy is known and acceptable
when creating descriptors.
* Add update option for `CoreDb` API function `hash()`
why;
The hash function is typically used to get the state root of the MPT.
Due to lazy hashing, this might be not available on the `Aristo` DB.
So the `update` function asks for re-hashing the gurrent state changes
if needed.
* Update API tracking log mode: `info` => `debug
* Use shared `Kvt` descriptor in new Ledger API
why:
No need to create a new descriptor all the time
2023-11-16 19:35:03 +00:00
|
|
|
# Copyright (c) 2022-2023 Status Research & Development GmbH
|
2022-04-06 14:11:13 +00:00
|
|
|
# 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
|
2023-01-31 01:32:17 +00:00
|
|
|
std/[base64, json, options, os, strutils, times],
|
2022-04-06 14:11:13 +00:00
|
|
|
../nimbus/config,
|
|
|
|
../nimbus/rpc/jwt_auth,
|
|
|
|
./replay/pp,
|
|
|
|
chronicles,
|
2022-07-18 04:35:50 +00:00
|
|
|
chronos/apps/http/httpclient as chronoshttpclient,
|
|
|
|
chronos/apps/http/httptable,
|
2022-04-06 14:11:13 +00:00
|
|
|
eth/[common, keys, p2p],
|
|
|
|
json_rpc/rpcserver,
|
|
|
|
nimcrypto/[hmac, utils],
|
|
|
|
stew/results,
|
|
|
|
stint,
|
2022-07-18 04:35:50 +00:00
|
|
|
unittest2,
|
|
|
|
graphql,
|
|
|
|
graphql/[httpserver, httpclient]
|
2022-04-06 14:11:13 +00:00
|
|
|
|
|
|
|
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)
|
2022-04-08 04:54:11 +00:00
|
|
|
sData & "." & sha256.hmac(key, sData).data.base64urlEncode
|
2022-04-06 14:11:13 +00:00
|
|
|
|
|
|
|
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)
|
2022-04-08 04:54:11 +00:00
|
|
|
sData & "." & sha256.hmac(key, sData).data.base64urlEncode
|
2022-04-06 14:11:13 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2022-07-18 04:35:50 +00:00
|
|
|
proc createServer(serverAddress: TransportAddress, authHooks: seq[HttpAuthHook] = @[]): GraphqlHttpServerRef =
|
|
|
|
let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
|
|
|
|
var ctx = GraphqlRef.new()
|
|
|
|
|
|
|
|
const schema = """type Query {name: String}"""
|
|
|
|
let r = ctx.parseSchema(schema)
|
|
|
|
if r.isErr:
|
|
|
|
debugEcho r.error
|
|
|
|
return
|
|
|
|
|
|
|
|
let res = GraphqlHttpServerRef.new(
|
|
|
|
graphql = ctx,
|
|
|
|
address = serverAddress,
|
|
|
|
socketFlags = socketFlags,
|
|
|
|
authHooks = authHooks
|
|
|
|
)
|
|
|
|
|
|
|
|
if res.isErr():
|
|
|
|
debugEcho res.error
|
|
|
|
return
|
|
|
|
|
|
|
|
res.get()
|
|
|
|
|
|
|
|
proc setupClient(address: TransportAddress): GraphqlHttpClientRef =
|
|
|
|
GraphqlHttpClientRef.new(address, secure = false).get()
|
|
|
|
|
2022-04-06 14:11:13 +00:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# 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
|
2022-07-18 04:35:50 +00:00
|
|
|
authHook = secret.value.httpJwtAuth
|
|
|
|
|
|
|
|
const
|
|
|
|
serverAddress = initTAddress("127.0.0.1:8547")
|
|
|
|
query = """{ __type(name: "ID") { kind }}"""
|
2022-04-06 14:11:13 +00:00
|
|
|
|
|
|
|
suite "EngineAuth: Http/rpc authentication mechanics":
|
2022-07-18 04:35:50 +00:00
|
|
|
let server = createServer(serverAddress, @[authHook])
|
|
|
|
server.start()
|
2022-04-06 14:11:13 +00:00
|
|
|
|
|
|
|
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
|
2022-07-18 04:35:50 +00:00
|
|
|
let client = setupClient(serverAddress)
|
|
|
|
let res = waitFor client.sendRequest(query, req.toList)
|
|
|
|
check res.isOk
|
|
|
|
if res.isErr:
|
|
|
|
noisy.say "***", res.error
|
|
|
|
return
|
|
|
|
|
|
|
|
let resp = res.get()
|
|
|
|
check resp.status == 200
|
|
|
|
check resp.reason == "OK"
|
|
|
|
check resp.response == """{"data":{"__type":{"kind":"SCALAR"}}}"""
|
2022-04-06 14:11:13 +00:00
|
|
|
|
|
|
|
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
|
2022-07-18 04:35:50 +00:00
|
|
|
let client = setupClient(serverAddress)
|
|
|
|
let res = waitFor client.sendRequest(query, req.toList)
|
|
|
|
check res.isOk
|
|
|
|
if res.isErr:
|
|
|
|
noisy.say "***", res.error
|
|
|
|
return
|
|
|
|
|
|
|
|
let resp = res.get()
|
|
|
|
check resp.status == 200
|
|
|
|
check resp.reason == "OK"
|
|
|
|
check resp.response == """{"data":{"__type":{"kind":"SCALAR"}}}"""
|
2022-04-06 14:11:13 +00:00
|
|
|
|
|
|
|
setErrorLevel()
|
|
|
|
|
2022-07-18 04:35:50 +00:00
|
|
|
waitFor server.closeWait()
|
|
|
|
|
2022-04-06 14:11:13 +00:00
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# Main function(s)
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
proc jwtAuthMain*(noisy = defined(debug)) =
|
|
|
|
noisy.runKeyLoader
|
|
|
|
noisy.runJwtAuth
|
|
|
|
|
|
|
|
when isMainModule:
|
|
|
|
const
|
2022-04-07 09:37:35 +00:00
|
|
|
noisy = defined(debug)
|
2022-04-06 14:11:13 +00:00
|
|
|
|
|
|
|
setErrorLevel()
|
|
|
|
|
|
|
|
noisy.runKeyLoader
|
|
|
|
noisy.runJwtAuth
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------------------
|
|
|
|
# End
|
|
|
|
# ------------------------------------------------------------------------------
|