From 40f2b74f73f658291e36e97497fa85ef11d9f8b7 Mon Sep 17 00:00:00 2001 From: cheatfate Date: Mon, 24 Aug 2020 19:06:41 +0300 Subject: [PATCH] Add keystore management and interactive password handling. --- beacon_chain/eth2_network.nim | 87 ++++++++--------- beacon_chain/keystore_management.nim | 134 ++++++++++++++++++++++++--- 2 files changed, 159 insertions(+), 62 deletions(-) diff --git a/beacon_chain/eth2_network.nim b/beacon_chain/eth2_network.nim index a2f23f781..b6abe9588 100644 --- a/beacon_chain/eth2_network.nim +++ b/beacon_chain/eth2_network.nim @@ -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). diff --git a/beacon_chain/keystore_management.nim b/beacon_chain/keystore_management.nim index 00a2baff5..e661894d0 100644 --- a/beacon_chain/keystore_management.nim +++ b/beacon_chain/keystore_management.nim @@ -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, ": "