Add keystore management and interactive password handling.

This commit is contained in:
cheatfate 2020-08-24 19:06:41 +03:00 committed by zah
parent ae72c08573
commit 40f2b74f73
2 changed files with 159 additions and 62 deletions

View File

@ -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).

View File

@ -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, ": "