Keystore cache implementation. (#4372)

This commit is contained in:
Eugene Kabanov 2023-02-16 19:25:48 +02:00 committed by GitHub
parent d63179ab57
commit e91415662b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 358 additions and 104 deletions

View File

@ -73,6 +73,7 @@ type
restServer*: RestServerRef restServer*: RestServerRef
keymanagerHost*: ref KeymanagerHost keymanagerHost*: ref KeymanagerHost
keymanagerServer*: RestServerRef keymanagerServer*: RestServerRef
keystoreCache*: KeystoreCacheRef
eventBus*: EventBus eventBus*: EventBus
vcProcess*: Process vcProcess*: Process
requestManager*: RequestManager requestManager*: RequestManager

View File

@ -122,6 +122,10 @@ type
Mainnet = "mainnet" Mainnet = "mainnet"
None = "none" None = "none"
ImportMethod* {.pure.} = enum
Normal = "normal"
SingleSalt = "single-salt"
BeaconNodeConf* = object BeaconNodeConf* = object
configFile* {. configFile* {.
desc: "Loads the configuration from a TOML file" desc: "Loads the configuration from a TOML file"
@ -714,6 +718,12 @@ type
argument argument
desc: "A directory with keystores to import" .}: Option[InputDir] desc: "A directory with keystores to import" .}: Option[InputDir]
importMethod* {.
desc: "Specifies which import method will be used (" &
"normal, single-salt)"
defaultValue: ImportMethod.Normal
name: "method" .}: ImportMethod
of DepositsCmd.exit: of DepositsCmd.exit:
exitedValidator* {. exitedValidator* {.
name: "validator" name: "validator"

View File

@ -34,7 +34,8 @@ proc getSignedExitMessage(config: BeaconNodeConf,
validatorsDir, validatorsDir,
config.secretsDir, config.secretsDir,
validatorKeyAsStr, validatorKeyAsStr,
config.nonInteractive) config.nonInteractive,
nil)
if signingItem.isNone: if signingItem.isNone:
fatal "Unable to continue without decrypted signing key" fatal "Unable to continue without decrypted signing key"
@ -344,7 +345,7 @@ proc doDeposits*(config: BeaconNodeConf, rng: var HmacDrbgContext) {.
quit 1 quit 1
importKeystoresFromDir( importKeystoresFromDir(
rng, rng, config.importMethod,
validatorKeysDir.string, validatorKeysDir.string,
config.validatorsDir, config.secretsDir) config.validatorsDir, config.secretsDir)

View File

@ -675,6 +675,7 @@ proc init*(T: type BeaconNode,
restServer: restServer, restServer: restServer,
keymanagerHost: keymanagerHost, keymanagerHost: keymanagerHost,
keymanagerServer: keymanagerInitResult.server, keymanagerServer: keymanagerInitResult.server,
keystoreCache: KeystoreCacheRef.init(),
eventBus: eventBus, eventBus: eventBus,
gossipState: {}, gossipState: {},
blocksGossipState: {}, blocksGossipState: {},
@ -1612,6 +1613,7 @@ proc run(node: BeaconNode) {.raises: [Defect, CatchableError].} =
asyncSpawn runSlotLoop(node, wallTime, onSlotStart) asyncSpawn runSlotLoop(node, wallTime, onSlotStart)
asyncSpawn runOnSecondLoop(node) asyncSpawn runOnSecondLoop(node)
asyncSpawn runQueueProcessingLoop(node.blockProcessor) asyncSpawn runQueueProcessingLoop(node.blockProcessor)
asyncSpawn runKeystoreCachePruningLoop(node.keystoreCache)
## Ctrl+C handling ## Ctrl+C handling
proc controlCHandler() {.noconv.} = proc controlCHandler() {.noconv.} =

View File

@ -19,7 +19,7 @@ import
stew/io2, stew/io2,
# Local modules # Local modules
./spec/[helpers], ./spec/[helpers, keystore],
./spec/datatypes/base, ./spec/datatypes/base,
"."/[beacon_clock, beacon_node_status, conf, version] "."/[beacon_clock, beacon_node_status, conf, version]
@ -245,6 +245,18 @@ proc resetStdin*() =
attrs.c_lflag = attrs.c_lflag or Cflag(ECHO) attrs.c_lflag = attrs.c_lflag or Cflag(ECHO)
discard fd.tcSetAttr(TCSANOW, attrs.addr) discard fd.tcSetAttr(TCSANOW, attrs.addr)
proc runKeystoreCachePruningLoop*(cache: KeystoreCacheRef) {.async.} =
while true:
let exitLoop =
try:
await sleepAsync(60.seconds)
false
except CatchableError:
cache.clear()
true
if exitLoop: break
cache.pruneExpiredKeys()
proc runSlotLoop*[T](node: T, startTime: BeaconTime, proc runSlotLoop*[T](node: T, startTime: BeaconTime,
slotProc: SlotStartProc[T]) {.async.} = slotProc: SlotStartProc[T]) {.async.} =
var var

View File

@ -34,6 +34,7 @@ type
config: SigningNodeConf config: SigningNodeConf
attachedValidators: ValidatorPool attachedValidators: ValidatorPool
signingServer: SigningNodeServer signingServer: SigningNodeServer
keystoreCache: KeystoreCacheRef
keysList: string keysList: string
proc getRouter*(): RestRouter proc getRouter*(): RestRouter
@ -97,7 +98,7 @@ proc loadTLSKey(pathName: InputFile): Result[TLSPrivateKey, cstring] =
proc initValidators(sn: var SigningNode): bool = proc initValidators(sn: var SigningNode): bool =
info "Initializaing validators", path = sn.config.validatorsDir() info "Initializaing validators", path = sn.config.validatorsDir()
var publicKeyIdents: seq[string] var publicKeyIdents: seq[string]
for keystore in listLoadableKeystores(sn.config): for keystore in listLoadableKeystores(sn.config, sn.keystoreCache):
# Not relevant in signing node # Not relevant in signing node
# TODO don't print when loading validators # TODO don't print when loading validators
let feeRecipient = default(Eth1Address) let feeRecipient = default(Eth1Address)
@ -115,12 +116,17 @@ proc initValidators(sn: var SigningNode): bool =
true true
proc init(t: typedesc[SigningNode], config: SigningNodeConf): SigningNode = proc init(t: typedesc[SigningNode], config: SigningNodeConf): SigningNode =
var sn = SigningNode(config: config) var sn = SigningNode(
config: config,
keystoreCache: KeystoreCacheRef.init()
)
if not(initValidators(sn)): if not(initValidators(sn)):
fatal "Could not find/initialize local validators" fatal "Could not find/initialize local validators"
quit 1 quit 1
asyncSpawn runKeystoreCachePruningLoop(sn.keystoreCache)
let let
address = initTAddress(config.bindAddress, config.bindPort) address = initTAddress(config.bindAddress, config.bindPort)
serverFlags = {HttpServerFlags.QueryCommaSeparatedArray, serverFlags = {HttpServerFlags.QueryCommaSeparatedArray,

View File

@ -86,7 +86,7 @@ proc initGenesis(vc: ValidatorClientRef): Future[RestGenesis] {.async.} =
proc initValidators(vc: ValidatorClientRef): Future[bool] {.async.} = proc initValidators(vc: ValidatorClientRef): Future[bool] {.async.} =
info "Loading validators", validatorsDir = vc.config.validatorsDir() info "Loading validators", validatorsDir = vc.config.validatorsDir()
var duplicates: seq[ValidatorPubKey] var duplicates: seq[ValidatorPubKey]
for keystore in listLoadableKeystores(vc.config): for keystore in listLoadableKeystores(vc.config, vc.keystoreCache):
vc.addValidator(keystore) vc.addValidator(keystore)
return true return true
@ -208,7 +208,8 @@ proc new*(T: type ValidatorClientRef,
indicesAvailable: newAsyncEvent(), indicesAvailable: newAsyncEvent(),
dynamicFeeRecipientsStore: newClone(DynamicFeeRecipientsStore.init()), dynamicFeeRecipientsStore: newClone(DynamicFeeRecipientsStore.init()),
sigintHandleFut: waitSignal(SIGINT), sigintHandleFut: waitSignal(SIGINT),
sigtermHandleFut: waitSignal(SIGTERM) sigtermHandleFut: waitSignal(SIGTERM),
keystoreCache: KeystoreCacheRef.init()
) )
else: else:
ValidatorClientRef( ValidatorClientRef(
@ -222,7 +223,8 @@ proc new*(T: type ValidatorClientRef,
doppelExit: newAsyncEvent(), doppelExit: newAsyncEvent(),
dynamicFeeRecipientsStore: newClone(DynamicFeeRecipientsStore.init()), dynamicFeeRecipientsStore: newClone(DynamicFeeRecipientsStore.init()),
sigintHandleFut: newFuture[void]("sigint_placeholder"), sigintHandleFut: newFuture[void]("sigint_placeholder"),
sigtermHandleFut: newFuture[void]("sigterm_placeholder") sigtermHandleFut: newFuture[void]("sigterm_placeholder"),
keystoreCache: KeystoreCacheRef.init()
) )
proc asyncInit(vc: ValidatorClientRef): Future[ValidatorClientRef] {.async.} = proc asyncInit(vc: ValidatorClientRef): Future[ValidatorClientRef] {.async.} =
@ -311,6 +313,8 @@ proc asyncRun*(vc: ValidatorClientRef) {.async.} =
var doppelEventFut = vc.doppelExit.wait() var doppelEventFut = vc.doppelExit.wait()
try: try:
vc.runSlotLoopFut = runSlotLoop(vc, vc.beaconClock.now(), onSlotStart) vc.runSlotLoopFut = runSlotLoop(vc, vc.beaconClock.now(), onSlotStart)
vc.runKeystoreCachePruningLoopFut =
runKeystorecachePruningLoop(vc.keystoreCache)
discard await race(vc.runSlotLoopFut, doppelEventFut) discard await race(vc.runSlotLoopFut, doppelEventFut)
if not(vc.runSlotLoopFut.finished()): if not(vc.runSlotLoopFut.finished()):
notice "Received shutdown event, exiting" notice "Received shutdown event, exiting"
@ -334,6 +338,8 @@ proc asyncRun*(vc: ValidatorClientRef) {.async.} =
var pending: seq[Future[void]] var pending: seq[Future[void]]
if not(vc.runSlotLoopFut.finished()): if not(vc.runSlotLoopFut.finished()):
pending.add(vc.runSlotLoopFut.cancelAndWait()) pending.add(vc.runSlotLoopFut.cancelAndWait())
if not(vc.runKeystoreCachePruningLoopFut.finished()):
pending.add(vc.runKeystoreCachePruningLoopFut.cancelAndWait())
if not(doppelEventFut.finished()): if not(doppelEventFut.finished()):
pending.add(doppelEventFut.cancelAndWait()) pending.add(doppelEventFut.cancelAndWait())
debug "Stopping running services" debug "Stopping running services"

View File

@ -10,13 +10,14 @@
import import
# Standard library # Standard library
std/[algorithm, math, parseutils, strformat, strutils, typetraits, unicode, std/[algorithm, math, parseutils, strformat, strutils, typetraits, unicode,
uri], uri, hashes],
# Third-party libraries # Third-party libraries
normalize, normalize,
# Status libraries # Status libraries
stew/[results, bitops2, base10, io2], stew/shims/macros, stew/[results, bitops2, base10, io2, endians2], stew/shims/macros,
eth/keyfile/uuid, blscurve, eth/keyfile/uuid, blscurve,
json_serialization, json_serialization/std/options, json_serialization, json_serialization/std/options,
chronos/timer,
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, scrypt], nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, scrypt],
# Local modules # Local modules
libp2p/crypto/crypto as lcrypto, libp2p/crypto/crypto as lcrypto,
@ -198,6 +199,24 @@ type
SimpleHexEncodedTypes* = ScryptSalt|ChecksumBytes|CipherBytes SimpleHexEncodedTypes* = ScryptSalt|ChecksumBytes|CipherBytes
CacheItemFlag {.pure.} = enum
Missing, Present
KeystoreCacheItem = object
flag: CacheItemFlag
kdf: Kdf
cipher: Cipher
decryptionKey: seq[byte]
timestamp: Moment
KdfSaltKey* = distinct array[32, byte]
KeystoreCache* = object
expireTime*: Duration
table*: Table[KdfSaltKey, KeystoreCacheItem]
KeystoreCacheRef* = ref KeystoreCache
const const
keyLen = 32 keyLen = 32
@ -223,6 +242,8 @@ const
wordListLen = 2048 wordListLen = 2048
maxWordLen = 16 maxWordLen = 16
KeystoreCachePruningTime* = 5.minutes
UUID.serializesAsBaseIn Json UUID.serializesAsBaseIn Json
KeyPath.serializesAsBaseIn Json KeyPath.serializesAsBaseIn Json
WalletName.serializesAsBaseIn Json WalletName.serializesAsBaseIn Json
@ -736,48 +757,56 @@ func areValid(params: ScryptParams): bool =
params.p == scryptParams.p and params.p == scryptParams.p and
params.salt.bytes.len > 0 params.salt.bytes.len > 0
proc decryptCryptoField*(crypto: Crypto, decKey: openArray[byte],
outSecret: var seq[byte]): DecryptionStatus =
if crypto.cipher.message.bytes.len == 0:
return DecryptionStatus.InvalidKeystore
if len(decKey) < keyLen:
return DecryptionStatus.InvalidKeystore
let derivedChecksum = shaChecksum(decKey.toOpenArray(16, 31),
crypto.cipher.message.bytes)
if derivedChecksum != crypto.checksum.message:
return DecryptionStatus.InvalidPassword
var aesCipher: CTR[aes128]
outSecret.setLen(crypto.cipher.message.bytes.len)
aesCipher.init(decKey.toOpenArray(0, 15), crypto.cipher.params.iv.bytes)
aesCipher.decrypt(crypto.cipher.message.bytes, outSecret)
aesCipher.clear()
DecryptionStatus.Success
proc getDecryptionKey*(crypto: Crypto, password: KeystorePass,
decKey: var seq[byte]): DecryptionStatus =
let res =
case crypto.kdf.function
of kdfPbkdf2:
template params: auto = crypto.kdf.pbkdf2Params
if not params.areValid or params.c > high(int).uint64:
return DecryptionStatus.InvalidKeystore
Eth2DigestCtx.pbkdf2(password.str, params.salt.bytes, int(params.c),
int(params.dklen))
of kdfScrypt:
template params: auto = crypto.kdf.scryptParams
if not params.areValid:
return DecryptionStatus.InvalidKeystore
@(scrypt(password.str, params.salt.bytes, scryptParams.n,
scryptParams.r, scryptParams.p, int(scryptParams.dklen)))
decKey = res
DecryptionStatus.Success
proc decryptCryptoField*(crypto: Crypto, proc decryptCryptoField*(crypto: Crypto,
password: KeystorePass, password: KeystorePass,
outSecret: var seq[byte]): DecryptionStatus = outSecret: var seq[byte]): DecryptionStatus =
# https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition # https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition
var decKey: seq[byte]
if crypto.cipher.message.bytes.len == 0: if crypto.cipher.message.bytes.len == 0:
return InvalidKeystore return InvalidKeystore
let decKey = case crypto.kdf.function let res = getDecryptionKey(crypto, password, decKey)
of kdfPbkdf2: if res != DecryptionStatus.Success:
template params: auto = crypto.kdf.pbkdf2Params return res
if not params.areValid or params.c > high(int).uint64:
return InvalidKeystore
Eth2DigestCtx.pbkdf2(
password.str,
params.salt.bytes,
int params.c,
int params.dklen)
of kdfScrypt:
template params: auto = crypto.kdf.scryptParams
if not params.areValid:
return InvalidKeystore
@(scrypt(password.str,
params.salt.bytes,
scryptParams.n,
scryptParams.r,
scryptParams.p,
int scryptParams.dklen))
let derivedChecksum = shaChecksum(decKey.toOpenArray(16, 31), decryptCryptoField(crypto, decKey, outSecret)
crypto.cipher.message.bytes)
if derivedChecksum != crypto.checksum.message:
return InvalidPassword
var aesCipher: CTR[aes128]
outSecret.setLen(crypto.cipher.message.bytes.len)
aesCipher.init(decKey.toOpenArray(0, 15), crypto.cipher.params.iv.bytes)
aesCipher.decrypt(crypto.cipher.message.bytes, outSecret)
aesCipher.clear()
return Success
func cstringToStr(v: cstring): string = $v func cstringToStr(v: cstring): string = $v
@ -796,23 +825,167 @@ template parseRemoteKeystore*(jsonContent: string): RemoteKeystore =
requireAllFields = false, requireAllFields = false,
allowUnknownFields = true) allowUnknownFields = true)
proc getSaltKey(keystore: Keystore, password: KeystorePass): KdfSaltKey =
let digest =
case keystore.crypto.kdf.function
of kdfPbkdf2:
template params: auto = keystore.crypto.kdf.pbkdf2Params
withEth2Hash:
h.update(seq[byte](params.salt))
h.update(password.str.toOpenArrayByte(0, len(password.str) - 1))
h.update(toBytesLE(params.dklen))
h.update(toBytesLE(params.c))
let prf = $params.prf
h.update(prf.toOpenArrayByte(0, len(prf) - 1))
of kdfScrypt:
template params: auto = keystore.crypto.kdf.scryptParams
withEth2Hash:
h.update(seq[byte](params.salt))
h.update(password.str.toOpenArrayByte(0, len(password.str) - 1))
h.update(toBytesLE(params.dklen))
h.update(toBytesLE(uint64(params.n)))
h.update(toBytesLE(uint64(params.p)))
h.update(toBytesLE(uint64(params.r)))
KdfSaltKey(digest.data)
proc `==`*(a, b: KdfSaltKey): bool {.borrow.}
proc hash*(salt: KdfSaltKey): Hash {.borrow.}
func `==`*(a, b: Kdf): bool =
# We do not care about `message` field.
if a.function != b.function:
return false
case a.function
of kdfPbkdf2:
template aparams: auto = a.pbkdf2Params
template bparams: auto = b.pbkdf2Params
(aparams.dklen == bparams.dklen) and (aparams.c == bparams.c) and
(aparams.prf == bparams.prf) and (len(seq[byte](aparams.salt)) > 0) and
(seq[byte](aparams.salt) == seq[byte](bparams.salt))
of kdfScrypt:
template aparams: auto = a.scryptParams
template bparams: auto = b.scryptParams
(aparams.dklen == bparams.dklen) and (aparams.n == bparams.n) and
(aparams.p == bparams.p) and (aparams.r == bparams.r) and
(len(seq[byte](aparams.salt)) > 0) and
(seq[byte](aparams.salt) == seq[byte](bparams.salt))
func `==`*(a, b: Cipher): bool =
# We do not care about `params` and `message` fields.
a.function == b.function
func `==`*(a, b: KeystoreCacheItem): bool =
(a.kdf == b.kdf) and (a.cipher == b.cipher) and
(a.decryptionKey == b.decryptionKey)
func init*(t: typedesc[KeystoreCacheRef],
expireTime = KeystoreCachePruningTime): KeystoreCacheRef =
KeystoreCacheRef(
table: initTable[KdfSaltKey, KeystoreCacheItem](),
expireTime: expireTime
)
proc clear*(cache: KeystoreCacheRef) =
cache.table.clear()
proc pruneExpiredKeys*(cache: KeystoreCacheRef) =
if cache.expireTime == InfiniteDuration:
return
let currentTime = Moment.now()
var keys: seq[KdfSaltKey]
for key, value in cache.table.mpairs():
if currentTime - value.timestamp >= cache.expireTime:
keys.add(key)
burnMem(value.decryptionKey)
for item in keys:
cache.table.del(item)
proc init*(t: typedesc[KeystoreCacheItem], keystore: Keystore,
key: openArray[byte]): KeystoreCacheItem =
KeystoreCacheItem(flag: CacheItemFlag.Present, kdf: keystore.crypto.kdf,
cipher: keystore.crypto.cipher, decryptionKey: @key,
timestamp: Moment.now())
proc getCachedKey*(cache: KeystoreCacheRef,
keystore: Keystore, password: KeystorePass): Opt[seq[byte]] =
if isNil(cache): return Opt.none(seq[byte])
let
saltKey = keystore.getSaltKey(password)
item = cache.table.getOrDefault(saltKey)
case item.flag
of CacheItemFlag.Present:
if (item.kdf == keystore.crypto.kdf) and
(item.cipher == keystore.crypto.cipher):
Opt.some(item.decryptionKey)
else:
Opt.none(seq[byte])
else:
Opt.none(seq[byte])
proc setCachedKey*(cache: KeystoreCacheRef, keystore: Keystore,
password: KeystorePass, key: openArray[byte]) =
if isNil(cache): return
let saltKey = keystore.getSaltKey(password)
cache.table[saltKey] = KeystoreCacheItem.init(keystore, key)
proc destroyCacheKey*(cache: KeystoreCacheRef,
keystore: Keystore, password: KeystorePass) =
if isNil(cache): return
let saltKey = keystore.getSaltKey(password)
cache.table.withValue(saltKey, item):
burnMem(item[].decryptionKey)
cache.table.del(saltKey)
proc decryptKeystore*(keystore: Keystore, proc decryptKeystore*(keystore: Keystore,
password: KeystorePass): KsResult[ValidatorPrivKey] = password: KeystorePass,
cache: KeystoreCacheRef): KsResult[ValidatorPrivKey] =
var secret: seq[byte] var secret: seq[byte]
defer: burnMem(secret) defer: burnMem(secret)
let status = decryptCryptoField(keystore.crypto, password, secret)
case status while true:
of Success: let res = cache.getCachedKey(keystore, password)
ValidatorPrivKey.fromRaw(secret).mapErr(cstringToStr) if res.isNone():
var decKey: seq[byte]
defer: burnMem(decKey)
let kres = getDecryptionKey(keystore.crypto, password, decKey)
if kres != DecryptionStatus.Success:
return err($kres)
let dres = decryptCryptoField(keystore.crypto, decKey, secret)
if dres != DecryptionStatus.Success:
return err($dres)
cache.setCachedKey(keystore, password, decKey)
break
else: else:
err $status var decKey = res.get()
defer: burnMem(decKey)
let dres = decryptCryptoField(keystore.crypto, decKey, secret)
if dres == DecryptionStatus.Success:
break
cache.destroyCacheKey(keystore, password)
ValidatorPrivKey.fromRaw(secret).mapErr(cstringToStr)
proc decryptKeystore*(keystore: JsonString,
password: KeystorePass,
cache: KeystoreCacheRef): KsResult[ValidatorPrivKey] =
let keystore =
try:
parseKeystore(string(keystore))
except SerializationError as e:
return err(e.formatMsg("<keystore>"))
decryptKeystore(keystore, password, cache)
proc decryptKeystore*(keystore: Keystore,
password: KeystorePass): KsResult[ValidatorPrivKey] =
decryptKeystore(keystore, password, nil)
proc decryptKeystore*(keystore: JsonString, proc decryptKeystore*(keystore: JsonString,
password: KeystorePass): KsResult[ValidatorPrivKey] = password: KeystorePass): KsResult[ValidatorPrivKey] =
let keystore = try: parseKeystore(string keystore) decryptKeystore(keystore, password, nil)
except SerializationError as e:
return err e.formatMsg("<keystore>")
decryptKeystore(keystore, password)
proc writeValue*(writer: var JsonWriter, value: lcrypto.PublicKey) {. proc writeValue*(writer: var JsonWriter, value: lcrypto.PublicKey) {.
inline, raises: [IOError, Defect].} = inline, raises: [IOError, Defect].} =
@ -851,6 +1024,9 @@ proc decryptNetKeystore*(nkeystore: JsonString,
except SerializationError as exc: except SerializationError as exc:
return err(exc.formatMsg("<keystore>")) return err(exc.formatMsg("<keystore>"))
proc generateKeystoreSalt*(rng: var HmacDrbgContext): seq[byte] =
rng.generateBytes(keyLen)
proc createCryptoField(kdfKind: KdfKind, proc createCryptoField(kdfKind: KdfKind,
rng: var HmacDrbgContext, rng: var HmacDrbgContext,
secret: openArray[byte], secret: openArray[byte],

View File

@ -154,10 +154,12 @@ type
syncCommitteeService*: SyncCommitteeServiceRef syncCommitteeService*: SyncCommitteeServiceRef
doppelgangerService*: DoppelgangerServiceRef doppelgangerService*: DoppelgangerServiceRef
runSlotLoopFut*: Future[void] runSlotLoopFut*: Future[void]
runKeystoreCachePruningLoopFut*: Future[void]
sigintHandleFut*: Future[void] sigintHandleFut*: Future[void]
sigtermHandleFut*: Future[void] sigtermHandleFut*: Future[void]
keymanagerHost*: ref KeymanagerHost keymanagerHost*: ref KeymanagerHost
keymanagerServer*: RestServerRef keymanagerServer*: RestServerRef
keystoreCache*: KeystoreCacheRef
beaconClock*: BeaconClock beaconClock*: BeaconClock
attachedValidators*: ref ValidatorPool attachedValidators*: ref ValidatorPool
forks*: seq[Fork] forks*: seq[Fork]

View File

@ -381,13 +381,13 @@ proc loadSecretFile*(path: string): KsResult[KeystorePass] {.
ok(KeystorePass.init(res.get())) ok(KeystorePass.init(res.get()))
proc loadRemoteKeystoreImpl(validatorsDir, proc loadRemoteKeystoreImpl(validatorsDir,
keyName: string): Option[KeystoreData] = keyName: string): Opt[KeystoreData] =
let keystorePath = validatorsDir / keyName / RemoteKeystoreFileName let keystorePath = validatorsDir / keyName / RemoteKeystoreFileName
if not(checkSensitiveFilePermissions(keystorePath)): if not(checkSensitiveFilePermissions(keystorePath)):
error "Remote keystorage file has insecure permissions", error "Remote keystorage file has insecure permissions",
key_path = keystorePath key_path = keystorePath
return return Opt.none(KeystoreData)
let handle = let handle =
block: block:
@ -395,7 +395,7 @@ proc loadRemoteKeystoreImpl(validatorsDir,
if res.isErr(): if res.isErr():
error "Unable to lock keystore file", key_path = keystorePath, error "Unable to lock keystore file", key_path = keystorePath,
error_msg = ioErrorMsg(res.error()) error_msg = ioErrorMsg(res.error())
return return Opt.none(KeystoreData)
res.get() res.get()
var success = false var success = false
@ -409,7 +409,7 @@ proc loadRemoteKeystoreImpl(validatorsDir,
if gres.isErr(): if gres.isErr():
error "Could not read remote keystore file", key_path = keystorePath, error "Could not read remote keystore file", key_path = keystorePath,
error_msg = ioErrorMsg(gres.error()) error_msg = ioErrorMsg(gres.error())
return return Opt.none(KeystoreData)
let buffer = gres.get() let buffer = gres.get()
let data = let data =
try: try:
@ -417,19 +417,20 @@ proc loadRemoteKeystoreImpl(validatorsDir,
except SerializationError as e: except SerializationError as e:
error "Invalid remote keystore file", key_path = keystorePath, error "Invalid remote keystore file", key_path = keystorePath,
error_msg = e.formatMsg(keystorePath) error_msg = e.formatMsg(keystorePath)
return return Opt.none(KeystoreData)
let kres = KeystoreData.init(data, handle) let kres = KeystoreData.init(data, handle)
if kres.isErr(): if kres.isErr():
error "Invalid remote keystore file", key_path = keystorePath, error "Invalid remote keystore file", key_path = keystorePath,
error_msg = kres.error() error_msg = kres.error()
return return Opt.none(KeystoreData)
kres.get() kres.get()
success = true success = true
some(keystore) Opt.some(keystore)
proc loadLocalKeystoreImpl(validatorsDir, secretsDir, keyName: string, proc loadLocalKeystoreImpl(validatorsDir, secretsDir, keyName: string,
nonInteractive: bool): Option[KeystoreData] = nonInteractive: bool,
cache: KeystoreCacheRef): Opt[KeystoreData] =
let let
keystorePath = validatorsDir / keyName / KeystoreFileName keystorePath = validatorsDir / keyName / KeystoreFileName
passphrasePath = secretsDir / keyName passphrasePath = secretsDir / keyName
@ -439,7 +440,7 @@ proc loadLocalKeystoreImpl(validatorsDir, secretsDir, keyName: string,
if res.isErr(): if res.isErr():
error "Unable to lock keystore file", key_path = keystorePath, error "Unable to lock keystore file", key_path = keystorePath,
error_msg = ioErrorMsg(res.error()) error_msg = ioErrorMsg(res.error())
return return Opt.none(KeystoreData)
res.get() res.get()
var success = false var success = false
@ -454,7 +455,7 @@ proc loadLocalKeystoreImpl(validatorsDir, secretsDir, keyName: string,
if gres.isErr(): if gres.isErr():
error "Could not read local keystore file", key_path = keystorePath, error "Could not read local keystore file", key_path = keystorePath,
error_msg = ioErrorMsg(gres.error()) error_msg = ioErrorMsg(gres.error())
return return Opt.none(KeystoreData)
let buffer = gres.get() let buffer = gres.get()
let data = let data =
try: try:
@ -462,13 +463,13 @@ proc loadLocalKeystoreImpl(validatorsDir, secretsDir, keyName: string,
except SerializationError as e: except SerializationError as e:
error "Invalid local keystore file", key_path = keystorePath, error "Invalid local keystore file", key_path = keystorePath,
error_msg = e.formatMsg(keystorePath) error_msg = e.formatMsg(keystorePath)
return return Opt.none(KeystoreData)
data data
if fileExists(passphrasePath): if fileExists(passphrasePath):
if not(checkSensitiveFilePermissions(passphrasePath)): if not(checkSensitiveFilePermissions(passphrasePath)):
error "Password file has insecure permissions", key_path = keystorePath error "Password file has insecure permissions", key_path = keystorePath
return return Opt.none(KeystoreData)
let passphrase = let passphrase =
block: block:
@ -476,29 +477,30 @@ proc loadLocalKeystoreImpl(validatorsDir, secretsDir, keyName: string,
if res.isErr(): if res.isErr():
error "Failed to read passphrase file", error_msg = res.error(), error "Failed to read passphrase file", error_msg = res.error(),
path = passphrasePath path = passphrasePath
return return Opt.none(KeystoreData)
res.get() res.get()
let res = decryptKeystore(keystore, passphrase) let res = decryptKeystore(keystore, passphrase, cache)
if res.isOk(): if res.isOk():
success = true success = true
return some(KeystoreData.init(res.get(), keystore, handle)) return Opt.some(KeystoreData.init(res.get(), keystore, handle))
else: else:
error "Failed to decrypt keystore", key_path = keystorePath, error "Failed to decrypt keystore", key_path = keystorePath,
secure_path = passphrasePath secure_path = passphrasePath
return return Opt.none(KeystoreData)
if nonInteractive: if nonInteractive:
error "Unable to load validator key store. Please ensure matching " & error "Unable to load validator key store. Please ensure matching " &
"passphrase exists in the secrets dir", key_path = keystorePath, "passphrase exists in the secrets dir", key_path = keystorePath,
key_name = keyName, validatorsDir, secretsDir = secretsDir key_name = keyName, validatorsDir, secretsDir = secretsDir
return return Opt.none(KeystoreData)
let prompt = "Please enter passphrase for key \"" & let prompt = "Please enter passphrase for key \"" &
(validatorsDir / keyName) & "\": " (validatorsDir / keyName) & "\": "
let res = keyboardGetPassword[ValidatorPrivKey](prompt, 3, let res = keyboardGetPassword[ValidatorPrivKey](prompt, 3,
proc (password: string): KsResult[ValidatorPrivKey] = proc (password: string): KsResult[ValidatorPrivKey] =
let decrypted = decryptKeystore(keystore, KeystorePass.init password) let decrypted = decryptKeystore(keystore, KeystorePass.init password,
cache)
if decrypted.isErr(): if decrypted.isErr():
error "Keystore decryption failed. Please try again", error "Keystore decryption failed. Please try again",
keystore_path = keystorePath keystore_path = keystorePath
@ -506,25 +508,27 @@ proc loadLocalKeystoreImpl(validatorsDir, secretsDir, keyName: string,
) )
if res.isErr(): if res.isErr():
return return Opt.none(KeystoreData)
success = true success = true
some(KeystoreData.init(res.get(), keystore, handle)) Opt.some(KeystoreData.init(res.get(), keystore, handle))
proc loadKeystore*(validatorsDir, secretsDir, keyName: string, proc loadKeystore*(validatorsDir, secretsDir, keyName: string,
nonInteractive: bool): Option[KeystoreData] = nonInteractive: bool,
cache: KeystoreCacheRef): Opt[KeystoreData] =
let let
keystorePath = validatorsDir / keyName keystorePath = validatorsDir / keyName
localKeystorePath = keystorePath / KeystoreFileName localKeystorePath = keystorePath / KeystoreFileName
remoteKeystorePath = keystorePath / RemoteKeystoreFileName remoteKeystorePath = keystorePath / RemoteKeystoreFileName
if fileExists(localKeystorePath): if fileExists(localKeystorePath):
loadLocalKeystoreImpl(validatorsDir, secretsDir, keyName, nonInteractive) loadLocalKeystoreImpl(validatorsDir, secretsDir, keyName, nonInteractive,
cache)
elif fileExists(remoteKeystorePath): elif fileExists(remoteKeystorePath):
loadRemoteKeystoreImpl(validatorsDir, keyName) loadRemoteKeystoreImpl(validatorsDir, keyName)
else: else:
error "Unable to find any keystore files", keystorePath error "Unable to find any keystore files", keystorePath
none[KeystoreData]() Opt.none(KeystoreData)
proc removeValidatorFiles*(validatorsDir, secretsDir, keyName: string, proc removeValidatorFiles*(validatorsDir, secretsDir, keyName: string,
kind: KeystoreKind kind: KeystoreKind
@ -662,7 +666,8 @@ iterator listLoadableKeys*(validatorsDir, secretsDir: string,
iterator listLoadableKeystores*(validatorsDir, secretsDir: string, iterator listLoadableKeystores*(validatorsDir, secretsDir: string,
nonInteractive: bool, nonInteractive: bool,
keysMask: set[KeystoreKind]): KeystoreData = keysMask: set[KeystoreKind],
cache: KeystoreCacheRef): KeystoreData =
try: try:
for kind, file in walkDir(validatorsDir): for kind, file in walkDir(validatorsDir):
if kind == pcDir: if kind == pcDir:
@ -683,23 +688,24 @@ iterator listLoadableKeystores*(validatorsDir, secretsDir: string,
let let
secretFile = secretsDir / keyName secretFile = secretsDir / keyName
keystore = loadKeystore(validatorsDir, secretsDir, keyName, keystore = loadKeystore(validatorsDir, secretsDir, keyName,
nonInteractive) nonInteractive, cache).valueOr:
if keystore.isSome():
yield keystore.get()
else:
fatal "Unable to load keystore", keystore = file fatal "Unable to load keystore", keystore = file
quit 1 quit 1
yield keystore
except OSError as err: except OSError as err:
error "Validator keystores directory not accessible", error "Validator keystores directory not accessible",
path = validatorsDir, err = err.msg path = validatorsDir, err = err.msg
quit 1 quit 1
iterator listLoadableKeystores*(config: AnyConf): KeystoreData = iterator listLoadableKeystores*(config: AnyConf,
cache: KeystoreCacheRef): KeystoreData =
for el in listLoadableKeystores(config.validatorsDir(), for el in listLoadableKeystores(config.validatorsDir(),
config.secretsDir(), config.secretsDir(),
config.nonInteractive, config.nonInteractive,
{KeystoreKind.Local, KeystoreKind.Remote}): {KeystoreKind.Local, KeystoreKind.Remote},
cache):
yield el yield el
type type
@ -1031,6 +1037,7 @@ proc saveKeystore*(
signingPubKey: CookedPubKey, signingPubKey: CookedPubKey,
signingKeyPath: KeyPath, signingKeyPath: KeyPath,
password: string, password: string,
salt: openArray[byte] = @[],
mode = Secure mode = Secure
): Result[void, KeystoreGenerationError] {.raises: [Defect].} = ): Result[void, KeystoreGenerationError] {.raises: [Defect].} =
let let
@ -1048,7 +1055,7 @@ proc saveKeystore*(
let keyStore = createKeystore(kdfPbkdf2, rng, signingKey, let keyStore = createKeystore(kdfPbkdf2, rng, signingKey,
keypass, signingKeyPath, keypass, signingKeyPath,
mode = mode) mode = mode, salt = salt)
let encodedStorage = let encodedStorage =
try: try:
@ -1276,7 +1283,7 @@ proc importKeystore*(pool: var ValidatorPool,
ok(KeystoreData.init(privateKey, keystore, res.get())) ok(KeystoreData.init(privateKey, keystore, res.get()))
proc generateDistirbutedStore*(rng: var HmacDrbgContext, proc generateDistributedStore*(rng: var HmacDrbgContext,
shares: seq[SecretShare], shares: seq[SecretShare],
pubKey: ValidatorPubKey, pubKey: ValidatorPubKey,
validatorIdx: Natural, validatorIdx: Natural,
@ -1296,7 +1303,7 @@ proc generateDistirbutedStore*(rng: var HmacDrbgContext,
shareSecretsDir / $share.id, shareSecretsDir / $share.id,
share.key, share.key.toPubKey, share.key, share.key.toPubKey,
makeKeyPath(validatorIdx, signingKeyKind), makeKeyPath(validatorIdx, signingKeyKind),
password.str, password.str, @[],
mode) mode)
signers.add RemoteSignerInfo( signers.add RemoteSignerInfo(
@ -1402,6 +1409,14 @@ proc generateDeposits*(cfg: RuntimeConfig,
defer: burnMem(baseKey) defer: burnMem(baseKey)
baseKey = deriveChildKey(baseKey, baseKeyPath) baseKey = deriveChildKey(baseKey, baseKeyPath)
var
salt = rng.generateKeystoreSalt()
password = KeystorePass.init ncrutils.toHex(rng.generateBytes(32))
defer:
burnMem(salt)
burnMem(password)
let localValidatorsCount = totalNewValidators - int(remoteValidatorsCount) let localValidatorsCount = totalNewValidators - int(remoteValidatorsCount)
for i in 0 ..< localValidatorsCount: for i in 0 ..< localValidatorsCount:
let validatorIdx = firstValidatorIdx + i let validatorIdx = firstValidatorIdx + i
@ -1416,12 +1431,10 @@ proc generateDeposits*(cfg: RuntimeConfig,
derivedKey = deriveChildKey(derivedKey, 0) # This is the signing key derivedKey = deriveChildKey(derivedKey, 0) # This is the signing key
let signingPubKey = derivedKey.toPubKey let signingPubKey = derivedKey.toPubKey
var password = KeystorePass.init ncrutils.toHex(rng.generateBytes(32))
defer: burnMem(password)
? saveKeystore(rng, validatorsDir, secretsDir, ? saveKeystore(rng, validatorsDir, secretsDir,
derivedKey, signingPubKey, derivedKey, signingPubKey,
makeKeyPath(validatorIdx, signingKeyKind), password.str, makeKeyPath(validatorIdx, signingKeyKind), password.str,
mode) salt, mode)
deposits.add prepareDeposit( deposits.add prepareDeposit(
cfg, withdrawalPubKey, derivedKey, signingPubKey) cfg, withdrawalPubKey, derivedKey, signingPubKey)
@ -1446,7 +1459,7 @@ proc generateDeposits*(cfg: RuntimeConfig,
error "Failed to generate distributed key: ", threshold, sharesCount error "Failed to generate distributed key: ", threshold, sharesCount
continue continue
? generateDistirbutedStore(rng, ? generateDistributedStore(rng,
shares.get, shares.get,
signingPubKey.toPubKey, signingPubKey.toPubKey,
validatorIdx, validatorIdx,
@ -1517,11 +1530,24 @@ proc resetAttributesNoError() =
try: stdout.resetAttributes() try: stdout.resetAttributes()
except IOError: discard except IOError: discard
proc importKeystoresFromDir*(rng: var HmacDrbgContext, proc importKeystoresFromDir*(rng: var HmacDrbgContext, meth: ImportMethod,
importedDir, validatorsDir, secretsDir: string) = importedDir, validatorsDir, secretsDir: string) =
var password: string # TODO consider using a SecretString type var password: string # TODO consider using a SecretString type
defer: burnMem(password) defer: burnMem(password)
var (singleSaltPassword, singleSaltSalt) =
case meth
of ImportMethod.Normal:
var defaultSeq: seq[byte]
(KeystorePass.init(""), defaultSeq)
of ImportMethod.SingleSalt:
(KeystorePass.init(ncrutils.toHex(rng.generateBytes(32))),
rng.generateBytes(32))
defer:
burnMem(singleSaltPassword)
burnMem(singleSaltSalt)
try: try:
for file in walkDirRec(importedDir): for file in walkDirRec(importedDir):
let filenameParts = splitFile(file) let filenameParts = splitFile(file)
@ -1559,12 +1585,23 @@ proc importKeystoresFromDir*(rng: var HmacDrbgContext,
let privKey = ValidatorPrivKey.fromRaw(secret) let privKey = ValidatorPrivKey.fromRaw(secret)
if privKey.isOk: if privKey.isOk:
let pubkey = privKey.value.toPubKey let pubkey = privKey.value.toPubKey
var var (password, salt) =
password = KeystorePass.init ncrutils.toHex(rng.generateBytes(32)) case meth
defer: burnMem(password) of ImportMethod.Normal:
var defaultSeq: seq[byte]
(KeystorePass.init ncrutils.toHex(rng.generateBytes(32)),
defaultSeq)
of ImportMethod.SingleSalt:
(singleSaltPassword, singleSaltSalt)
defer:
burnMem(password)
burnMem(salt)
let status = saveKeystore(rng, validatorsDir, secretsDir, let status = saveKeystore(rng, validatorsDir, secretsDir,
privKey.value, pubkey, privKey.value, pubkey,
keystore.path, password.str) keystore.path, password.str,
salt)
if status.isOk: if status.isOk:
notice "Keystore imported", file notice "Keystore imported", file
else: else:

View File

@ -105,10 +105,11 @@ proc getValidator*(validators: auto,
validator: validators[idx]) validator: validators[idx])
proc addValidators*(node: BeaconNode) = proc addValidators*(node: BeaconNode) =
info "Loading validators", validatorsDir = node.config.validatorsDir() info "Loading validators", validatorsDir = node.config.validatorsDir(),
keystore_cache_available = not(isNil(node.keystoreCache))
let let
epoch = node.currentSlot().epoch epoch = node.currentSlot().epoch
for keystore in listLoadableKeystores(node.config): for keystore in listLoadableKeystores(node.config, node.keystoreCache):
let let
data = withState(node.dag.headState): data = withState(node.dag.headState):
getValidator(forkyState.data.validators.asSeq(), keystore.pubkey) getValidator(forkyState.data.validators.asSeq(), keystore.pubkey)

View File

@ -67,7 +67,7 @@ proc main =
secretsDir = conf.secretsDir secretsDir = conf.secretsDir
keystore = loadKeystore(validatorsDir, keystore = loadKeystore(validatorsDir,
secretsDir, secretsDir,
conf.key, true).valueOr: conf.key, true, nil).valueOr:
error "Can't load keystore", validatorsDir, secretsDir, pubkey = conf.key error "Can't load keystore", validatorsDir, secretsDir, pubkey = conf.key
quit 1 quit 1
@ -88,7 +88,7 @@ proc main =
let let
outSharesDir = conf.outDir / "shares" outSharesDir = conf.outDir / "shares"
status = generateDistirbutedStore( status = generateDistributedStore(
rngCtx, rngCtx,
shares, shares,
signingPubKey, signingPubKey,

View File

@ -57,7 +57,7 @@ cli do(validatorsDir: string, secretsDir: string,
validatorKeys: Table[ValidatorPubKey, ValidatorPrivKey] validatorKeys: Table[ValidatorPubKey, ValidatorPrivKey]
for item in listLoadableKeystores(validatorsDir, secretsDir, true, for item in listLoadableKeystores(validatorsDir, secretsDir, true,
{KeystoreKind.Local}): {KeystoreKind.Local}, nil):
let let
pubkey = item.privateKey.toPubKey().toPubKey() pubkey = item.privateKey.toPubKey().toPubKey()
idx = findValidator(getStateField(state[], validators).toSeq, pubkey) idx = findValidator(getStateField(state[], validators).toSeq, pubkey)