JWT support (#3561)

This commit is contained in:
tersec 2022-03-31 16:43:05 +02:00 committed by GitHub
parent f89c604fb0
commit a18b39c9c1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 123 additions and 57 deletions

View File

@ -497,10 +497,16 @@ type
proposerBoosting* {.
hidden
desc: "Enable proposer boosting; temporary option feature gate (debugging; option will be removed)",
desc: "Enable proposer boosting; temporary option feature gate (debugging; option may be removed without warning)",
defaultValue: false
name: "proposer-boosting-debug" }: bool
useJwt* {.
hidden
desc: "Enable JWT authentication headers; temporary option feature gate (debugging; option may be remove without warning)",
defaultValue: false
name: "use-jwt-debug" }: bool
# https://github.com/ethereum/consensus-specs/blob/v1.1.10/sync/optimistic.md#fork-choice-poisoning
safeSlotsToImportOptimistically* {.
hidden

View File

@ -23,6 +23,9 @@ import
".."/[beacon_chain_db, beacon_node_status, beacon_clock],
./merkle_minimal
from std/times import getTime, inSeconds, initTime, `-`
from ../spec/engine_authentication import getSignedIatToken
export
web3Types, deques
@ -107,7 +110,7 @@ type
eth1Network: Option[Eth1Network]
depositContractAddress*: Eth1Address
forcePolling: bool
jwtSecret: seq[byte]
jwtSecret: Option[seq[byte]]
dataProvider: Web3DataProviderRef
latestEth1Block: Option[FullBlockId]
@ -117,6 +120,7 @@ type
runFut: Future[void]
stopFut: Future[void]
getBeaconTime: GetBeaconTimeFn
when hasGenesisDetection:
genesisValidators: seq[ImmutableValidatorData]
@ -862,13 +866,28 @@ proc getBlockProposalData*(chain: var Eth1Chain,
template getBlockProposalData*(m: Eth1Monitor,
state: ForkedHashedBeaconState,
finalizedEth1Data: Eth1Data,
finalizedStateDepositIndex: uint64): BlockProposalEth1Data =
getBlockProposalData(m.depositsChain, state, finalizedEth1Data, finalizedStateDepositIndex)
finalizedStateDepositIndex: uint64):
BlockProposalEth1Data =
getBlockProposalData(
m.depositsChain, state, finalizedEth1Data, finalizedStateDepositIndex)
proc getJsonRpcRequestHeaders(jwtSecret: Option[seq[byte]]):
auto =
if jwtSecret.isSome:
let secret = jwtSecret.get
(proc(): seq[(string, string)] =
# https://www.rfc-editor.org/rfc/rfc6750#section-6.1.1
@[("Authorization", "Bearer " & getSignedIatToken(
secret, (getTime() - initTime(0, 0)).inSeconds))])
else:
(proc(): seq[(string, string)] = @[])
proc new*(T: type Web3DataProvider,
depositContractAddress: Eth1Address,
web3Url: string): Future[Result[Web3DataProviderRef, string]] {.async.} =
let web3Fut = newWeb3(web3Url)
web3Url: string,
jwtSecret: Option[seq[byte]]):
Future[Result[Web3DataProviderRef, string]] {.async.} =
let web3Fut = newWeb3(web3Url, getJsonRpcRequestHeaders(jwtSecret))
yield web3Fut or sleepAsync(chronos.seconds(10))
if (not web3Fut.finished) or web3Fut.failed:
await cancelAndWait(web3Fut)
@ -905,10 +924,12 @@ proc init*(T: type Eth1Chain, cfg: RuntimeConfig, db: BeaconChainDB): T =
proc createInitialDepositSnapshot*(
depositContractAddress: Eth1Address,
depositContractDeployedAt: BlockHashOrNumber,
web3Url: string): Future[Result[DepositContractSnapshot, string]] {.async.} =
web3Url: string,
jwtSecret: Option[seq[byte]]): Future[Result[DepositContractSnapshot, string]]
{.async.} =
let dataProviderRes =
await Web3DataProvider.new(depositContractAddress, web3Url)
await Web3DataProvider.new(depositContractAddress, web3Url, jwtSecret)
if dataProviderRes.isErr:
return err(dataProviderRes.error)
var dataProvider = dataProviderRes.get
@ -929,11 +950,12 @@ proc createInitialDepositSnapshot*(
proc init*(T: type Eth1Monitor,
cfg: RuntimeConfig,
db: BeaconChainDB,
getBeaconTime: GetBeaconTimeFn,
web3Urls: seq[string],
depositContractSnapshot: Option[DepositContractSnapshot],
eth1Network: Option[Eth1Network],
forcePolling: bool,
jwtSecret: seq[byte]): T =
jwtSecret: Option[seq[byte]]): T =
doAssert web3Urls.len > 0
var web3Urls = web3Urls
for url in mitems(web3Urls):
@ -945,6 +967,7 @@ proc init*(T: type Eth1Monitor,
T(state: Initialized,
depositsChain: Eth1Chain.init(cfg, db),
depositContractAddress: cfg.DEPOSIT_CONTRACT_ADDRESS,
getBeaconTime: getBeaconTime,
web3Urls: web3Urls,
eth1Network: eth1Network,
eth1Progress: newAsyncEvent(),
@ -973,7 +996,8 @@ proc detectPrimaryProviderComingOnline(m: Eth1Monitor) {.async.} =
while m.runFut == initialRunFut:
let tempProviderRes = await Web3DataProvider.new(
m.depositContractAddress,
web3Url)
web3Url,
m.jwtSecret)
if tempProviderRes.isErr:
await sleepAsync(checkInterval)
@ -1007,7 +1031,8 @@ proc ensureDataProvider*(m: Eth1Monitor) {.async.} =
inc m.startIdx
m.dataProvider = block:
let v = await Web3DataProvider.new(m.depositContractAddress, web3Url)
let v = await Web3DataProvider.new(
m.depositContractAddress, web3Url, m.jwtSecret)
if v.isErr():
raise (ref CatchableError)(msg: v.error())
v.get()
@ -1382,8 +1407,10 @@ proc start(m: Eth1Monitor, delayBeforeStart: Duration) {.gcsafe.} =
proc start*(m: Eth1Monitor) =
m.start(0.seconds)
proc getEth1BlockHash*(url: string, blockId: RtBlockIdentifier): Future[BlockHash] {.async.} =
let web3 = await newWeb3(url)
proc getEth1BlockHash*(
url: string, blockId: RtBlockIdentifier, jwtSecret: Option[seq[byte]]):
Future[BlockHash] {.async.} =
let web3 = await newWeb3(url, getJsonRpcRequestHeaders(jwtSecret))
try:
let blk = awaitWithRetries(
web3.provider.eth_getBlockByNumber(blockId, false))
@ -1392,7 +1419,8 @@ proc getEth1BlockHash*(url: string, blockId: RtBlockIdentifier): Future[BlockHas
await web3.close()
proc testWeb3Provider*(web3Url: Uri,
depositContractAddress: Eth1Address) {.async.} =
depositContractAddress: Eth1Address,
jwtSecret: Option[seq[byte]]) {.async.} =
template mustSucceed(action: static string, expr: untyped): untyped =
try: expr
except CatchableError as err:
@ -1401,7 +1429,8 @@ proc testWeb3Provider*(web3Url: Uri,
let
web3 = mustSucceed "connect to web3 provider":
await newWeb3($web3Url)
await newWeb3(
$web3Url, getJsonRpcRequestHeaders(jwtSecret))
networkVersion = mustSucceed "get network version":
awaitWithRetries web3.provider.net_version()
latestBlock = mustSucceed "get latest block":

View File

@ -405,6 +405,15 @@ proc init*(T: type BeaconNode,
fatal "--finalized-checkpoint-block cannot be specified without --finalized-checkpoint-state"
quit 1
let jwtSecret = rng[].checkJwtSecret(string(config.dataDir), config.jwtSecret)
if jwtSecret.isErr:
fatal "Specified a JWT secret file which couldn't be loaded",
err = jwtSecret.error
quit 1
# The JWT secret created always exists, it just might not always be used
let optJwtSecret = if config.useJwt: some jwtSecret.get else: none(seq[byte])
template getDepositContractSnapshot: auto =
if depositContractSnapshot.isSome:
depositContractSnapshot
@ -412,7 +421,8 @@ proc init*(T: type BeaconNode,
let snapshotRes = waitFor createInitialDepositSnapshot(
cfg.DEPOSIT_CONTRACT_ADDRESS,
depositContractDeployedAt,
config.web3Urls[0])
config.web3Urls[0],
optJwtSecret)
if snapshotRes.isErr:
fatal "Failed to locate the deposit contract deployment block",
depositContract = cfg.DEPOSIT_CONTRACT_ADDRESS,
@ -423,12 +433,6 @@ proc init*(T: type BeaconNode,
else:
none(DepositContractSnapshot)
let jwtSecret = rng[].checkJwtSecret(string(config.dataDir), config.jwtSecret)
if jwtSecret.isErr:
fatal "Specified a JWT secret file which couldn't be loaded",
err = jwtSecret.error
quit 1
var eth1Monitor: Eth1Monitor
if not ChainDAGRef.isInitialized(db).isOk():
var
@ -453,11 +457,12 @@ proc init*(T: type BeaconNode,
let eth1Monitor = Eth1Monitor.init(
cfg,
db,
nil,
config.web3Urls,
getDepositContractSnapshot(),
eth1Network,
config.web3ForcePolling,
jwtSecret.get)
optJwtSecret)
eth1Monitor.loadPersistedDeposits()
@ -539,6 +544,7 @@ proc init*(T: type BeaconNode,
validatorMonitor, networkGenesisValidatorsRoot)
beaconClock = BeaconClock.init(
getStateField(dag.headState, genesis_time))
getBeaconTime = beaconClock.getBeaconTimeFn()
if config.weakSubjectivityCheckpoint.isSome:
dag.checkWeakSubjectivityCheckpoint(
@ -548,11 +554,12 @@ proc init*(T: type BeaconNode,
eth1Monitor = Eth1Monitor.init(
cfg,
db,
getBeaconTime,
config.web3Urls,
getDepositContractSnapshot(),
eth1Network,
config.web3ForcePolling,
jwtSecret.get)
optJwtSecret)
let rpcServer = if config.rpcEnabled:
RpcServer.init(config.rpcAddress, config.rpcPort)
@ -612,7 +619,6 @@ proc init*(T: type BeaconNode,
netKeys = getPersistentNetKeys(rng[], config)
nickname = if config.nodeName == "auto": shortForm(netKeys)
else: config.nodeName
getBeaconTime = beaconClock.getBeaconTimeFn()
network = createEth2Node(
rng, config, netKeys, cfg, dag.forkDigests, getBeaconTime,
getStateField(dag.headState, genesis_validators_root))
@ -1742,11 +1748,22 @@ proc doCreateTestnet*(config: BeaconNodeConf, rng: var BrHmacDrbgContext) {.rais
for i in 0 ..< launchPadDeposits.len:
deposits.add(launchPadDeposits[i] as DepositData)
let jwtSecret = rng.checkJwtSecret(string(config.dataDir), config.jwtSecret)
if jwtSecret.isErr:
fatal "Specified a JWT secret file which couldn't be loaded",
err = jwtSecret.error
quit 1
let
startTime = uint64(times.toUnix(times.getTime()) + config.genesisOffset)
outGenesis = config.outputGenesis.string
eth1Hash = if config.web3Urls.len == 0: eth1BlockHash
else: (waitFor getEth1BlockHash(config.web3Urls[0], blockId("latest"))).asEth2Digest
else: (waitFor getEth1BlockHash(
config.web3Urls[0], blockId("latest"),
if config.useJwt:
some jwtSecret.get
else:
none(seq[byte]))).asEth2Digest
cfg = getRuntimeConfig(config.eth2Network)
var
initialState = newClone(initialize_beacon_state_from_eth1(
@ -1819,12 +1836,25 @@ proc doRecord(config: BeaconNodeConf, rng: var BrHmacDrbgContext) {.
of RecordCmd.print:
echo $config.recordPrint
proc doWeb3Cmd(config: BeaconNodeConf) {.raises: [Defect, CatchableError].} =
proc doWeb3Cmd(config: BeaconNodeConf, rng: var BrHmacDrbgContext)
{.raises: [Defect, CatchableError].} =
case config.web3Cmd:
of Web3Cmd.test:
let metadata = config.loadEth2Network()
let
metadata = config.loadEth2Network()
jwtSecret = rng.checkJwtSecret(string(config.dataDir), config.jwtSecret)
if jwtSecret.isErr:
fatal "Specified a JWT secret file which couldn't be loaded",
err = jwtSecret.error
quit 1
waitFor testWeb3Provider(config.web3TestUrl,
metadata.cfg.DEPOSIT_CONTRACT_ADDRESS)
metadata.cfg.DEPOSIT_CONTRACT_ADDRESS,
if config.useJwt:
some jwtSecret.get
else:
none(seq[byte]))
proc doSlashingExport(conf: BeaconNodeConf) {.raises: [IOError, Defect].}=
let
@ -1889,7 +1919,7 @@ proc handleStartUpCmd(config: var BeaconNodeConf) {.raises: [Defect, CatchableEr
of BNStartUpCmd.deposits: doDeposits(config, rng[])
of BNStartUpCmd.wallets: doWallets(config, rng[])
of BNStartUpCmd.record: doRecord(config, rng[])
of BNStartUpCmd.web3: doWeb3Cmd(config)
of BNStartUpCmd.web3: doWeb3Cmd(config, rng[])
of BNStartUpCmd.slashingdb: doSlashingInterchange(config)
of BNStartupCmd.trustedNodeSync:
let

View File

@ -19,7 +19,7 @@ proc base64urlEncode(x: auto): string =
# encoding quirks.
base64.encode(x, safe = true).replace("=", "")
func getIatToken*(time: uint64): JsonNode =
func getIatToken*(time: int64): JsonNode =
# https://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."
@ -46,7 +46,7 @@ proc getSignedToken*(key: openArray[byte], payload: string): string =
signingInput & "." & base64_urlencode(sha256.hmac(key, signingInput).data)
proc getSignedIatToken*(key: openArray[byte], time: uint64): string =
proc getSignedIatToken*(key: openArray[byte], time: int64): string =
getSignedToken(key, $getIatToken(time))
proc checkJwtSecret*(
@ -74,11 +74,12 @@ proc checkJwtSecret*(
rng.brHmacDrbgGenerate(newSecret)
try:
writeFile(jwtSecretPath, newSecret.to0xHex())
except IOError as e:
except IOError as exc:
# 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
jwtSecretPath,
err = exc.msg
return ok(newSecret)
try:

View File

@ -15,27 +15,27 @@ import
suite "engine API authentication":
test "getIatToken":
check:
$getIatToken(0) == "{\"iat\":0}"
$getIatToken(1) == "{\"iat\":1}"
$getIatToken(2) == "{\"iat\":2}"
$getIatToken(14) == "{\"iat\":14}"
$getIatToken(60) == "{\"iat\":60}"
$getIatToken(95) == "{\"iat\":95}"
$getIatToken(487) == "{\"iat\":487}"
$getIatToken(529) == "{\"iat\":529}"
$getIatToken(666) == "{\"iat\":666}"
$getIatToken(2669) == "{\"iat\":2669}"
$getIatToken(6082) == "{\"iat\":6082}"
$getIatToken(6234) == "{\"iat\":6234}"
$getIatToken(230158) == "{\"iat\":230158}"
$getIatToken(675817) == "{\"iat\":675817}"
$getIatToken(695159) == "{\"iat\":695159}"
$getIatToken(19257188) == "{\"iat\":19257188}"
$getIatToken(52639657) == "{\"iat\":52639657}"
$getIatToken(71947005) == "{\"iat\":71947005}"
$getIatToken(1169144470) == "{\"iat\":1169144470}"
$getIatToken(2931679730'u64) == "{\"iat\":2931679730}"
$getIatToken(3339327695'u64) == "{\"iat\":3339327695}"
$getIatToken(0) == "{\"iat\":0}"
$getIatToken(1) == "{\"iat\":1}"
$getIatToken(2) == "{\"iat\":2}"
$getIatToken(14) == "{\"iat\":14}"
$getIatToken(60) == "{\"iat\":60}"
$getIatToken(95) == "{\"iat\":95}"
$getIatToken(487) == "{\"iat\":487}"
$getIatToken(529) == "{\"iat\":529}"
$getIatToken(666) == "{\"iat\":666}"
$getIatToken(2669) == "{\"iat\":2669}"
$getIatToken(6082) == "{\"iat\":6082}"
$getIatToken(6234) == "{\"iat\":6234}"
$getIatToken(230158) == "{\"iat\":230158}"
$getIatToken(675817) == "{\"iat\":675817}"
$getIatToken(695159) == "{\"iat\":695159}"
$getIatToken(19257188) == "{\"iat\":19257188}"
$getIatToken(52639657) == "{\"iat\":52639657}"
$getIatToken(71947005) == "{\"iat\":71947005}"
$getIatToken(1169144470) == "{\"iat\":1169144470}"
$getIatToken(2931679730) == "{\"iat\":2931679730}"
$getIatToken(3339327695) == "{\"iat\":3339327695}"
test "HS256 JWS signing":
let secret = mapIt("secret", byte(it))
@ -71,5 +71,5 @@ suite "engine API authentication":
getSignedIatToken(secret, 52639657) == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjUyNjM5NjU3fQ.O0cI2U_kEW1MbWyXcAh146mRU2CwzMNegAQit_1-TNU"
getSignedIatToken(secret, 71947005) == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjcxOTQ3MDA1fQ.pQwPWxMHzWGvTTfRfWKiGX8qEI2NcZbnB3ruh4Wcftg"
getSignedIatToken(secret, 1169144470) == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjExNjkxNDQ0NzB9.5JS0pVVh1g8hxO_PDQpwCvFnh1tdRtodpALXU1xol4I"
getSignedIatToken(secret, 2931679730'u64) == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjI5MzE2Nzk3MzB9.0ZR8DiVy6Y_pOleGC9Ti3M8ShtH5hyCBhceO1C2OTj0"
getSignedIatToken(secret, 3339327695'u64) == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjMzMzkzMjc2OTV9.ZRYaNrsvcIzppVeNorYUgEmVXcwOOQbqPlCQcoAaO4k"
getSignedIatToken(secret, 2931679730) == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjI5MzE2Nzk3MzB9.0ZR8DiVy6Y_pOleGC9Ti3M8ShtH5hyCBhceO1C2OTj0"
getSignedIatToken(secret, 3339327695) == "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjMzMzkzMjc2OTV9.ZRYaNrsvcIzppVeNorYUgEmVXcwOOQbqPlCQcoAaO4k"