From c5c788a9dbe7d17cf322c398e25cfd6efcce3bdc Mon Sep 17 00:00:00 2001 From: cheatfate Date: Wed, 19 Aug 2020 16:12:10 +0300 Subject: [PATCH] Secure network key file and data directory. --- Makefile | 10 +-- beacon_chain/beacon_node.nim | 52 ++++++++++++- beacon_chain/conf.nim | 10 +++ beacon_chain/eth2_network.nim | 133 ++++++++++++++++++++++++++-------- vendor/nim-stew | 2 +- 5 files changed, 168 insertions(+), 39 deletions(-) diff --git a/Makefile b/Makefile index b53dd265d..235439e0b 100644 --- a/Makefile +++ b/Makefile @@ -188,8 +188,8 @@ testnet0 testnet1: | beacon_node signing_process $(GOERLI_TESTNETS_PARAMS) $(NODE_PARAMS) # https://www.gnu.org/software/make/manual/html_node/Multi_002dLine.html -define CONNECT_TO_NETWORK - mkdir -p build/data/shared_$(1)_$(NODE_ID) +define CONNECT_TO_NETWORK = + mkdir -m 0750 -p build/data/shared_$(1)_$(NODE_ID) scripts/make_prometheus_config.sh \ --nodes 1 \ @@ -204,8 +204,8 @@ define CONNECT_TO_NETWORK $(GOERLI_TESTNETS_PARAMS) $(NODE_PARAMS) endef -define CONNECT_TO_NETWORK_IN_DEV_MODE - mkdir -p build/data/shared_$(1)_$(NODE_ID) +define CONNECT_TO_NETWORK_IN_DEV_MODE = + mkdir -m 0750 -p build/data/shared_$(1)_$(NODE_ID) scripts/make_prometheus_config.sh \ --nodes 1 \ @@ -221,7 +221,7 @@ endef define CONNECT_TO_NETWORK_WITH_VALIDATOR_CLIENT # if launching a VC as well - send the BN looking nowhere for validators/secrets - mkdir -p build/data/shared_$(1)_$(NODE_ID)/empty_dummy_folder + mkdir -m 0750 -p build/data/shared_$(1)_$(NODE_ID)/empty_dummy_folder scripts/make_prometheus_config.sh \ --nodes 1 \ diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index f4957fcc9..1f3c23f07 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -11,7 +11,7 @@ import std/[osproc, random], # Nimble packages - stew/[objects, byteutils, endians2], stew/shims/macros, + stew/[objects, byteutils, endians2, io2], stew/shims/macros, chronos, confutils, metrics, json_rpc/[rpcserver, jsonmarshal], chronicles, bearssl, blscurve, json_serialization/std/[options, sets, net], serialization/errors, @@ -243,7 +243,7 @@ proc init*(T: type BeaconNode, enrForkId = enrForkIdFromState(chainDag.headState.data.data) topicBeaconBlocks = getBeaconBlocksTopic(enrForkId.forkDigest) topicAggregateAndProofs = getAggregateAndProofsTopic(enrForkId.forkDigest) - network = createEth2Node(rng, conf, enrForkId) + network = createEth2Node(rng, conf, netKeys, enrForkId) attestationPool = newClone(AttestationPool.init(chainDag, quarantine)) exitPool = newClone(ExitPool.init(chainDag, quarantine)) var res = BeaconNode( @@ -894,10 +894,50 @@ proc run*(node: BeaconNode) = var gPidFile: string proc createPidFile(filename: string) = - createDir splitFile(filename).dir writeFile filename, $os.getCurrentProcessId() gPidFile = filename - addQuitProc proc {.noconv.} = removeFile gPidFile + addQuitProc proc {.noconv.} = discard io2.removeFile(gPidFile) + +proc checkDataDir(conf: BeaconNodeConf) = + ## Checks `conf.dataDir`. + ## If folder exists, prcoedure will check it for access and + ## permissions `0750 (rwxr-x---)`, if folder do not exists it will be created + ## with permissions `0750 (rwxr-x---)`. + let dataDir = string(conf.dataDir) + when defined(posix): + let amask = {AccessFlags.Read, AccessFlags.Write, AccessFlags.Execute} + if fileAccessible(dataDir, amask): + let gmask = {UserRead, UserWrite, UserExec, GroupRead, GroupExec} + let pmask = {OtherRead, OtherWrite, OtherExec, GroupWrite} + let pres = getPermissionsSet(dataDir) + if pres.isErr(): + fatal "Could not check data folder permissions", + data_dir = dataDir, errorCode = $pres.error, + errorMsg = ioErrorMsg(pres.error) + quit QuitFailure + let insecurePermissions = pres.get() * pmask + if insecurePermissions != {}: + fatal "Data folder has insecure permissions", + data_dir = dataDir, + insecure_permissions = $insecurePermissions, + current_permissions = pres.get().toString(), + required_permissions = gmask.toString() + quit QuitFailure + else: + let res = createPath(dataDir, 0o750) + if res.isErr(): + fatal "Could not create data folder", data_dir = dataDir, + errorMsg = ioErrorMsg(res.error), errorCode = $res.error + quit QuitFailure + elif defined(windows): + let res = createPath(dataDir, 0o750) + if res.isErr(): + fatal "Could not create data folder", data_dir = dataDir, + errorMsg = ioErrorMsg(res.error), errorCode = $res.error + quit QuitFailure + else: + fatal "Unsupported operation system" + quit QuitFailure proc initializeNetworking(node: BeaconNode) {.async.} = await node.network.startListening() @@ -1133,6 +1173,8 @@ programMain: case config.cmd of createTestnet: + checkDataDir(config) + let launchPadDeposits = try: Json.loadFile(config.testnetDepositsFile.string, seq[LaunchPadDeposit]) except SerializationError as err: @@ -1193,6 +1235,8 @@ programMain: cmdParams = commandLineParams(), config + checkDataDir(config) + createPidFile(config.dataDir.string / "beacon_node.pid") config.createDumpDirs() diff --git a/beacon_chain/conf.nim b/beacon_chain/conf.nim index 819a5e640..fb95c850c 100644 --- a/beacon_chain/conf.nim +++ b/beacon_chain/conf.nim @@ -218,6 +218,12 @@ type desc: "Write SSZ dumps of blocks, attestations and states to data dir" name: "dump" }: bool + netKeyFile* {. + defaultValue: "random", + desc: "Source of network (secp256k1) private key file " & + "(random|) (default: random)" + name: "netkey-file" }: string + of createTestnet: testnetDepositsFile* {. desc: "A LaunchPad deposits file for the genesis state validators" @@ -265,6 +271,10 @@ type desc: "Output file with list of bootstrap nodes for the network" name: "output-bootstrap-file" }: OutFile + outputNetkeyFile* {. + desc: "Output file with network private key for the network" + name: "output-netkey-file" }: OutFile + of wallets: case walletsCmd* {.command.}: WalletsCmd of WalletsCmd.create: diff --git a/beacon_chain/eth2_network.nim b/beacon_chain/eth2_network.nim index 44d75a2ba..a2f23f781 100644 --- a/beacon_chain/eth2_network.nim +++ b/beacon_chain/eth2_network.nim @@ -4,7 +4,7 @@ import std/options as stdOptions, # Status libs - stew/[varints, base58, base64, endians2, results, byteutils], bearssl, + stew/[varints, base58, base64, endians2, results, byteutils, io2], bearssl, stew/shims/net as stewNet, stew/shims/[macros, tables], faststreams/[inputs, outputs, buffers], snappy, snappy/framing, @@ -204,7 +204,6 @@ type const clientId* = "Nimbus beacon node v" & fullVersionStr - networkKeyFilename = "privkey.protobuf" nodeMetadataFilename = "node-metadata.json" TCP = net.Protocol.IPPROTO_TCP @@ -1201,21 +1200,103 @@ proc initAddress*(T: type MultiAddress, str: string): T = template tcpEndPoint(address, port): auto = MultiAddress.init(address, tcpProtocol, port) -proc getPersistentNetKeys*( - rng: var BrHmacDrbgContext, conf: BeaconNodeConf): KeyPair = - let - privKeyPath = conf.dataDir / networkKeyFilename - privKey = - if not fileExists(privKeyPath): - createDir conf.dataDir.string - let key = PrivateKey.random(Secp256k1, rng).tryGet() - writeFile(privKeyPath, key.getBytes().tryGet()) - key - else: - let keyBytes = readFile(privKeyPath) - PrivateKey.init(keyBytes.toOpenArrayByte(0, keyBytes.high)).tryGet() +proc getPersistentNetKeys*(rng: var BrHmacDrbgContext, + conf: BeaconNodeConf): KeyPair = + case conf.cmd + of noCommand: + if conf.netKeyFile == "random": + let res = PrivateKey.random(Secp256k1, rng) + if res.isErr(): + fatal "Could not generate random network key file" + quit QuitFailure + let privKey = res.get() + return KeyPair(seckey: privKey, pubkey: privKey.getKey().tryGet()) + else: + let keyPath = + if isAbsolute(conf.netKeyFile): + conf.netKeyFile + else: + conf.dataDir / conf.netKeyFile - KeyPair(seckey: privKey, pubkey: privKey.getKey().tryGet()) + 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) + 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()) + + else: + let res = PrivateKey.random(Secp256k1, rng) + if res.isErr(): + fatal "Could not generate random network key file" + quit QuitFailure + + let privKey = res.get() + + let wres = writeFile(keyPath, privKey.getBytes().tryGet(), 0o600) + if not(wres.isOk()): + fatal "Could not write network key file", key_path = keyPath + quit QuitFailure + + return KeyPair(seckey: privKey, pubkey: privkey.getKey().tryGet()) + of createTestnet: + let netKeyFile = string(conf.outputNetkeyFile) + let keyPath = + if isAbsolute(netKeyFile): + netKeyFile + else: + conf.dataDir / netKeyFile + + let res = PrivateKey.random(Secp256k1, rng) + if res.isErr(): + fatal "Could not generate random network key file" + quit QuitFailure + + let privKey = res.get() + + let wres = writeFile(keyPath, privKey.getBytes().tryGet(), 0o600) + if not(wres.isOk()): + fatal "Could not write network key file", key_path = keyPath + quit QuitFailure + + return KeyPair(seckey: privKey, pubkey: privkey.getKey().tryGet()) + else: + let res = PrivateKey.random(Secp256k1, rng) + if res.isErr(): + fatal "Could not generate random network key file" + quit QuitFailure + + let privKey = res.get() + return KeyPair(seckey: privKey, pubkey: privkey.getKey().tryGet()) func gossipId(data: openArray[byte]): string = # https://github.com/ethereum/eth2.0-specs/blob/v0.12.3/specs/phase0/p2p-interface.md#topics-and-messages @@ -1224,9 +1305,10 @@ func gossipId(data: openArray[byte]): string = func msgIdProvider(m: messages.Message): string = gossipId(m.data) -proc createEth2Node*( - rng: ref BrHmacDrbgContext, conf: BeaconNodeConf, - enrForkId: ENRForkID): Eth2Node = +proc createEth2Node*(rng: ref BrHmacDrbgContext, + conf: BeaconNodeConf, + netKeys: KeyPair, + enrForkId: ENRForkID): Eth2Node = var (extIp, extTcpPort, extUdpPort) = setupNat(conf) hostAddress = tcpEndPoint(conf.listenAddress, conf.tcpPort) @@ -1236,11 +1318,10 @@ proc createEth2Node*( notice "Initializing networking", hostAddress, announcedAddresses - let keys = getPersistentNetKeys(rng[], conf) # 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). - var switch = newStandardSwitch(some keys.seckey, hostAddress, + var switch = newStandardSwitch(some netKeys.seckey, hostAddress, transportFlags = {ServerFlags.ReuseAddr}, secureManagers = [ SecureProtocol.Noise, # Only noise in ETH2! @@ -1257,16 +1338,10 @@ proc createEth2Node*( result = Eth2Node.init(conf, enrForkId, switch, pubsub, extIp, extTcpPort, extUdpPort, - keys.seckey.asEthKey, discovery = conf.discv5Enabled, + netKeys.seckey.asEthKey, + discovery = conf.discv5Enabled, rng = rng) -proc getPersistenBootstrapAddr*(rng: var BrHmacDrbgContext, conf: BeaconNodeConf, - ip: ValidIpAddress, port: Port): EnrResult[enr.Record] = - let pair = getPersistentNetKeys(rng, conf) - return enr.Record.init(1'u64, # sequence number - pair.seckey.asEthKey, - some(ip), port, port, @[]) - proc announcedENR*(node: Eth2Node): enr.Record = doAssert node.discovery != nil, "The Eth2Node must be initialized" node.discovery.localNode.record diff --git a/vendor/nim-stew b/vendor/nim-stew index 47ff49aae..7a2b6dbdd 160000 --- a/vendor/nim-stew +++ b/vendor/nim-stew @@ -1 +1 @@ -Subproject commit 47ff49aae7fdcf9fb7b4af44c6ab2377f04c731e +Subproject commit 7a2b6dbddafeeba8c33f5587f08276f58d26deae