Refactor keystore.nim to include network key storage too.
Use constant-time hex encoding/decoding procedures. Add tests for network key storage.
This commit is contained in:
parent
c5c788a9db
commit
cec4ad2717
|
@ -11,10 +11,14 @@ import
|
|||
# Status libraries
|
||||
stew/[results, byteutils, bitseqs, bitops2], stew/shims/macros,
|
||||
bearssl, eth/keyfile/uuid, blscurve, faststreams/textio, json_serialization,
|
||||
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, utils, scrypt],
|
||||
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, scrypt],
|
||||
# Internal
|
||||
libp2p/crypto/crypto as lcrypto,
|
||||
./datatypes, ./crypto, ./digest, ./signatures
|
||||
|
||||
# We use `ncrutils` for constant-time hexadecimal encoding/decoding procedures.
|
||||
import nimcrypto/utils as ncrutils
|
||||
|
||||
export
|
||||
results, burnMem, writeValue, readValue
|
||||
|
||||
|
@ -105,6 +109,14 @@ type
|
|||
uuid*: string
|
||||
version*: int
|
||||
|
||||
NetKeystore* = object
|
||||
crypto*: Crypto
|
||||
description*: ref string
|
||||
pubkey*: lcrypto.PublicKey
|
||||
path*: KeyPath
|
||||
uuid*: string
|
||||
version*: int
|
||||
|
||||
KsResult*[T] = Result[T, string]
|
||||
|
||||
Eth2KeyKind* = enum
|
||||
|
@ -375,7 +387,7 @@ proc shaChecksum(key, cipher: openarray[byte]): Sha256Digest =
|
|||
proc writeJsonHexString(s: OutputStream, data: openarray[byte])
|
||||
{.raises: [IOError, Defect].} =
|
||||
s.write '"'
|
||||
s.writeHex data
|
||||
s.write ncrutils.toHex(data, {HexFlags.LowerCase})
|
||||
s.write '"'
|
||||
|
||||
proc readValue*(r: var JsonReader, value: var Pbkdf2Salt)
|
||||
|
@ -384,11 +396,11 @@ proc readValue*(r: var JsonReader, value: var Pbkdf2Salt)
|
|||
|
||||
if s.len == 0 or s.len mod 16 != 0:
|
||||
r.raiseUnexpectedValue(
|
||||
"The Pbkdf2Salt salf must have a non-zero length divisible by 16")
|
||||
"The Pbkdf2Salt salt must have a non-zero length divisible by 16")
|
||||
|
||||
try:
|
||||
value = Pbkdf2Salt hexToSeqByte(s)
|
||||
except ValueError:
|
||||
value = Pbkdf2Salt ncrutils.fromHex(s)
|
||||
let length = len(seq[byte](value))
|
||||
if length == 0 or (length mod 8) != 0:
|
||||
r.raiseUnexpectedValue(
|
||||
"The Pbkdf2Salt must be a valid hex string")
|
||||
|
||||
|
@ -400,17 +412,15 @@ proc readValue*(r: var JsonReader, value: var Aes128CtrIv)
|
|||
r.raiseUnexpectedValue(
|
||||
"The aes-128-ctr IV must be a string of length 32")
|
||||
|
||||
try:
|
||||
value = Aes128CtrIv hexToSeqByte(s)
|
||||
except ValueError:
|
||||
value = Aes128CtrIv ncrutils.fromHex(s)
|
||||
if len(seq[byte](value)) != 16:
|
||||
r.raiseUnexpectedValue(
|
||||
"The aes-128-ctr IV must be a valid hex string")
|
||||
|
||||
proc readValue*[T: SimpleHexEncodedTypes](r: var JsonReader, value: var T)
|
||||
{.raises: [SerializationError, IOError, Defect].} =
|
||||
try:
|
||||
value = T hexToSeqByte(r.readValue(string))
|
||||
except ValueError:
|
||||
value = T ncrutils.fromHex(r.readValue(string))
|
||||
if len(seq[byte](value)) == 0:
|
||||
r.raiseUnexpectedValue("Valid hex string expected")
|
||||
|
||||
proc readValue*(r: var JsonReader, value: var Kdf)
|
||||
|
@ -513,6 +523,40 @@ proc decryptKeystore*(keystore: JsonString,
|
|||
return err e.formatMsg("<keystore>")
|
||||
decryptKeystore(keystore, password)
|
||||
|
||||
proc writeValue*(writer: var JsonWriter, value: lcrypto.PublicKey) {.
|
||||
inline, raises: [IOError, Defect].} =
|
||||
writer.writeValue(ncrutils.toHex(value.getBytes().get(),
|
||||
{HexFlags.LowerCase}))
|
||||
|
||||
proc readValue*(reader: var JsonReader, value: var lcrypto.PublicKey) {.
|
||||
raises: [SerializationError, IOError, Defect].} =
|
||||
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] =
|
||||
let decryptedBytes = decryptCryptoField(nkeystore.crypto, password)
|
||||
if len(decryptedBytes) > 0:
|
||||
let res = init(lcrypto.PrivateKey, decryptedBytes)
|
||||
if res.isOk():
|
||||
ok(res.get())
|
||||
else:
|
||||
err("Incorrect network private key")
|
||||
else:
|
||||
err("Empty network private key")
|
||||
|
||||
proc decryptNetKeystore*(nkeystore: JsonString,
|
||||
password: KeystorePass): KsResult[lcrypto.PrivateKey] =
|
||||
try:
|
||||
let keystore = Json.decode(string(nkeystore), NetKeystore)
|
||||
return decryptNetKeystore(keystore, password)
|
||||
except SerializationError as exc:
|
||||
return err(exc.formatMsg("<keystore>"))
|
||||
|
||||
proc createCryptoField(kdfKind: KdfKind,
|
||||
rng: var BrHmacDrbgContext,
|
||||
secret: openarray[byte],
|
||||
|
@ -571,6 +615,29 @@ proc createCryptoField(kdfKind: KdfKind,
|
|||
params: Aes128CtrParams(iv: Aes128CtrIv aesIv),
|
||||
message: CipherBytes cipherMsg))
|
||||
|
||||
proc createNetKeystore*(kdfKind: KdfKind,
|
||||
rng: var BrHmacDrbgContext,
|
||||
privKey: lcrypto.PrivateKey,
|
||||
password = KeystorePass "",
|
||||
path = KeyPath "",
|
||||
description = "",
|
||||
salt: openarray[byte] = @[],
|
||||
iv: openarray[byte] = @[]): NetKeystore =
|
||||
let
|
||||
secret = privKey.getBytes().get()
|
||||
cryptoField = createCryptoField(kdfKind, rng, secret, password, salt, iv)
|
||||
pubKey = privKey.getKey().get()
|
||||
uuid = uuidGenerate().expect("Random bytes should be available")
|
||||
|
||||
NetKeystore(
|
||||
crypto: cryptoField,
|
||||
pubkey: pubKey,
|
||||
path: path,
|
||||
description: newClone(description),
|
||||
uuid: $uuid,
|
||||
version: 4
|
||||
)
|
||||
|
||||
proc createKeystore*(kdfKind: KdfKind,
|
||||
rng: var BrHmacDrbgContext,
|
||||
privKey: ValidatorPrivkey,
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
import
|
||||
json, unittest,
|
||||
stew/byteutils, blscurve, eth/keys, json_serialization,
|
||||
libp2p/crypto/crypto as lcrypto,
|
||||
nimcrypto/utils as ncrutils,
|
||||
../beacon_chain/spec/[crypto, keystore],
|
||||
./testutil
|
||||
|
||||
|
@ -89,20 +91,89 @@ const
|
|||
"version": 4
|
||||
}"""
|
||||
|
||||
pbkdf2NetVector = """{
|
||||
"crypto":{
|
||||
"kdf":{
|
||||
"function":"pbkdf2",
|
||||
"params":{
|
||||
"dklen":32,
|
||||
"c":262144,
|
||||
"prf":"hmac-sha256",
|
||||
"salt":"d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"
|
||||
},
|
||||
"message":""
|
||||
},
|
||||
"checksum":{
|
||||
"function":"sha256",
|
||||
"params":{
|
||||
|
||||
},
|
||||
"message":"3aaebceb5e81cce464d62287414befaa03522eb8f56cad4296c0dc9301e5f091"
|
||||
},
|
||||
"cipher":{
|
||||
"function":"aes-128-ctr",
|
||||
"params":{
|
||||
"iv":"264daa3f303d7259501c93d997d84fe6"
|
||||
},
|
||||
"message":"c6e22dfed4aec458af6e46efff72937972a9360a8b4dc32c8c266de73a90b421d8892db3"
|
||||
}
|
||||
},
|
||||
"description":"PBKDF2 Network private key storage",
|
||||
"pubkey":"08021221031873e6f4e1bf837b93493d570653cb219743d4fab0ff468d4e005e1679730b0b",
|
||||
"path":"m/12381/60/0/0",
|
||||
"uuid":"7a053160-1cdf-4faf-a2bb-331e1bc2eb5f",
|
||||
"version":4
|
||||
}"""
|
||||
|
||||
scryptNetVector = """{
|
||||
"crypto":{
|
||||
"kdf":{
|
||||
"function":"scrypt",
|
||||
"params":{
|
||||
"dklen":32,
|
||||
"n":262144,
|
||||
"p":1,
|
||||
"r":8,
|
||||
"salt":"d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"
|
||||
},
|
||||
"message":""
|
||||
},
|
||||
"checksum":{
|
||||
"function":"sha256",
|
||||
"params":{
|
||||
|
||||
},
|
||||
"message":"9a7d03a3f2107a11b6e34a081fb13d551012ff081efb81fc94ec114381fa707f"
|
||||
},
|
||||
"cipher":{
|
||||
"function":"aes-128-ctr",
|
||||
"params":{
|
||||
"iv":"264daa3f303d7259501c93d997d84fe6"
|
||||
},
|
||||
"message":"0eac82f5a1bd53f81df688970ffeea8425ad7b8f877bcba5a74b87f090c340836cd52095"
|
||||
}
|
||||
},
|
||||
"description":"SCRYPT Network private key storage",
|
||||
"pubkey":"08021221031873e6f4e1bf837b93493d570653cb219743d4fab0ff468d4e005e1679730b0b",
|
||||
"path":"m/12382/60/0/0",
|
||||
"uuid":"83d77fa3-86cb-466a-af11-eeb338b0e258",
|
||||
"version":4
|
||||
}"""
|
||||
password = string.fromBytes hexToSeqByte("7465737470617373776f7264f09f9491")
|
||||
secretBytes = hexToSeqByte "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
|
||||
|
||||
secretNetBytes = hexToSeqByte "08021220fe442379443d6e2d7d75d3a58f96fbb35f0a9c7217796825fc9040e3b89c5736"
|
||||
salt = hexToSeqByte "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"
|
||||
iv = hexToSeqByte "264daa3f303d7259501c93d997d84fe6"
|
||||
|
||||
let
|
||||
rng = newRng()
|
||||
rng = keys.newRng()
|
||||
|
||||
suiteReport "Keystore":
|
||||
suiteReport "KeyStorage testing suite":
|
||||
setup:
|
||||
let secret = ValidatorPrivKey.fromRaw(secretBytes).get
|
||||
let nsecret = init(lcrypto.PrivateKey, secretNetBytes).get
|
||||
|
||||
timedTest "Pbkdf2 decryption":
|
||||
timedTest "[PBKDF2] Keystore decryption":
|
||||
let
|
||||
keystore = Json.decode(pbkdf2Vector, Keystore)
|
||||
decrypt = decryptKeystore(keystore, KeystorePass password)
|
||||
|
@ -110,7 +181,7 @@ suiteReport "Keystore":
|
|||
check decrypt.isOk
|
||||
check secret.isEqual(decrypt.get())
|
||||
|
||||
timedTest "Scrypt decryption":
|
||||
timedTest "[SCRYPT] Keystore decryption":
|
||||
let
|
||||
keystore = Json.decode(scryptVector, Keystore)
|
||||
decrypt = decryptKeystore(keystore, KeystorePass password)
|
||||
|
@ -118,7 +189,23 @@ suiteReport "Keystore":
|
|||
check decrypt.isOk
|
||||
check secret.isEqual(decrypt.get())
|
||||
|
||||
timedTest "Pbkdf2 encryption":
|
||||
timedTest "[PBKDF2] Network Keystore decryption":
|
||||
let
|
||||
keystore = Json.decode(pbkdf2NetVector, NetKeystore)
|
||||
decrypt = decryptNetKeystore(keystore, KeystorePass password)
|
||||
|
||||
check decrypt.isOk
|
||||
check nsecret == decrypt.get()
|
||||
|
||||
timedTest "[SCRYPT] Network Keystore decryption":
|
||||
let
|
||||
keystore = Json.decode(scryptNetVector, NetKeystore)
|
||||
decrypt = decryptNetKeystore(keystore, KeystorePass password)
|
||||
|
||||
check decrypt.isOk
|
||||
check nsecret == decrypt.get()
|
||||
|
||||
timedTest "[PBKDF2] Keystore encryption":
|
||||
let keystore = createKeystore(kdfPbkdf2, rng[], secret,
|
||||
KeystorePass password,
|
||||
salt=salt, iv=iv,
|
||||
|
@ -132,7 +219,20 @@ suiteReport "Keystore":
|
|||
|
||||
check encryptJson == pbkdf2Json
|
||||
|
||||
timedTest "Scrypt encryption":
|
||||
timedTest "[PBKDF2] Network Keystore encryption":
|
||||
let nkeystore = createNetKeystore(kdfPbkdf2, rng[], nsecret,
|
||||
KeystorePass password,
|
||||
salt = salt, iv = iv,
|
||||
description = "PBKDF2 Network private key storage",
|
||||
path = validateKeyPath "m/12381/60/0/0")
|
||||
var
|
||||
encryptJson = parseJson Json.encode(nkeystore)
|
||||
pbkdf2Json = parseJson(pbkdf2NetVector)
|
||||
encryptJson{"uuid"} = %""
|
||||
pbkdf2Json{"uuid"} = %""
|
||||
check encryptJson == pbkdf2Json
|
||||
|
||||
timedTest "[SCRYPT] Keystore encryption":
|
||||
let keystore = createKeystore(kdfScrypt, rng[], secret,
|
||||
KeystorePass password,
|
||||
salt=salt, iv=iv,
|
||||
|
@ -146,6 +246,19 @@ suiteReport "Keystore":
|
|||
|
||||
check encryptJson == scryptJson
|
||||
|
||||
timedTest "[SCRYPT] Network Keystore encryption":
|
||||
let nkeystore = createNetKeystore(kdfScrypt, rng[], nsecret,
|
||||
KeystorePass password,
|
||||
salt = salt, iv = iv,
|
||||
description = "SCRYPT Network private key storage",
|
||||
path = validateKeyPath "m/12382/60/0/0")
|
||||
var
|
||||
encryptJson = parseJson Json.encode(nkeystore)
|
||||
pbkdf2Json = parseJson(scryptNetVector)
|
||||
encryptJson{"uuid"} = %""
|
||||
pbkdf2Json{"uuid"} = %""
|
||||
check encryptJson == pbkdf2Json
|
||||
|
||||
timedTest "Pbkdf2 errors":
|
||||
expect Defect:
|
||||
echo createKeystore(kdfPbkdf2, rng[], secret, salt = [byte 1])
|
||||
|
|
Loading…
Reference in New Issue