nimbus-eth2/beacon_chain/validators/keystore_management.nim

1209 lines
41 KiB
Nim
Raw Permalink Normal View History

# beacon_chain
# Copyright (c) 2018-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.
{.push raises: [Defect].}
import
std/[os, strutils, terminal, wordwrap, unicode],
chronicles, chronos, json_serialization, zxcvbn,
2020-07-08 12:36:03 +00:00
serialization, blscurve, eth/common/eth_types, eth/keys, confutils, bearssl,
2021-11-30 01:20:21 +00:00
".."/spec/[eth2_merkleization, keystore, crypto],
".."/spec/datatypes/base,
stew/io2, libp2p/crypto/crypto as lcrypto,
nimcrypto/utils as ncrutils,
".."/[conf, filepath],
".."/networking/network_metadata,
./validator_pool
export
2021-11-30 01:20:21 +00:00
keystore, validator_pool, crypto
2020-10-15 12:50:21 +00:00
when defined(windows):
import stew/[windows/acl]
{.localPassC: "-fno-lto".} # no LTO for crypto
2019-07-12 14:24:11 +00:00
const
KeystoreFileName* = "keystore.json"
2021-11-30 01:20:21 +00:00
RemoteKeystoreFileName* = "remote_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
wallet*: Wallet
path*: string
CreatedWallet* = object
walletPath*: WalletPathPair
seed*: KeySeed
2019-07-12 14:24:11 +00:00
AnyConf* = BeaconNodeConf | ValidatorClientConf | SigningNodeConf
KmResult*[T] = Result[T, cstring]
RemoveValidatorStatus* = enum
deleted = "Deleted"
missingDir = "Could not find keystore directory to remove"
AddValidatorStatus* = enum
added = "Validator added"
existingArtifacts = "Keystore artifacts already exists"
const
2020-10-02 15:58:08 +00:00
minPasswordLen = 12
2020-10-06 18:55:04 +00:00
minPasswordEntropy = 60.0
mostCommonPasswords = wordListArray(
currentSourcePath.parentDir /
"../../vendor/nimbus-security-resources/passwords/10-million-password-list-top-100000.txt",
minWordLen = minPasswordLen)
proc echoP*(msg: string) =
## Prints a paragraph aligned to 80 columns
echo ""
echo wrapWords(msg, 80)
func init*(T: type KeystoreData,
privateKey: ValidatorPrivKey,
keystore: Keystore): T =
KeystoreData(
kind: KeystoreKind.Local,
privateKey: privateKey,
description: if keystore.description == nil: none(string)
else: some(keystore.description[]),
path: keystore.path,
uuid: keystore.uuid,
version: uint64(keystore.version),
pubkey: privateKey.toPubKey().toPubKey()
)
func init*(T: type KeystoreData,
keystore: RemoteKeystore): Result[T, cstring] =
2021-11-30 01:20:21 +00:00
let cookedKey =
block:
let res = keystore.pubkey.load()
if res.isNone():
return err("Invalid validator's public key")
res.get()
ok KeystoreData(
kind: KeystoreKind.Remote,
pubkey: cookedKey.toPubKey,
2021-11-30 01:20:21 +00:00
description: keystore.description,
version: keystore.version,
remoteUrl: keystore.remote
)
2021-11-30 01:20:21 +00:00
2020-08-26 06:42:26 +00:00
proc checkAndCreateDataDir*(dataDir: string): bool =
when defined(posix):
2020-10-30 00:36:47 +00:00
let requiredPerms = 0o700
if isDir(dataDir):
let currPermsRes = getPermissions(dataDir)
if currPermsRes.isErr():
fatal "Could not check data directory permissions",
data_dir = dataDir, errorCode = $currPermsRes.error,
errorMsg = ioErrorMsg(currPermsRes.error)
return false
else:
2020-10-30 00:36:47 +00:00
let currPerms = currPermsRes.get()
if currPerms != requiredPerms:
warn "Data directory has insecure permissions. Correcting them.",
data_dir = dataDir,
current_permissions = currPerms.toOct(4),
required_permissions = requiredPerms.toOct(4)
let newPermsRes = setPermissions(dataDir, requiredPerms)
if newPermsRes.isErr():
fatal "Could not set data directory permissions",
data_dir = dataDir,
errorCode = $newPermsRes.error,
errorMsg = ioErrorMsg(newPermsRes.error),
old_permissions = currPerms.toOct(4),
new_permissions = requiredPerms.toOct(4)
return false
2020-08-26 06:42:26 +00:00
else:
let res = secureCreatePath(dataDir)
2020-08-26 06:42:26 +00:00
if res.isErr():
2020-10-30 00:36:47 +00:00
fatal "Could not create data directory", data_dir = dataDir,
2020-08-26 06:42:26 +00:00
errorMsg = ioErrorMsg(res.error), errorCode = $res.error
2020-10-30 00:36:47 +00:00
return false
2020-08-26 06:42:26 +00:00
elif defined(windows):
2020-10-30 00:36:47 +00:00
let amask = {AccessFlags.Read, AccessFlags.Write, AccessFlags.Execute}
if fileAccessible(dataDir, amask):
let cres = checkCurrentUserOnlyACL(dataDir)
if cres.isErr():
fatal "Could not check data folder's ACL",
data_dir = dataDir, errorCode = $cres.error,
errorMsg = ioErrorMsg(cres.error)
2020-10-30 00:36:47 +00:00
return false
else:
if cres.get() == false:
fatal "Data folder has insecure ACL", data_dir = dataDir
2020-10-30 00:36:47 +00:00
return false
else:
let res = secureCreatePath(dataDir)
if res.isErr():
fatal "Could not create data folder", data_dir = dataDir,
errorMsg = ioErrorMsg(res.error), errorCode = $res.error
2020-10-30 00:36:47 +00:00
return false
2020-08-26 06:42:26 +00:00
else:
fatal "Unsupported operation system"
return false
2020-10-30 00:36:47 +00:00
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.
2020-10-30 00:36:47 +00:00
## Procedure returns ``false`` if permissions are different and we can't
## correct them.
when defined(windows):
let cres = checkCurrentUserOnlyACL(filePath)
if cres.isErr():
fatal "Could not check file's ACL",
key_path = filePath, errorCode = $cres.error,
errorMsg = ioErrorMsg(cres.error)
2020-10-30 00:36:47 +00:00
return false
else:
if cres.get() == false:
fatal "File has insecure permissions", key_path = filePath
2020-10-30 00:36:47 +00:00
return false
else:
2020-10-30 00:36:47 +00:00
let requiredPerms = 0o600
let currPermsRes = getPermissions(filePath)
if currPermsRes.isErr():
error "Could not check file permissions",
2020-10-30 00:36:47 +00:00
key_path = filePath, errorCode = $currPermsRes.error,
errorMsg = ioErrorMsg(currPermsRes.error)
return false
else:
2020-10-30 00:36:47 +00:00
let currPerms = currPermsRes.get()
if currPerms != requiredPerms:
warn "File has insecure permissions. Correcting them.",
key_path = filePath,
2020-10-30 00:36:47 +00:00
current_permissions = currPerms.toOct(4),
required_permissions = requiredPerms.toOct(4)
let newPermsRes = setPermissions(filePath, requiredPerms)
if newPermsRes.isErr():
fatal "Could not set data directory permissions",
key_path = filePath,
errorCode = $newPermsRes.error,
errorMsg = ioErrorMsg(newPermsRes.error),
old_permissions = currPerms.toOct(4),
new_permissions = requiredPerms.toOct(4)
return false
return true
proc keyboardCreatePassword(prompt: string,
confirm: string,
allowEmpty = false): KsResult[string] =
while true:
let password =
try:
readPasswordFromStdin(prompt)
except IOError:
error "Could not read password from stdin"
return err("Could not read password from stdin")
if password.len == 0 and allowEmpty:
return ok("")
# We treat `password` as UTF-8 encoded string.
if validateUtf8(password) == -1:
if runeLen(password) < minPasswordLen:
echoP "The entered password should be at least " & $minPasswordLen &
" characters."
echo ""
continue
2020-10-06 18:55:04 +00:00
elif passwordEntropy(password) < minPasswordEntropy:
echoP "The entered password has low entropy and may be easy to " &
"brute-force with automated tools. Please increase the " &
"variety of the user characters."
continue
elif password in mostCommonPasswords:
echoP "The entered password is too commonly used and it would be " &
"easy to brute-force with automated tools."
echo ""
continue
else:
echoP "Entered password is not valid UTF-8 string"
echo ""
continue
let confirmedPassword =
try:
readPasswordFromStdin(confirm)
except IOError:
error "Could not read password from stdin"
return err("Could not read password from stdin")
if password != confirmedPassword:
echo "Passwords don't match, please try again\n"
continue
return ok(password)
proc keyboardGetPassword[T](prompt: string, attempts: int,
pred: proc(p: string): KsResult[T] {.
gcsafe, raises: [Defect].}): KsResult[T] =
var
remainingAttempts = attempts
counter = 1
while remainingAttempts > 0:
let passphrase =
try:
readPasswordFromStdin(prompt)
except IOError:
error "Could not read password from stdin"
return
os.sleep(1000 * counter)
let res = pred(passphrase)
if res.isOk():
return res
else:
inc(counter)
dec(remainingAttempts)
err("Failed to decrypt keystore")
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: " & err.formatMsg(path))
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[KeystoreData] =
## Load keystore without any checks on keystore/secret permissions.
let
keystorePath = validatorsDir / keyName / KeystoreFileName
keystore = ? loadKeystoreFile(keystorePath)
let
passphrasePath = secretsDir / keyName
passphrase = ? loadSecretFile(passphrasePath)
let res = decryptKeystore(keystore, passphrase)
if res.isOk():
ok(KeystoreData.init(res.get(), keystore))
else:
err("Failed to decrypt keystore")
2021-11-30 01:20:21 +00:00
proc loadRemoteKeystoreImpl(validatorsDir,
keyName: string): Option[KeystoreData] =
2021-11-30 01:20:21 +00:00
let remoteKeystorePath = validatorsDir / keyName / RemoteKeystoreFileName
let privateItem =
block:
let keystore =
try:
Json.decode(remoteKeystorePath, RemoteKeystore)
except SerializationError as e:
error "Failed to read remote keystore file",
keystore_path = remoteKeystorePath,
err_msg = e.formatMsg(remoteKeystorePath)
return
let res = init(KeystoreData, keystore)
2021-11-30 01:20:21 +00:00
if res.isErr():
error "Invalid validator's public key in keystore file",
keystore_path = remoteKeystorePath
return
res.get()
some(privateItem)
proc loadKeystoreImpl(validatorsDir, secretsDir, keyName: string,
nonInteractive: bool): Option[KeystoreData] =
let
keystorePath = validatorsDir / keyName / KeystoreFileName
keystore =
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):
if not(checkSensitiveFilePermissions(passphrasePath)):
error "Password file has insecure permissions", key_path = keyStorePath
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 some(KeystoreData.init(res.get(), keystore))
else:
error "Failed to decrypt keystore", keystorePath, passphrasePath
return
if nonInteractive:
error "Unable to load validator key store. Please ensure matching passphrase exists in the secrets dir",
keyName, validatorsDir, secretsDir = secretsDir
return
let prompt = "Please enter passphrase for key \"" &
(validatorsDir / keyName) & "\": "
let res = keyboardGetPassword[ValidatorPrivKey](prompt, 3,
proc (password: string): KsResult[ValidatorPrivKey] =
2020-10-02 15:46:05 +00:00
let decrypted = decryptKeystore(keystore, KeystorePass.init password)
if decrypted.isErr():
error "Keystore decryption failed. Please try again", keystorePath
decrypted
)
2020-10-02 15:46:05 +00:00
if res.isOk():
some(KeystoreData.init(res.get(), keystore))
else:
return
2021-11-30 01:20:21 +00:00
proc loadKeystore*(validatorsDir, secretsDir, keyName: string,
nonInteractive: bool): Option[KeystoreData] =
2021-11-30 01:20:21 +00:00
let
keystorePath = validatorsDir / keyName
localKeystorePath = keystorePath / KeystoreFileName
remoteKeystorePath = keystorePath / RemoteKeystoreFileName
if fileExists(localKeystorePath):
loadKeystoreImpl(validatorsDir, secretsDir, keyName, nonInteractive)
elif fileExists(remoteKeystorePath):
loadRemoteKeystoreImpl(validatorsDir, keyName)
else:
error "Unable to find any keystore files", keystorePath
none[KeystoreData]()
2021-11-30 01:20:21 +00:00
proc removeValidatorFiles*(validatorsDir,
secretsDir ,
publicKeyDir: string): KmResult[RemoveValidatorStatus] {.
raises: [Defect].} =
let keystoreDir = validatorsDir / publicKeyDir
let keystoreFile = keystoreDir / KeystoreFileName
let secretFile = secretsDir / publicKeyDir
if not (dirExists(keystoreDir)):
return ok(missingDir)
try:
removeDir(keystoreDir, false)
except OSError:
return err("Could not remove keystore directory")
let res = io2.removeFile(secretFile)
if res.isErr():
return err("Could not remove password file")
ok deleted
proc removeValidator*(pool: var ValidatorPool, conf: AnyConf,
publicKey: ValidatorPubKey): KmResult[RemoveValidatorStatus] {.
raises: [Defect].} =
let publicKeyHex: string = "0x" & publicKey.toHex()
let res = removeValidatorFiles(conf.validatorsDir(),
conf.secretsDir(),
publicKeyHex)
if res.isErr():
return err(res.error())
pool.removeValidator(publicKey)
ok res.value()
iterator listLoadableKeystores*(validatorsDir,
secretsDir: string,
nonInteractive: bool): KeystoreData =
try:
for kind, file in walkDir(validatorsDir):
if kind == pcDir:
let
keyName = splitFile(file).name
keystoreDir = validatorsDir / keyName
keystoreFile = keystoreDir / KeystoreFileName
if not(fileExists(keystoreFile)):
# Skip folders which do not have keystore file inside.
continue
let
secretFile = secretsDir / keyName
keystore = loadKeystore(validatorsDir, secretsDir, keyName, nonInteractive)
if keystore.isSome():
let pubkey = keystore.get().privateKey.toPubKey().toPubKey()
yield KeystoreData(kind: KeystoreKind.Local,
privateKey: keystore.get().privateKey,
description: keystore.get().description,
path: keystore.get().path,
uuid: keystore.get().uuid,
version: keystore.get().version,
pubkey: pubkey)
else:
fatal "Unable to load keystore", keystore_file = keystoreFile
quit 1
except OSError as err:
error "Validator keystores directory not accessible",
path = validatorsDir, err = err.msg
quit 1
iterator listLoadableKeystores*(config: AnyConf): KeystoreData =
for el in listLoadableKeystores(config.validatorsDir(),
config.secretsDir(),
config.nonInteractive):
yield el
type
KeystoreGenerationErrorKind* = enum
FailedToCreateValidatorsDir
FailedToCreateKeystoreDir
2020-08-02 18:47:15 +00:00
FailedToCreateSecretsDir
FailedToCreateSecretFile
FailedToCreateKeystoreFile
KeystoreGenerationError* = object
case kind*: KeystoreGenerationErrorKind
of FailedToCreateKeystoreDir,
FailedToCreateValidatorsDir,
FailedToCreateSecretsDir,
FailedToCreateSecretFile,
FailedToCreateKeystoreFile:
error*: string
proc mapErrTo*[T, E](r: Result[T, E], v: static KeystoreGenerationErrorKind):
Result[T, KeystoreGenerationError] =
r.mapErr(proc (e: E): KeystoreGenerationError =
KeystoreGenerationError(kind: v, error: $e))
proc loadNetKeystore*(keyStorePath: string,
insecurePwd: Option[string]): Option[lcrypto.PrivateKey] =
if not(checkSensitiveFilePermissions(keystorePath)):
error "Network keystorage file has insecure permissions",
key_path = keyStorePath
return
let keyStore =
try:
Json.loadFile(keystorePath, NetKeystore)
except IOError as err:
error "Failed to read network keystore", err = err.msg,
path = keystorePath
return
except SerializationError as err:
error "Invalid network keystore", err = err.formatMsg(keystorePath)
return
if insecurePwd.isSome():
warn "Using insecure password to unlock networking key"
2020-10-02 15:46:05 +00:00
let decrypted = decryptNetKeystore(keystore, KeystorePass.init insecurePwd.get)
if decrypted.isOk:
return some(decrypted.get())
else:
error "Network keystore decryption failed", key_store = keyStorePath
return
else:
let prompt = "Please enter passphrase to unlock networking key: "
let res = keyboardGetPassword[lcrypto.PrivateKey](prompt, 3,
proc (password: string): KsResult[lcrypto.PrivateKey] =
2020-10-02 15:46:05 +00:00
let decrypted = decryptNetKeystore(keystore, KeystorePass.init password)
if decrypted.isErr():
error "Keystore decryption failed. Please try again", keystorePath
decrypted
)
if res.isOk():
some(res.get())
else:
return
proc saveNetKeystore*(rng: var BrHmacDrbgContext, keyStorePath: string,
netKey: lcrypto.PrivateKey, insecurePwd: Option[string]
): Result[void, KeystoreGenerationError] =
let password =
if insecurePwd.isSome():
warn "Using insecure password to lock networking key",
key_path = keyStorePath
insecurePwd.get()
else:
let prompt = "Please enter NEW password to lock network key storage: "
let confirm = "Please confirm, network key storage password: "
? keyboardCreatePassword(prompt, confirm).mapErrTo(
FailedToCreateKeystoreFile)
let keyStore = createNetKeystore(kdfScrypt, rng, netKey,
2020-10-02 15:46:05 +00:00
KeystorePass.init password)
var encodedStorage: string
try:
encodedStorage = Json.encode(keyStore)
except SerializationError as exc:
error "Could not serialize network key storage", key_path = keyStorePath
return err(KeystoreGenerationError(
kind: FailedToCreateKeystoreFile, error: exc.msg))
let res = secureWriteFile(keyStorePath, encodedStorage)
if res.isOk():
ok()
else:
error "Could not write to network key storage file",
key_path = keyStorePath
res.mapErrTo(FailedToCreateKeystoreFile)
proc createValidatorFiles*(
secretsDir, validatorsDir,
keystoreDir, secretFile,
passwordAsString, keystoreFile,
encodedStorage: string): Result[void, KeystoreGenerationError] =
var
success = false # becomes true when everything is created successfully
cleanupSecretsDir = true # becomes false if secretsDir already existed
cleanupValidatorsDir = true # becomes false if validatorsDir already existed
# secretsDir:
let secretsDirExisted: bool = dirExists(secretsDir)
if not(secretsDirExisted):
? secureCreatePath(secretsDir).mapErrTo(FailedToCreateSecretsDir)
defer:
if not (success or secretsDirExisted):
discard io2.removeDir(secretsDir)
# validatorsDir:
let validatorsDirExisted: bool = dirExists(validatorsDir)
if not(validatorsDirExisted):
? secureCreatePath(validatorsDir).mapErrTo(FailedToCreateValidatorsDir)
defer:
if not (success or validatorsDirExisted):
discard io2.removeDir(validatorsDir)
# keystoreDir:
? secureCreatePath(keystoreDir).mapErrTo(FailedToCreateKeystoreDir)
defer:
if not success:
discard io2.removeDir(keystoreDir)
# secretFile:
? secureWriteFile(secretFile, passwordAsString).mapErrTo(FailedToCreateSecretFile)
defer:
if not success:
discard io2.removeFile(secretFile)
# keystoreFile:
? secureWriteFile(keystoreFile, encodedStorage).mapErrTo(FailedToCreateKeystoreFile)
success = true
ok()
proc saveKeystore*(rng: var BrHmacDrbgContext,
validatorsDir, secretsDir: string,
signingKey: ValidatorPrivKey,
signingPubKey: CookedPubKey,
signingKeyPath: KeyPath,
password: string,
mode = Secure): Result[void, KeystoreGenerationError] =
let
keypass = KeystorePass.init(password)
keyName = "0x" & signingPubKey.toHex()
keystoreDir = validatorsDir / keyName
if not existsDir(keystoreDir):
let
keyStore = createKeystore(kdfPbkdf2, rng, signingKey,
keypass, signingKeyPath,
mode = mode)
keystoreFile = keystoreDir / KeystoreFileName
var encodedStorage: string
try:
encodedStorage = Json.encode(keyStore)
except SerializationError as e:
error "Could not serialize keystorage", key_path = keystoreFile
return err(KeystoreGenerationError(
kind: FailedToCreateKeystoreFile, error: e.msg))
? createValidatorFiles(secretsDir, validatorsDir,
keystoreDir,
secretsDir / keyName, keypass.str,
keystoreFile, encodedStorage)
ok()
proc importKeystore*(pool: var ValidatorPool,
rng: var BrHmacDrbgContext,
conf: AnyConf, keystore: Keystore,
password: string): KmResult[AddValidatorStatus]
{.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.toPubKey()
let keyName = "0x" & publicKey.toHex()
let validatorsDir = conf.validatorsDir()
let secretsDir = conf.secretsDir()
let secretFile = secretsDir / keyName
let keystoreDir = validatorsDir / keyName
let keystoreFile = keystoreDir / KeystoreFileName
if fileExists(keystoreFile) or fileExists(secretFile):
return ok(AddValidatorStatus.existingArtifacts)
let res = saveKeystore(rng,
validatorsDir, secretsDir,
privateKey, publicKey,
keystoreDir.KeyPath, password)
if res.isErr():
return err("Keystore Generation Error")
pool.addLocalValidator(KeystoreData.init(privateKey, keystore))
ok AddValidatorStatus.added
Implement split preset/config support (#2710) * Implement split preset/config support This is the initial bulk refactor to introduce runtime config values in a number of places, somewhat replacing the existing mechanism of loading network metadata. It still needs more work, this is the initial refactor that introduces runtime configuration in some of the places that need it. The PR changes the way presets and constants work, to match the spec. In particular, a "preset" now refers to the compile-time configuration while a "cfg" or "RuntimeConfig" is the dynamic part. A single binary can support either mainnet or minimal, but not both. Support for other presets has been removed completely (can be readded, in case there's need). There's a number of outstanding tasks: * `SECONDS_PER_SLOT` still needs fixing * loading custom runtime configs needs redoing * checking constants against YAML file * yeerongpilly support `build/nimbus_beacon_node --network=yeerongpilly --discv5:no --log-level=DEBUG` * load fork epoch from config * fix fork digest sent in status * nicer error string for request failures * fix tools * one more * fixup * fixup * fixup * use "standard" network definition folder in local testnet Files are loaded from their standard locations, including genesis etc, to conform to the format used in the `eth2-networks` repo. * fix launch scripts, allow unknown config values * fix base config of rest test * cleanups * bundle mainnet config using common loader * fix spec links and names * only include supported preset in binary * drop yeerongpilly, add altair-devnet-0, support boot_enr.yaml
2021-07-12 13:01:38 +00:00
proc generateDeposits*(cfg: RuntimeConfig,
2020-07-08 12:36:03 +00:00
rng: var BrHmacDrbgContext,
seed: KeySeed,
firstValidatorIdx, totalNewValidators: int,
validatorsDir: string,
secretsDir: string,
mode = Secure): Result[seq[DepositData], KeystoreGenerationError] =
var deposits: seq[DepositData]
notice "Generating deposits", totalNewValidators, validatorsDir, secretsDir
# We'll reuse a single variable here to make the secret
# scrubbing (burnMem) easier to handle:
var baseKey = deriveMasterKey(seed)
defer: burnMem(baseKey)
baseKey = deriveChildKey(baseKey, baseKeyPath)
for i in 0 ..< totalNewValidators:
let validatorIdx = firstValidatorIdx + i
# We'll reuse a single variable here to make the secret
# scrubbing (burnMem) easier to handle:
var derivedKey = baseKey
defer: burnMem(derivedKey)
derivedKey = deriveChildKey(derivedKey, validatorIdx)
derivedKey = deriveChildKey(derivedKey, 0) # This is witdrawal key
let withdrawalPubKey = derivedKey.toPubKey
derivedKey = deriveChildKey(derivedKey, 0) # This is the signing key
let signingPubKey = derivedKey.toPubKey
var password = KeystorePass.init ncrutils.toHex(getRandomBytes(rng, 32))
defer: burnMem(password)
? saveKeystore(rng, validatorsDir, secretsDir,
derivedKey, signingPubKey,
makeKeyPath(validatorIdx, signingKeyKind), password.str,
mode)
Implement split preset/config support (#2710) * Implement split preset/config support This is the initial bulk refactor to introduce runtime config values in a number of places, somewhat replacing the existing mechanism of loading network metadata. It still needs more work, this is the initial refactor that introduces runtime configuration in some of the places that need it. The PR changes the way presets and constants work, to match the spec. In particular, a "preset" now refers to the compile-time configuration while a "cfg" or "RuntimeConfig" is the dynamic part. A single binary can support either mainnet or minimal, but not both. Support for other presets has been removed completely (can be readded, in case there's need). There's a number of outstanding tasks: * `SECONDS_PER_SLOT` still needs fixing * loading custom runtime configs needs redoing * checking constants against YAML file * yeerongpilly support `build/nimbus_beacon_node --network=yeerongpilly --discv5:no --log-level=DEBUG` * load fork epoch from config * fix fork digest sent in status * nicer error string for request failures * fix tools * one more * fixup * fixup * fixup * use "standard" network definition folder in local testnet Files are loaded from their standard locations, including genesis etc, to conform to the format used in the `eth2-networks` repo. * fix launch scripts, allow unknown config values * fix base config of rest test * cleanups * bundle mainnet config using common loader * fix spec links and names * only include supported preset in binary * drop yeerongpilly, add altair-devnet-0, support boot_enr.yaml
2021-07-12 13:01:38 +00:00
deposits.add prepareDeposit(
cfg, withdrawalPubKey, derivedKey, signingPubKey)
ok deposits
proc saveWallet*(wallet: Wallet, outWalletPath: string): Result[void, string] =
let walletDir = splitFile(outWalletPath).dir
var encodedWallet: string
try:
encodedWallet = Json.encode(wallet, pretty = true)
except SerializationError:
return err("Could not serialize wallet")
? secureCreatePath(walletDir).mapErr(proc(e: auto): string =
"Could not create wallet directory [" & walletDir & "]: " & $e)
? secureWriteFile(outWalletPath, encodedWallet).mapErr(proc(e: auto): string =
"Could not write wallet to file [" & outWalletPath & "]: " & $e)
ok()
proc saveWallet*(wallet: WalletPathPair): Result[void, string] =
saveWallet(wallet.wallet, wallet.path)
proc readPasswordInput(prompt: string, password: var TaintedString): bool =
try:
when defined(windows):
# readPasswordFromStdin() on Windows always returns `false`.
# https://github.com/nim-lang/Nim/issues/15207
discard readPasswordFromStdin(prompt, password)
true
else:
readPasswordFromStdin(prompt, password)
except IOError:
false
proc setStyleNoError(styles: set[Style]) =
when defined(windows):
try: stdout.setStyle(styles)
except: discard
else:
try: stdout.setStyle(styles)
except IOError, ValueError: discard
proc setForegroundColorNoError(color: ForegroundColor) =
when defined(windows):
try: stdout.setForegroundColor(color)
except: discard
else:
try: stdout.setForegroundColor(color)
except IOError, ValueError: discard
proc resetAttributesNoError() =
when defined(windows):
try: stdout.resetAttributes()
except: discard
else:
try: stdout.resetAttributes()
except IOError: discard
proc importKeystoresFromDir*(rng: var BrHmacDrbgContext,
importedDir, validatorsDir, secretsDir: string) =
var password: TaintedString
defer: burnMem(password)
try:
for file in walkDirRec(importedDir):
2020-10-20 13:01:21 +00:00
let filenameParts = splitFile(file)
if toLowerAscii(filenameParts.ext) != ".json":
continue
# In case we are importing from eth2.0-deposits-cli, the imported
# validator_keys directory will also include a "deposit_data" file
# intended for uploading to the launchpad. We'll skip it to avoid
# the "Invalid keystore" warning that it will trigger.
if filenameParts.name.startsWith("deposit_data"):
continue
let keystore =
try:
Json.loadFile(file, Keystore)
except SerializationError as e:
warn "Invalid keystore", err = e.formatMsg(file)
continue
except IOError as e:
warn "Failed to read keystore file", file, err = e.msg
continue
var firstDecryptionAttempt = true
while true:
var secret: seq[byte]
let status = decryptCryptoField(keystore.crypto,
KeystorePass.init password,
secret)
case status
2021-11-30 01:20:21 +00:00
of DecryptionStatus.Success:
let privKey = ValidatorPrivKey.fromRaw(secret)
if privKey.isOk:
let pubkey = privKey.value.toPubKey
var password = KeystorePass.init ncrutils.toHex(getRandomBytes(rng, 32))
defer: burnMem(password)
let status = saveKeystore(rng, validatorsDir, secretsDir,
privKey.value, pubkey,
keystore.path, password.str)
if status.isOk:
notice "Keystore imported", file
else:
error "Failed to import keystore",
file, validatorsDir, secretsDir, err = status.error
else:
error "Imported keystore holds invalid key", file, err = privKey.error
break
2021-11-30 01:20:21 +00:00
of DecryptionStatus.InvalidKeystore:
warn "Invalid keystore", file
break
2021-11-30 01:20:21 +00:00
of DecryptionStatus.InvalidPassword:
if firstDecryptionAttempt:
try:
2020-08-02 18:47:15 +00:00
const msg = "Please enter the password for decrypting '$1' " &
"or press ENTER to skip importing this keystore"
echo msg % [file]
except ValueError:
raiseAssert "The format string above is correct"
else:
echo "The entered password was incorrect. Please try again."
firstDecryptionAttempt = false
if not readPasswordInput("Password: ", password):
echo "System error while entering password. Please try again."
if password.len == 0:
break
except OSError:
fatal "Failed to access the imported deposits directory"
quit 1
template ask(prompt: string): string =
try:
stdout.write prompt, ": "
stdin.readLine()
except IOError:
return err "failure to read data from stdin"
proc pickPasswordAndSaveWallet(rng: var BrHmacDrbgContext,
config: BeaconNodeConf,
seed: KeySeed): Result[WalletPathPair, string] =
echoP "When you perform operations with your wallet such as withdrawals " &
"and additional deposits, you'll be asked to enter a signing " &
"password. Please note that this password is local to the current " &
"machine and you can change it at any time."
echo ""
2020-10-05 15:27:05 +00:00
var password =
block:
let prompt = "Please enter a password: "
let confirm = "Please repeat the password: "
? keyboardCreatePassword(prompt, confirm).mapErr(proc(e: auto): string = $e)
2020-10-01 19:18:56 +00:00
defer: burnMem(password)
var name: WalletName
let outWalletName = config.outWalletName
if outWalletName.isSome:
name = outWalletName.get
else:
echoP "For your convenience, the wallet can be identified with a name " &
"of your choice. Please enter a wallet name below or press ENTER " &
"to continue with a machine-generated name."
echo ""
while true:
var enteredName = ask "Wallet name"
if enteredName.len > 0:
name =
try:
WalletName.parseCmdArg(enteredName)
except CatchableError as err:
echo err.msg & ". Please try again."
continue
break
let nextAccount =
if config.cmd == wallets and config.walletsCmd == WalletsCmd.restore:
config.restoredDepositsCount
else:
none Natural
let wallet = createWallet(kdfPbkdf2, rng, seed,
name = name,
nextAccount = nextAccount,
password = KeystorePass.init password)
let outWalletFileFlag = config.outWalletFile
let outWalletFile =
if outWalletFileFlag.isSome:
string outWalletFileFlag.get
else:
config.walletsDir / addFileExt(string wallet.name, "json")
let status = saveWallet(wallet, outWalletFile)
if status.isErr:
return err("failure to create wallet file due to " & status.error)
echo "\nWallet file successfully written to \"", outWalletFile, "\""
return ok WalletPathPair(wallet: wallet, path: outWalletFile)
when defined(windows):
2020-10-12 19:58:09 +00:00
proc clearScreen =
discard execShellCmd("cls")
else:
template clearScreen =
echo "\e[1;1H\e[2J\e[3J"
proc createWalletInteractively*(
rng: var BrHmacDrbgContext,
config: BeaconNodeConf): Result[CreatedWallet, string] =
if config.nonInteractive:
return err "not running in interactive mode"
echoP "The generated wallet is uniquely identified by a seed phrase " &
"consisting of 24 words. In case you lose your wallet and you " &
"need to restore it on a different machine, you can use the " &
"seed phrase to re-generate your signing and withdrawal keys."
echoP "The seed phrase should be kept secret in a safe location as if " &
"you are protecting a sensitive password. It can be used to withdraw " &
"funds from your wallet."
echoP "We will display the seed phrase on the next screen. Please make sure " &
"you are in a safe environment and there are no cameras or potentially " &
"unwanted eye witnesses around you. Please prepare everything necessary " &
"to copy the seed phrase to a safe location and type 'continue' in " &
"the prompt below to proceed to the next screen or 'q' to exit now."
echo ""
while true:
let answer = ask "Action"
if answer.len > 0 and answer[0] == 'q': quit 1
if answer == "continue": break
echoP "To proceed to your seed phrase, please type 'continue' (without the quotes). " &
"Type 'q' to exit now."
echo ""
var mnemonic = generateMnemonic(rng)
defer: burnMem(mnemonic)
try:
echoP "Your seed phrase is:"
setStyleNoError({styleBright})
setForegroundColorNoError fgCyan
echoP $mnemonic
resetAttributesNoError()
except IOError, ValueError:
return err "failure to write to the standard output"
echoP "Press any key to continue."
try:
2020-10-12 19:58:09 +00:00
discard getch()
except IOError as err:
fatal "Failed to read a key from stdin", err = err.msg
quit 1
clearScreen()
echoP "To confirm that you've saved the seed phrase, please enter the " &
"first and the last three words of it. In case you've saved the " &
"seek phrase in your clipboard, we strongly advice clearing the " &
"clipboard now."
echo ""
for i in countdown(2, 0):
let answer = ask "Answer"
let parts = answer.split(' ', maxsplit = 1)
if parts.len == 2:
if count(parts[1], ' ') == 2 and
mnemonic.string.startsWith(parts[0]) and
mnemonic.string.endsWith(parts[1]):
break
else:
doAssert parts.len == 1
if i > 0:
echo "\nYour answer was not correct. You have ", i, " more attempts"
echoP "Please enter 4 words separated with a single space " &
"(the first word from the seed phrase, followed by the last 3)"
echo ""
else:
quit 1
clearScreen()
var mnenomicPassword = KeystorePass.init ""
defer: burnMem(mnenomicPassword)
echoP "The recovery of your wallet can be additionally protected by a" &
"recovery password. Since the seed phrase itself can be considered " &
"a password, setting such an additional password is optional. " &
"To ensure the strongest possible security, we recommend writing " &
"down your seed phrase and remembering your recovery password. " &
"If you don'n want to set a recovery password, just press ENTER."
var recoveryPassword = keyboardCreatePassword(
"Recovery password: ", "Confirm password: ", allowEmpty = true)
defer:
if recoveryPassword.isOk:
burnMem(recoveryPassword.get)
if recoveryPassword.isErr:
fatal "Failed to read password from stdin: "
quit 1
var keystorePass = KeystorePass.init recoveryPassword.get
defer: burnMem(keystorePass)
var seed = getSeed(mnemonic, keystorePass)
defer: burnMem(seed)
let walletPath = ? pickPasswordAndSaveWallet(rng, config, seed)
return ok CreatedWallet(walletPath: walletPath, seed: seed)
proc restoreWalletInteractively*(rng: var BrHmacDrbgContext,
config: BeaconNodeConf) =
var
enteredMnemonic: TaintedString
validatedMnemonic: Mnemonic
defer:
burnMem enteredMnemonic
burnMem validatedMnemonic
echo "To restore your wallet, please enter your backed-up seed phrase."
while true:
if not readPasswordInput("Seedphrase: ", enteredMnemonic):
fatal "failure to read password from stdin"
quit 1
if validateMnemonic(enteredMnemonic, validatedMnemonic):
break
else:
echo "The entered mnemonic was not valid. Please try again."
echoP "If your seed phrase was protected with a recovery password, " &
"please enter it below. Please ENTER to attempt to restore " &
"the wallet without a recovery password."
var recoveryPassword = keyboardCreatePassword(
"Recovery password: ", "Confirm password: ", allowEmpty = true)
defer:
if recoveryPassword.isOk:
burnMem(recoveryPassword.get)
if recoveryPassword.isErr:
fatal "Failed to read password from stdin"
quit 1
var keystorePass = KeystorePass.init recoveryPassword.get
defer: burnMem(keystorePass)
var seed = getSeed(validatedMnemonic, keystorePass)
defer: burnMem(seed)
discard pickPasswordAndSaveWallet(rng, config, seed)
proc unlockWalletInteractively*(wallet: Wallet): Result[KeySeed, string] =
echo "Please enter the password for unlocking the wallet"
let res = keyboardGetPassword[KeySeed]("Password: ", 3,
proc (password: string): KsResult[KeySeed] =
var secret: seq[byte]
defer: burnMem(secret)
let status = decryptCryptoField(wallet.crypto, KeystorePass.init password, secret)
case status
2021-11-30 01:20:21 +00:00
of DecryptionStatus.Success:
ok(KeySeed secret)
else:
# TODO Handle InvalidKeystore in a special way here
let failed = "Unlocking of the wallet failed. Please try again"
echo failed
err(failed)
)
if res.isOk():
ok(res.get())
else:
err "Unlocking of the wallet failed."
proc loadWallet*(fileName: string): Result[Wallet, string] =
try:
ok Json.loadFile(fileName, Wallet)
except SerializationError as err:
err "Invalid wallet syntax: " & err.formatMsg(fileName)
except IOError as err:
err "Error accessing wallet file \"" & fileName & "\": " & err.msg
proc findWallet*(config: BeaconNodeConf,
name: WalletName): Result[Option[WalletPathPair], string] =
var walletFiles = newSeq[string]()
try:
for kind, walletFile in walkDir(config.walletsDir):
if kind != pcFile: continue
let walletId = splitFile(walletFile).name
if cmpIgnoreCase(walletId, name.string) == 0:
let wallet = ? loadWallet(walletFile)
return ok some WalletPathPair(wallet: wallet, path: walletFile)
walletFiles.add walletFile
except OSError as err:
return err("Error accessing the wallets directory \"" &
config.walletsDir & "\": " & err.msg)
for walletFile in walletFiles:
let wallet = ? loadWallet(walletFile)
2020-10-01 19:18:56 +00:00
if cmpIgnoreCase(wallet.name.string, name.string) == 0 or
cmpIgnoreCase(wallet.uuid.string, name.string) == 0:
return ok some WalletPathPair(wallet: wallet, path: walletFile)
return ok none(WalletPathPair)
type
# This is not particularly well-standardized yet.
# Some relevant code for generating (1) and validating (2) the data can be found below:
# 1) https://github.com/ethereum/eth2.0-deposit-cli/blob/dev/eth2deposit/credentials.py
# 2) https://github.com/ethereum/eth2.0-deposit/blob/dev/src/pages/UploadValidator/validateDepositKey.ts
LaunchPadDeposit* = object
pubkey*: ValidatorPubKey
withdrawal_credentials*: Eth2Digest
amount*: Gwei
signature*: ValidatorSig
deposit_message_root*: Eth2Digest
deposit_data_root*: Eth2Digest
fork_version*: Version
func init*(T: type LaunchPadDeposit,
Implement split preset/config support (#2710) * Implement split preset/config support This is the initial bulk refactor to introduce runtime config values in a number of places, somewhat replacing the existing mechanism of loading network metadata. It still needs more work, this is the initial refactor that introduces runtime configuration in some of the places that need it. The PR changes the way presets and constants work, to match the spec. In particular, a "preset" now refers to the compile-time configuration while a "cfg" or "RuntimeConfig" is the dynamic part. A single binary can support either mainnet or minimal, but not both. Support for other presets has been removed completely (can be readded, in case there's need). There's a number of outstanding tasks: * `SECONDS_PER_SLOT` still needs fixing * loading custom runtime configs needs redoing * checking constants against YAML file * yeerongpilly support `build/nimbus_beacon_node --network=yeerongpilly --discv5:no --log-level=DEBUG` * load fork epoch from config * fix fork digest sent in status * nicer error string for request failures * fix tools * one more * fixup * fixup * fixup * use "standard" network definition folder in local testnet Files are loaded from their standard locations, including genesis etc, to conform to the format used in the `eth2-networks` repo. * fix launch scripts, allow unknown config values * fix base config of rest test * cleanups * bundle mainnet config using common loader * fix spec links and names * only include supported preset in binary * drop yeerongpilly, add altair-devnet-0, support boot_enr.yaml
2021-07-12 13:01:38 +00:00
cfg: RuntimeConfig, d: DepositData): T =
T(pubkey: d.pubkey,
withdrawal_credentials: d.withdrawal_credentials,
amount: d.amount,
signature: d.signature,
deposit_message_root: hash_tree_root(d as DepositMessage),
deposit_data_root: hash_tree_root(d),
Implement split preset/config support (#2710) * Implement split preset/config support This is the initial bulk refactor to introduce runtime config values in a number of places, somewhat replacing the existing mechanism of loading network metadata. It still needs more work, this is the initial refactor that introduces runtime configuration in some of the places that need it. The PR changes the way presets and constants work, to match the spec. In particular, a "preset" now refers to the compile-time configuration while a "cfg" or "RuntimeConfig" is the dynamic part. A single binary can support either mainnet or minimal, but not both. Support for other presets has been removed completely (can be readded, in case there's need). There's a number of outstanding tasks: * `SECONDS_PER_SLOT` still needs fixing * loading custom runtime configs needs redoing * checking constants against YAML file * yeerongpilly support `build/nimbus_beacon_node --network=yeerongpilly --discv5:no --log-level=DEBUG` * load fork epoch from config * fix fork digest sent in status * nicer error string for request failures * fix tools * one more * fixup * fixup * fixup * use "standard" network definition folder in local testnet Files are loaded from their standard locations, including genesis etc, to conform to the format used in the `eth2-networks` repo. * fix launch scripts, allow unknown config values * fix base config of rest test * cleanups * bundle mainnet config using common loader * fix spec links and names * only include supported preset in binary * drop yeerongpilly, add altair-devnet-0, support boot_enr.yaml
2021-07-12 13:01:38 +00:00
fork_version: cfg.GENESIS_FORK_VERSION)
func `as`*(copied: LaunchPadDeposit, T: type DepositData): T =
T(pubkey: copied.pubkey,
withdrawal_credentials: copied.withdrawal_credentials,
amount: copied.amount,
signature: copied.signature)