Validator key management API (#2755)

Implements https://github.com/ethereum/beacon-APIs/pull/151
This commit is contained in:
Eugene Kabanov 2021-10-04 22:08:31 +03:00 committed by GitHub
parent 05eb8846bf
commit 65257b82f8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 720 additions and 126 deletions

View File

@ -8,14 +8,15 @@
{.push raises: [Defect].}
import
std/[deques, intsets, streams, tables, hashes],
std/[deques, intsets, streams, tables, hashes, options],
stew/endians2,
./spec/datatypes/[phase0, altair],
./spec/keystore,
./consensus_object_pools/block_pools_types,
./fork_choice/fork_choice_types,
./validators/slashing_protection
export deques, tables, hashes, block_pools_types
export deques, tables, hashes, options, block_pools_types
const
ATTESTATION_LOOKBACK* =
@ -146,22 +147,27 @@ type
# Validator Pool
#
# #############################################
ValidatorKind* = enum
inProcess
remote
ValidatorKind* {.pure.} = enum
Local, Remote
ValidatorConnection* = object
inStream*: Stream
outStream*: Stream
pubKeyStr*: string
ValidatorPrivateItem* = object
privateKey*: ValidatorPrivKey
description*: Option[string]
path*: Option[KeyPath]
uuid*: Option[string]
version*: Option[uint64]
AttachedValidator* = ref object
pubKey*: ValidatorPubKey
case kind*: ValidatorKind
of inProcess:
privKey*: ValidatorPrivKey
else:
of ValidatorKind.Local:
data*: ValidatorPrivateItem
of ValidatorKind.Remote:
connection*: ValidatorConnection
# The index at which this validator has been observed in the chain -

View File

@ -312,6 +312,12 @@ type
defaultValueDesc: "127.0.0.1"
name: "rest-address" }: ValidIpAddress
validatorApiEnabled* {.
desc: "Enable the REST (BETA version) validator keystore management " &
"API",
defaultValue: false,
name: "validator-api"}: bool
inProcessValidators* {.
desc: "Disable the push model (the beacon node tells a signing process with the private keys of the validators what to sign and when) and load the validators in the beacon node itself"
defaultValue: true # the use of the nimbus_signing_process binary by default will be delayed until async I/O over stdin/stdout is developed for the child process.
@ -546,6 +552,12 @@ type
desc: "A directory containing validator keystore passwords"
name: "secrets-dir" }: Option[InputDir]
validatorApiEnabled* {.
desc: "Enable the REST (BETA version) validator keystore management " &
"API",
defaultValue: false,
name: "validator-api"}: bool
case cmd* {.
command
defaultValue: VCNoCommand }: VCStartUpCmd

View File

@ -1252,6 +1252,8 @@ proc installRestHandlers(restServer: RestServerRef, node: BeaconNode) =
restServer.router.installNimbusApiHandlers(node)
restServer.router.installNodeApiHandlers(node)
restServer.router.installValidatorApiHandlers(node)
if node.config.validatorApiEnabled:
restServer.router.installValidatorManagementHandlers(node)
proc installMessageValidators(node: BeaconNode) =
# https://github.com/ethereum/eth2.0-specs/blob/v1.0.1/specs/phase0/p2p-interface.md#attestations-and-aggregation
@ -1641,13 +1643,13 @@ proc handleValidatorExitCommand(config: BeaconNodeConf) {.async.} =
"key '" & validatorKeyAsStr & "'."
quit 1
let signingKey = loadKeystore(
let signingItem = loadKeystore(
validatorsDir,
config.secretsDir,
validatorKeyAsStr,
config.nonInteractive)
if signingKey.isNone:
if signingItem.isNone:
fatal "Unable to continue without decrypted signing key"
quit 1
@ -1669,8 +1671,11 @@ proc handleValidatorExitCommand(config: BeaconNodeConf) {.async.} =
epoch: exitAtEpoch,
validator_index: validatorIdx))
signedExit.signature = get_voluntary_exit_signature(
fork, genesisValidatorsRoot, signedExit.message, signingKey.get).toValidatorSig()
signedExit.signature =
block:
let key = signingItem.get().privateKey
get_voluntary_exit_signature(fork, genesisValidatorsRoot,
signedExit.message, key).toValidatorSig()
template ask(prompt: string): string =
try:

View File

@ -7,23 +7,20 @@
{.push raises: [Defect].}
import
# Standard library
std/[os, strutils, tables],
# Local modules
./spec/[digest, crypto],
./validators/keystore_management
import std/[os, strutils, tables]
import "."/spec/[digest, crypto],
"."/validators/keystore_management,
"."/beacon_node_types
{.pop.} # TODO moduletests exceptions
programMain:
var validators: Table[ValidatorPubKey, ValidatorPrivKey]
var validators: Table[ValidatorPubKey, ValidatorPrivateItem]
# load and send all public keys so the BN knows for which ones to ping us
doAssert paramCount() == 2
for curr in validatorKeysFromDirs(paramStr(1), paramStr(2)):
validators[curr.toPubKey.toPubKey()] = curr
echo curr.toPubKey
validators[curr.privateKey.toPubKey().toPubKey()] = curr
echo curr.privateKey.toPubKey
echo "end"
# simple format: `<pubkey> <eth2digest_to_sign>` => `<signature>`
@ -31,6 +28,7 @@ programMain:
let args = stdin.readLine.split(" ")
doAssert args.len == 2
let privKey = validators[ValidatorPubKey.fromHex(args[0]).get()]
let item = validators[ValidatorPubKey.fromHex(args[0]).get()]
echo blsSign(privKey, Eth2Digest.fromHex(args[1]).data).toValidatorSig()
echo blsSign(item.privateKey,
Eth2Digest.fromHex(args[1]).data).toValidatorSig()

View File

@ -76,14 +76,14 @@ proc initGenesis*(vc: ValidatorClientRef): Future[RestGenesis] {.async.} =
proc initValidators*(vc: ValidatorClientRef): Future[bool] {.async.} =
info "Initializaing validators", path = vc.config.validatorsDir()
var duplicates: seq[ValidatorPubKey]
for key in vc.config.validatorKeys():
let pubkey = key.toPubKey().toPubKey()
for item in vc.config.validatorItems():
let pubkey = item.privateKey.toPubKey().toPubKey()
if pubkey in duplicates:
error "Duplicate validator's key found", validator_pubkey = pubkey
return false
else:
duplicates.add(pubkey)
vc.attachedValidators.addLocalValidator(key)
vc.attachedValidators.addLocalValidator(item)
return true
proc initClock*(vc: ValidatorClientRef): Future[BeaconClock] {.async.} =

View File

@ -14,9 +14,9 @@ import
"."/[
rest_utils,
rest_beacon_api, rest_config_api, rest_debug_api, rest_event_api,
rest_nimbus_api, rest_node_api, rest_validator_api]
rest_nimbus_api, rest_node_api, rest_validator_api, rest_key_management_api]
export
rest_utils,
rest_beacon_api, rest_config_api, rest_debug_api, rest_event_api,
rest_nimbus_api, rest_node_api, rest_validator_api
rest_nimbus_api, rest_node_api, rest_validator_api, rest_key_management_api

View File

@ -0,0 +1,424 @@
# Copyright (c) 2021 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, sequtils, strutils]
import chronos, chronicles, confutils,
stew/[base10, results, byteutils, io2], bearssl, blscurve
# Local modules
import ".."/[conf, version, filepath, beacon_node_types, beacon_node_common]
import ".."/spec/[keystore, crypto]
import ".."/rpc/rest_utils
import ".."/validators/[keystore_management, validator_pool]
export results
type
ValidatorToggleAction {.pure.} = enum
Enable, Disable
KmResult*[T] = Result[T, cstring]
StoredValidatorKeyFlag* {.pure.} = enum
Valid, NoPassword, NoPermission, Disabled
StoredValidatorKey* = object
name*: string
filename*: string
flag*: StoredValidatorKeyFlag
path*: KeyPath
description*: string
pubkey*: ValidatorPubKey
ValidatorListItem* = object
pubkey*: ValidatorPubKey
status*: string
description*: string
path*: string
ValidatorKeystoreItem* = object
keystore*: Keystore
password*: string
proc `$`*(s: StoredValidatorKeyFlag): string =
case s
of StoredValidatorKeyFlag.Valid:
"enabled"
of StoredValidatorKeyFlag.NoPassword:
"failed"
of StoredValidatorKeyFlag.NoPermission:
"failed"
of StoredValidatorKeyFlag.Disabled:
"disabled"
proc init*(t: typedesc[ValidatorListItem],
key: StoredValidatorKey): ValidatorListItem {.
raises: [Defect].} =
ValidatorListItem(pubkey: key.pubkey, status: $key.flag,
description: key.description, path: string(key.path))
proc listValidators*(conf: AnyConf): seq[StoredValidatorKey] {.
raises: [Defect].} =
var validators: seq[StoredValidatorKey]
try:
for kind, file in walkDir(conf.validatorsDir()):
if kind == pcDir:
let keyName = splitFile(file).name
let rkey = ValidatorPubKey.fromHex(keyName)
if rkey.isErr():
# Skip folders which represents invalid public key
continue
let secretFile = conf.secretsDir() / keyName
let keystorePath = conf.validatorsDir() / keyName
let keystoreFile = keystorePath / KeystoreFileName
let disableFile = keystorePath / DisableFileName
if not(fileExists(keystoreFile)):
# Skip folders which do not have keystore file inside.
continue
let keystore =
block:
let res = loadKeystoreFile(keystoreFile)
if res.isErr():
# Skip folders which do not have keystore of proper format.
continue
res.get()
let flag =
if fileExists(secretFile):
if checkSensitiveFilePermissions(secretFile):
if not(fileExists(disableFile)):
StoredValidatorKeyFlag.Valid
else:
StoredValidatorKeyFlag.Disabled
else:
StoredValidatorKeyFlag.NoPermission
else:
StoredValidatorKeyFlag.NoPassword
let item = StoredValidatorKey(name: keyName,
filename: keystoreFile, flag: flag,
path: keystore.path,
description: keystore.description[],
pubkey: rkey.get())
validators.add(item)
validators
except OSError:
return validators
func getPubKey*(privkey: ValidatorPrivKey): KmResult[ValidatorPubKey] {.
raises: [Defect].} =
## Derive a public key from a private key
var pubKey: blscurve.PublicKey
let ok = publicFromSecret(pubKey, SecretKey privkey)
if not(ok):
return err("Invalid private key or zero key")
ok(ValidatorPubKey(blob: pubKey.exportRaw()))
proc addValidator(pool: var ValidatorPool,
rng: var BrHmacDrbgContext,
conf: AnyConf, keystore: Keystore,
password: string): KmResult[void] {.
raises: [Defect].} =
let keypass = KeystorePass.init(password)
let privateKey =
block:
let res = decryptKeystore(keystore, keypass)
if res.isOk():
res.get()
else:
return err("Keystore decryption failed")
let publicKey = ? privateKey.getPubKey()
let keyName = publicKey.toHex()
let secretFile = conf.secretsDir() / keyName
let keystorePath = conf.validatorsDir() / keyName
let keystoreFile = keystorePath / KeystoreFileName
if fileExists(keystoreFile) or fileExists(secretFile):
return err("Keystore artifacts already exists")
let plainStorage = createKeystore(kdfScrypt, rng, privateKey, keypass)
let encodedStorage =
try:
Json.encode(plainStorage)
except SerializationError:
error "Could not serialize keystore", key_path = keystoreFile
return err("Could not serialize keystore")
let cleanupSecretsDir =
if not(dirExists(conf.secretsDir())):
let res = secureCreatePath(conf.secretsDir())
if res.isErr():
return err("Unable to create data secrets folder")
true
else:
false
let cleanupValidatorsDir =
if not(dirExists(conf.validatorsDir())):
let res = secureCreatePath(conf.validatorsDir())
if res.isErr():
if cleanupSecretsDir: discard io2.removeDir(conf.secretsDir())
return err("Unable to create data validators folder")
true
else:
false
block:
let res = secureCreatePath(keystorePath)
if res.isErr():
if cleanupSecretsDir: discard io2.removeDir(conf.secretsDir())
if cleanupValidatorsDir: discard io2.removeDir(conf.validatorsDir())
return err("Unable to create folder for keystore")
block:
let res = secureWriteFile(secretFile, keypass.str)
if res.isErr():
discard io2.removeDir(keystorePath)
if cleanupSecretsDir: discard io2.removeDir(conf.secretsDir())
if cleanupValidatorsDir: discard io2.removeDir(conf.validatorsDir())
return err("Could not store password file")
block:
let res = secureWriteFile(keystoreFile, encodedStorage)
if res.isErr():
discard io2.removeFile(secretFile)
discard io2.removeDir(keystorePath)
if cleanupSecretsDir: discard io2.removeDir(conf.secretsDir())
if cleanupValidatorsDir: discard io2.removeDir(conf.validatorsDir())
return err("Could not store keystore file")
pool.addLocalValidator(ValidatorPrivateItem.init(privateKey, keystore))
ok()
proc removeValidator(pool: var ValidatorPool, conf: AnyConf,
publicKey: ValidatorPubKey): KmResult[void] {.
raises: [Defect].} =
let keyName = publicKey.toHex()
let keystorePath = conf.validatorsDir() / keyName
let keystoreFile = keystorePath / KeystoreFileName
let secretFile = conf.secretsDir() / keyName
try:
removeDir(keystorePath, false)
except OSError:
return err("Could not remove keystore directory")
if dirExists(keystorePath):
return err("Could not remove keystore directory")
let res = io2.removeFile(secretFile)
if res.isErr():
return err("Could not remove password file")
pool.removeValidator(publicKey)
ok()
proc toggleValidator(pool: var ValidatorPool,
conf: AnyConf,
publicKey: ValidatorPubKey,
action: ValidatorToggleAction): KmResult[void] {.
raises:[Defect].} =
let keyName = publicKey.toHex()
let keystorePath = conf.validatorsDir() / keyName
let disableFile = keystorePath / DisableFileName
let secretFile = conf.secretsDir() / keyName
let keystoreFile = keystorePath / KeystoreFileName
if dirExists(keystorePath) and checkSensitivePathPermissions(keyStorePath):
case action
of ValidatorToggleAction.Enable:
if fileExists(disableFile):
if checkSensitivePathPermissions(secretFile) and
checkSensitivePathPermissions(keystoreFile):
let privateKey =
block:
let res = loadKeystoreUnsafe(conf.validatorsDir(),
conf.secretsDir(), keyName)
if res.isErr():
return err("Could not decrypt validator's keystore")
res.get()
let res = io2.removeFile(disableFile)
if res.isErr():
return err("Could not enable validator's keystore")
if isNil(pool.getValidator(publicKey)):
pool.addLocalValidator(privateKey)
ok()
else:
err("Could not read validator's keystore")
else:
# Disable file is already missing.
if isNil(pool.getValidator(publicKey)):
# If validator pool do not have ``publicKey`` validator we going to
# add it.
if checkSensitivePathPermissions(secretFile) and
checkSensitivePathPermissions(keystoreFile):
let privateKey =
block:
let res = loadKeystoreUnsafe(conf.validatorsDir(),
conf.secretsDir(), keyName)
if res.isErr():
return err("Could not decrypt validator's keystore")
res.get()
if isNil(pool.getValidator(publicKey)):
pool.addLocalValidator(privateKey)
ok()
else:
err("Could not read validator's keystore")
else:
ok()
of ValidatorToggleAction.Disable:
if not(fileExists(disableFile)):
# Disable file is not present, we first create `.disable` file and in
# case of success we removing validator from validators pool.
block:
let res = secureWriteFile(disableFile, DisableFileContent)
if res.isErr():
return err("Could not create disable file")
pool.removeValidator(publicKey)
ok()
else:
# Disable file is already present.
pool.removeValidator(publicKey)
ok()
else:
err("No validator keystore found")
proc installValidatorManagementHandlers*(router: var RestRouter,
node: BeaconNode) =
router.api(MethodGet, "/api/nimbus/v1/validators") do (
) -> RestApiResponse:
let validators = node.config.listValidators().mapIt(
ValidatorListItem.init(it)
)
return RestApiResponse.jsonResponse(validators)
router.api(MethodPost, "/api/nimbus/v1/validators") do (
contentBody: Option[ContentBody]) -> RestApiResponse:
let keystores =
block:
if contentBody.isNone():
return RestApiResponse.jsonError(Http404, EmptyRequestBodyError)
let dres = decodeBody(seq[ValidatorKeystoreItem], contentBody.get())
if dres.isErr():
return RestApiResponse.jsonError(Http400, InvalidKeystoreObjects,
$dres.error())
dres.get()
var failures: seq[RestFailureItem]
for index, item in keystores.pairs():
let res = addValidator(node.attachedValidators[], node.network.rng[],
node.config, item.keystore, item.password)
if res.isErr():
failures.add(RestFailureItem(index: uint64(index),
message: $res.error()))
if len(failures) > 0:
return RestApiResponse.jsonErrorList(Http400, KeystoreAdditionFailure,
failures)
else:
return RestApiResponse.jsonMsgResponse(KeystoreAdditionSuccess)
router.api(MethodPost, "/api/nimbus/v1/validators/enable") do (
contentBody: Option[ContentBody]) -> RestApiResponse:
let keys =
block:
if contentBody.isNone():
return RestApiResponse.jsonError(Http404, EmptyRequestBodyError)
let dres = decodeBody(seq[ValidatorPubKey], contentBody.get())
if dres.isErr():
return RestApiResponse.jsonError(Http400, InvalidValidatorPublicKey,
$dres.error())
dres.get()
var failures: seq[RestFailureItem]
for index, key in keys.pairs():
let res = toggleValidator(node.attachedValidators[], node.config, key,
ValidatorToggleAction.Enable)
if res.isErr():
failures.add(RestFailureItem(index: uint64(index),
message: $res.error()))
if len(failures) > 0:
return RestApiResponse.jsonErrorList(Http400, KeystoreModificationFailure,
failures)
else:
return RestApiResponse.jsonMsgResponse(KeystoreModificationSuccess)
router.api(MethodPost, "/api/nimbus/v1/validators/disable") do (
contentBody: Option[ContentBody]) -> RestApiResponse:
let keys =
block:
if contentBody.isNone():
return RestApiResponse.jsonError(Http404, EmptyRequestBodyError)
let dres = decodeBody(seq[ValidatorPubKey], contentBody.get())
if dres.isErr():
return RestApiResponse.jsonError(Http400, InvalidValidatorPublicKey,
$dres.error())
dres.get()
var failures: seq[RestFailureItem]
for index, key in keys.pairs():
let res = toggleValidator(node.attachedValidators[], node.config, key,
ValidatorToggleAction.Disable)
if res.isErr():
failures.add(RestFailureItem(index: uint64(index),
message: $res.error()))
if len(failures) > 0:
return RestApiResponse.jsonErrorList(Http400, KeystoreModificationFailure,
failures)
else:
return RestApiResponse.jsonMsgResponse(KeystoreModificationSuccess)
router.api(MethodPost, "/api/nimbus/v1/validators/remove") do (
contentBody: Option[ContentBody]) -> RestApiResponse:
let keys =
block:
if contentBody.isNone():
return RestApiResponse.jsonError(Http404, EmptyRequestBodyError)
let dres = decodeBody(seq[ValidatorPubKey], contentBody.get())
if dres.isErr():
return RestApiResponse.jsonError(Http400, InvalidValidatorPublicKey,
$dres.error())
dres.get()
var failures: seq[RestFailureItem]
for index, key in keys.pairs():
let res = removeValidator(node.attachedValidators[], node.config, key)
if res.isErr():
failures.add(RestFailureItem(index: uint64(index),
message: $res.error()))
if len(failures) > 0:
return RestApiResponse.jsonErrorList(Http400, KeystoreRemovalFailure,
failures)
else:
return RestApiResponse.jsonMsgResponse(KeystoreRemovalSuccess)
router.redirect(
MethodGet,
"/nimbus/v1/validators",
"/api/nimbus/v1/validators"
)
router.redirect(
MethodPost,
"/nimbus/v1/validators",
"/api/nimbus/v1/validators"
)
router.redirect(
MethodPost,
"/nimbus/v1/validators/enable",
"/api/nimbus/v1/validators/enable"
)
router.redirect(
MethodPost,
"/nimbus/v1/validators/disable",
"/api/nimbus/v1/validators/disable"
)
router.redirect(
MethodPost,
"/nimbus/v1/validators/remove",
"/api/nimbus/v1/validators/remove"
)

View File

@ -175,6 +175,22 @@ const
"Internal server error"
NoImplementationError* =
"Not implemented yet"
KeystoreAdditionFailure* =
"Could not add some keystores"
InvalidKeystoreObjects* =
"Invalid keystore objects found"
KeystoreAdditionSuccess* =
"All keystores has been added"
KeystoreModificationFailure* =
"Could not change keystore(s) state"
KeystoreModificationSuccess* =
"Keystore(s) state was successfully modified"
KeystoreRemovalSuccess* =
"Keystore(s) was successfully removed"
KeystoreRemovalFailure* =
"Could not remove keystore(s)"
InvalidValidatorPublicKey* =
"Invalid validator's public key(s) found"
type
ValidatorIndexError* {.pure.} = enum

View File

@ -43,7 +43,7 @@ type
RestAttestationError* = object
code*: uint64
message*: string
failures*: seq[RestAttestationsFailure]
failures*: seq[RestFailureItem]
EncodeTypes* =
AttesterSlashing |

View File

@ -132,6 +132,10 @@ type
slot*: Slot
validators*: seq[ValidatorIndex]
RestFailureItem* = object
index*: uint64
message*: string
RestAttestationsFailure* = object
index*: uint64
message*: string

View File

@ -11,12 +11,12 @@ import
std/[os, strutils, terminal, wordwrap, unicode],
chronicles, chronos, json_serialization, zxcvbn,
serialization, blscurve, eth/common/eth_types, eth/keys, confutils, bearssl,
../spec/[eth2_merkleization, keystore],
../spec/datatypes/base,
".."/spec/[eth2_merkleization, keystore],
".."/spec/datatypes/base,
stew/io2, libp2p/crypto/crypto as lcrypto,
nimcrypto/utils as ncrutils,
".."/[conf, filepath],
../networking/network_metadata
".."/[conf, filepath, beacon_node_types],
".."/networking/network_metadata
export
keystore
@ -27,8 +27,11 @@ when defined(windows):
{.localPassC: "-fno-lto".} # no LTO for crypto
const
keystoreFileName* = "keystore.json"
netKeystoreFileName* = "network_keystore.json"
KeystoreFileName* = "keystore.json"
NetKeystoreFileName* = "network_keystore.json"
DisableFileName* = ".disable"
DisableFileContent* = "Please do not remove this file manually. " &
"This can lead to slashing of this validator's key."
type
WalletPathPair* = object
@ -39,6 +42,8 @@ type
walletPath*: WalletPathPair
seed*: KeySeed
AnyConf* = BeaconNodeConf | ValidatorClientConf
const
minPasswordLen = 12
minPasswordEntropy = 60.0
@ -53,6 +58,16 @@ proc echoP*(msg: string) =
echo ""
echo wrapWords(msg, 80)
proc init*(t: typedesc[ValidatorPrivateItem], privateKey: ValidatorPrivKey,
keystore: Keystore): ValidatorPrivateItem =
ValidatorPrivateItem(
privateKey: privateKey,
description: some(keystore.description[]),
path: some(keystore.path),
uuid: some(keystore.uuid),
version: some(uint64(keystore.version))
)
proc checkAndCreateDataDir*(dataDir: string): bool =
when defined(posix):
let requiredPerms = 0o700
@ -110,6 +125,44 @@ proc checkAndCreateDataDir*(dataDir: string): bool =
return true
proc checkSensitivePathPermissions*(dirFilePath: string): bool =
## If ``dirFilePath`` is file, then check if file has only
##
## - "(600) rwx------" permissions on Posix (Linux, MacOS, BSD)
## - current user only ACL on Windows
##
## If ``dirFilePath`` is directory, then check if directory has only
##
## - "(700) rwx------" permissions on Posix (Linux, MacOS, BSD)
## - current user only ACL on Windows
##
## Procedure returns ``true`` if directory/file is present and all required
## permissions are set.
let r1 = isDir(dirFilePath)
let r2 = isFile(dirFilePath)
if r1 or r2:
when defined(windows):
let res = checkCurrentUserOnlyACL(dirFilePath)
if res.isErr():
false
else:
if res.get() == false:
false
else:
true
else:
let requiredPermissions = if r1: 0o700 else: 0o600
let res = getPermissions(dirFilePath)
if res.isErr():
false
else:
if res.get() != requiredPermissions:
false
else:
true
else:
false
proc checkSensitiveFilePermissions*(filePath: string): bool =
## Check if ``filePath`` has only "(600) rw-------" permissions.
## Procedure returns ``false`` if permissions are different and we can't
@ -202,7 +255,8 @@ proc keyboardCreatePassword(prompt: string,
return ok(password)
proc keyboardGetPassword[T](prompt: string, attempts: int,
pred: proc(p: string): KsResult[T] {.gcsafe, raises: [Defect].}): KsResult[T] =
pred: proc(p: string): KsResult[T] {.
gcsafe, raises: [Defect].}): KsResult[T] =
var
remainingAttempts = attempts
counter = 1
@ -223,18 +277,51 @@ proc keyboardGetPassword[T](prompt: string, attempts: int,
dec(remainingAttempts)
err("Failed to decrypt keystore")
proc loadKeystore*(validatorsDir, secretsDir, keyName: string,
nonInteractive: bool): Option[ValidatorPrivKey] =
proc loadKeystoreFile*(path: string): KsResult[Keystore] {.
raises: [Defect].} =
try:
ok(Json.loadFile(path, Keystore))
except IOError as err:
return err("Could not read keystore file")
except SerializationError as err:
return err("Could not decode keystore file")
proc loadSecretFile*(path: string): KsResult[KeystorePass] {.
raises: [Defect].} =
try:
ok(KeystorePass.init(readFile(path)))
except IOError:
return err("Could not read password file")
proc loadKeystoreUnsafe*(validatorsDir, secretsDir,
keyName: string): KsResult[ValidatorPrivateItem] =
## Load keystore without any checks on keystore/secret permissions.
let
keystorePath = validatorsDir / keyName / keystoreFileName
keystorePath = validatorsDir / keyName / KeystoreFileName
keystore = ? loadKeystoreFile(keystorePath)
let
passphrasePath = secretsDir / keyName
passphrase = ? loadSecretFile(passphrasePath)
let res = decryptKeystore(keystore, passphrase)
if res.isOk():
ok(ValidatorPrivateItem.init(res.get(), keystore))
else:
err("Failed to decrypt keystore")
proc loadKeystore*(validatorsDir, secretsDir, keyName: string,
nonInteractive: bool): Option[ValidatorPrivateItem] =
let
keystorePath = validatorsDir / keyName / KeystoreFileName
keystore =
try: Json.loadFile(keystorePath, Keystore)
except IOError as err:
error "Failed to read keystore", err = err.msg, path = keystorePath
return
except SerializationError as err:
error "Invalid keystore", err = err.formatMsg(keystorePath)
return
block:
let res = loadKeystoreFile(keystorePath)
if res.isErr():
error "Failed to read keystore file", error = res.error(),
path = keystorePath
return
res.get()
let passphrasePath = secretsDir / keyName
if fileExists(passphrasePath):
@ -242,17 +329,18 @@ proc loadKeystore*(validatorsDir, secretsDir, keyName: string,
error "Password file has insecure permissions", key_path = keyStorePath
return
let passphrase = KeystorePass.init:
try:
readFile(passphrasePath)
except IOError as err:
error "Failed to read passphrase file", err = err.msg,
path = passphrasePath
return
let passphrase =
block:
let res = loadSecretFile(passphrasePath)
if res.isErr():
error "Failed to read passphrase file", err = res.error(),
path = passphrasePath
return
res.get()
let res = decryptKeystore(keystore, passphrase)
if res.isOk:
return res.get.some
if res.isOk():
return some(ValidatorPrivateItem.init(res.get(), keystore))
else:
error "Failed to decrypt keystore", keystorePath, passphrasePath
return
@ -273,34 +361,65 @@ proc loadKeystore*(validatorsDir, secretsDir, keyName: string,
)
if res.isOk():
some(res.get())
some(ValidatorPrivateItem.init(res.get(), keystore))
else:
return
iterator validatorKeysFromDirs*(validatorsDir, secretsDir: string): ValidatorPrivKey =
proc isEnabled*(validatorsDir, keyName: string): bool {.
raises: [Defect].} =
## Returns ``true`` if specific validator with key ``keyName`` in validators
## directory ``validatorsDir`` is not disabled.
let keystorePath = validatorsDir / keyName
let disableFile = keystorePath / DisableFileName
if dirExists(keystorePath):
if fileExists(disableFile):
false
else:
true
else:
false
proc isEnabled*(conf: AnyConf, keyName: string): bool {.
raises: [Defect].} =
## Returns ``true`` if specific validator with key ``keyName`` is not
## disabled.
isEnabled(conf.validatorsDir(), keyName)
proc isEnabled*(conf: AnyConf, publicKey: ValidatorPubKey): bool {.
raises:[Defect].} =
## Returns ``true`` if specific validator with public key ``publicKey`` is
## not disabled.
isEnabled(conf, publicKey.toHex())
iterator validatorKeysFromDirs*(validatorsDir,
secretsDir: string): ValidatorPrivateItem =
try:
for kind, file in walkDir(validatorsDir):
if kind == pcDir:
let keyName = splitFile(file).name
let key = loadKeystore(validatorsDir, secretsDir, keyName, true)
if key.isSome:
yield key.get
else:
quit 1
if isEnabled(validatorsDir, keyName):
let item = loadKeystore(validatorsDir, secretsDir, keyName, true)
if item.isSome():
yield item.get()
else:
quit 1
except OSError:
quit 1
iterator validatorKeys*(config: BeaconNodeConf|ValidatorClientConf): ValidatorPrivKey =
let validatorsDir = config.validatorsDir
iterator validatorItems*(config: AnyConf): ValidatorPrivateItem =
let validatorsDir = config.validatorsDir()
let secretsDir = config.secretsDir()
try:
for kind, file in walkDir(validatorsDir):
if kind == pcDir:
let keyName = splitFile(file).name
let key = loadKeystore(validatorsDir, config.secretsDir, keyName, config.nonInteractive)
if key.isSome:
yield key.get
else:
quit 1
if isEnabled(config, keyName):
let item = loadKeystore(validatorsDir, secretsDir, keyName,
config.nonInteractive)
if item.isSome():
yield item.get()
else:
quit 1
except OSError as err:
error "Validator keystores directory not accessible",
path = validatorsDir, err = err.msg
@ -411,7 +530,7 @@ proc saveKeystore(rng: var BrHmacDrbgContext,
let
keyStore = createKeystore(kdfPbkdf2, rng, signingKey,
password, signingKeyPath)
keystoreFile = validatorDir / keystoreFileName
keystoreFile = validatorDir / KeystoreFileName
var encodedStorage: string
try:

View File

@ -82,13 +82,12 @@ proc findValidator(validators: auto, pubKey: ValidatorPubKey):
else:
some(idx.ValidatorIndex)
proc addLocalValidator(node: BeaconNode,
privKey: ValidatorPrivKey) =
node.attachedValidators[].addLocalValidator(privKey)
proc addLocalValidator(node: BeaconNode, item: ValidatorPrivateItem) =
node.attachedValidators[].addLocalValidator(item)
proc addLocalValidators*(node: BeaconNode) =
for validatorKey in node.config.validatorKeys:
node.addLocalValidator(validatorKey)
for validatorItem in node.config.validatorItems():
node.addLocalValidator(validatorItem)
proc addRemoteValidators*(node: BeaconNode) {.raises: [Defect, OSError, IOError].} =
# load all the validators from the child process - loop until `end`
@ -103,7 +102,7 @@ proc addRemoteValidators*(node: BeaconNode) {.raises: [Defect, OSError, IOError]
if pk.isSome():
let v = AttachedValidator(pubKey: key,
index: index,
kind: ValidatorKind.remote,
kind: ValidatorKind.Remote,
connection: ValidatorConnection(
inStream: node.vcProcess.inputStream,
outStream: node.vcProcess.outputStream,

View File

@ -30,22 +30,17 @@ func init*(T: type ValidatorPool,
template count*(pool: ValidatorPool): int =
len(pool.validators)
proc addLocalValidator*(pool: var ValidatorPool,
privKey: ValidatorPrivKey,
proc addLocalValidator*(pool: var ValidatorPool, item: ValidatorPrivateItem,
index: Option[ValidatorIndex]) =
let pubKey = privKey.toPubKey().toPubKey()
let v = AttachedValidator(kind: inProcess, pubKey: pubKey, index: index,
privKey: privKey)
let pubKey = item.privateKey.toPubKey().toPubKey()
let v = AttachedValidator(kind: ValidatorKind.Local, pubKey: pubKey,
index: index, data: item)
pool.validators[pubKey] = v
notice "Local validator attached", pubKey, validator = shortLog(v)
validators.set(pool.count().int64)
proc addLocalValidator*(pool: var ValidatorPool, privKey: ValidatorPrivKey) =
let pubKey = privKey.toPubKey().toPubKey()
let v = AttachedValidator(kind: inProcess, pubKey: pubKey, privKey: privKey)
pool.validators[pubKey] = v
notice "Local validator attached", pubKey, validator = shortLog(v)
validators.set(pool.count().int64)
proc addLocalValidator*(pool: var ValidatorPool, item: ValidatorPrivateItem) =
addLocalValidator(pool, item, none[ValidatorIndex]())
proc addRemoteValidator*(pool: var ValidatorPool, pubKey: ValidatorPubKey,
v: AttachedValidator) =
@ -61,9 +56,14 @@ proc contains*(pool: ValidatorPool, pubKey: ValidatorPubKey): bool =
## Returns ``true`` if validator with key ``pubKey`` present in ``pool``.
pool.validators.contains(pubKey)
proc removeValidator*(pool: var ValidatorPool, pubKey: ValidatorPubKey) =
proc removeValidator*(pool: var ValidatorPool, validatorKey: ValidatorPubKey) =
## Delete validator with public key ``pubKey`` from ``pool``.
pool.validators.del(pubKey)
let validator = pool.validators.getOrDefault(validatorKey)
if not(isNil(validator)):
pool.validators.del(validatorKey)
notice "Local or remote validator detached", validatorKey,
validator = shortLog(validator)
validators.set(pool.count().int64)
proc updateValidator*(pool: var ValidatorPool, pubKey: ValidatorPubKey,
index: ValidatorIndex) =
@ -101,23 +101,26 @@ proc signWithRemoteValidator(v: AttachedValidator,
proc signBlockProposal*(v: AttachedValidator, fork: Fork,
genesis_validators_root: Eth2Digest, slot: Slot,
blockRoot: Eth2Digest): Future[ValidatorSig] {.async.} =
return if v.kind == inProcess:
get_block_signature(fork, genesis_validators_root, slot, blockRoot,
v.privKey).toValidatorSig()
else:
let root = compute_block_root(fork, genesis_validators_root, slot,
blockRoot)
await signWithRemoteValidator(v, root)
return
case v.kind
of ValidatorKind.Local:
get_block_signature(fork, genesis_validators_root, slot, blockRoot,
v.data.privateKey).toValidatorSig()
of ValidatorKind.Remote:
let root = compute_block_root(fork, genesis_validators_root, slot,
blockRoot)
await signWithRemoteValidator(v, root)
proc signAttestation*(v: AttachedValidator,
data: AttestationData,
fork: Fork, genesis_validators_root: Eth2Digest):
Future[ValidatorSig] {.async.} =
return
if v.kind == inProcess:
case v.kind
of ValidatorKind.Local:
get_attestation_signature(fork, genesis_validators_root, data,
v.privKey).toValidatorSig()
else:
v.data.privateKey).toValidatorSig()
of ValidatorKind.Remote:
let root = compute_attestation_root(fork, genesis_validators_root, data)
await signWithRemoteValidator(v, root)
@ -142,11 +145,12 @@ proc signAggregateAndProof*(v: AttachedValidator,
fork: Fork, genesis_validators_root: Eth2Digest):
Future[ValidatorSig] {.async.} =
return
if v.kind == inProcess:
case v.kind
of ValidatorKind.Local:
get_aggregate_and_proof_signature(fork, genesis_validators_root,
aggregate_and_proof,
v.privKey).toValidatorSig()
else:
v.data.privateKey).toValidatorSig()
of ValidatorKind.Remote:
let root = compute_aggregate_and_proof_root(fork, genesis_validators_root,
aggregate_and_proof)
await signWithRemoteValidator(v, root)
@ -161,10 +165,12 @@ proc signSyncCommitteeMessage*(v: AttachedValidator,
signing_root = sync_committee_msg_signing_root(
fork, slot.epoch, genesis_validators_root, block_root)
let signature = if v.kind == inProcess:
blsSign(v.privkey, signing_root.data).toValidatorSig
else:
await signWithRemoteValidator(v, signing_root)
let signature =
case v.kind
of ValidatorKind.Local:
blsSign(v.data.privateKey, signing_root.data).toValidatorSig
of ValidatorKind.Remote:
await signWithRemoteValidator(v, signing_root)
return SyncCommitteeMessage(
slot: slot,
@ -183,10 +189,12 @@ proc getSyncCommitteeSelectionProof*(
signing_root = sync_committee_selection_proof_signing_root(
fork, genesis_validators_root, slot, subcommittee_index)
return if v.kind == inProcess:
blsSign(v.privkey, signing_root.data).toValidatorSig
else:
await signWithRemoteValidator(v, signing_root)
return
case v.kind
of ValidatorKind.Local:
blsSign(v.data.privateKey, signing_root.data).toValidatorSig
of ValidatorKind.Remote:
await signWithRemoteValidator(v, signing_root)
# https://github.com/ethereum/consensus-specs/blob/v1.1.0-beta.4/specs/altair/validator.md#signature
proc sign*(
@ -198,10 +206,12 @@ proc sign*(
signing_root = contribution_and_proof_signing_root(
fork, genesis_validators_root, msg.message)
msg.signature = if v.kind == inProcess:
blsSign(v.privkey, signing_root.data).toValidatorSig
else:
await signWithRemoteValidator(v, signing_root)
msg.signature =
case v.kind
of ValidatorKind.Local:
blsSign(v.data.privateKey, signing_root.data).toValidatorSig
of ValidatorKind.Remote:
await signWithRemoteValidator(v, signing_root)
# https://github.com/ethereum/consensus-specs/blob/v1.1.0/specs/phase0/validator.md#randao-reveal
func genRandaoReveal*(k: ValidatorPrivKey, fork: Fork,
@ -214,10 +224,11 @@ proc genRandaoReveal*(v: AttachedValidator, fork: Fork,
genesis_validators_root: Eth2Digest, slot: Slot):
Future[ValidatorSig] {.async.} =
return
if v.kind == inProcess:
genRandaoReveal(v.privKey, fork, genesis_validators_root,
case v.kind
of ValidatorKind.Local:
genRandaoReveal(v.data.privateKey, fork, genesis_validators_root,
slot).toValidatorSig()
else:
of ValidatorKind.Remote:
let root = compute_epoch_root(fork, genesis_validators_root,
slot.compute_epoch_at_slot)
await signWithRemoteValidator(v, root)
@ -226,9 +237,10 @@ proc getSlotSig*(v: AttachedValidator, fork: Fork,
genesis_validators_root: Eth2Digest, slot: Slot
): Future[ValidatorSig] {.async.} =
return
if v.kind == inProcess:
case v.kind
of ValidatorKind.Local:
get_slot_signature(fork, genesis_validators_root, slot,
v.privKey).toValidatorSig()
else:
v.data.privateKey).toValidatorSig()
of ValidatorKind.Remote:
let root = compute_slot_root(fork, genesis_validators_root, slot)
await signWithRemoteValidator(v, root)

View File

@ -216,10 +216,9 @@ suite "Gossip validation - Extra": # Not based on preset config
expectedCount = subcommittee.count(pubkey)
index = ValidatorIndex(
state[].data.validators.mapIt(it.pubkey).find(pubKey))
validator = AttachedValidator(
pubKey: pubkey,
kind: inProcess, privKey: MockPrivKeys[index],
index: some(index))
privateItem = ValidatorPrivateItem(privateKey: MockPrivKeys[index])
validator = AttachedValidator(pubKey: pubkey,
kind: ValidatorKind.Local, data: privateItem, index: some(index))
msg = waitFor signSyncCommitteeMessage(
validator, state[].data.slot,
state[].data.fork, state[].data.genesis_validators_root, state[].root)