Add keystore management and interactive password handling.
This commit is contained in:
parent
ae72c08573
commit
40f2b74f73
|
@ -23,7 +23,8 @@ import
|
|||
# Beacon node modules
|
||||
version, conf, eth2_discovery, libp2p_json_serialization, conf,
|
||||
ssz/ssz_serialization,
|
||||
peer_pool, spec/[datatypes, network], ./time
|
||||
peer_pool, spec/[datatypes, network], ./time,
|
||||
keystore_management
|
||||
|
||||
when defined(nbc_gossipsub_11):
|
||||
import libp2p/protocols/pubsub/gossipsub
|
||||
|
@ -1219,55 +1220,37 @@ proc getPersistentNetKeys*(rng: var BrHmacDrbgContext,
|
|||
conf.dataDir / conf.netKeyFile
|
||||
|
||||
if fileAccessible(keyPath, {AccessFlags.Find}):
|
||||
let gmask = {UserRead, UserWrite}
|
||||
let pmask = {UserExec,
|
||||
GroupRead, GroupWrite, GroupExec,
|
||||
OtherRead, OtherWrite, OtherExec}
|
||||
let pres = getPermissionsSet(keyPath)
|
||||
if pres.isErr():
|
||||
fatal "Could not check key file permissions",
|
||||
key_path = keyPath, errorCode = $pres.error,
|
||||
errorMsg = ioErrorMsg(pres.error)
|
||||
info "Network key storage is present, unlocking", key_path = keyPath
|
||||
let res = loadNetKeystore(keyPath)
|
||||
if res.isNone():
|
||||
fatal "Could not load network key file"
|
||||
quit QuitFailure
|
||||
|
||||
let insecurePermissions = pres.get() * pmask
|
||||
if insecurePermissions != {}:
|
||||
fatal "Network key file has insecure permissions",
|
||||
key_path = keyPath,
|
||||
insecure_permissions = $insecurePermissions,
|
||||
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())
|
||||
|
||||
let privKey = res.get()
|
||||
let pubKey = privKey.getKey().tryGet()
|
||||
info "Network key storage was successfully unlocked",
|
||||
key_path = keyPath,
|
||||
network_public_key = byteutils.toHex(pubKey.getBytes().tryGet())
|
||||
return KeyPair(seckey: privKey, pubkey: pubKey)
|
||||
else:
|
||||
let res = PrivateKey.random(Secp256k1, rng)
|
||||
if res.isErr():
|
||||
info "Network key storage is missing, creating a new one",
|
||||
key_path = keyPath
|
||||
let rres = PrivateKey.random(Secp256k1, rng)
|
||||
if rres.isErr():
|
||||
fatal "Could not generate random network key file"
|
||||
quit QuitFailure
|
||||
|
||||
let privKey = res.get()
|
||||
let privKey = rres.get()
|
||||
let pubKey = privKey.getKey().tryGet()
|
||||
|
||||
let wres = writeFile(keyPath, privKey.getBytes().tryGet(), 0o600)
|
||||
if not(wres.isOk()):
|
||||
fatal "Could not write network key file", key_path = keyPath
|
||||
let sres = saveNetKeystore(rng, keyPath, privKey)
|
||||
if sres.isErr():
|
||||
fatal "Could not create network key file", key_path = keyPath
|
||||
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:
|
||||
let netKeyFile = string(conf.outputNetkeyFile)
|
||||
let keyPath =
|
||||
|
@ -1276,18 +1259,22 @@ proc getPersistentNetKeys*(rng: var BrHmacDrbgContext,
|
|||
else:
|
||||
conf.dataDir / netKeyFile
|
||||
|
||||
let res = PrivateKey.random(Secp256k1, rng)
|
||||
if res.isErr():
|
||||
let rres = PrivateKey.random(Secp256k1, rng)
|
||||
if rres.isErr():
|
||||
fatal "Could not generate random network key file"
|
||||
quit QuitFailure
|
||||
|
||||
let privKey = res.get()
|
||||
let privKey = rres.get()
|
||||
let pubKey = privKey.getKey().tryGet()
|
||||
|
||||
let wres = writeFile(keyPath, privKey.getBytes().tryGet(), 0o600)
|
||||
if not(wres.isOk()):
|
||||
fatal "Could not write network key file", key_path = keyPath
|
||||
let sres = saveNetKeystore(rng, keyPath, privKey)
|
||||
if sres.isErr():
|
||||
fatal "Could not create network key file"
|
||||
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())
|
||||
else:
|
||||
let res = PrivateKey.random(Secp256k1, rng)
|
||||
|
@ -1314,10 +1301,12 @@ proc createEth2Node*(rng: ref BrHmacDrbgContext,
|
|||
hostAddress = tcpEndPoint(conf.listenAddress, conf.tcpPort)
|
||||
announcedAddresses = if extIp.isNone(): @[]
|
||||
else: @[tcpEndPoint(extIp.get(), extTcpPort)]
|
||||
|
||||
let networkPublicKey = byteutils.toHex(netKeys.pubkey.getBytes().tryGet())
|
||||
notice "Initializing networking", hostAddress,
|
||||
networkPublicKey,
|
||||
announcedAddresses
|
||||
|
||||
|
||||
# TODO nim-libp2p still doesn't have support for announcing addresses
|
||||
# that are different from the host address (this is relevant when we
|
||||
# are running behind a NAT).
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import
|
||||
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,
|
||||
spec/[datatypes, digest, crypto, keystore],
|
||||
stew/io2, libp2p/crypto/crypto as lcrypto,
|
||||
nimcrypto/utils as ncrutils,
|
||||
conf, ssz/merkleization, network_metadata
|
||||
|
||||
export
|
||||
|
@ -12,6 +14,7 @@ export
|
|||
|
||||
const
|
||||
keystoreFileName* = "keystore.json"
|
||||
netKeystoreFileName* = "network_keystore.json"
|
||||
|
||||
type
|
||||
WalletPathPair* = object
|
||||
|
@ -22,6 +25,17 @@ type
|
|||
walletPath*: WalletPathPair
|
||||
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,
|
||||
nonInteractive: bool): Option[ValidatorPrivKey] =
|
||||
let
|
||||
|
@ -117,6 +131,111 @@ type
|
|||
FailedToCreateSecretFile
|
||||
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,
|
||||
validatorsDir, secretsDir: string,
|
||||
signingKey: ValidatorPrivKey, signingPubKey: ValidatorPubKey,
|
||||
|
@ -126,7 +245,7 @@ proc saveKeystore(rng: var BrHmacDrbgContext,
|
|||
validatorDir = validatorsDir / keyName
|
||||
|
||||
if not existsDir(validatorDir):
|
||||
var password = KeystorePass getRandomBytes(rng, 32).toHex
|
||||
var password = KeystorePass ncrutils.toHex(getRandomBytes(rng, 32))
|
||||
defer: burnMem(password)
|
||||
|
||||
let
|
||||
|
@ -179,14 +298,6 @@ proc generateDeposits*(preset: RuntimePreset,
|
|||
|
||||
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] =
|
||||
try: createDir splitFile(outWalletPath).dir
|
||||
except OSError, IOError:
|
||||
|
@ -302,9 +413,6 @@ proc importKeystoresFromDir*(rng: var BrHmacDrbgContext,
|
|||
fatal "Failed to access the imported deposits directory"
|
||||
quit 1
|
||||
|
||||
template echo80(msg: string) =
|
||||
echo wrapWords(msg, 80)
|
||||
|
||||
template ask(prompt: string): string =
|
||||
try:
|
||||
stdout.write prompt, ": "
|
||||
|
|
Loading…
Reference in New Issue