nimbus-eth2/beacon_chain/rpc/rest_key_management_api.nim

186 lines
7.5 KiB
Nim

# Copyright (c) 2021-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.
import std/[tables, os, strutils]
import chronos, chronicles, confutils,
stew/[base10, results, io2], bearssl, blscurve
import ".."/validators/slashing_protection
import ".."/[conf, version, filepath, beacon_node]
import ".."/spec/[keystore, crypto]
import ".."/rpc/rest_utils
import ".."/validators/[keystore_management, validator_pool]
import ".."/spec/eth2_apis/rest_keymanager_types
export
rest_utils,
results
proc listValidators*(validatorsDir,
secretsDir: string): seq[KeystoreInfo]
{.raises: [Defect].} =
var validators: seq[KeystoreInfo]
try:
for el in listLoadableKeystores(validatorsDir, secretsDir, true):
validators.add KeystoreInfo(validating_pubkey: el.pubkey,
derivation_path: el.path.string,
readonly: false)
except OSError as err:
error "Failure to list the validator directories",
validatorsDir, secretsDir, err = err.msg
validators
proc checkAuthorization*(request: HttpRequestRef,
node: BeaconNode): Result[void, AuthorizationError] =
let authorizations = request.headers.getList("authorization")
if authorizations.len > 0:
for authHeader in authorizations:
let parts = authHeader.split(' ', maxsplit = 1)
if parts.len == 2 and parts[0] == "Bearer":
if parts[1] == node.keymanagerToken.get:
return ok()
else:
return err incorrectToken
return err missingBearerScheme
else:
return err noAuthorizationHeader
proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) =
# https://ethereum.github.io/keymanager-APIs/#/Keymanager/ListKeys
router.api(MethodGet, "/api/eth/v1/keystores") do () -> RestApiResponse:
let authStatus = checkAuthorization(request, node)
if authStatus.isErr():
return RestApiResponse.jsonError(Http401, InvalidAuthorization,
$authStatus.error())
let response = GetKeystoresResponse(
data: listValidators(node.config.validatorsDir(),
node.config.secretsDir()))
return RestApiResponse.jsonResponsePlain(response)
# https://ethereum.github.io/keymanager-APIs/#/Keymanager/ImportKeystores
router.api(MethodPost, "/api/eth/v1/keystores") do (
contentBody: Option[ContentBody]) -> RestApiResponse:
let authStatus = checkAuthorization(request, node)
if authStatus.isErr():
return RestApiResponse.jsonError(Http401, InvalidAuthorization,
$authStatus.error())
let request =
block:
if contentBody.isNone():
return RestApiResponse.jsonError(Http404, EmptyRequestBodyError)
let dres = decodeBody(KeystoresAndSlashingProtection, contentBody.get())
if dres.isErr():
return RestApiResponse.jsonError(Http400, InvalidKeystoreObjects,
$dres.error())
dres.get()
let nodeSPDIR = toSPDIR(node.attachedValidators.slashingProtection)
if nodeSPDIR.metadata.genesis_validators_root.Eth2Digest !=
request.slashing_protection.metadata.genesis_validators_root.Eth2Digest:
return RestApiResponse.jsonError(
Http400,
"The slashing protection database and imported file refer to different blockchains.")
var
response: PostKeystoresResponse
inclRes = inclSPDIR(node.attachedValidators.slashingProtection,
request.slashing_protection)
if inclRes == siFailure:
return RestApiResponse.jsonError(
Http500,
"Internal server error; Failed to import slashing protection data")
for index, item in request.keystores.pairs():
let res = importKeystore(node.attachedValidators[], node.network.rng[],
node.config, item, request.passwords[index])
if res.isErr():
response.data.add(RequestItemStatus(status: $KeystoreStatus.error,
message: $res.error()))
elif res.value() == AddValidatorStatus.existingArtifacts:
response.data.add(RequestItemStatus(status: $KeystoreStatus.duplicate))
else:
response.data.add(RequestItemStatus(status: $KeystoreStatus.imported))
return RestApiResponse.jsonResponsePlain(response)
# https://ethereum.github.io/keymanager-APIs/#/Keymanager/DeleteKeys
router.api(MethodPost, "/api/eth/v1/keystores/delete") do (
contentBody: Option[ContentBody]) -> RestApiResponse:
let authStatus = checkAuthorization(request, node)
if authStatus.isErr():
return RestApiResponse.jsonError(Http401, InvalidAuthorization,
$authStatus.error())
let keys =
block:
if contentBody.isNone():
return RestApiResponse.jsonError(Http404, EmptyRequestBodyError)
let dres = decodeBody(DeleteKeystoresBody, contentBody.get())
if dres.isErr():
return RestApiResponse.jsonError(Http400, InvalidValidatorPublicKey,
$dres.error())
dres.get().pubkeys
var
response: DeleteKeystoresResponse
nodeSPDIR = toSPDIR(node.attachedValidators.slashingProtection)
# Hash table to keep the removal status of all keys form request
keysAndDeleteStatus = initTable[PubKeyBytes, RequestItemStatus]()
response.slashing_protection.metadata = nodeSPDIR.metadata
for index, key in keys.pairs():
let
res = removeValidator(node.attachedValidators[], node.config, key)
pubkey = key.blob.PubKey0x.PubKeyBytes
if res.isOk:
case res.value()
of RemoveValidatorStatus.deleted:
keysAndDeleteStatus.add(pubkey,
RequestItemStatus(status: $KeystoreStatus.deleted))
# At first all keys with status missing directory after removal receive status 'not_found'
of RemoveValidatorStatus.missingDir:
keysAndDeleteStatus.add(pubkey,
RequestItemStatus(status: $KeystoreStatus.notFound))
else:
keysAndDeleteStatus.add(pubkey,
RequestItemStatus(status: $KeystoreStatus.error,
message: $res.error()))
# If we discover slashing protection data for a validator that was not
# found, this means the validator was active in the past, so we must
# respond with `not_active`:
for validator in nodeSPDIR.data:
keysAndDeleteStatus.withValue(validator.pubkey.PubKeyBytes, value) do:
response.slashing_protection.data.add(validator)
if value.status == $KeystoreStatus.notFound:
value.status = $KeystoreStatus.notActive
for index, key in keys.pairs():
response.data.add(keysAndDeleteStatus[key.blob.PubKey0x.PubKeyBytes])
return RestApiResponse.jsonResponsePlain(response)
router.redirect(
MethodGet,
"/eth/v1/keystores",
"/api/eth/v1/keystores")
router.redirect(
MethodPost,
"/eth/v1/keystores",
"/api/eth/v1/keystores")
router.redirect(
MethodPost,
"/eth/v1/keystores/delete",
"/api/eth/v1/keystores/delete")