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
keymanagerHost*: ref KeymanagerHost
keymanagerServer*: RestServerRef
keystoreCache*: KeystoreCacheRef
eventBus*: EventBus
vcProcess*: Process
requestManager*: RequestManager

View File

@ -122,6 +122,10 @@ type
Mainnet = "mainnet"
None = "none"
ImportMethod* {.pure.} = enum
Normal = "normal"
SingleSalt = "single-salt"
BeaconNodeConf* = object
configFile* {.
desc: "Loads the configuration from a TOML file"
@ -714,6 +718,12 @@ type
argument
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:
exitedValidator* {.
name: "validator"

View File

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

View File

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

View File

@ -19,7 +19,7 @@ import
stew/io2,
# Local modules
./spec/[helpers],
./spec/[helpers, keystore],
./spec/datatypes/base,
"."/[beacon_clock, beacon_node_status, conf, version]
@ -245,6 +245,18 @@ proc resetStdin*() =
attrs.c_lflag = attrs.c_lflag or Cflag(ECHO)
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,
slotProc: SlotStartProc[T]) {.async.} =
var

View File

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

View File

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

View File

@ -10,13 +10,14 @@
import
# Standard library
std/[algorithm, math, parseutils, strformat, strutils, typetraits, unicode,
uri],
uri, hashes],
# Third-party libraries
normalize,
# Status libraries
stew/[results, bitops2, base10, io2], stew/shims/macros,
stew/[results, bitops2, base10, io2, endians2], stew/shims/macros,
eth/keyfile/uuid, blscurve,
json_serialization, json_serialization/std/options,
chronos/timer,
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, scrypt],
# Local modules
libp2p/crypto/crypto as lcrypto,
@ -198,6 +199,24 @@ type
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
keyLen = 32
@ -223,6 +242,8 @@ const
wordListLen = 2048
maxWordLen = 16
KeystoreCachePruningTime* = 5.minutes
UUID.serializesAsBaseIn Json
KeyPath.serializesAsBaseIn Json
WalletName.serializesAsBaseIn Json
@ -736,48 +757,56 @@ func areValid(params: ScryptParams): bool =
params.p == scryptParams.p and
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,
password: KeystorePass,
outSecret: var seq[byte]): DecryptionStatus =
# https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition
var decKey: seq[byte]
if crypto.cipher.message.bytes.len == 0:
return InvalidKeystore
let decKey = case crypto.kdf.function
of kdfPbkdf2:
template params: auto = crypto.kdf.pbkdf2Params
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 res = getDecryptionKey(crypto, password, decKey)
if res != DecryptionStatus.Success:
return res
let derivedChecksum = shaChecksum(decKey.toOpenArray(16, 31),
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
decryptCryptoField(crypto, decKey, outSecret)
func cstringToStr(v: cstring): string = $v
@ -796,23 +825,167 @@ template parseRemoteKeystore*(jsonContent: string): RemoteKeystore =
requireAllFields = false,
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,
password: KeystorePass): KsResult[ValidatorPrivKey] =
password: KeystorePass,
cache: KeystoreCacheRef): KsResult[ValidatorPrivKey] =
var secret: seq[byte]
defer: burnMem(secret)
let status = decryptCryptoField(keystore.crypto, password, secret)
case status
of Success:
ValidatorPrivKey.fromRaw(secret).mapErr(cstringToStr)
else:
err $status
while true:
let res = cache.getCachedKey(keystore, password)
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:
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,
password: KeystorePass): KsResult[ValidatorPrivKey] =
let keystore = try: parseKeystore(string keystore)
except SerializationError as e:
return err e.formatMsg("<keystore>")
decryptKeystore(keystore, password)
decryptKeystore(keystore, password, nil)
proc writeValue*(writer: var JsonWriter, value: lcrypto.PublicKey) {.
inline, raises: [IOError, Defect].} =
@ -851,6 +1024,9 @@ proc decryptNetKeystore*(nkeystore: JsonString,
except SerializationError as exc:
return err(exc.formatMsg("<keystore>"))
proc generateKeystoreSalt*(rng: var HmacDrbgContext): seq[byte] =
rng.generateBytes(keyLen)
proc createCryptoField(kdfKind: KdfKind,
rng: var HmacDrbgContext,
secret: openArray[byte],

View File

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

View File

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

View File

@ -105,10 +105,11 @@ proc getValidator*(validators: auto,
validator: validators[idx])
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
epoch = node.currentSlot().epoch
for keystore in listLoadableKeystores(node.config):
for keystore in listLoadableKeystores(node.config, node.keystoreCache):
let
data = withState(node.dag.headState):
getValidator(forkyState.data.validators.asSeq(), keystore.pubkey)

View File

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

View File

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