Discv5 small adjustments (#307)

* Use checkedEnumAssign in discv5.1

* Some renaming according to discv5 spec nomenclature
This commit is contained in:
Kim De Mey 2020-10-29 22:04:37 +01:00 committed by GitHub
parent 3626755529
commit 71d6dbd1b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 49 additions and 42 deletions

View File

@ -1,8 +1,10 @@
import import
std/[tables, options], std/[tables, options],
nimcrypto, stint, chronicles, stew/results, bearssl, stew/byteutils, nimcrypto, stint, chronicles, bearssl, stew/[results, byteutils],
eth/[rlp, keys], typesv1, node, enr, hkdf, sessions eth/[rlp, keys], typesv1, node, enr, hkdf, sessions
from stew/objects import checkedEnumAssign
export keys export keys
{.push raises: [Defect].} {.push raises: [Defect].}
@ -44,8 +46,8 @@ type
authdataSize: uint16 authdataSize: uint16
HandshakeSecrets* = object HandshakeSecrets* = object
writeKey*: AesKey initiatorKey*: AesKey
readKey*: AesKey recipientKey*: AesKey
Flag* = enum Flag* = enum
OrdinaryMessage = 0x00 OrdinaryMessage = 0x00
@ -74,7 +76,7 @@ type
DecodeResult*[T] = Result[T, cstring] DecodeResult*[T] = Result[T, cstring]
proc idNonceHash*(challengeData, ephkey: openarray[byte], nodeId: NodeId): proc idHash(challengeData, ephkey: openarray[byte], nodeId: NodeId):
MDigest[256] = MDigest[256] =
var ctx: sha256 var ctx: sha256
ctx.init() ctx.init()
@ -85,9 +87,14 @@ proc idNonceHash*(challengeData, ephkey: openarray[byte], nodeId: NodeId):
result = ctx.finish() result = ctx.finish()
ctx.clear() ctx.clear()
proc signIDNonce*(privKey: PrivateKey, challengeData, proc createIdSignature*(privKey: PrivateKey, challengeData,
ephKey: openarray[byte], nodeId: NodeId): SignatureNR = ephKey: openarray[byte], nodeId: NodeId): SignatureNR =
signNR(privKey, SkMessage(idNonceHash(challengeData, ephKey, nodeId).data)) signNR(privKey, SkMessage(idHash(challengeData, ephKey, nodeId).data))
proc verifyIdSignature*(sig: SignatureNR, challengeData, ephKey: openarray[byte],
nodeId: NodeId, pubKey: PublicKey): bool =
let h = idHash(challengeData, ephKey, nodeId)
verify(sig, SkMessage(h.data), pubKey)
proc deriveKeys*(n1, n2: NodeID, priv: PrivateKey, pub: PublicKey, proc deriveKeys*(n1, n2: NodeID, priv: PrivateKey, pub: PublicKey,
challengeData: openarray[byte]): HandshakeSecrets = challengeData: openarray[byte]): HandshakeSecrets =
@ -171,9 +178,9 @@ proc encodeMessagePacket*(rng: var BrHmacDrbgContext, c: var Codec,
# message # message
var messageEncrypted: seq[byte] var messageEncrypted: seq[byte]
var writeKey, readKey: AesKey var initiatorKey, recipientKey: AesKey
if c.sessions.load(toId, toAddr, readKey, writeKey): if c.sessions.load(toId, toAddr, recipientKey, initiatorKey):
messageEncrypted = encryptGCM(writeKey, nonce, message, @iv & header) messageEncrypted = encryptGCM(initiatorKey, nonce, message, @iv & header)
else: else:
# We might not have the node's keys if the handshake hasn't been performed # We might not have the node's keys if the handshake hasn't been performed
# yet. That's fine, we send a random-packet and we will be responded with # yet. That's fine, we send a random-packet and we will be responded with
@ -255,7 +262,7 @@ proc encodeHandshakePacket*(rng: var BrHmacDrbgContext, c: var Codec,
authdata.add(authdataHead) authdata.add(authdataHead)
let ephKeys = KeyPair.random(rng) let ephKeys = KeyPair.random(rng)
let signature = signIDNonce(c.privKey, whoareyouData.challengeData, let signature = createIdSignature(c.privKey, whoareyouData.challengeData,
ephKeys.pubkey.toRawCompressed(), toId) ephKeys.pubkey.toRawCompressed(), toId)
authdata.add(signature.toRaw()) authdata.add(signature.toRaw())
@ -276,8 +283,9 @@ proc encodeHandshakePacket*(rng: var BrHmacDrbgContext, c: var Codec,
header.add(staticHeader) header.add(staticHeader)
header.add(authdata) header.add(authdata)
c.sessions.store(toId, toAddr, secrets.readKey, secrets.writeKey) c.sessions.store(toId, toAddr, secrets.recipientKey, secrets.initiatorKey)
let messageEncrypted = encryptGCM(secrets.writeKey, nonce, message, @iv & header) let messageEncrypted = encryptGCM(secrets.initiatorKey, nonce, message,
@iv & header)
let maskedHeader = encryptHeader(toId, iv, header) let maskedHeader = encryptHeader(toId, iv, header)
@ -305,9 +313,9 @@ proc decodeHeader*(id: NodeId, iv, maskedHeader: openarray[byte]):
if uint16.fromBytesBE(staticHeader.toOpenArray(6, 7)) != version: if uint16.fromBytesBE(staticHeader.toOpenArray(6, 7)) != version:
return err("Invalid protocol version") return err("Invalid protocol version")
if staticHeader[8] < Flag.low.byte or staticHeader[8] > Flag.high.byte: var flag: Flag
if not checkedEnumAssign(flag, staticHeader[8]):
return err("Invalid packet flag") return err("Invalid packet flag")
let flag = cast[Flag](staticHeader[8])
var nonce: AESGCMNonce var nonce: AESGCMNonce
copyMem(addr nonce[0], unsafeAddr staticHeader[9], gcmNonceSize) copyMem(addr nonce[0], unsafeAddr staticHeader[9], gcmNonceSize)
@ -333,12 +341,10 @@ proc decodeMessage*(body: openarray[byte]): DecodeResult[Message] =
if body.len < 1: if body.len < 1:
return err("No message data") return err("No message data")
if body[0] < MessageKind.low.byte or body[0] > MessageKind.high.byte: var kind: MessageKind
if not checkedEnumAssign(kind, body[0]):
return err("Invalid message type") return err("Invalid message type")
# This cast is covered by the above check (else we could get enum with invalid
# data!). However, can't we do this in a cleaner way?
let kind = cast[MessageKind](body[0])
var message = Message(kind: kind) var message = Message(kind: kind)
var rlp = rlpFromBytes(body.toOpenArray(1, body.high)) var rlp = rlpFromBytes(body.toOpenArray(1, body.high))
if rlp.enterList: if rlp.enterList:
@ -387,15 +393,15 @@ proc decodeMessagePacket(c: var Codec, fromAddr: Address, nonce: AESGCMNonce,
let srcId = NodeId.fromBytesBE(header.toOpenArray(staticHeaderSize, let srcId = NodeId.fromBytesBE(header.toOpenArray(staticHeaderSize,
header.high)) header.high))
var writeKey, readKey: AesKey var initiatorKey, recipientKey: AesKey
if not c.sessions.load(srcId, fromAddr, readKey, writeKey): if not c.sessions.load(srcId, fromAddr, recipientKey, initiatorKey):
# Don't consider this an error, simply haven't done a handshake yet or # Don't consider this an error, simply haven't done a handshake yet or
# the session got removed. # the session got removed.
trace "Decrypting failed (no keys)" trace "Decrypting failed (no keys)"
return ok(Packet(flag: Flag.OrdinaryMessage, requestNonce: nonce, return ok(Packet(flag: Flag.OrdinaryMessage, requestNonce: nonce,
srcId: srcId)) srcId: srcId))
let pt = decryptGCM(readKey, nonce, ct, @iv & @header) let pt = decryptGCM(recipientKey, nonce, ct, @iv & @header)
if pt.isNone(): if pt.isNone():
# Don't consider this an error, the session got probably removed at the # Don't consider this an error, the session got probably removed at the
# peer's side and a random message is send. # peer's side and a random message is send.
@ -495,22 +501,21 @@ proc decodeHandshakePacket(c: var Codec, fromAddr: Address, nonce: AESGCMNonce,
# We should have received a Record in this case. # We should have received a Record in this case.
return err("Missing ENR in handshake packet") return err("Missing ENR in handshake packet")
# Verify the id-nonce-sig # Verify the id-signature
let sig = ? SignatureNR.fromRaw( let sig = ? SignatureNR.fromRaw(
authdata.toOpenArray(authdataHeadSize, authdataHeadSize + int(sigSize) - 1)) authdata.toOpenArray(authdataHeadSize, authdataHeadSize + int(sigSize) - 1))
if not verifyIdSignature(sig, challenge.whoareyouData.challengeData,
let h = idNonceHash(challenge.whoareyouData.challengeData, ephKeyRaw, ephKeyRaw, c.localNode.id, pubkey):
c.localNode.id)
if not verify(sig, SkMessage(h.data), pubkey):
return err("Invalid id-signature") return err("Invalid id-signature")
# Do the key derivation step only after id-nonce-sig is verified! # Do the key derivation step only after id-signature is verified as this is
# costly.
var secrets = deriveKeys(srcId, c.localNode.id, c.privKey, var secrets = deriveKeys(srcId, c.localNode.id, c.privKey,
ephKey, challenge.whoareyouData.challengeData) ephKey, challenge.whoareyouData.challengeData)
swap(secrets.readKey, secrets.writeKey) swap(secrets.recipientKey, secrets.initiatorKey)
let pt = decryptGCM(secrets.readKey, nonce, ct, @iv & @header) let pt = decryptGCM(secrets.recipientKey, nonce, ct, @iv & @header)
if pt.isNone(): if pt.isNone():
c.sessions.del(srcId, fromAddr) c.sessions.del(srcId, fromAddr)
# Differently from an ordinary message, this is seen as an error as the # Differently from an ordinary message, this is seen as an error as the
@ -524,7 +529,7 @@ proc decodeHandshakePacket(c: var Codec, fromAddr: Address, nonce: AESGCMNonce,
# Only store the session secrets in case decryption was successful and also # Only store the session secrets in case decryption was successful and also
# in case the message can get decoded. # in case the message can get decoded.
c.sessions.store(srcId, fromAddr, secrets.readKey, secrets.writeKey) c.sessions.store(srcId, fromAddr, secrets.recipientKey, secrets.initiatorKey)
return ok(Packet(flag: Flag.HandshakeMessage, message: message, return ok(Packet(flag: Flag.HandshakeMessage, message: message,
srcIdHs: srcId, node: newNode)) srcIdHs: srcId, node: newNode))

View File

@ -121,7 +121,7 @@ suite "Discovery v5.1 Protocol Message Encodings":
check decoded.isErr() check decoded.isErr()
# According to test vectors: # According to test vectors:
# https://github.com/fjl/devp2p/blob/discv5-v1-update/discv5/discv5-wire-test-vectors.md#cryptographic-primitives # https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire-test-vectors.md#cryptographic-primitives
suite "Discovery v5.1 Cryptographic Primitives Test Vectors": suite "Discovery v5.1 Cryptographic Primitives Test Vectors":
test "ECDH": test "ECDH":
const const
@ -158,8 +158,8 @@ suite "Discovery v5.1 Cryptographic Primitives Test Vectors":
hexToSeqByte(challengeData)) hexToSeqByte(challengeData))
check: check:
secrets.writeKey == hexToByteArray[aesKeySize](initiatorKey) secrets.initiatorKey == hexToByteArray[aesKeySize](initiatorKey)
secrets.readKey == hexToByteArray[aesKeySize](recipientKey) secrets.recipientKey == hexToByteArray[aesKeySize](recipientKey)
test "Nonce Signing": test "Nonce Signing":
const const
@ -173,16 +173,16 @@ suite "Discovery v5.1 Cryptographic Primitives Test Vectors":
let let
privKey = PrivateKey.fromHex(staticKey)[] privKey = PrivateKey.fromHex(staticKey)[]
signature = signIDNonce( signature = createIdSignature(
privKey, privKey,
hexToSeqByte(challengeData), hexToSeqByte(challengeData),
hexToSeqByte(ephemeralPubkey), hexToSeqByte(ephemeralPubkey),
NodeId.fromHex(nodeIdB)) NodeId.fromHex(nodeIdB))
check signature.toRaw() == hexToByteArray[64](idSignature) check:
signature.toRaw() == hexToByteArray[64](idSignature)
let h = idNonceHash(hexToSeqByte(challengeData), hexToSeqByte(ephemeralPubkey), verifyIdSignature(signature, hexToSeqByte(challengeData),
NodeId.fromHex(nodeIdB)) hexToSeqByte(ephemeralPubkey), NodeId.fromHex(nodeIdB),
check verify(signature, SkMessage(h.data), privKey.toPublicKey()) privKey.toPublicKey())
test "Encryption/Decryption": test "Encryption/Decryption":
const const
@ -201,7 +201,7 @@ suite "Discovery v5.1 Cryptographic Primitives Test Vectors":
check encrypted == hexToSeqByte(messageCiphertext) check encrypted == hexToSeqByte(messageCiphertext)
# According to test vectors: # According to test vectors:
# https://github.com/fjl/devp2p/blob/discv5-v1-update/discv5/discv5-wire-test-vectors.md#packet-encodings # https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire-test-vectors.md#packet-encodings
suite "Discovery v5.1 Packet Encodings Test Vectors": suite "Discovery v5.1 Packet Encodings Test Vectors":
const const
nodeAKey = "0xeef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f" nodeAKey = "0xeef77acb6c6a6eebc5b363a475ac583ec7eccdb42b6481424c60f59aa326547f"
@ -546,8 +546,10 @@ suite "Discovery v5.1 Additional Encode/Decode":
# Need to manually add the secrets that normally get negotiated in the # Need to manually add the secrets that normally get negotiated in the
# handshake packet. # handshake packet.
var secrets: HandshakeSecrets var secrets: HandshakeSecrets
codecA.sessions.store(nodeB.id, nodeB.address.get(), secrets.readKey, secrets.writeKey) codecA.sessions.store(nodeB.id, nodeB.address.get(), secrets.recipientKey,
codecB.sessions.store(nodeA.id, nodeA.address.get(), secrets.writeKey, secrets.readKey) secrets.initiatorKey)
codecB.sessions.store(nodeA.id, nodeA.address.get(), secrets.initiatorKey,
secrets.recipientKey)
let (data, nonce) = encodeMessagePacket(rng[], codecA, nodeB.id, let (data, nonce) = encodeMessagePacket(rng[], codecA, nodeB.id,
nodeB.address.get(), message) nodeB.address.get(), message)