2020-05-19 17:30:28 +00:00
|
|
|
# beacon_chain
|
2024-01-06 14:26:56 +00:00
|
|
|
# Copyright (c) 2018-2024 Status Research & Development GmbH
|
2020-05-19 17:30:28 +00:00
|
|
|
# Licensed and distributed under either of
|
|
|
|
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
|
|
|
|
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
|
|
|
|
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
|
|
|
|
2023-01-20 14:14:37 +00:00
|
|
|
{.push raises: [].}
|
2020-11-27 22:16:13 +00:00
|
|
|
|
2020-05-19 17:30:28 +00:00
|
|
|
import
|
2020-09-24 05:27:56 +00:00
|
|
|
# Standard library
|
2021-11-30 01:20:21 +00:00
|
|
|
std/[algorithm, math, parseutils, strformat, strutils, typetraits, unicode,
|
2023-02-16 17:25:48 +00:00
|
|
|
uri, hashes],
|
2020-10-02 15:46:05 +00:00
|
|
|
# Third-party libraries
|
|
|
|
normalize,
|
2020-09-24 05:27:56 +00:00
|
|
|
# Status libraries
|
2024-01-16 22:37:14 +00:00
|
|
|
results,
|
|
|
|
stew/[bitops2, base10, io2, endians2], stew/shims/macros,
|
2022-08-19 10:30:07 +00:00
|
|
|
eth/keyfile/uuid, blscurve,
|
|
|
|
json_serialization, json_serialization/std/options,
|
2023-02-16 17:25:48 +00:00
|
|
|
chronos/timer,
|
2020-08-20 13:01:08 +00:00
|
|
|
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, scrypt],
|
2020-10-02 15:46:05 +00:00
|
|
|
# Local modules
|
2020-08-20 13:01:08 +00:00
|
|
|
libp2p/crypto/crypto as lcrypto,
|
2021-08-12 13:08:20 +00:00
|
|
|
./datatypes/base, ./signatures
|
|
|
|
|
2022-08-19 10:30:07 +00:00
|
|
|
export base, uri, io2, options
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-20 13:01:08 +00:00
|
|
|
# We use `ncrutils` for constant-time hexadecimal encoding/decoding procedures.
|
|
|
|
import nimcrypto/utils as ncrutils
|
|
|
|
|
2020-06-01 19:48:20 +00:00
|
|
|
export
|
2020-08-02 17:26:57 +00:00
|
|
|
results, burnMem, writeValue, readValue
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2022-06-09 14:30:13 +00:00
|
|
|
{.localPassC: "-fno-lto".} # no LTO for crypto
|
2020-05-20 07:01:20 +00:00
|
|
|
|
2020-05-19 17:30:28 +00:00
|
|
|
type
|
2021-12-22 12:37:31 +00:00
|
|
|
KeystoreMode* = enum
|
|
|
|
Secure, Fast
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
ChecksumFunctionKind* = enum
|
|
|
|
sha256Checksum = "sha256"
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
Sha256Params* = object
|
|
|
|
Sha256Digest* = MDigest[256]
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
ChecksumBytes* = distinct seq[byte]
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
Checksum* = object
|
|
|
|
case function*: ChecksumFunctionKind
|
|
|
|
of sha256Checksum:
|
|
|
|
params*: Sha256Params
|
|
|
|
message*: Sha256Digest
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
Aes128CtrIv* = distinct seq[byte]
|
|
|
|
|
|
|
|
Aes128CtrParams* = object
|
|
|
|
iv*: Aes128CtrIv
|
|
|
|
|
|
|
|
CipherFunctionKind* = enum
|
|
|
|
aes128CtrCipher = "aes-128-ctr"
|
|
|
|
|
|
|
|
CipherBytes* = distinct seq[byte]
|
|
|
|
|
|
|
|
Cipher* = object
|
|
|
|
case function*: CipherFunctionKind
|
2022-04-08 16:22:49 +00:00
|
|
|
of aes128CtrCipher:
|
2020-08-02 17:26:57 +00:00
|
|
|
params*: Aes128CtrParams
|
|
|
|
message*: CipherBytes
|
|
|
|
|
|
|
|
KdfKind* = enum
|
|
|
|
kdfPbkdf2 = "pbkdf2"
|
|
|
|
kdfScrypt = "scrypt"
|
|
|
|
|
|
|
|
ScryptSalt* = distinct seq[byte]
|
|
|
|
|
|
|
|
ScryptParams* = object
|
2022-02-07 20:36:09 +00:00
|
|
|
dklen*: uint64
|
|
|
|
n*, p*, r*: int
|
|
|
|
salt*: ScryptSalt
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
Pbkdf2Salt* = distinct seq[byte]
|
|
|
|
|
|
|
|
PrfKind* = enum # Pseudo-random-function Kind
|
|
|
|
HmacSha256 = "hmac-sha256"
|
|
|
|
|
|
|
|
Pbkdf2Params* = object
|
2020-10-09 16:41:53 +00:00
|
|
|
dklen*: uint64
|
|
|
|
c*: uint64
|
2020-08-02 17:26:57 +00:00
|
|
|
prf*: PrfKind
|
|
|
|
salt*: Pbkdf2Salt
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-10-09 16:41:53 +00:00
|
|
|
DecryptionStatus* = enum
|
|
|
|
Success = "Success"
|
|
|
|
InvalidPassword = "Invalid password"
|
|
|
|
InvalidKeystore = "Invalid keystore"
|
|
|
|
|
2020-06-23 19:11:07 +00:00
|
|
|
# https://github.com/ethereum/EIPs/blob/4494da0966afa7318ec0157948821b19c4248805/EIPS/eip-2386.md#specification
|
|
|
|
Wallet* = object
|
|
|
|
uuid*: UUID
|
|
|
|
name*: WalletName
|
|
|
|
version*: uint
|
|
|
|
walletType* {.serializedFieldName: "type"}: string
|
|
|
|
# TODO: The use of `JsonString` can be removed once we
|
|
|
|
# solve the serialization problem for `Crypto[T]`
|
2020-08-02 17:26:57 +00:00
|
|
|
crypto*: Crypto
|
2020-06-23 19:11:07 +00:00
|
|
|
nextAccount* {.serializedFieldName: "nextaccount".}: Natural
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
Kdf* = object
|
|
|
|
case function*: KdfKind
|
|
|
|
of kdfPbkdf2:
|
|
|
|
pbkdf2Params* {.serializedFieldName: "params".}: Pbkdf2Params
|
|
|
|
of kdfScrypt:
|
|
|
|
scryptParams* {.serializedFieldName: "params".}: ScryptParams
|
|
|
|
message*: string
|
|
|
|
|
|
|
|
Crypto* = object
|
|
|
|
kdf*: Kdf
|
|
|
|
checksum*: Checksum
|
|
|
|
cipher*: Cipher
|
|
|
|
|
|
|
|
Keystore* = object
|
|
|
|
crypto*: Crypto
|
2022-08-19 10:30:07 +00:00
|
|
|
description*: Option[string]
|
2020-08-02 17:26:57 +00:00
|
|
|
pubkey*: ValidatorPubKey
|
|
|
|
path*: KeyPath
|
|
|
|
uuid*: string
|
|
|
|
version*: int
|
|
|
|
|
2021-12-22 12:37:31 +00:00
|
|
|
KeystoreKind* = enum
|
|
|
|
Local, Remote
|
|
|
|
|
|
|
|
RemoteKeystoreFlag* {.pure.} = enum
|
2023-08-31 12:16:15 +00:00
|
|
|
IgnoreSSLVerification, DynamicKeystore
|
2021-12-22 12:37:31 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
HttpHostUri* = distinct Uri
|
|
|
|
|
2022-05-10 00:32:12 +00:00
|
|
|
RemoteSignerInfo* = object
|
|
|
|
url*: HttpHostUri
|
|
|
|
id*: uint32
|
|
|
|
pubkey*: ValidatorPubKey
|
|
|
|
|
2022-08-07 21:53:20 +00:00
|
|
|
FileLockHandle* = ref object
|
|
|
|
ioHandle*: IoLockHandle
|
|
|
|
opened*: bool
|
|
|
|
|
2023-05-09 08:16:43 +00:00
|
|
|
RemoteSignerType* {.pure.} = enum
|
|
|
|
Web3Signer, VerifyingWeb3Signer
|
|
|
|
|
|
|
|
ProvenProperty* = object
|
|
|
|
path*: string
|
|
|
|
description*: Option[string]
|
|
|
|
phase0Index*: Option[GeneralizedIndex]
|
|
|
|
altairIndex*: Option[GeneralizedIndex]
|
|
|
|
bellatrixIndex*: Option[GeneralizedIndex]
|
|
|
|
capellaIndex*: Option[GeneralizedIndex]
|
|
|
|
denebIndex*: Option[GeneralizedIndex]
|
|
|
|
|
2021-12-22 12:37:31 +00:00
|
|
|
KeystoreData* = object
|
|
|
|
version*: uint64
|
|
|
|
pubkey*: ValidatorPubKey
|
|
|
|
description*: Option[string]
|
2022-08-07 21:53:20 +00:00
|
|
|
handle*: FileLockHandle
|
2021-12-22 12:37:31 +00:00
|
|
|
case kind*: KeystoreKind
|
|
|
|
of KeystoreKind.Local:
|
|
|
|
privateKey*: ValidatorPrivKey
|
|
|
|
path*: KeyPath
|
|
|
|
uuid*: string
|
|
|
|
of KeystoreKind.Remote:
|
|
|
|
flags*: set[RemoteKeystoreFlag]
|
2022-05-10 00:32:12 +00:00
|
|
|
remotes*: seq[RemoteSignerInfo]
|
|
|
|
threshold*: uint32
|
2023-05-09 08:16:43 +00:00
|
|
|
case remoteType*: RemoteSignerType
|
|
|
|
of RemoteSignerType.Web3Signer:
|
|
|
|
discard
|
|
|
|
of RemoteSignerType.VerifyingWeb3Signer:
|
|
|
|
provenBlockProperties*: seq[ProvenProperty]
|
2021-12-22 12:37:31 +00:00
|
|
|
|
2020-08-20 13:01:08 +00:00
|
|
|
NetKeystore* = object
|
|
|
|
crypto*: Crypto
|
2022-08-19 10:30:07 +00:00
|
|
|
description*: Option[string]
|
2020-08-20 13:01:08 +00:00
|
|
|
pubkey*: lcrypto.PublicKey
|
|
|
|
uuid*: string
|
|
|
|
version*: int
|
|
|
|
|
2021-11-30 01:20:21 +00:00
|
|
|
RemoteKeystore* = object
|
2021-12-22 12:37:31 +00:00
|
|
|
version*: uint64
|
2021-11-30 01:20:21 +00:00
|
|
|
description*: Option[string]
|
2023-05-09 08:16:43 +00:00
|
|
|
case remoteType*: RemoteSignerType
|
|
|
|
of RemoteSignerType.Web3Signer:
|
|
|
|
discard
|
|
|
|
of RemoteSignerType.VerifyingWeb3Signer:
|
|
|
|
provenBlockProperties*: seq[ProvenProperty]
|
2021-11-30 01:20:21 +00:00
|
|
|
pubkey*: ValidatorPubKey
|
|
|
|
flags*: set[RemoteKeystoreFlag]
|
2022-05-10 00:32:12 +00:00
|
|
|
remotes*: seq[RemoteSignerInfo]
|
|
|
|
threshold*: uint32
|
2021-11-30 01:20:21 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
KsResult*[T] = Result[T, string]
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-06-01 19:48:20 +00:00
|
|
|
Eth2KeyKind* = enum
|
|
|
|
signingKeyKind # Also known as voting key
|
|
|
|
withdrawalKeyKind
|
|
|
|
|
2020-06-23 19:11:07 +00:00
|
|
|
UUID* = distinct string
|
|
|
|
WalletName* = distinct string
|
2020-06-01 19:48:20 +00:00
|
|
|
Mnemonic* = distinct string
|
|
|
|
KeyPath* = distinct string
|
|
|
|
KeySeed* = distinct seq[byte]
|
2020-10-02 15:46:05 +00:00
|
|
|
KeystorePass* = object
|
|
|
|
str*: string
|
2020-06-01 19:48:20 +00:00
|
|
|
|
|
|
|
Credentials* = object
|
|
|
|
mnemonic*: Mnemonic
|
2020-08-02 17:26:57 +00:00
|
|
|
keystore*: Keystore
|
2020-06-01 19:48:20 +00:00
|
|
|
signingKey*: ValidatorPrivKey
|
|
|
|
withdrawalKey*: ValidatorPrivKey
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
SimpleHexEncodedTypes* = ScryptSalt|ChecksumBytes|CipherBytes
|
2020-08-02 17:26:57 +00:00
|
|
|
|
2023-02-16 17:25:48 +00:00
|
|
|
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
|
|
|
|
|
2020-05-19 17:30:28 +00:00
|
|
|
const
|
2020-08-02 17:26:57 +00:00
|
|
|
keyLen = 32
|
2020-05-20 07:22:21 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
scryptParams = ScryptParams(
|
2020-10-09 16:41:53 +00:00
|
|
|
dklen: uint64 keyLen,
|
2020-05-19 17:30:28 +00:00
|
|
|
n: 2^18,
|
2020-08-02 18:46:12 +00:00
|
|
|
p: 1,
|
|
|
|
r: 8
|
2020-05-19 17:30:28 +00:00
|
|
|
)
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
pbkdf2Params = Pbkdf2Params(
|
2020-10-09 16:41:53 +00:00
|
|
|
dklen: uint64 keyLen,
|
|
|
|
c: uint64(2^18),
|
2020-08-02 17:26:57 +00:00
|
|
|
prf: HmacSha256
|
2020-05-19 17:30:28 +00:00
|
|
|
)
|
|
|
|
|
2020-06-01 19:48:20 +00:00
|
|
|
# https://eips.ethereum.org/EIPS/eip-2334
|
|
|
|
eth2KeyPurpose = 12381
|
|
|
|
eth2CoinType* = 3600
|
2020-10-19 19:02:48 +00:00
|
|
|
baseKeyPath* = [Natural eth2KeyPurpose, eth2CoinType]
|
2020-06-01 19:48:20 +00:00
|
|
|
|
|
|
|
# https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md
|
|
|
|
wordListLen = 2048
|
2020-08-21 19:36:42 +00:00
|
|
|
maxWordLen = 16
|
2020-06-02 19:59:51 +00:00
|
|
|
|
2023-02-16 17:25:48 +00:00
|
|
|
KeystoreCachePruningTime* = 5.minutes
|
|
|
|
|
2020-06-23 19:11:07 +00:00
|
|
|
UUID.serializesAsBaseIn Json
|
2020-08-02 17:26:57 +00:00
|
|
|
KeyPath.serializesAsBaseIn Json
|
2020-06-23 19:11:07 +00:00
|
|
|
WalletName.serializesAsBaseIn Json
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
ChecksumFunctionKind.serializesAsTextInJson
|
|
|
|
CipherFunctionKind.serializesAsTextInJson
|
|
|
|
PrfKind.serializesAsTextInJson
|
|
|
|
KdfKind.serializesAsTextInJson
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
template `$`*(u: HttpHostUri): string =
|
|
|
|
`$`(Uri(u))
|
|
|
|
|
|
|
|
template `==`*(lhs, rhs: HttpHostUri): bool =
|
|
|
|
Uri(lhs) == Uri(rhs)
|
|
|
|
|
|
|
|
template `<`*(lhs, rhs: HttpHostUri): bool =
|
|
|
|
$Uri(lhs) < $Uri(rhs)
|
|
|
|
|
2020-06-23 19:11:07 +00:00
|
|
|
template `$`*(m: Mnemonic): string =
|
|
|
|
string(m)
|
|
|
|
|
2020-07-17 20:59:50 +00:00
|
|
|
template `==`*(lhs, rhs: WalletName): bool =
|
|
|
|
string(lhs) == string(rhs)
|
|
|
|
|
|
|
|
template `$`*(x: WalletName): string =
|
|
|
|
string(x)
|
|
|
|
|
2020-10-19 19:02:48 +00:00
|
|
|
# TODO: `burnMem` in nimcrypto could use distinctBase
|
|
|
|
# to make its usage less error-prone.
|
2022-03-24 21:44:34 +00:00
|
|
|
template burnMem*(m: var (Mnemonic|string)) =
|
2020-08-20 14:59:59 +00:00
|
|
|
ncrutils.burnMem(string m)
|
2020-06-23 19:11:07 +00:00
|
|
|
|
2020-10-19 19:02:48 +00:00
|
|
|
template burnMem*(m: var KeySeed) =
|
|
|
|
ncrutils.burnMem(distinctBase m)
|
|
|
|
|
2020-10-02 15:46:05 +00:00
|
|
|
template burnMem*(m: var KeystorePass) =
|
|
|
|
ncrutils.burnMem(m.str)
|
|
|
|
|
2020-08-21 19:36:42 +00:00
|
|
|
func longName*(wallet: Wallet): string =
|
|
|
|
if wallet.name.string == wallet.uuid.string:
|
|
|
|
wallet.name.string
|
|
|
|
else:
|
|
|
|
wallet.name.string & " (" & wallet.uuid.string & ")"
|
|
|
|
|
2020-07-14 19:00:35 +00:00
|
|
|
macro wordListArray*(filename: static string,
|
|
|
|
maxWords: static int = 0,
|
2020-08-21 19:36:42 +00:00
|
|
|
minWordLen: static int = 0,
|
|
|
|
maxWordLen: static int = high(int)): untyped =
|
2020-06-02 19:59:51 +00:00
|
|
|
result = newTree(nnkBracket)
|
2022-12-08 16:21:53 +00:00
|
|
|
let words = slurp(filename.replace('\\', '/')).splitLines()
|
2020-06-02 19:59:51 +00:00
|
|
|
for word in words:
|
2020-08-21 19:36:42 +00:00
|
|
|
if word.len >= minWordLen and word.len <= maxWordLen:
|
2020-07-14 19:00:35 +00:00
|
|
|
result.add newCall("cstring", newLit(word))
|
|
|
|
if maxWords > 0 and result.len >= maxWords:
|
|
|
|
return
|
2020-06-02 19:59:51 +00:00
|
|
|
|
|
|
|
const
|
2020-07-14 19:00:35 +00:00
|
|
|
englishWords = wordListArray("english_word_list.txt",
|
2020-08-21 19:36:42 +00:00
|
|
|
maxWords = wordListLen,
|
|
|
|
maxWordLen = maxWordLen)
|
2020-09-29 16:49:09 +00:00
|
|
|
englishWordsDigest =
|
|
|
|
"AD90BF3BEB7B0EB7E5ACD74727DC0DA96E0A280A258354E7293FB7E211AC03DB".toDigest
|
|
|
|
|
|
|
|
proc checkEnglishWords(): bool =
|
|
|
|
if len(englishWords) != wordListLen:
|
|
|
|
false
|
|
|
|
else:
|
|
|
|
var ctx: sha256
|
|
|
|
ctx.init()
|
|
|
|
for item in englishWords:
|
|
|
|
ctx.update($item)
|
|
|
|
ctx.finish() == englishWordsDigest
|
2020-08-21 19:36:42 +00:00
|
|
|
|
|
|
|
static:
|
2020-09-29 16:49:09 +00:00
|
|
|
doAssert(checkEnglishWords(), "English words array is corrupted!")
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2022-03-24 21:44:34 +00:00
|
|
|
func validateKeyPath*(path: string): Result[KeyPath, cstring] =
|
2020-09-24 05:27:56 +00:00
|
|
|
var digitCount: int
|
2022-04-08 16:22:49 +00:00
|
|
|
var number: BiggestUInt
|
2020-09-24 05:27:56 +00:00
|
|
|
try:
|
|
|
|
for elem in path.string.split("/"):
|
2020-09-29 16:49:09 +00:00
|
|
|
# TODO: doesn't "m" have to be the first character and is it the only
|
|
|
|
# place where it is valid?
|
2020-09-24 05:27:56 +00:00
|
|
|
if elem == "m":
|
|
|
|
continue
|
|
|
|
# parseBiggestUInt can raise if overflow
|
|
|
|
digitCount = elem.parseBiggestUInt(number)
|
|
|
|
if digitCount == 0:
|
|
|
|
return err("Invalid derivation path")
|
|
|
|
except ValueError:
|
|
|
|
return err("KeyPath contains invalid number(s)")
|
|
|
|
|
|
|
|
return ok(KeyPath path)
|
2020-06-01 19:48:20 +00:00
|
|
|
|
|
|
|
iterator pathNodes(path: KeyPath): Natural =
|
2020-09-24 05:27:56 +00:00
|
|
|
# TODO: we have exceptions there
|
|
|
|
# and this iterator is used to derive secret keys
|
|
|
|
# if we fail we want to scrub secrets from memory
|
2020-06-01 19:48:20 +00:00
|
|
|
try:
|
2020-09-24 05:27:56 +00:00
|
|
|
for elem in path.string.split("/"):
|
|
|
|
if elem == "m": continue
|
|
|
|
yield parseBiggestUInt(elem)
|
2020-06-01 19:48:20 +00:00
|
|
|
except ValueError:
|
|
|
|
doAssert false, "Make sure you've validated the key path with `validateKeyPath`"
|
|
|
|
|
|
|
|
func makeKeyPath*(validatorIdx: Natural,
|
|
|
|
keyType: Eth2KeyKind): KeyPath =
|
|
|
|
# https://eips.ethereum.org/EIPS/eip-2334
|
|
|
|
let use = case keyType
|
|
|
|
of withdrawalKeyKind: "0"
|
|
|
|
of signingKeyKind: "0/0"
|
|
|
|
|
|
|
|
try:
|
|
|
|
KeyPath &"m/{eth2KeyPurpose}/{eth2CoinType}/{validatorIdx}/{use}"
|
|
|
|
except ValueError:
|
|
|
|
raiseAssert "All values above can be converted successfully to strings"
|
|
|
|
|
2020-10-02 15:46:05 +00:00
|
|
|
func isControlRune(r: Rune): bool =
|
|
|
|
let r = int r
|
|
|
|
(r >= 0 and r < 0x20) or (r >= 0x7F and r < 0xA0)
|
|
|
|
|
|
|
|
proc init*(T: type KeystorePass, input: string): T =
|
|
|
|
for rune in toNFKD(input):
|
|
|
|
if not isControlRune(rune):
|
|
|
|
result.str.add rune
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
func getSeed*(mnemonic: Mnemonic, password: KeystorePass): KeySeed =
|
2020-06-01 19:48:20 +00:00
|
|
|
# https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed
|
2020-10-02 16:08:57 +00:00
|
|
|
let salt = toNFKD("mnemonic" & password.str)
|
2020-06-01 19:48:20 +00:00
|
|
|
KeySeed sha512.pbkdf2(mnemonic.string, salt, 2048, 64)
|
|
|
|
|
2020-08-21 19:36:42 +00:00
|
|
|
template add(m: var Mnemonic, s: cstring) =
|
|
|
|
m.string.add s
|
|
|
|
|
2020-07-07 15:51:02 +00:00
|
|
|
proc generateMnemonic*(
|
2022-06-21 08:29:16 +00:00
|
|
|
rng: var HmacDrbgContext,
|
2020-10-28 18:35:31 +00:00
|
|
|
words: openArray[cstring] = englishWords,
|
|
|
|
entropyParam: openArray[byte] = @[]): Mnemonic =
|
2020-08-21 19:36:42 +00:00
|
|
|
## Generates a valid BIP-0039 mnenomic:
|
|
|
|
## https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic
|
2022-06-21 08:29:16 +00:00
|
|
|
var entropy =
|
|
|
|
if entropyParam.len == 0:
|
|
|
|
rng.generateBytes(32)
|
|
|
|
else:
|
|
|
|
doAssert entropyParam.len >= 128 and
|
|
|
|
entropyParam.len <= 256 and
|
|
|
|
entropyParam.len mod 32 == 0
|
|
|
|
@entropyParam
|
2020-06-01 19:48:20 +00:00
|
|
|
|
|
|
|
let
|
|
|
|
checksumBits = entropy.len div 4 # ranges from 4 to 8
|
|
|
|
mnemonicWordCount = 12 + (checksumBits - 4) * 3
|
|
|
|
checksum = sha256.digest(entropy)
|
|
|
|
|
|
|
|
entropy.add byte(checksum.data.getBitsBE(0 ..< checksumBits))
|
|
|
|
|
2020-08-21 19:36:42 +00:00
|
|
|
# Make sure the string won't be reallocated as this may
|
|
|
|
# leave partial copies of the mnemonic in memory:
|
|
|
|
result = Mnemonic newStringOfCap(mnemonicWordCount * maxWordLen)
|
|
|
|
result.add words[entropy.getBitsBE(0..10)]
|
2020-06-02 19:59:51 +00:00
|
|
|
|
2020-06-01 19:48:20 +00:00
|
|
|
for i in 1 ..< mnemonicWordCount:
|
|
|
|
let
|
|
|
|
firstBit = i*11
|
|
|
|
lastBit = firstBit + 10
|
2020-08-21 19:36:42 +00:00
|
|
|
result.add " "
|
|
|
|
result.add words[entropy.getBitsBE(firstBit..lastBit)]
|
|
|
|
|
|
|
|
proc cmpIgnoreCase(lhs: cstring, rhs: string): int =
|
|
|
|
# TODO: This is a bit silly.
|
|
|
|
# Nim should have a `cmp` function for C strings.
|
|
|
|
cmpIgnoreCase($lhs, rhs)
|
|
|
|
|
2022-03-24 21:44:34 +00:00
|
|
|
proc validateMnemonic*(inputWords: string,
|
2020-08-21 19:36:42 +00:00
|
|
|
outputMnemonic: var Mnemonic): bool =
|
|
|
|
## Accept a case-insensitive input string and returns `true`
|
|
|
|
## if it represents a valid mnenomic. The `outputMnemonic`
|
|
|
|
## value will be populated with a normalized lower-case
|
|
|
|
## version of the mnemonic using a single space separator.
|
|
|
|
##
|
|
|
|
## The `outputMnemonic` value may be populated partially
|
|
|
|
## with sensitive data even in case of validator failure.
|
|
|
|
## Make sure to burn the received data after usage.
|
|
|
|
|
2022-03-24 21:44:34 +00:00
|
|
|
# TODO consider using a SecretString type for inputWords
|
|
|
|
|
2024-02-07 16:51:12 +00:00
|
|
|
let words = strutils.strip(inputWords.toNFKD).split(Whitespace)
|
2020-08-21 19:36:42 +00:00
|
|
|
if words.len < 12 or words.len > 24 or words.len mod 3 != 0:
|
|
|
|
return false
|
|
|
|
|
|
|
|
# Make sure the string won't be re-allocated as this may
|
|
|
|
# leave partial copies of the mnemonic in memory:
|
|
|
|
outputMnemonic = Mnemonic newStringOfCap(words.len * maxWordLen)
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-08-21 19:36:42 +00:00
|
|
|
for word in words:
|
|
|
|
let foundIdx = binarySearch(englishWords, word, cmpIgnoreCase)
|
|
|
|
if foundIdx == -1:
|
|
|
|
return false
|
|
|
|
if outputMnemonic.string.len > 0:
|
|
|
|
outputMnemonic.add " "
|
|
|
|
outputMnemonic.add englishWords[foundIdx]
|
|
|
|
|
|
|
|
return true
|
2020-06-01 19:48:20 +00:00
|
|
|
|
|
|
|
proc deriveChildKey*(parentKey: ValidatorPrivKey,
|
|
|
|
index: Natural): ValidatorPrivKey =
|
2020-06-08 14:56:56 +00:00
|
|
|
let success = derive_child_secretKey(SecretKey result,
|
|
|
|
SecretKey parentKey,
|
|
|
|
uint32 index)
|
|
|
|
# TODO `derive_child_secretKey` is reporting pre-condition
|
|
|
|
# failures with return values. We should turn the checks
|
|
|
|
# into asserts inside the function.
|
|
|
|
doAssert success
|
2020-06-01 19:48:20 +00:00
|
|
|
|
|
|
|
proc deriveMasterKey*(seed: KeySeed): ValidatorPrivKey =
|
2020-06-08 14:56:56 +00:00
|
|
|
let success = derive_master_secretKey(SecretKey result,
|
|
|
|
seq[byte] seed)
|
|
|
|
# TODO `derive_master_secretKey` is reporting pre-condition
|
|
|
|
# failures with return values. We should turn the checks
|
|
|
|
# into asserts inside the function.
|
|
|
|
doAssert success
|
2020-06-01 19:48:20 +00:00
|
|
|
|
|
|
|
proc deriveMasterKey*(mnemonic: Mnemonic,
|
2020-08-02 17:26:57 +00:00
|
|
|
password: KeystorePass): ValidatorPrivKey =
|
2020-06-01 19:48:20 +00:00
|
|
|
deriveMasterKey(getSeed(mnemonic, password))
|
|
|
|
|
|
|
|
proc deriveChildKey*(masterKey: ValidatorPrivKey,
|
|
|
|
path: KeyPath): ValidatorPrivKey =
|
|
|
|
result = masterKey
|
|
|
|
for idx in pathNodes(path):
|
2020-10-19 19:02:48 +00:00
|
|
|
result = deriveChildKey(result, idx)
|
|
|
|
|
|
|
|
proc deriveChildKey*(masterKey: ValidatorPrivKey,
|
|
|
|
path: openArray[Natural]): ValidatorPrivKey =
|
|
|
|
result = masterKey
|
|
|
|
for idx in path:
|
2020-09-24 05:27:56 +00:00
|
|
|
# TODO: we have exceptions in pathNodes unless `validateKeyPath`
|
|
|
|
# was called,
|
|
|
|
# and this iterator is used to derive secret keys
|
|
|
|
# if we fail we want to scrub secrets from memory
|
2020-06-01 19:48:20 +00:00
|
|
|
result = deriveChildKey(result, idx)
|
|
|
|
|
|
|
|
proc keyFromPath*(mnemonic: Mnemonic,
|
2020-08-02 17:26:57 +00:00
|
|
|
password: KeystorePass,
|
2020-06-01 19:48:20 +00:00
|
|
|
path: KeyPath): ValidatorPrivKey =
|
|
|
|
deriveChildKey(deriveMasterKey(mnemonic, password), path)
|
|
|
|
|
2020-10-28 18:35:31 +00:00
|
|
|
proc shaChecksum(key, cipher: openArray[byte]): Sha256Digest =
|
2020-05-19 17:30:28 +00:00
|
|
|
var ctx: sha256
|
|
|
|
ctx.init()
|
|
|
|
ctx.update(key)
|
|
|
|
ctx.update(cipher)
|
2020-08-02 17:26:57 +00:00
|
|
|
result = ctx.finish()
|
2020-05-20 07:22:21 +00:00
|
|
|
ctx.clear()
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-10-28 18:35:31 +00:00
|
|
|
proc writeJsonHexString(s: OutputStream, data: openArray[byte])
|
2023-08-25 09:29:07 +00:00
|
|
|
{.raises: [IOError].} =
|
2020-08-02 17:26:57 +00:00
|
|
|
s.write '"'
|
2020-08-20 13:01:08 +00:00
|
|
|
s.write ncrutils.toHex(data, {HexFlags.LowerCase})
|
2020-08-02 17:26:57 +00:00
|
|
|
s.write '"'
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
proc readValue*(r: var JsonReader, value: var Pbkdf2Salt)
|
2023-08-25 09:29:07 +00:00
|
|
|
{.raises: [SerializationError, IOError].} =
|
2022-12-08 16:21:53 +00:00
|
|
|
let s = r.readValue(string)
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
if s.len == 0 or s.len mod 16 != 0:
|
|
|
|
r.raiseUnexpectedValue(
|
2020-08-20 13:01:08 +00:00
|
|
|
"The Pbkdf2Salt salt must have a non-zero length divisible by 16")
|
2020-05-27 14:05:32 +00:00
|
|
|
|
2020-08-20 13:01:08 +00:00
|
|
|
value = Pbkdf2Salt ncrutils.fromHex(s)
|
|
|
|
let length = len(seq[byte](value))
|
|
|
|
if length == 0 or (length mod 8) != 0:
|
2020-08-02 17:26:57 +00:00
|
|
|
r.raiseUnexpectedValue(
|
|
|
|
"The Pbkdf2Salt must be a valid hex string")
|
|
|
|
|
|
|
|
proc readValue*(r: var JsonReader, value: var Aes128CtrIv)
|
2023-08-25 09:29:07 +00:00
|
|
|
{.raises: [SerializationError, IOError].} =
|
2022-12-08 16:21:53 +00:00
|
|
|
let s = r.readValue(string)
|
2020-08-02 17:26:57 +00:00
|
|
|
|
|
|
|
if s.len != 32:
|
|
|
|
r.raiseUnexpectedValue(
|
|
|
|
"The aes-128-ctr IV must be a string of length 32")
|
|
|
|
|
2020-08-20 13:01:08 +00:00
|
|
|
value = Aes128CtrIv ncrutils.fromHex(s)
|
|
|
|
if len(seq[byte](value)) != 16:
|
2020-08-02 17:26:57 +00:00
|
|
|
r.raiseUnexpectedValue(
|
|
|
|
"The aes-128-ctr IV must be a valid hex string")
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
proc readValue*[T: SimpleHexEncodedTypes](r: var JsonReader, value: var T) {.
|
2023-08-25 09:29:07 +00:00
|
|
|
raises: [SerializationError, IOError].} =
|
2020-08-20 13:01:08 +00:00
|
|
|
value = T ncrutils.fromHex(r.readValue(string))
|
|
|
|
if len(seq[byte](value)) == 0:
|
2020-08-02 17:26:57 +00:00
|
|
|
r.raiseUnexpectedValue("Valid hex string expected")
|
|
|
|
|
2023-06-16 17:02:53 +00:00
|
|
|
template readValueImpl(r: var JsonReader, value: var Checksum) =
|
|
|
|
var
|
|
|
|
functionSpecified = false
|
|
|
|
paramsSpecified = false
|
|
|
|
messageSpecified = false
|
|
|
|
|
|
|
|
for fieldName in readObjectFields(r):
|
|
|
|
case fieldName
|
|
|
|
of "function":
|
|
|
|
value = Checksum(function: r.readValue(ChecksumFunctionKind))
|
|
|
|
functionSpecified = true
|
|
|
|
|
|
|
|
of "params":
|
|
|
|
if functionSpecified:
|
|
|
|
case value.function
|
|
|
|
of sha256Checksum:
|
|
|
|
r.readValue(value.params)
|
|
|
|
else:
|
|
|
|
r.raiseUnexpectedValue(
|
|
|
|
"The 'params' field must be specified after the 'function' field")
|
|
|
|
paramsSpecified = true
|
|
|
|
|
|
|
|
of "message":
|
|
|
|
if functionSpecified:
|
|
|
|
case value.function
|
|
|
|
of sha256Checksum:
|
|
|
|
r.readValue(value.message)
|
|
|
|
else:
|
|
|
|
r.raiseUnexpectedValue(
|
|
|
|
"The 'message' field must be specified after the 'function' field")
|
|
|
|
messageSpecified = true
|
|
|
|
|
|
|
|
else:
|
|
|
|
r.raiseUnexpectedField(fieldName, "Checksum")
|
|
|
|
|
|
|
|
if not (functionSpecified and paramsSpecified and messageSpecified):
|
|
|
|
r.raiseUnexpectedValue(
|
|
|
|
"The Checksum value should have sub-fields named " &
|
|
|
|
"'function', 'params', and 'message'")
|
|
|
|
|
|
|
|
{.push warning[ProveField]:off.} # https://github.com/nim-lang/Nim/issues/22060
|
|
|
|
proc readValue*(r: var JsonReader[DefaultFlavor], value: var Checksum)
|
|
|
|
{.raises: [SerializationError, IOError].} =
|
|
|
|
readValueImpl(r, value)
|
|
|
|
{.pop.}
|
|
|
|
|
|
|
|
template readValueImpl(r: var JsonReader, value: var Cipher) =
|
|
|
|
var
|
|
|
|
functionSpecified = false
|
|
|
|
paramsSpecified = false
|
|
|
|
messageSpecified = false
|
|
|
|
|
|
|
|
for fieldName in readObjectFields(r):
|
|
|
|
case fieldName
|
|
|
|
of "function":
|
|
|
|
value = Cipher(
|
|
|
|
function: r.readValue(CipherFunctionKind), message: value.message)
|
|
|
|
functionSpecified = true
|
|
|
|
|
|
|
|
of "params":
|
|
|
|
if functionSpecified:
|
|
|
|
case value.function
|
|
|
|
of aes128CtrCipher:
|
|
|
|
r.readValue(value.params)
|
|
|
|
else:
|
|
|
|
r.raiseUnexpectedValue(
|
|
|
|
"The 'params' field must be specified after the 'function' field")
|
|
|
|
paramsSpecified = true
|
|
|
|
|
|
|
|
of "message":
|
|
|
|
r.readValue(value.message)
|
|
|
|
messageSpecified = true
|
|
|
|
|
|
|
|
else:
|
|
|
|
r.raiseUnexpectedField(fieldName, "Cipher")
|
|
|
|
|
|
|
|
if not (functionSpecified and paramsSpecified and messageSpecified):
|
|
|
|
r.raiseUnexpectedValue(
|
|
|
|
"The Cipher value should have sub-fields named " &
|
|
|
|
"'function', 'params', and 'message'")
|
|
|
|
|
|
|
|
{.push warning[ProveField]:off.} # https://github.com/nim-lang/Nim/issues/22060
|
|
|
|
proc readValue*(r: var JsonReader[DefaultFlavor], value: var Cipher)
|
|
|
|
{.raises: [SerializationError, IOError].} =
|
|
|
|
readValueImpl(r, value)
|
|
|
|
{.pop.}
|
|
|
|
|
|
|
|
template readValueImpl(r: var JsonReader, value: var Kdf) =
|
2020-08-02 17:26:57 +00:00
|
|
|
var
|
|
|
|
functionSpecified = false
|
|
|
|
paramsSpecified = false
|
|
|
|
|
|
|
|
for fieldName in readObjectFields(r):
|
|
|
|
case fieldName
|
|
|
|
of "function":
|
2023-06-16 17:02:53 +00:00
|
|
|
value = Kdf(function: r.readValue(KdfKind), message: value.message)
|
2020-08-02 17:26:57 +00:00
|
|
|
functionSpecified = true
|
|
|
|
|
|
|
|
of "params":
|
|
|
|
if functionSpecified:
|
|
|
|
case value.function
|
|
|
|
of kdfPbkdf2:
|
|
|
|
r.readValue(value.pbkdf2Params)
|
|
|
|
of kdfScrypt:
|
|
|
|
r.readValue(value.scryptParams)
|
|
|
|
else:
|
|
|
|
r.raiseUnexpectedValue(
|
|
|
|
"The 'params' field must be specified after the 'function' field")
|
|
|
|
paramsSpecified = true
|
|
|
|
|
|
|
|
of "message":
|
|
|
|
r.readValue(value.message)
|
|
|
|
|
|
|
|
else:
|
|
|
|
r.raiseUnexpectedField(fieldName, "Kdf")
|
|
|
|
|
|
|
|
if not (functionSpecified and paramsSpecified):
|
|
|
|
r.raiseUnexpectedValue(
|
|
|
|
"The Kdf value should have sub-fields named 'function' and 'params'")
|
|
|
|
|
2023-06-16 17:02:53 +00:00
|
|
|
{.push warning[ProveField]:off.} # https://github.com/nim-lang/Nim/issues/22060
|
|
|
|
proc readValue*(r: var JsonReader[DefaultFlavor], value: var Kdf)
|
|
|
|
{.raises: [SerializationError, IOError].} =
|
|
|
|
readValueImpl(r, value)
|
|
|
|
{.pop.}
|
|
|
|
|
|
|
|
proc readValue*(r: var JsonReader, value: var (Checksum|Cipher|Kdf)) =
|
|
|
|
static: raiseAssert "Unknown flavor `JsonReader[" & $typeof(r).Flavor &
|
|
|
|
"]` for `readValue` of `" & $typeof(value) & "`"
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
# HttpHostUri
|
|
|
|
proc readValue*(reader: var JsonReader, value: var HttpHostUri) {.
|
2023-08-25 09:29:07 +00:00
|
|
|
raises: [IOError, SerializationError].} =
|
2022-02-07 20:36:09 +00:00
|
|
|
let svalue = reader.readValue(string)
|
|
|
|
let res = parseUri(svalue)
|
|
|
|
if res.scheme != "http" and res.scheme != "https":
|
|
|
|
reader.raiseUnexpectedValue("Incorrect URL scheme")
|
|
|
|
if len(res.hostname) == 0:
|
|
|
|
reader.raiseUnexpectedValue("Missing URL hostname")
|
|
|
|
value = HttpHostUri(res)
|
|
|
|
|
2023-08-19 15:11:56 +00:00
|
|
|
proc writeValue*(
|
|
|
|
writer: var JsonWriter, value: HttpHostUri) {.raises: [IOError].} =
|
2022-02-07 20:36:09 +00:00
|
|
|
writer.writeValue($distinctBase(value))
|
|
|
|
|
|
|
|
# RemoteKeystore
|
2023-08-19 15:11:56 +00:00
|
|
|
proc writeValue*(
|
|
|
|
writer: var JsonWriter, value: RemoteKeystore) {.raises: [IOError].} =
|
2022-02-07 20:36:09 +00:00
|
|
|
writer.beginRecord()
|
|
|
|
writer.writeField("version", value.version)
|
|
|
|
writer.writeField("pubkey", "0x" & value.pubkey.toHex())
|
2022-05-10 00:32:12 +00:00
|
|
|
writer.writeField("remotes", value.remotes)
|
|
|
|
writer.writeField("threshold", value.threshold)
|
2022-02-07 20:36:09 +00:00
|
|
|
case value.remoteType
|
|
|
|
of RemoteSignerType.Web3Signer:
|
|
|
|
writer.writeField("type", "web3signer")
|
2023-05-09 08:16:43 +00:00
|
|
|
of RemoteSignerType.VerifyingWeb3Signer:
|
|
|
|
writer.writeField("type", "verifying-web3signer")
|
|
|
|
writer.writeField("proven_block_properties", value.provenBlockProperties)
|
2022-02-07 20:36:09 +00:00
|
|
|
if value.description.isSome():
|
|
|
|
writer.writeField("description", value.description.get())
|
|
|
|
if RemoteKeystoreFlag.IgnoreSSLVerification in value.flags:
|
|
|
|
writer.writeField("ignore_ssl_verification", true)
|
|
|
|
writer.endRecord()
|
|
|
|
|
|
|
|
template writeValue*(w: var JsonWriter,
|
|
|
|
value: Pbkdf2Salt|SimpleHexEncodedTypes|Aes128CtrIv) =
|
|
|
|
writeJsonHexString(w.stream, distinctBase value)
|
|
|
|
|
2023-10-13 12:42:00 +00:00
|
|
|
func parseProvenBlockProperty*(propertyPath: string): Result[ProvenProperty, string] =
|
|
|
|
if propertyPath == ".execution_payload.fee_recipient":
|
|
|
|
ok ProvenProperty(
|
|
|
|
path: propertyPath,
|
|
|
|
bellatrixIndex: some GeneralizedIndex(401),
|
|
|
|
capellaIndex: some GeneralizedIndex(401),
|
|
|
|
denebIndex: some GeneralizedIndex(801))
|
|
|
|
elif propertyPath == ".graffiti":
|
|
|
|
ok ProvenProperty(
|
|
|
|
path: propertyPath,
|
|
|
|
# TODO: graffiti is present since genesis, so the correct index in the early
|
|
|
|
# forks can be supplied here
|
|
|
|
bellatrixIndex: some GeneralizedIndex(18),
|
|
|
|
capellaIndex: some GeneralizedIndex(18),
|
|
|
|
denebIndex: some GeneralizedIndex(18))
|
|
|
|
else:
|
|
|
|
err("Keystores with proven properties different than " &
|
|
|
|
"`.execution_payload.fee_recipient` and `.graffiti` " &
|
|
|
|
"require a more recent version of Nimbus")
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
proc readValue*(reader: var JsonReader, value: var RemoteKeystore)
|
2023-08-25 09:29:07 +00:00
|
|
|
{.raises: [SerializationError, IOError].} =
|
2021-11-30 01:20:21 +00:00
|
|
|
var
|
2022-02-07 20:36:09 +00:00
|
|
|
version: Option[uint64]
|
2021-11-30 01:20:21 +00:00
|
|
|
description: Option[string]
|
2022-02-07 20:36:09 +00:00
|
|
|
remote: Option[HttpHostUri]
|
2022-05-10 00:32:12 +00:00
|
|
|
remotes: Option[seq[RemoteSignerInfo]]
|
2023-05-09 08:16:43 +00:00
|
|
|
remoteType: Option[RemoteSignerType]
|
|
|
|
provenBlockProperties: Option[seq[ProvenProperty]]
|
2021-11-30 01:20:21 +00:00
|
|
|
ignoreSslVerification: Option[bool]
|
|
|
|
pubkey: Option[ValidatorPubKey]
|
2022-05-10 00:32:12 +00:00
|
|
|
threshold: Option[uint32]
|
2022-02-07 20:36:09 +00:00
|
|
|
|
2022-05-10 00:32:12 +00:00
|
|
|
# TODO: implementing deserializers for versioned objects
|
|
|
|
# manually is extremely error-prone. This should use
|
|
|
|
# the auto-generated deserializer from nim-json-serialization
|
2022-02-07 20:36:09 +00:00
|
|
|
for fieldName in readObjectFields(reader):
|
2021-11-30 01:20:21 +00:00
|
|
|
case fieldName:
|
|
|
|
of "pubkey":
|
2023-05-09 08:16:43 +00:00
|
|
|
if pubkey.isSome:
|
2022-02-07 20:36:09 +00:00
|
|
|
reader.raiseUnexpectedField("Multiple `pubkey` fields found",
|
|
|
|
"RemoteKeystore")
|
|
|
|
pubkey = some(reader.readValue(ValidatorPubKey))
|
2021-11-30 01:20:21 +00:00
|
|
|
of "remote":
|
2023-05-09 08:16:43 +00:00
|
|
|
if remote.isSome:
|
2022-02-07 20:36:09 +00:00
|
|
|
reader.raiseUnexpectedField("Multiple `remote` fields found",
|
|
|
|
"RemoteKeystore")
|
2023-05-09 08:16:43 +00:00
|
|
|
if remotes.isSome:
|
|
|
|
reader.raiseUnexpectedField("The `remote` field cannot be specified together with `remotes`",
|
|
|
|
"RemoteKeystore")
|
2022-02-07 20:36:09 +00:00
|
|
|
remote = some(reader.readValue(HttpHostUri))
|
2022-05-10 00:32:12 +00:00
|
|
|
of "remotes":
|
2023-05-09 08:16:43 +00:00
|
|
|
if remotes.isSome:
|
2022-05-10 00:32:12 +00:00
|
|
|
reader.raiseUnexpectedField("Multiple `remote` fields found",
|
|
|
|
"RemoteKeystore")
|
2023-05-09 08:16:43 +00:00
|
|
|
if remote.isSome:
|
|
|
|
reader.raiseUnexpectedField("The `remotes` field cannot be specified together with `remote`",
|
|
|
|
"RemoteKeystore")
|
|
|
|
if version.isNone:
|
|
|
|
reader.raiseUnexpectedField(
|
|
|
|
"The `remotes` field should be specified after the `version` field of the keystore",
|
|
|
|
"RemoteKeystore")
|
|
|
|
if version.get < 2:
|
|
|
|
reader.raiseUnexpectedField(
|
|
|
|
"The `remotes` field is valid only past version 2 of the remote keystore format",
|
|
|
|
"RemoteKeystore")
|
2022-05-10 00:32:12 +00:00
|
|
|
remotes = some(reader.readValue(seq[RemoteSignerInfo]))
|
2021-11-30 01:20:21 +00:00
|
|
|
of "version":
|
2023-05-09 08:16:43 +00:00
|
|
|
if version.isSome:
|
2022-02-07 20:36:09 +00:00
|
|
|
reader.raiseUnexpectedField("Multiple `version` fields found",
|
|
|
|
"RemoteKeystore")
|
|
|
|
version = some(reader.readValue(uint64))
|
2023-05-09 08:16:43 +00:00
|
|
|
if version.get > 3'u64:
|
2022-05-10 00:32:12 +00:00
|
|
|
reader.raiseUnexpectedValue(
|
|
|
|
"Remote keystore version " & $version.get &
|
|
|
|
" requires a more recent version of Nimbus")
|
2021-11-30 01:20:21 +00:00
|
|
|
of "description":
|
2023-05-09 08:16:43 +00:00
|
|
|
if description.isSome:
|
|
|
|
reader.raiseUnexpectedField("Multiple `description` fields found",
|
|
|
|
"RemoteKeystore")
|
|
|
|
description = some(reader.readValue(string))
|
2021-11-30 01:20:21 +00:00
|
|
|
of "ignore_ssl_verification":
|
2023-05-09 08:16:43 +00:00
|
|
|
if ignoreSslVerification.isSome:
|
2022-02-07 20:36:09 +00:00
|
|
|
reader.raiseUnexpectedField("Multiple conflicting options found",
|
|
|
|
"RemoteKeystore")
|
|
|
|
ignoreSslVerification = some(reader.readValue(bool))
|
2021-11-30 01:20:21 +00:00
|
|
|
of "type":
|
2023-05-09 08:16:43 +00:00
|
|
|
if remoteType.isSome:
|
2022-02-07 20:36:09 +00:00
|
|
|
reader.raiseUnexpectedField("Multiple `type` fields found",
|
2022-05-10 00:32:12 +00:00
|
|
|
"RemoteKeystore")
|
2023-05-09 08:16:43 +00:00
|
|
|
let remoteTypeValue = case reader.readValue(string).toLowerAscii()
|
|
|
|
of "web3signer":
|
|
|
|
RemoteSignerType.Web3Signer
|
|
|
|
of "verifying-web3signer":
|
|
|
|
RemoteSignerType.VerifyingWeb3Signer
|
|
|
|
else:
|
|
|
|
reader.raiseUnexpectedValue("Unsupported remote signer `type` value")
|
|
|
|
remoteType = some remoteTypeValue
|
|
|
|
of "proven_block_properties":
|
|
|
|
if provenBlockProperties.isSome:
|
|
|
|
reader.raiseUnexpectedField("Multiple `proven_block_properties` fields found",
|
|
|
|
"RemoteKeystore")
|
|
|
|
if version.isNone:
|
|
|
|
reader.raiseUnexpectedField(
|
|
|
|
"The `proven_block_properties` field should be specified after the `version` field of the keystore",
|
|
|
|
"RemoteKeystore")
|
|
|
|
if version.get < 3:
|
|
|
|
reader.raiseUnexpectedField(
|
|
|
|
"The `proven_block_properties` field is valid only past version 3 of the remote keystore format",
|
|
|
|
"RemoteKeystore")
|
|
|
|
if remoteType.isNone:
|
|
|
|
reader.raiseUnexpectedField(
|
|
|
|
"The `proven_block_properties` field should be specified after the `type` field of the keystore",
|
|
|
|
"RemoteKeystore")
|
|
|
|
if remoteType.get != RemoteSignerType.VerifyingWeb3Signer:
|
|
|
|
reader.raiseUnexpectedField(
|
|
|
|
"The `proven_block_properties` field can be specified only when the remote signer type is 'verifying-web3signer'",
|
|
|
|
"RemoteKeystore")
|
|
|
|
var provenProperties = reader.readValue(seq[ProvenProperty])
|
|
|
|
for prop in provenProperties.mitems:
|
|
|
|
if prop.path == ".execution_payload.fee_recipient":
|
|
|
|
prop.bellatrixIndex = some GeneralizedIndex(401)
|
|
|
|
prop.capellaIndex = some GeneralizedIndex(401)
|
2023-06-03 21:55:08 +00:00
|
|
|
prop.denebIndex = some GeneralizedIndex(801)
|
2023-05-09 08:16:43 +00:00
|
|
|
elif prop.path == ".graffiti":
|
2023-10-13 12:42:00 +00:00
|
|
|
# TODO: graffiti is present since genesis, so the correct index in the early
|
|
|
|
# forks can be supplied here
|
2023-05-09 08:16:43 +00:00
|
|
|
prop.bellatrixIndex = some GeneralizedIndex(18)
|
|
|
|
prop.capellaIndex = some GeneralizedIndex(18)
|
|
|
|
prop.denebIndex = some GeneralizedIndex(18)
|
|
|
|
else:
|
|
|
|
reader.raiseUnexpectedValue("Keystores with proven properties different than " &
|
|
|
|
"`.execution_payload.fee_recipient` and `.graffiti` " &
|
|
|
|
"require a more recent version of Nimbus")
|
|
|
|
provenBlockProperties = some provenProperties
|
2022-05-10 00:32:12 +00:00
|
|
|
of "threshold":
|
2023-05-09 08:16:43 +00:00
|
|
|
if threshold.isSome:
|
2022-05-10 00:32:12 +00:00
|
|
|
reader.raiseUnexpectedField("Multiple `threshold` fields found",
|
|
|
|
"RemoteKeystore")
|
2023-05-09 08:16:43 +00:00
|
|
|
if version.isNone:
|
|
|
|
reader.raiseUnexpectedField(
|
|
|
|
"The `threshold` field should be specified after the `version` field of the keystore",
|
|
|
|
"RemoteKeystore")
|
|
|
|
if version.get < 2:
|
|
|
|
reader.raiseUnexpectedField(
|
|
|
|
"The `threshold` field is valid only past version 2 of the remote keystore format",
|
|
|
|
"RemoteKeystore")
|
2022-05-10 00:32:12 +00:00
|
|
|
threshold = some(reader.readValue(uint32))
|
2021-11-30 01:20:21 +00:00
|
|
|
else:
|
|
|
|
# Ignore unknown field names.
|
|
|
|
discard
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
if version.isNone():
|
2023-05-09 08:16:43 +00:00
|
|
|
reader.raiseUnexpectedValue("The required field `version` is missing")
|
2022-05-10 00:32:12 +00:00
|
|
|
if remotes.isNone():
|
|
|
|
if remote.isSome and pubkey.isSome:
|
|
|
|
remotes = some @[RemoteSignerInfo(
|
|
|
|
pubkey: pubkey.get,
|
|
|
|
id: 0,
|
|
|
|
url: remote.get
|
|
|
|
)]
|
|
|
|
else:
|
2023-05-09 08:16:43 +00:00
|
|
|
reader.raiseUnexpectedValue("The required field `remotes` is missing")
|
|
|
|
|
|
|
|
if threshold.isNone:
|
|
|
|
if remotes.get.len > 1:
|
|
|
|
reader.raiseUnexpectedValue("The `threshold` field must be specified when using distributed keystores")
|
|
|
|
else:
|
|
|
|
if threshold.get.uint64 > remotes.get.lenu64:
|
|
|
|
reader.raiseUnexpectedValue("The specified `threshold` must be lower than the number of remote signers")
|
|
|
|
|
2021-11-30 01:20:21 +00:00
|
|
|
if pubkey.isNone():
|
2022-02-07 20:36:09 +00:00
|
|
|
reader.raiseUnexpectedValue("Field `pubkey` is missing")
|
|
|
|
|
2023-05-09 08:16:43 +00:00
|
|
|
if version.get >= 3:
|
|
|
|
if remoteType.isNone:
|
|
|
|
reader.raiseUnexpectedValue("The required field `type` is missing")
|
|
|
|
case remoteType.get
|
|
|
|
of RemoteSignerType.Web3Signer:
|
|
|
|
discard
|
|
|
|
of RemoteSignerType.VerifyingWeb3Signer:
|
|
|
|
if provenBlockProperties.isNone:
|
|
|
|
reader.raiseUnexpectedValue("The required field `proven_block_properties` is missing")
|
2022-02-07 20:36:09 +00:00
|
|
|
|
2023-05-09 08:16:43 +00:00
|
|
|
value = case remoteType.get(RemoteSignerType.Web3Signer)
|
|
|
|
of RemoteSignerType.Web3Signer:
|
|
|
|
RemoteKeystore(
|
|
|
|
version: 2'u64,
|
|
|
|
pubkey: pubkey.get,
|
|
|
|
description: description,
|
|
|
|
remoteType: RemoteSignerType.Web3Signer,
|
|
|
|
remotes: remotes.get,
|
|
|
|
threshold: threshold.get(1))
|
|
|
|
of RemoteSignerType.VerifyingWeb3Signer:
|
|
|
|
RemoteKeystore(
|
|
|
|
version: 2'u64,
|
|
|
|
pubkey: pubkey.get,
|
|
|
|
description: description,
|
|
|
|
remoteType: RemoteSignerType.VerifyingWeb3Signer,
|
|
|
|
provenBlockProperties: provenBlockProperties.get,
|
|
|
|
remotes: remotes.get,
|
|
|
|
threshold: threshold.get(1))
|
2021-11-30 01:20:21 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
template bytes(value: Pbkdf2Salt|SimpleHexEncodedTypes|Aes128CtrIv): seq[byte] =
|
|
|
|
distinctBase value
|
|
|
|
|
2020-08-02 18:46:12 +00:00
|
|
|
func scrypt(password: openArray[char], salt: openArray[byte],
|
2021-12-22 12:37:31 +00:00
|
|
|
N, r, p: int; keyLen: static[int]): array[keyLen, byte] =
|
2020-08-02 18:46:12 +00:00
|
|
|
let (xyvLen, bLen) = scryptCalc(N, r, p)
|
|
|
|
var xyv = newSeq[uint32](xyvLen)
|
|
|
|
var b = newSeq[byte](bLen)
|
|
|
|
discard scrypt(password, salt, N, r, p, xyv, b, result)
|
|
|
|
|
2020-10-09 16:41:53 +00:00
|
|
|
func areValid(params: Pbkdf2Params): bool =
|
2022-04-08 16:22:49 +00:00
|
|
|
if params.c == 0 or params.dklen < 32 or params.salt.bytes.len == 0:
|
2020-10-09 16:41:53 +00:00
|
|
|
return false
|
|
|
|
|
2021-12-15 18:55:11 +00:00
|
|
|
# https://www.ietf.org/rfc/rfc2898.txt
|
2020-10-09 16:41:53 +00:00
|
|
|
let hLen = case params.prf
|
|
|
|
of HmacSha256: 256 / 8
|
|
|
|
params.dklen <= high(uint32).uint64 * hLen.uint64
|
|
|
|
|
|
|
|
func areValid(params: ScryptParams): bool =
|
2021-12-15 18:55:11 +00:00
|
|
|
static: doAssert scryptParams.dklen >= 32
|
|
|
|
|
2020-10-09 16:41:53 +00:00
|
|
|
params.dklen == scryptParams.dklen and
|
|
|
|
params.n == scryptParams.n and
|
|
|
|
params.r == scryptParams.r and
|
|
|
|
params.p == scryptParams.p and
|
|
|
|
params.salt.bytes.len > 0
|
|
|
|
|
2023-02-16 17:25:48 +00:00
|
|
|
proc decryptCryptoField*(crypto: Crypto, decKey: openArray[byte],
|
2020-10-09 16:41:53 +00:00
|
|
|
outSecret: var seq[byte]): DecryptionStatus =
|
|
|
|
if crypto.cipher.message.bytes.len == 0:
|
2023-02-16 17:25:48 +00:00
|
|
|
return DecryptionStatus.InvalidKeystore
|
|
|
|
if len(decKey) < keyLen:
|
|
|
|
return DecryptionStatus.InvalidKeystore
|
2023-06-15 12:53:42 +00:00
|
|
|
let valid =
|
|
|
|
case crypto.checksum.function
|
|
|
|
of sha256Checksum:
|
|
|
|
template params: auto {.used.} = crypto.checksum.params
|
|
|
|
template message: auto = crypto.checksum.message
|
|
|
|
message == shaChecksum(decKey.toOpenArray(16, 31),
|
|
|
|
crypto.cipher.message.bytes)
|
|
|
|
if not valid:
|
2023-02-16 17:25:48 +00:00
|
|
|
return DecryptionStatus.InvalidPassword
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2023-06-15 12:53:42 +00:00
|
|
|
case crypto.cipher.function
|
|
|
|
of aes128CtrCipher:
|
|
|
|
template params: auto = crypto.cipher.params
|
|
|
|
var aesCipher: CTR[aes128]
|
|
|
|
outSecret.setLen(crypto.cipher.message.bytes.len)
|
|
|
|
aesCipher.init(decKey.toOpenArray(0, 15), params.iv.bytes)
|
|
|
|
aesCipher.decrypt(crypto.cipher.message.bytes, outSecret)
|
|
|
|
aesCipher.clear()
|
2023-02-16 17:25:48 +00:00
|
|
|
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
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2023-02-16 17:25:48 +00:00
|
|
|
let res = getDecryptionKey(crypto, password, decKey)
|
|
|
|
if res != DecryptionStatus.Success:
|
|
|
|
return res
|
|
|
|
|
|
|
|
decryptCryptoField(crypto, decKey, outSecret)
|
2020-06-23 19:11:07 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
func cstringToStr(v: cstring): string = $v
|
|
|
|
|
2022-08-19 10:30:07 +00:00
|
|
|
template parseKeystore*(jsonContent: string): Keystore =
|
|
|
|
Json.decode(jsonContent, Keystore,
|
|
|
|
requireAllFields = true,
|
|
|
|
allowUnknownFields = true)
|
|
|
|
|
|
|
|
template parseNetKeystore*(jsonContent: string): NetKeystore =
|
|
|
|
Json.decode(jsonContent, NetKeystore,
|
|
|
|
requireAllFields = true,
|
|
|
|
allowUnknownFields = true)
|
|
|
|
|
|
|
|
template parseRemoteKeystore*(jsonContent: string): RemoteKeystore =
|
|
|
|
Json.decode(jsonContent, RemoteKeystore,
|
|
|
|
requireAllFields = false,
|
|
|
|
allowUnknownFields = true)
|
|
|
|
|
2023-02-16 17:25:48 +00:00
|
|
|
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.}
|
|
|
|
|
2023-06-15 12:53:42 +00:00
|
|
|
{.push warning[ProveField]:off.}
|
2023-02-16 17:25:48 +00:00
|
|
|
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))
|
2023-06-15 12:53:42 +00:00
|
|
|
{.pop.}
|
2023-02-16 17:25:48 +00:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
proc decryptKeystore*(keystore: Keystore,
|
2023-02-16 17:25:48 +00:00
|
|
|
password: KeystorePass,
|
|
|
|
cache: KeystoreCacheRef): KsResult[ValidatorPrivKey] =
|
2020-10-09 16:41:53 +00:00
|
|
|
var secret: seq[byte]
|
|
|
|
defer: burnMem(secret)
|
2023-02-16 17:25:48 +00:00
|
|
|
|
|
|
|
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)
|
2020-06-23 19:11:07 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
proc decryptKeystore*(keystore: JsonString,
|
2023-02-16 17:25:48 +00:00
|
|
|
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,
|
2020-08-02 17:26:57 +00:00
|
|
|
password: KeystorePass): KsResult[ValidatorPrivKey] =
|
2023-02-16 17:25:48 +00:00
|
|
|
decryptKeystore(keystore, password, nil)
|
|
|
|
|
|
|
|
proc decryptKeystore*(keystore: JsonString,
|
|
|
|
password: KeystorePass): KsResult[ValidatorPrivKey] =
|
|
|
|
decryptKeystore(keystore, password, nil)
|
2020-06-23 19:11:07 +00:00
|
|
|
|
2023-08-19 15:11:56 +00:00
|
|
|
proc writeValue*(
|
|
|
|
writer: var JsonWriter, value: lcrypto.PublicKey
|
|
|
|
) {.inline, raises: [IOError].} =
|
2020-08-20 13:01:08 +00:00
|
|
|
writer.writeValue(ncrutils.toHex(value.getBytes().get(),
|
|
|
|
{HexFlags.LowerCase}))
|
|
|
|
|
|
|
|
proc readValue*(reader: var JsonReader, value: var lcrypto.PublicKey) {.
|
2023-08-25 09:29:07 +00:00
|
|
|
raises: [SerializationError, IOError].} =
|
2020-08-20 13:01:08 +00:00
|
|
|
let res = init(lcrypto.PublicKey, reader.readValue(string))
|
|
|
|
if res.isOk():
|
|
|
|
value = res.get()
|
|
|
|
else:
|
|
|
|
# TODO: Can we provide better diagnostic?
|
|
|
|
raiseUnexpectedValue(reader, "Valid hex-encoded public key expected")
|
|
|
|
|
|
|
|
proc decryptNetKeystore*(nkeystore: NetKeystore,
|
|
|
|
password: KeystorePass): KsResult[lcrypto.PrivateKey] =
|
2020-10-09 16:41:53 +00:00
|
|
|
var secret: seq[byte]
|
|
|
|
defer: burnMem(secret)
|
|
|
|
let status = decryptCryptoField(nkeystore.crypto, password, secret)
|
|
|
|
case status
|
|
|
|
of Success:
|
|
|
|
let res = lcrypto.PrivateKey.init(secret)
|
|
|
|
if res.isOk:
|
|
|
|
ok res.get
|
2020-08-20 13:01:08 +00:00
|
|
|
else:
|
2020-10-09 16:41:53 +00:00
|
|
|
err "Invalid key"
|
2020-08-20 13:01:08 +00:00
|
|
|
else:
|
2020-10-09 16:41:53 +00:00
|
|
|
err $status
|
2020-08-20 13:01:08 +00:00
|
|
|
|
|
|
|
proc decryptNetKeystore*(nkeystore: JsonString,
|
|
|
|
password: KeystorePass): KsResult[lcrypto.PrivateKey] =
|
|
|
|
try:
|
2022-08-19 10:30:07 +00:00
|
|
|
let keystore = parseNetKeystore(string nkeystore)
|
2020-08-20 13:01:08 +00:00
|
|
|
return decryptNetKeystore(keystore, password)
|
|
|
|
except SerializationError as exc:
|
|
|
|
return err(exc.formatMsg("<keystore>"))
|
|
|
|
|
2023-02-16 17:25:48 +00:00
|
|
|
proc generateKeystoreSalt*(rng: var HmacDrbgContext): seq[byte] =
|
|
|
|
rng.generateBytes(keyLen)
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
proc createCryptoField(kdfKind: KdfKind,
|
2022-06-21 08:29:16 +00:00
|
|
|
rng: var HmacDrbgContext,
|
2020-10-28 18:35:31 +00:00
|
|
|
secret: openArray[byte],
|
2020-10-02 15:46:05 +00:00
|
|
|
password = KeystorePass.init "",
|
2020-10-28 18:35:31 +00:00
|
|
|
salt: openArray[byte] = @[],
|
2021-12-22 12:37:31 +00:00
|
|
|
iv: openArray[byte] = @[],
|
|
|
|
mode = Secure): Crypto =
|
2020-06-23 19:11:07 +00:00
|
|
|
type AES = aes128
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 18:46:12 +00:00
|
|
|
let kdfSalt =
|
2020-08-02 17:26:57 +00:00
|
|
|
if salt.len > 0:
|
|
|
|
doAssert salt.len == keyLen
|
|
|
|
@salt
|
|
|
|
else:
|
2022-06-21 08:29:16 +00:00
|
|
|
rng.generateBytes(keyLen)
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-06-23 19:11:07 +00:00
|
|
|
let aesIv = if iv.len > 0:
|
|
|
|
doAssert iv.len == AES.sizeBlock
|
|
|
|
@iv
|
2020-06-01 19:48:20 +00:00
|
|
|
else:
|
2022-06-21 08:29:16 +00:00
|
|
|
rng.generateBytes(AES.sizeBlock)
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 18:46:12 +00:00
|
|
|
var decKey: seq[byte]
|
2020-08-02 17:26:57 +00:00
|
|
|
let kdf = case kdfKind
|
|
|
|
of kdfPbkdf2:
|
|
|
|
var params = pbkdf2Params
|
2020-08-02 18:46:12 +00:00
|
|
|
params.salt = Pbkdf2Salt kdfSalt
|
2021-12-22 12:37:31 +00:00
|
|
|
if mode == Fast: params.c = 1
|
|
|
|
decKey = sha256.pbkdf2(password.str,
|
|
|
|
kdfSalt,
|
|
|
|
int params.c,
|
|
|
|
int params.dklen)
|
2020-08-02 17:26:57 +00:00
|
|
|
Kdf(function: kdfPbkdf2, pbkdf2Params: params, message: "")
|
|
|
|
of kdfScrypt:
|
2020-08-02 18:46:12 +00:00
|
|
|
var params = scryptParams
|
|
|
|
params.salt = ScryptSalt kdfSalt
|
2021-12-22 12:37:31 +00:00
|
|
|
if mode == Fast: params.n = 1
|
|
|
|
decKey = @(scrypt(password.str, kdfSalt,
|
|
|
|
params.n, params.r, params.p, keyLen))
|
2020-08-02 18:46:12 +00:00
|
|
|
Kdf(function: kdfScrypt, scryptParams: params, message: "")
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
var
|
|
|
|
aesCipher: CTR[AES]
|
|
|
|
cipherMsg = newSeq[byte](secret.len)
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-05-20 07:22:21 +00:00
|
|
|
aesCipher.init(decKey.toOpenArray(0, 15), aesIv)
|
2020-05-19 17:30:28 +00:00
|
|
|
aesCipher.encrypt(secret, cipherMsg)
|
|
|
|
aesCipher.clear()
|
|
|
|
|
2020-06-23 19:11:07 +00:00
|
|
|
let sum = shaChecksum(decKey.toOpenArray(16, 31), cipherMsg)
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
Crypto(
|
2020-06-23 19:11:07 +00:00
|
|
|
kdf: kdf,
|
|
|
|
checksum: Checksum(
|
2020-08-02 17:26:57 +00:00
|
|
|
function: sha256Checksum,
|
|
|
|
message: sum),
|
2020-06-23 19:11:07 +00:00
|
|
|
cipher: Cipher(
|
2020-08-02 17:26:57 +00:00
|
|
|
function: aes128CtrCipher,
|
|
|
|
params: Aes128CtrParams(iv: Aes128CtrIv aesIv),
|
|
|
|
message: CipherBytes cipherMsg))
|
|
|
|
|
2020-08-20 13:01:08 +00:00
|
|
|
proc createNetKeystore*(kdfKind: KdfKind,
|
2022-06-21 08:29:16 +00:00
|
|
|
rng: var HmacDrbgContext,
|
2020-08-20 13:01:08 +00:00
|
|
|
privKey: lcrypto.PrivateKey,
|
2020-10-02 15:46:05 +00:00
|
|
|
password = KeystorePass.init "",
|
2020-08-20 13:01:08 +00:00
|
|
|
description = "",
|
2020-10-28 18:35:31 +00:00
|
|
|
salt: openArray[byte] = @[],
|
|
|
|
iv: openArray[byte] = @[]): NetKeystore =
|
2020-08-20 13:01:08 +00:00
|
|
|
let
|
|
|
|
secret = privKey.getBytes().get()
|
|
|
|
cryptoField = createCryptoField(kdfKind, rng, secret, password, salt, iv)
|
2021-12-22 12:37:31 +00:00
|
|
|
pubkey = privKey.getPublicKey().get()
|
2020-08-20 13:01:08 +00:00
|
|
|
uuid = uuidGenerate().expect("Random bytes should be available")
|
|
|
|
|
|
|
|
NetKeystore(
|
|
|
|
crypto: cryptoField,
|
2021-12-22 12:37:31 +00:00
|
|
|
pubkey: pubkey,
|
2022-08-19 10:30:07 +00:00
|
|
|
description: if len(description) > 0: some(description)
|
|
|
|
else: none[string](),
|
2020-08-20 13:01:08 +00:00
|
|
|
uuid: $uuid,
|
2020-09-30 11:04:54 +00:00
|
|
|
version: 1
|
2020-08-20 13:01:08 +00:00
|
|
|
)
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
proc createKeystore*(kdfKind: KdfKind,
|
2022-06-21 08:29:16 +00:00
|
|
|
rng: var HmacDrbgContext,
|
2022-04-08 16:22:49 +00:00
|
|
|
privKey: ValidatorPrivKey,
|
2020-10-02 15:46:05 +00:00
|
|
|
password = KeystorePass.init "",
|
2020-08-02 17:26:57 +00:00
|
|
|
path = KeyPath "",
|
2020-08-02 17:54:48 +00:00
|
|
|
description = "",
|
2020-10-28 18:35:31 +00:00
|
|
|
salt: openArray[byte] = @[],
|
2021-12-22 12:37:31 +00:00
|
|
|
iv: openArray[byte] = @[],
|
|
|
|
mode = Secure): Keystore =
|
2020-06-23 19:11:07 +00:00
|
|
|
let
|
|
|
|
secret = privKey.toRaw[^32..^1]
|
2021-12-22 12:37:31 +00:00
|
|
|
cryptoField = createCryptoField(kdfKind, rng, secret, password, salt, iv, mode)
|
2020-06-23 19:11:07 +00:00
|
|
|
pubkey = privKey.toPubKey()
|
|
|
|
uuid = uuidGenerate().expect("Random bytes should be available")
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
Keystore(
|
|
|
|
crypto: cryptoField,
|
2021-06-01 11:13:40 +00:00
|
|
|
pubkey: pubkey.toPubKey(),
|
2020-08-02 17:26:57 +00:00
|
|
|
path: path,
|
2022-08-19 10:30:07 +00:00
|
|
|
description: if len(description) > 0: some(description)
|
|
|
|
else: none[string](),
|
2020-08-02 17:26:57 +00:00
|
|
|
uuid: $uuid,
|
|
|
|
version: 4)
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
proc createRemoteKeystore*(pubKey: ValidatorPubKey, remoteUri: HttpHostUri,
|
|
|
|
version = 1'u64, description = "",
|
|
|
|
remoteType = RemoteSignerType.Web3Signer,
|
|
|
|
flags: set[RemoteKeystoreFlag] = {}): RemoteKeystore =
|
2022-05-10 00:32:12 +00:00
|
|
|
let signerInfo = RemoteSignerInfo(
|
|
|
|
url: remoteUri,
|
|
|
|
pubkey: pubKey,
|
|
|
|
id: 0
|
|
|
|
)
|
2022-02-07 20:36:09 +00:00
|
|
|
RemoteKeystore(
|
|
|
|
version: version,
|
|
|
|
description: if len(description) > 0: some(description)
|
|
|
|
else: none[string](),
|
|
|
|
remoteType: remoteType,
|
|
|
|
pubkey: pubKey,
|
2022-05-10 00:32:12 +00:00
|
|
|
remotes: @[signerInfo],
|
2022-02-07 20:36:09 +00:00
|
|
|
flags: flags
|
|
|
|
)
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
proc createWallet*(kdfKind: KdfKind,
|
2022-06-21 08:29:16 +00:00
|
|
|
rng: var HmacDrbgContext,
|
2020-10-19 19:02:48 +00:00
|
|
|
seed: KeySeed,
|
2020-06-23 19:11:07 +00:00
|
|
|
name = WalletName "",
|
2020-10-28 18:35:31 +00:00
|
|
|
salt: openArray[byte] = @[],
|
|
|
|
iv: openArray[byte] = @[],
|
2020-10-02 15:46:05 +00:00
|
|
|
password = KeystorePass.init "",
|
2020-06-23 19:11:07 +00:00
|
|
|
nextAccount = none(Natural),
|
|
|
|
pretty = true): Wallet =
|
|
|
|
let
|
|
|
|
uuid = UUID $(uuidGenerate().expect("Random bytes should be available"))
|
2020-08-02 17:26:57 +00:00
|
|
|
crypto = createCryptoField(kdfKind, rng, distinctBase seed,
|
|
|
|
password, salt, iv)
|
2020-06-23 19:11:07 +00:00
|
|
|
Wallet(
|
|
|
|
uuid: uuid,
|
|
|
|
name: if name.string.len > 0: name
|
|
|
|
else: WalletName(uuid),
|
|
|
|
version: 1,
|
|
|
|
walletType: "hierarchical deterministic",
|
2020-08-02 17:26:57 +00:00
|
|
|
crypto: crypto,
|
2020-06-23 19:11:07 +00:00
|
|
|
nextAccount: nextAccount.get(0))
|
|
|
|
|
2024-01-20 11:19:47 +00:00
|
|
|
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.6/specs/phase0/validator.md#bls_withdrawal_prefix
|
2021-06-01 11:13:40 +00:00
|
|
|
func makeWithdrawalCredentials*(k: ValidatorPubKey): Eth2Digest =
|
2020-06-16 12:16:43 +00:00
|
|
|
var bytes = eth2digest(k.toRaw())
|
2020-06-01 19:48:20 +00:00
|
|
|
bytes.data[0] = BLS_WITHDRAWAL_PREFIX.uint8
|
|
|
|
bytes
|
|
|
|
|
2024-01-20 11:19:47 +00:00
|
|
|
# https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.6/specs/phase0/deposit-contract.md#withdrawal-credentials
|
2021-06-01 11:13:40 +00:00
|
|
|
proc makeWithdrawalCredentials*(k: CookedPubKey): Eth2Digest =
|
|
|
|
makeWithdrawalCredentials(k.toPubKey())
|
|
|
|
|
Implement split preset/config support (#2710)
* Implement split preset/config support
This is the initial bulk refactor to introduce runtime config values in
a number of places, somewhat replacing the existing mechanism of loading
network metadata.
It still needs more work, this is the initial refactor that introduces
runtime configuration in some of the places that need it.
The PR changes the way presets and constants work, to match the spec. In
particular, a "preset" now refers to the compile-time configuration
while a "cfg" or "RuntimeConfig" is the dynamic part.
A single binary can support either mainnet or minimal, but not both.
Support for other presets has been removed completely (can be readded,
in case there's need).
There's a number of outstanding tasks:
* `SECONDS_PER_SLOT` still needs fixing
* loading custom runtime configs needs redoing
* checking constants against YAML file
* yeerongpilly support
`build/nimbus_beacon_node --network=yeerongpilly --discv5:no --log-level=DEBUG`
* load fork epoch from config
* fix fork digest sent in status
* nicer error string for request failures
* fix tools
* one more
* fixup
* fixup
* fixup
* use "standard" network definition folder in local testnet
Files are loaded from their standard locations, including genesis etc,
to conform to the format used in the `eth2-networks` repo.
* fix launch scripts, allow unknown config values
* fix base config of rest test
* cleanups
* bundle mainnet config using common loader
* fix spec links and names
* only include supported preset in binary
* drop yeerongpilly, add altair-devnet-0, support boot_enr.yaml
2021-07-12 13:01:38 +00:00
|
|
|
proc prepareDeposit*(cfg: RuntimeConfig,
|
2021-06-01 11:13:40 +00:00
|
|
|
withdrawalPubKey: CookedPubKey,
|
|
|
|
signingKey: ValidatorPrivKey, signingPubKey: CookedPubKey,
|
2020-07-17 20:59:50 +00:00
|
|
|
amount = MAX_EFFECTIVE_BALANCE.Gwei): DepositData =
|
|
|
|
var res = DepositData(
|
|
|
|
amount: amount,
|
2021-06-01 11:13:40 +00:00
|
|
|
pubkey: signingPubKey.toPubKey(),
|
2020-07-17 20:59:50 +00:00
|
|
|
withdrawal_credentials: makeWithdrawalCredentials(withdrawalPubKey))
|
|
|
|
|
Implement split preset/config support (#2710)
* Implement split preset/config support
This is the initial bulk refactor to introduce runtime config values in
a number of places, somewhat replacing the existing mechanism of loading
network metadata.
It still needs more work, this is the initial refactor that introduces
runtime configuration in some of the places that need it.
The PR changes the way presets and constants work, to match the spec. In
particular, a "preset" now refers to the compile-time configuration
while a "cfg" or "RuntimeConfig" is the dynamic part.
A single binary can support either mainnet or minimal, but not both.
Support for other presets has been removed completely (can be readded,
in case there's need).
There's a number of outstanding tasks:
* `SECONDS_PER_SLOT` still needs fixing
* loading custom runtime configs needs redoing
* checking constants against YAML file
* yeerongpilly support
`build/nimbus_beacon_node --network=yeerongpilly --discv5:no --log-level=DEBUG`
* load fork epoch from config
* fix fork digest sent in status
* nicer error string for request failures
* fix tools
* one more
* fixup
* fixup
* fixup
* use "standard" network definition folder in local testnet
Files are loaded from their standard locations, including genesis etc,
to conform to the format used in the `eth2-networks` repo.
* fix launch scripts, allow unknown config values
* fix base config of rest test
* cleanups
* bundle mainnet config using common loader
* fix spec links and names
* only include supported preset in binary
* drop yeerongpilly, add altair-devnet-0, support boot_enr.yaml
2021-07-12 13:01:38 +00:00
|
|
|
res.signature = get_deposit_signature(cfg, res, signingKey).toValidatorSig()
|
2020-07-17 20:59:50 +00:00
|
|
|
return res
|