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:
cheatfate 2020-08-20 16:01:08 +03:00 committed by zah
parent c5c788a9db
commit cec4ad2717
2 changed files with 199 additions and 19 deletions

View File

@ -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,

View File

@ -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])