Keystore cache implementation. (#4372)
This commit is contained in:
parent
d63179ab57
commit
e91415662b
|
@ -73,6 +73,7 @@ type
|
|||
restServer*: RestServerRef
|
||||
keymanagerHost*: ref KeymanagerHost
|
||||
keymanagerServer*: RestServerRef
|
||||
keystoreCache*: KeystoreCacheRef
|
||||
eventBus*: EventBus
|
||||
vcProcess*: Process
|
||||
requestManager*: RequestManager
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.} =
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
||||
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:
|
||||
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,
|
||||
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],
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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:
|
||||
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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue