Add keystore management and interactive password handling.
This commit is contained in:
parent
ae72c08573
commit
40f2b74f73
|
@ -23,7 +23,8 @@ import
|
||||||
# Beacon node modules
|
# Beacon node modules
|
||||||
version, conf, eth2_discovery, libp2p_json_serialization, conf,
|
version, conf, eth2_discovery, libp2p_json_serialization, conf,
|
||||||
ssz/ssz_serialization,
|
ssz/ssz_serialization,
|
||||||
peer_pool, spec/[datatypes, network], ./time
|
peer_pool, spec/[datatypes, network], ./time,
|
||||||
|
keystore_management
|
||||||
|
|
||||||
when defined(nbc_gossipsub_11):
|
when defined(nbc_gossipsub_11):
|
||||||
import libp2p/protocols/pubsub/gossipsub
|
import libp2p/protocols/pubsub/gossipsub
|
||||||
|
@ -1219,55 +1220,37 @@ proc getPersistentNetKeys*(rng: var BrHmacDrbgContext,
|
||||||
conf.dataDir / conf.netKeyFile
|
conf.dataDir / conf.netKeyFile
|
||||||
|
|
||||||
if fileAccessible(keyPath, {AccessFlags.Find}):
|
if fileAccessible(keyPath, {AccessFlags.Find}):
|
||||||
let gmask = {UserRead, UserWrite}
|
info "Network key storage is present, unlocking", key_path = keyPath
|
||||||
let pmask = {UserExec,
|
let res = loadNetKeystore(keyPath)
|
||||||
GroupRead, GroupWrite, GroupExec,
|
if res.isNone():
|
||||||
OtherRead, OtherWrite, OtherExec}
|
fatal "Could not load network key file"
|
||||||
let pres = getPermissionsSet(keyPath)
|
|
||||||
if pres.isErr():
|
|
||||||
fatal "Could not check key file permissions",
|
|
||||||
key_path = keyPath, errorCode = $pres.error,
|
|
||||||
errorMsg = ioErrorMsg(pres.error)
|
|
||||||
quit QuitFailure
|
quit QuitFailure
|
||||||
|
let privKey = res.get()
|
||||||
let insecurePermissions = pres.get() * pmask
|
let pubKey = privKey.getKey().tryGet()
|
||||||
if insecurePermissions != {}:
|
info "Network key storage was successfully unlocked",
|
||||||
fatal "Network key file has insecure permissions",
|
key_path = keyPath,
|
||||||
key_path = keyPath,
|
network_public_key = byteutils.toHex(pubKey.getBytes().tryGet())
|
||||||
insecure_permissions = $insecurePermissions,
|
return KeyPair(seckey: privKey, pubkey: pubKey)
|
||||||
current_permissions = pres.get().toString(),
|
|
||||||
required_permissions = gmask.toString()
|
|
||||||
quit QuitFailure
|
|
||||||
|
|
||||||
let kres = readAllFile(keyPath)
|
|
||||||
if not(kres.isOk()):
|
|
||||||
fatal "Could not read network key file", key_path = keyPath
|
|
||||||
quit QuitFailure
|
|
||||||
|
|
||||||
let keyBytes = kres.get()
|
|
||||||
|
|
||||||
let rres = PrivateKey.init(keyBytes)
|
|
||||||
if not(rres.isOk()):
|
|
||||||
fatal "Incorrect network key file", key_path = keyPath
|
|
||||||
quit QuitFailure
|
|
||||||
|
|
||||||
let privKey = rres.get()
|
|
||||||
return KeyPair(seckey: privKey, pubkey: privKey.getKey().tryGet())
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
let res = PrivateKey.random(Secp256k1, rng)
|
info "Network key storage is missing, creating a new one",
|
||||||
if res.isErr():
|
key_path = keyPath
|
||||||
|
let rres = PrivateKey.random(Secp256k1, rng)
|
||||||
|
if rres.isErr():
|
||||||
fatal "Could not generate random network key file"
|
fatal "Could not generate random network key file"
|
||||||
quit QuitFailure
|
quit QuitFailure
|
||||||
|
|
||||||
let privKey = res.get()
|
let privKey = rres.get()
|
||||||
|
let pubKey = privKey.getKey().tryGet()
|
||||||
|
|
||||||
let wres = writeFile(keyPath, privKey.getBytes().tryGet(), 0o600)
|
let sres = saveNetKeystore(rng, keyPath, privKey)
|
||||||
if not(wres.isOk()):
|
if sres.isErr():
|
||||||
fatal "Could not write network key file", key_path = keyPath
|
fatal "Could not create network key file", key_path = keyPath
|
||||||
quit QuitFailure
|
quit QuitFailure
|
||||||
|
|
||||||
return KeyPair(seckey: privKey, pubkey: privkey.getKey().tryGet())
|
info "New network key storage was created", key_path = keyPath,
|
||||||
|
network_public_key = byteutils.toHex(pubKey.getBytes().tryGet())
|
||||||
|
return KeyPair(seckey: privKey, pubkey: pubKey)
|
||||||
|
|
||||||
of createTestnet:
|
of createTestnet:
|
||||||
let netKeyFile = string(conf.outputNetkeyFile)
|
let netKeyFile = string(conf.outputNetkeyFile)
|
||||||
let keyPath =
|
let keyPath =
|
||||||
|
@ -1276,18 +1259,22 @@ proc getPersistentNetKeys*(rng: var BrHmacDrbgContext,
|
||||||
else:
|
else:
|
||||||
conf.dataDir / netKeyFile
|
conf.dataDir / netKeyFile
|
||||||
|
|
||||||
let res = PrivateKey.random(Secp256k1, rng)
|
let rres = PrivateKey.random(Secp256k1, rng)
|
||||||
if res.isErr():
|
if rres.isErr():
|
||||||
fatal "Could not generate random network key file"
|
fatal "Could not generate random network key file"
|
||||||
quit QuitFailure
|
quit QuitFailure
|
||||||
|
|
||||||
let privKey = res.get()
|
let privKey = rres.get()
|
||||||
|
let pubKey = privKey.getKey().tryGet()
|
||||||
|
|
||||||
let wres = writeFile(keyPath, privKey.getBytes().tryGet(), 0o600)
|
let sres = saveNetKeystore(rng, keyPath, privKey)
|
||||||
if not(wres.isOk()):
|
if sres.isErr():
|
||||||
fatal "Could not write network key file", key_path = keyPath
|
fatal "Could not create network key file"
|
||||||
quit QuitFailure
|
quit QuitFailure
|
||||||
|
|
||||||
|
info "New network key storage was created", key_path = keyPath,
|
||||||
|
network_public_key = byteutils.toHex(pubKey.getBytes().tryGet())
|
||||||
|
|
||||||
return KeyPair(seckey: privKey, pubkey: privkey.getKey().tryGet())
|
return KeyPair(seckey: privKey, pubkey: privkey.getKey().tryGet())
|
||||||
else:
|
else:
|
||||||
let res = PrivateKey.random(Secp256k1, rng)
|
let res = PrivateKey.random(Secp256k1, rng)
|
||||||
|
@ -1314,10 +1301,12 @@ proc createEth2Node*(rng: ref BrHmacDrbgContext,
|
||||||
hostAddress = tcpEndPoint(conf.listenAddress, conf.tcpPort)
|
hostAddress = tcpEndPoint(conf.listenAddress, conf.tcpPort)
|
||||||
announcedAddresses = if extIp.isNone(): @[]
|
announcedAddresses = if extIp.isNone(): @[]
|
||||||
else: @[tcpEndPoint(extIp.get(), extTcpPort)]
|
else: @[tcpEndPoint(extIp.get(), extTcpPort)]
|
||||||
|
let networkPublicKey = byteutils.toHex(netKeys.pubkey.getBytes().tryGet())
|
||||||
notice "Initializing networking", hostAddress,
|
notice "Initializing networking", hostAddress,
|
||||||
|
networkPublicKey,
|
||||||
announcedAddresses
|
announcedAddresses
|
||||||
|
|
||||||
|
|
||||||
# TODO nim-libp2p still doesn't have support for announcing addresses
|
# TODO nim-libp2p still doesn't have support for announcing addresses
|
||||||
# that are different from the host address (this is relevant when we
|
# that are different from the host address (this is relevant when we
|
||||||
# are running behind a NAT).
|
# are running behind a NAT).
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import
|
import
|
||||||
std/[os, strutils, terminal, wordwrap],
|
std/[os, strutils, terminal, wordwrap],
|
||||||
stew/byteutils, chronicles, chronos, web3, stint, json_serialization,
|
chronicles, chronos, web3, stint, json_serialization, stew/byteutils,
|
||||||
serialization, blscurve, eth/common/eth_types, eth/keys, confutils, bearssl,
|
serialization, blscurve, eth/common/eth_types, eth/keys, confutils, bearssl,
|
||||||
spec/[datatypes, digest, crypto, keystore],
|
spec/[datatypes, digest, crypto, keystore],
|
||||||
|
stew/io2, libp2p/crypto/crypto as lcrypto,
|
||||||
|
nimcrypto/utils as ncrutils,
|
||||||
conf, ssz/merkleization, network_metadata
|
conf, ssz/merkleization, network_metadata
|
||||||
|
|
||||||
export
|
export
|
||||||
|
@ -12,6 +14,7 @@ export
|
||||||
|
|
||||||
const
|
const
|
||||||
keystoreFileName* = "keystore.json"
|
keystoreFileName* = "keystore.json"
|
||||||
|
netKeystoreFileName* = "network_keystore.json"
|
||||||
|
|
||||||
type
|
type
|
||||||
WalletPathPair* = object
|
WalletPathPair* = object
|
||||||
|
@ -22,6 +25,17 @@ type
|
||||||
walletPath*: WalletPathPair
|
walletPath*: WalletPathPair
|
||||||
mnemonic*: Mnemonic
|
mnemonic*: Mnemonic
|
||||||
|
|
||||||
|
const
|
||||||
|
minPasswordLen = 10
|
||||||
|
|
||||||
|
mostCommonPasswords = wordListArray(
|
||||||
|
currentSourcePath.parentDir /
|
||||||
|
"../vendor/nimbus-security-resources/passwords/10-million-password-list-top-100000.txt",
|
||||||
|
minWordLen = minPasswordLen)
|
||||||
|
|
||||||
|
template echo80(msg: string) =
|
||||||
|
echo wrapWords(msg, 80)
|
||||||
|
|
||||||
proc loadKeystore(validatorsDir, secretsDir, keyName: string,
|
proc loadKeystore(validatorsDir, secretsDir, keyName: string,
|
||||||
nonInteractive: bool): Option[ValidatorPrivKey] =
|
nonInteractive: bool): Option[ValidatorPrivKey] =
|
||||||
let
|
let
|
||||||
|
@ -117,6 +131,111 @@ type
|
||||||
FailedToCreateSecretFile
|
FailedToCreateSecretFile
|
||||||
FailedToCreateKeystoreFile
|
FailedToCreateKeystoreFile
|
||||||
|
|
||||||
|
proc loadNetKeystore*(keyStorePath: string): Option[lcrypto.PrivateKey] =
|
||||||
|
when defined(windows):
|
||||||
|
# Windows do not support per-user permissions, skiping verification part.
|
||||||
|
discard
|
||||||
|
else:
|
||||||
|
let allowedMask = {UserRead, UserWrite}
|
||||||
|
let mask = {UserExec,
|
||||||
|
GroupRead, GroupWrite, GroupExec,
|
||||||
|
OtherRead, OtherWrite, OtherExec}
|
||||||
|
let pres = getPermissionsSet(keyStorePath)
|
||||||
|
if pres.isErr():
|
||||||
|
error "Could not check key file permissions",
|
||||||
|
key_path = keyStorePath, errorCode = $pres.error,
|
||||||
|
errorMsg = ioErrorMsg(pres.error)
|
||||||
|
return
|
||||||
|
|
||||||
|
let insecurePermissions = pres.get() * mask
|
||||||
|
if insecurePermissions != {}:
|
||||||
|
error "Network key file has insecure permissions",
|
||||||
|
key_path = keyStorePath,
|
||||||
|
insecure_permissions = $insecurePermissions,
|
||||||
|
current_permissions = pres.get().toString(),
|
||||||
|
required_permissions = allowedMask.toString()
|
||||||
|
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
|
||||||
|
|
||||||
|
var remainingAttempts = 3
|
||||||
|
var counter = 0
|
||||||
|
var prompt = "Please enter passphrase to unlock networking key: "
|
||||||
|
while remainingAttempts > 0:
|
||||||
|
let passphrase = KeystorePass:
|
||||||
|
try:
|
||||||
|
readPasswordFromStdin(prompt)
|
||||||
|
except IOError:
|
||||||
|
error "Could not read password from stdin"
|
||||||
|
return
|
||||||
|
|
||||||
|
let decrypted = decryptNetKeystore(keystore, passphrase)
|
||||||
|
if decrypted.isOk:
|
||||||
|
return some(decrypted.get())
|
||||||
|
else:
|
||||||
|
dec remainingAttempts
|
||||||
|
inc counter
|
||||||
|
os.sleep(1000 * counter)
|
||||||
|
error "Network keystore decryption failed", key_store = keyStorePath
|
||||||
|
|
||||||
|
proc saveNetKeystore*(rng: var BrHmacDrbgContext, keyStorePath: string,
|
||||||
|
netKey: lcrypto.PrivateKey): Result[void, KeystoreGenerationError] =
|
||||||
|
var password, confirmedPassword: TaintedString
|
||||||
|
while true:
|
||||||
|
let prompt = "Please enter NEW password to lock network key storage: "
|
||||||
|
|
||||||
|
password =
|
||||||
|
try:
|
||||||
|
readPasswordFromStdin(prompt)
|
||||||
|
except IOError:
|
||||||
|
error "Could not read password from stdin"
|
||||||
|
return err(FailedToCreateKeystoreFile)
|
||||||
|
|
||||||
|
if len(password) < minPasswordLen:
|
||||||
|
echo "The entered password should be at least ", minPasswordLen,
|
||||||
|
" characters"
|
||||||
|
continue
|
||||||
|
elif password in mostCommonPasswords:
|
||||||
|
echo80 "The entered password is too commonly used and it would be easy " &
|
||||||
|
"to brute-force with automated tools."
|
||||||
|
continue
|
||||||
|
|
||||||
|
confirmedPassword =
|
||||||
|
try:
|
||||||
|
readPasswordFromStdin("Please confirm, network key storage password: ")
|
||||||
|
except IOError:
|
||||||
|
error "Could not read password from stdin"
|
||||||
|
return err(FailedToCreateKeystoreFile)
|
||||||
|
|
||||||
|
if password != confirmedPassword:
|
||||||
|
echo "Passwords don't match, please try again"
|
||||||
|
continue
|
||||||
|
|
||||||
|
break
|
||||||
|
|
||||||
|
let keyStore = createNetKeystore(kdfScrypt, rng, netKey,
|
||||||
|
KeystorePass password)
|
||||||
|
var encodedStorage: string
|
||||||
|
try:
|
||||||
|
encodedStorage = Json.encode(keyStore)
|
||||||
|
except SerializationError:
|
||||||
|
return err(FailedToCreateKeystoreFile)
|
||||||
|
|
||||||
|
let res = writeFile(keyStorePath, encodedStorage, 0o600)
|
||||||
|
if res.isOk():
|
||||||
|
ok()
|
||||||
|
else:
|
||||||
|
err(FailedToCreateKeystoreFile)
|
||||||
|
|
||||||
proc saveKeystore(rng: var BrHmacDrbgContext,
|
proc saveKeystore(rng: var BrHmacDrbgContext,
|
||||||
validatorsDir, secretsDir: string,
|
validatorsDir, secretsDir: string,
|
||||||
signingKey: ValidatorPrivKey, signingPubKey: ValidatorPubKey,
|
signingKey: ValidatorPrivKey, signingPubKey: ValidatorPubKey,
|
||||||
|
@ -126,7 +245,7 @@ proc saveKeystore(rng: var BrHmacDrbgContext,
|
||||||
validatorDir = validatorsDir / keyName
|
validatorDir = validatorsDir / keyName
|
||||||
|
|
||||||
if not existsDir(validatorDir):
|
if not existsDir(validatorDir):
|
||||||
var password = KeystorePass getRandomBytes(rng, 32).toHex
|
var password = KeystorePass ncrutils.toHex(getRandomBytes(rng, 32))
|
||||||
defer: burnMem(password)
|
defer: burnMem(password)
|
||||||
|
|
||||||
let
|
let
|
||||||
|
@ -179,14 +298,6 @@ proc generateDeposits*(preset: RuntimePreset,
|
||||||
|
|
||||||
ok deposits
|
ok deposits
|
||||||
|
|
||||||
const
|
|
||||||
minPasswordLen = 10
|
|
||||||
|
|
||||||
mostCommonPasswords = wordListArray(
|
|
||||||
currentSourcePath.parentDir /
|
|
||||||
"../vendor/nimbus-security-resources/passwords/10-million-password-list-top-100000.txt",
|
|
||||||
minWordLen = minPasswordLen)
|
|
||||||
|
|
||||||
proc saveWallet*(wallet: Wallet, outWalletPath: string): Result[void, string] =
|
proc saveWallet*(wallet: Wallet, outWalletPath: string): Result[void, string] =
|
||||||
try: createDir splitFile(outWalletPath).dir
|
try: createDir splitFile(outWalletPath).dir
|
||||||
except OSError, IOError:
|
except OSError, IOError:
|
||||||
|
@ -302,9 +413,6 @@ proc importKeystoresFromDir*(rng: var BrHmacDrbgContext,
|
||||||
fatal "Failed to access the imported deposits directory"
|
fatal "Failed to access the imported deposits directory"
|
||||||
quit 1
|
quit 1
|
||||||
|
|
||||||
template echo80(msg: string) =
|
|
||||||
echo wrapWords(msg, 80)
|
|
||||||
|
|
||||||
template ask(prompt: string): string =
|
template ask(prompt: string): string =
|
||||||
try:
|
try:
|
||||||
stdout.write prompt, ": "
|
stdout.write prompt, ": "
|
||||||
|
|
Loading…
Reference in New Issue