mirror of https://github.com/status-im/nim-eth.git
discv5 encoding: First steps to move to result based error handling
This commit is contained in:
parent
28e684ce80
commit
5dec5c5a9b
|
@ -1,9 +1,11 @@
|
||||||
import
|
import
|
||||||
std/[tables, options], nimcrypto, stint, chronicles,
|
std/[tables, options], nimcrypto, stint, chronicles, stew/results,
|
||||||
types, node, enr, hkdf, ../enode, eth/[rlp, keys]
|
types, node, enr, hkdf, ../enode, eth/[rlp, keys]
|
||||||
|
|
||||||
export keys
|
export keys
|
||||||
|
|
||||||
|
{.push raises: [Defect].}
|
||||||
|
|
||||||
const
|
const
|
||||||
idNoncePrefix = "discovery-id-nonce"
|
idNoncePrefix = "discovery-id-nonce"
|
||||||
keyAgreementPrefix = "discovery v5 key agreement"
|
keyAgreementPrefix = "discovery v5 key agreement"
|
||||||
|
@ -14,7 +16,6 @@ const
|
||||||
## with
|
## with
|
||||||
|
|
||||||
type
|
type
|
||||||
|
|
||||||
PacketTag* = array[tagSize, byte]
|
PacketTag* = array[tagSize, byte]
|
||||||
|
|
||||||
AuthResponse = object
|
AuthResponse = object
|
||||||
|
@ -40,23 +41,20 @@ type
|
||||||
ephemeralKey*: array[64, byte]
|
ephemeralKey*: array[64, byte]
|
||||||
response*: seq[byte]
|
response*: seq[byte]
|
||||||
|
|
||||||
RandomSourceDepleted* = object of CatchableError
|
DecodeError* = enum
|
||||||
|
|
||||||
DecodeStatus* = enum
|
|
||||||
Success,
|
|
||||||
HandshakeError,
|
HandshakeError,
|
||||||
PacketError,
|
PacketError,
|
||||||
DecryptError
|
DecryptError,
|
||||||
|
UnsupportedPacketType
|
||||||
|
|
||||||
proc randomBytes2*(v: var openarray[byte]) =
|
DecodeResult*[T] = Result[T, DecodeError]
|
||||||
# TODO if this is called randomBytes it breaks calling the real randomBytes
|
EncodeResult*[T] = Result[T, cstring]
|
||||||
# in other modules... sigh, nim modules and global namespaces...
|
|
||||||
# ideally, a new random library will take the place of both these proc's
|
|
||||||
# in as setting without exceptions for such low-level constructs..
|
|
||||||
if randomBytes(v) != v.len:
|
|
||||||
raise newException(RandomSourceDepleted, "Could not randomize bytes")
|
|
||||||
|
|
||||||
proc idNonceHash(nonce, ephkey: openarray[byte]): MDigest[256] =
|
proc mapErrTo[T, E](r: Result[T, E], v: static DecodeError):
|
||||||
|
DecodeResult[T] {.raises:[].} =
|
||||||
|
r.mapErr(proc (e: E): DecodeError = v)
|
||||||
|
|
||||||
|
proc idNonceHash(nonce, ephkey: openarray[byte]): MDigest[256] {.raises:[].} =
|
||||||
var ctx: sha256
|
var ctx: sha256
|
||||||
ctx.init()
|
ctx.init()
|
||||||
ctx.update(idNoncePrefix)
|
ctx.update(idNoncePrefix)
|
||||||
|
@ -64,31 +62,27 @@ proc idNonceHash(nonce, ephkey: openarray[byte]): MDigest[256] =
|
||||||
ctx.update(ephkey)
|
ctx.update(ephkey)
|
||||||
ctx.finish()
|
ctx.finish()
|
||||||
|
|
||||||
proc signIDNonce*(c: Codec, idNonce, ephKey: openarray[byte]): SignatureNR =
|
proc signIDNonce*(privKey: PrivateKey, idNonce, ephKey: openarray[byte]):
|
||||||
let sig = signNR(c.privKey, idNonceHash(idNonce, ephKey))
|
Result[SignatureNR, cstring] =
|
||||||
if sig.isErr:
|
signNR(privKey, idNonceHash(idNonce, ephKey))
|
||||||
raise newException(CatchableError, "Could not sign idNonce")
|
|
||||||
sig[]
|
|
||||||
|
|
||||||
proc deriveKeys(n1, n2: NodeID, priv: PrivateKey, pub: PublicKey,
|
proc deriveKeys(n1, n2: NodeID, priv: PrivateKey, pub: PublicKey,
|
||||||
idNonce: openarray[byte], result: var HandshakeSecrets) =
|
idNonce: openarray[byte]): Result[HandshakeSecrets, cstring] =
|
||||||
let eph = ecdhRawFull(priv, pub)
|
let eph = ? ecdhRawFull(priv, pub)
|
||||||
if eph.isErr:
|
|
||||||
raise newException(CatchableError, "ecdhRawFull failed")
|
|
||||||
|
|
||||||
# TODO: Unneeded allocation here
|
|
||||||
var info = newSeqOfCap[byte](idNoncePrefix.len + 32 * 2)
|
var info = newSeqOfCap[byte](idNoncePrefix.len + 32 * 2)
|
||||||
for i, c in keyAgreementPrefix: info.add(byte(c))
|
for i, c in keyAgreementPrefix: info.add(byte(c))
|
||||||
info.add(n1.toByteArrayBE())
|
info.add(n1.toByteArrayBE())
|
||||||
info.add(n2.toByteArrayBE())
|
info.add(n2.toByteArrayBE())
|
||||||
|
|
||||||
# echo "EPH: ", eph.data.toHex, " idNonce: ", challenge.idNonce.toHex, "info: ", info.toHex
|
var secrets: HandshakeSecrets
|
||||||
|
static: assert(sizeof(secrets) == aesKeySize * 3)
|
||||||
|
var res = cast[ptr UncheckedArray[byte]](addr secrets)
|
||||||
|
hkdf(sha256, eph.data, idNonce, info, toOpenArray(res, 0, sizeof(secrets) - 1))
|
||||||
|
ok(secrets)
|
||||||
|
|
||||||
static: assert(sizeof(result) == aesKeySize * 3)
|
proc encryptGCM*(key, nonce, pt, authData: openarray[byte]):
|
||||||
var res = cast[ptr UncheckedArray[byte]](addr result)
|
seq[byte] {.raises:[].} =
|
||||||
hkdf(sha256, eph[].data, idNonce, info, toOpenArray(res, 0, sizeof(result) - 1))
|
|
||||||
|
|
||||||
proc encryptGCM*(key, nonce, pt, authData: openarray[byte]): seq[byte] =
|
|
||||||
var ectx: GCM[aes128]
|
var ectx: GCM[aes128]
|
||||||
ectx.init(key, nonce, authData)
|
ectx.init(key, nonce, authData)
|
||||||
result = newSeq[byte](pt.len + gcmTagSize)
|
result = newSeq[byte](pt.len + gcmTagSize)
|
||||||
|
@ -96,21 +90,26 @@ proc encryptGCM*(key, nonce, pt, authData: openarray[byte]): seq[byte] =
|
||||||
ectx.getTag(result.toOpenArray(pt.len, result.high))
|
ectx.getTag(result.toOpenArray(pt.len, result.high))
|
||||||
ectx.clear()
|
ectx.clear()
|
||||||
|
|
||||||
proc makeAuthHeader(c: Codec, toId: NodeID, nonce: array[gcmNonceSize, byte],
|
proc makeAuthHeader(c: Codec,
|
||||||
|
toId: NodeID,
|
||||||
|
nonce: array[gcmNonceSize, byte],
|
||||||
handshakeSecrets: var HandshakeSecrets,
|
handshakeSecrets: var HandshakeSecrets,
|
||||||
challenge: Whoareyou): seq[byte] =
|
challenge: Whoareyou):
|
||||||
|
EncodeResult[seq[byte]] =
|
||||||
var resp = AuthResponse(version: 5)
|
var resp = AuthResponse(version: 5)
|
||||||
let ln = c.localNode
|
let ln = c.localNode
|
||||||
|
|
||||||
|
# TODO: What goes over the wire now in case of no updated ENR?
|
||||||
if challenge.recordSeq < ln.record.seqNum:
|
if challenge.recordSeq < ln.record.seqNum:
|
||||||
resp.record = ln.record
|
resp.record = ln.record
|
||||||
|
|
||||||
let ephKeys = KeyPair.random().tryGet()
|
let ephKeys = ? KeyPair.random()
|
||||||
|
let signature = ? signIDNonce(c.privKey, challenge.idNonce,
|
||||||
|
ephKeys.pubkey.toRaw)
|
||||||
|
resp.signature = signature.toRaw
|
||||||
|
|
||||||
resp.signature = c.signIDNonce(challenge.idNonce, ephKeys.pubkey.toRaw).toRaw
|
handshakeSecrets = ? deriveKeys(ln.id, toId, ephKeys.seckey, challenge.pubKey,
|
||||||
|
challenge.idNonce)
|
||||||
deriveKeys(ln.id, toId, ephKeys.seckey, challenge.pubKey, challenge.idNonce,
|
|
||||||
handshakeSecrets)
|
|
||||||
|
|
||||||
let respRlp = rlp.encode(resp)
|
let respRlp = rlp.encode(resp)
|
||||||
|
|
||||||
|
@ -118,17 +117,19 @@ proc makeAuthHeader(c: Codec, toId: NodeID, nonce: array[gcmNonceSize, byte],
|
||||||
let respEnc = encryptGCM(handshakeSecrets.authRespKey, zeroNonce, respRLP, [])
|
let respEnc = encryptGCM(handshakeSecrets.authRespKey, zeroNonce, respRLP, [])
|
||||||
|
|
||||||
let header = AuthHeader(auth: nonce, idNonce: challenge.idNonce,
|
let header = AuthHeader(auth: nonce, idNonce: challenge.idNonce,
|
||||||
scheme: authSchemeName, ephemeralKey: ephKeys.pubkey.toRaw, response: respEnc)
|
scheme: authSchemeName, ephemeralKey: ephKeys.pubkey.toRaw,
|
||||||
rlp.encode(header)
|
response: respEnc)
|
||||||
|
ok(rlp.encode(header))
|
||||||
|
|
||||||
proc `xor`[N: static[int], T](a, b: array[N, T]): array[N, T] =
|
proc `xor`[N: static[int], T](a, b: array[N, T]): array[N, T] {.raises:[].} =
|
||||||
for i in 0 .. a.high:
|
for i in 0 .. a.high:
|
||||||
result[i] = a[i] xor b[i]
|
result[i] = a[i] xor b[i]
|
||||||
|
|
||||||
proc packetTag(destNode, srcNode: NodeID): PacketTag =
|
proc packetTag(destNode, srcNode: NodeID): PacketTag {.raises:[].} =
|
||||||
let destId = destNode.toByteArrayBE()
|
let
|
||||||
let srcId = srcNode.toByteArrayBE()
|
destId = destNode.toByteArrayBE()
|
||||||
let destidHash = sha256.digest(destId)
|
srcId = srcNode.toByteArrayBE()
|
||||||
|
destidHash = sha256.digest(destId)
|
||||||
result = srcId xor destidHash.data
|
result = srcId xor destidHash.data
|
||||||
|
|
||||||
proc encodeEncrypted*(c: Codec,
|
proc encodeEncrypted*(c: Codec,
|
||||||
|
@ -136,9 +137,11 @@ proc encodeEncrypted*(c: Codec,
|
||||||
toAddr: Address,
|
toAddr: Address,
|
||||||
packetData: seq[byte],
|
packetData: seq[byte],
|
||||||
challenge: Whoareyou):
|
challenge: Whoareyou):
|
||||||
(seq[byte], array[gcmNonceSize, byte]) =
|
EncodeResult[(seq[byte], array[gcmNonceSize, byte])] =
|
||||||
var nonce: array[gcmNonceSize, byte]
|
var nonce: array[gcmNonceSize, byte]
|
||||||
randomBytes2(nonce)
|
if randomBytes(nonce) != nonce.len:
|
||||||
|
return err("Could not randomize bytes")
|
||||||
|
|
||||||
var headEnc: seq[byte]
|
var headEnc: seq[byte]
|
||||||
|
|
||||||
var writeKey: AesKey
|
var writeKey: AesKey
|
||||||
|
@ -152,7 +155,7 @@ proc encodeEncrypted*(c: Codec,
|
||||||
discard c.db.loadKeys(toId, toAddr, readKey, writeKey)
|
discard c.db.loadKeys(toId, toAddr, readKey, writeKey)
|
||||||
else:
|
else:
|
||||||
var sec: HandshakeSecrets
|
var sec: HandshakeSecrets
|
||||||
headEnc = c.makeAuthHeader(toId, nonce, sec, challenge)
|
headEnc = ? c.makeAuthHeader(toId, nonce, sec, challenge)
|
||||||
|
|
||||||
writeKey = sec.writeKey
|
writeKey = sec.writeKey
|
||||||
# TODO: is it safe to ignore the error here?
|
# TODO: is it safe to ignore the error here?
|
||||||
|
@ -166,10 +169,10 @@ proc encodeEncrypted*(c: Codec,
|
||||||
headBuf.add(headEnc)
|
headBuf.add(headEnc)
|
||||||
|
|
||||||
headBuf.add(encryptGCM(writeKey, nonce, body, tag))
|
headBuf.add(encryptGCM(writeKey, nonce, body, tag))
|
||||||
return (headBuf, nonce)
|
ok((headBuf, nonce))
|
||||||
|
|
||||||
proc decryptGCM*(key: AesKey, nonce, ct, authData: openarray[byte]):
|
proc decryptGCM*(key: AesKey, nonce, ct, authData: openarray[byte]):
|
||||||
Option[seq[byte]] =
|
Option[seq[byte]] {.raises:[].} =
|
||||||
if ct.len <= gcmTagSize:
|
if ct.len <= gcmTagSize:
|
||||||
debug "cipher is missing tag", len = ct.len
|
debug "cipher is missing tag", len = ct.len
|
||||||
return
|
return
|
||||||
|
@ -187,62 +190,66 @@ proc decryptGCM*(key: AesKey, nonce, ct, authData: openarray[byte]):
|
||||||
|
|
||||||
return some(res)
|
return some(res)
|
||||||
|
|
||||||
type
|
|
||||||
DecodePacketResult = enum
|
|
||||||
decodingSuccessful
|
|
||||||
invalidPacketPayload
|
|
||||||
invalidPacketType
|
|
||||||
unsupportedPacketType
|
|
||||||
|
|
||||||
proc decodePacketBody(typ: byte,
|
proc decodePacketBody(typ: byte,
|
||||||
body: openarray[byte],
|
body: openarray[byte],
|
||||||
res: var Packet): DecodePacketResult =
|
res: var Packet):
|
||||||
|
DecodeResult[void] {.raises:[Defect].} =
|
||||||
if typ < PacketKind.low.byte or typ > PacketKind.high.byte:
|
if typ < PacketKind.low.byte or typ > PacketKind.high.byte:
|
||||||
return invalidPacketType
|
return err(PacketError)
|
||||||
|
|
||||||
let kind = cast[PacketKind](typ)
|
let kind = cast[PacketKind](typ)
|
||||||
res = Packet(kind: kind)
|
res = Packet(kind: kind)
|
||||||
var rlp = rlpFromBytes(body)
|
var rlp = rlpFromBytes(body)
|
||||||
if rlp.enterList:
|
if rlp.enterList:
|
||||||
res.reqId = rlp.read(RequestId)
|
try:
|
||||||
|
res.reqId = rlp.read(RequestId)
|
||||||
|
except RlpError:
|
||||||
|
return err(PacketError)
|
||||||
|
|
||||||
proc decode[T](rlp: var Rlp, v: var T) {.inline, nimcall.} =
|
proc decode[T](rlp: var Rlp, v: var T)
|
||||||
|
{.inline, nimcall, raises:[RlpError, ValueError, Defect].} =
|
||||||
for k, v in v.fieldPairs:
|
for k, v in v.fieldPairs:
|
||||||
v = rlp.read(typeof(v))
|
v = rlp.read(typeof(v))
|
||||||
|
|
||||||
case kind
|
try:
|
||||||
of unused: return invalidPacketPayload
|
case kind
|
||||||
of ping: rlp.decode(res.ping)
|
of unused: return err(PacketError)
|
||||||
of pong: rlp.decode(res.pong)
|
of ping: rlp.decode(res.ping)
|
||||||
of findNode: rlp.decode(res.findNode)
|
of pong: rlp.decode(res.pong)
|
||||||
of nodes: rlp.decode(res.nodes)
|
of findNode: rlp.decode(res.findNode)
|
||||||
of regtopic, ticket, regconfirmation, topicquery:
|
of nodes: rlp.decode(res.nodes)
|
||||||
# TODO Implement these packet types
|
of regtopic, ticket, regconfirmation, topicquery:
|
||||||
return unsupportedPacketType
|
# TODO: Implement support for topic advertisement
|
||||||
|
return err(UnsupportedPacketType)
|
||||||
|
except RlpError, ValueError:
|
||||||
|
return err(PacketError)
|
||||||
|
|
||||||
return decodingSuccessful
|
ok()
|
||||||
else:
|
else:
|
||||||
return invalidPacketPayload
|
err(PacketError)
|
||||||
|
|
||||||
proc decodeAuthResp(c: Codec, fromId: NodeId, head: AuthHeader,
|
proc decodeAuthResp(c: Codec, fromId: NodeId, head: AuthHeader,
|
||||||
challenge: Whoareyou, secrets: var HandshakeSecrets, newNode: var Node): bool =
|
challenge: Whoareyou, secrets: var HandshakeSecrets, newNode: var Node):
|
||||||
|
DecodeResult[void] {.raises:[Defect].} =
|
||||||
if head.scheme != authSchemeName:
|
if head.scheme != authSchemeName:
|
||||||
warn "Unknown auth scheme"
|
warn "Unknown auth scheme"
|
||||||
return false
|
return err(HandshakeError)
|
||||||
|
|
||||||
var ephKey = PublicKey.fromRaw(head.ephemeralKey)
|
let ephKey = ? PublicKey.fromRaw(head.ephemeralKey).mapErrTo(HandshakeError)
|
||||||
if ephKey.isErr:
|
|
||||||
return false
|
|
||||||
|
|
||||||
deriveKeys(fromId, c.localNode.id, c.privKey, ephKey[], challenge.idNonce,
|
secrets = ? deriveKeys(fromId, c.localNode.id, c.privKey, ephKey,
|
||||||
secrets)
|
challenge.idNonce).mapErrTo(HandshakeError)
|
||||||
|
|
||||||
var zeroNonce: array[gcmNonceSize, byte]
|
var zeroNonce: array[gcmNonceSize, byte]
|
||||||
let respData = decryptGCM(secrets.authRespKey, zeroNonce, head.response, [])
|
let respData = decryptGCM(secrets.authRespKey, zeroNonce, head.response, [])
|
||||||
if respData.isNone():
|
if respData.isNone():
|
||||||
return false
|
return err(HandshakeError)
|
||||||
|
|
||||||
let authResp = rlp.decode(respData.get(), AuthResponse)
|
var authResp: AuthResponse
|
||||||
|
try:
|
||||||
|
authResp = rlp.decode(respData.get(), AuthResponse)
|
||||||
|
except RlpError, ValueError:
|
||||||
|
return err(HandshakeError)
|
||||||
# TODO:
|
# TODO:
|
||||||
# 1. Should allow for not having an ENR included, solved for now by sending
|
# 1. Should allow for not having an ENR included, solved for now by sending
|
||||||
# whoareyou with always recordSeq of 0
|
# whoareyou with always recordSeq of 0
|
||||||
|
@ -252,8 +259,11 @@ proc decodeAuthResp(c: Codec, fromId: NodeId, head: AuthHeader,
|
||||||
# More TODO:
|
# More TODO:
|
||||||
# This will also not work if ENR does not contain an IP address or if the
|
# This will also not work if ENR does not contain an IP address or if the
|
||||||
# IP address is out of date and doesn't match current UDP end point
|
# IP address is out of date and doesn't match current UDP end point
|
||||||
newNode = newNode(authResp.record)
|
try:
|
||||||
return true
|
newNode = newNode(authResp.record)
|
||||||
|
ok()
|
||||||
|
except KeyError, ValueError:
|
||||||
|
err(HandshakeError)
|
||||||
|
|
||||||
proc decodeEncrypted*(c: var Codec,
|
proc decodeEncrypted*(c: var Codec,
|
||||||
fromId: NodeID,
|
fromId: NodeID,
|
||||||
|
@ -261,7 +271,7 @@ proc decodeEncrypted*(c: var Codec,
|
||||||
input: openArray[byte],
|
input: openArray[byte],
|
||||||
authTag: var AuthTag,
|
authTag: var AuthTag,
|
||||||
newNode: var Node,
|
newNode: var Node,
|
||||||
packet: var Packet): DecodeStatus =
|
packet: var Packet): DecodeResult[void] =
|
||||||
var r = rlpFromBytes(input.toOpenArray(tagSize, input.high))
|
var r = rlpFromBytes(input.toOpenArray(tagSize, input.high))
|
||||||
var auth: AuthHeader
|
var auth: AuthHeader
|
||||||
|
|
||||||
|
@ -270,23 +280,27 @@ proc decodeEncrypted*(c: var Codec,
|
||||||
|
|
||||||
if r.isList:
|
if r.isList:
|
||||||
# Handshake - rlp list indicates auth-header
|
# Handshake - rlp list indicates auth-header
|
||||||
auth = r.read(AuthHeader)
|
try:
|
||||||
|
auth = r.read(AuthHeader)
|
||||||
|
except RlpError:
|
||||||
|
return err(PacketError)
|
||||||
authTag = auth.auth
|
authTag = auth.auth
|
||||||
|
|
||||||
let key = HandShakeKey(nodeId: fromId, address: $fromAddr)
|
let key = HandShakeKey(nodeId: fromId, address: $fromAddr)
|
||||||
let challenge = c.handshakes.getOrDefault(key)
|
let challenge = c.handshakes.getOrDefault(key)
|
||||||
if challenge.isNil:
|
if challenge.isNil:
|
||||||
trace "Decoding failed (no challenge)"
|
trace "Decoding failed (no challenge)"
|
||||||
return HandshakeError
|
return err(HandshakeError)
|
||||||
|
|
||||||
if auth.idNonce != challenge.idNonce:
|
if auth.idNonce != challenge.idNonce:
|
||||||
trace "Decoding failed (different nonce)"
|
trace "Decoding failed (different nonce)"
|
||||||
return HandshakeError
|
return err(HandshakeError)
|
||||||
|
|
||||||
var sec: HandshakeSecrets
|
var sec: HandshakeSecrets
|
||||||
if not c.decodeAuthResp(fromId, auth, challenge, sec, newNode):
|
if c.decodeAuthResp(fromId, auth, challenge, sec, newNode).isErr:
|
||||||
trace "Decoding failed (bad auth)"
|
trace "Decoding failed (bad auth)"
|
||||||
return HandshakeError
|
return err(HandshakeError)
|
||||||
|
|
||||||
c.handshakes.del(key)
|
c.handshakes.del(key)
|
||||||
|
|
||||||
# For an incoming handshake, we are not sure the address in the ENR is there
|
# For an incoming handshake, we are not sure the address in the ENR is there
|
||||||
|
@ -299,16 +313,17 @@ proc decodeEncrypted*(c: var Codec,
|
||||||
# TODO: is it safe to ignore the error here?
|
# TODO: is it safe to ignore the error here?
|
||||||
discard c.db.storeKeys(fromId, fromAddr, sec.readKey, sec.writeKey)
|
discard c.db.storeKeys(fromId, fromAddr, sec.readKey, sec.writeKey)
|
||||||
readKey = sec.readKey
|
readKey = sec.readKey
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Message packet or random packet - rlp bytes (size 12) indicates auth-tag
|
# Message packet or random packet - rlp bytes (size 12) indicates auth-tag
|
||||||
authTag = r.read(AuthTag)
|
try:
|
||||||
|
authTag = r.read(AuthTag)
|
||||||
|
except RlpError:
|
||||||
|
return err(PacketError)
|
||||||
auth.auth = authTag
|
auth.auth = authTag
|
||||||
var writeKey: AesKey
|
var writeKey: AesKey
|
||||||
if not c.db.loadKeys(fromId, fromAddr, readKey, writeKey):
|
if not c.db.loadKeys(fromId, fromAddr, readKey, writeKey):
|
||||||
trace "Decoding failed (no keys)"
|
trace "Decoding failed (no keys)"
|
||||||
return DecryptError
|
return err(DecryptError)
|
||||||
# doAssert(false, "TODO: HANDLE ME!")
|
|
||||||
|
|
||||||
let headSize = tagSize + r.position
|
let headSize = tagSize + r.position
|
||||||
|
|
||||||
|
@ -318,31 +333,29 @@ proc decodeEncrypted*(c: var Codec,
|
||||||
input.toOpenArray(0, tagSize - 1))
|
input.toOpenArray(0, tagSize - 1))
|
||||||
if body.isNone():
|
if body.isNone():
|
||||||
discard c.db.deleteKeys(fromId, fromAddr)
|
discard c.db.deleteKeys(fromId, fromAddr)
|
||||||
return DecryptError
|
return err(DecryptError)
|
||||||
|
|
||||||
let packetData = body.get()
|
let packetData = body.get()
|
||||||
if packetData.len > 1:
|
if packetData.len > 1:
|
||||||
let status = decodePacketBody(packetData[0],
|
decodePacketBody(packetData[0],
|
||||||
packetData.toOpenArray(1, packetData.high), packet)
|
packetData.toOpenArray(1, packetData.high), packet)
|
||||||
if status == decodingSuccessful:
|
|
||||||
return Success
|
|
||||||
else:
|
|
||||||
debug "Failed to decode discovery packet", reason = status
|
|
||||||
return PacketError
|
|
||||||
else:
|
else:
|
||||||
return PacketError
|
err(PacketError)
|
||||||
|
|
||||||
proc newRequestId*(): RequestId =
|
proc newRequestId*(): Result[RequestId, cstring] {.raises:[].} =
|
||||||
if randomBytes(addr result, sizeof(result)) != sizeof(result):
|
var id: RequestId
|
||||||
raise newException(RandomSourceDepleted, "Could not randomize bytes")
|
if randomBytes(addr id, sizeof(id)) != sizeof(id):
|
||||||
|
err("Could not randomize bytes")
|
||||||
|
else:
|
||||||
|
ok(id)
|
||||||
|
|
||||||
proc numFields(T: typedesc): int =
|
proc numFields(T: typedesc): int {.raises:[].} =
|
||||||
for k, v in fieldPairs(default(T)): inc result
|
for k, v in fieldPairs(default(T)): inc result
|
||||||
|
|
||||||
proc encodePacket*[T: SomePacket](p: T, reqId: RequestId): seq[byte] =
|
proc encodePacket*[T: SomePacket](p: T, reqId: RequestId):
|
||||||
|
seq[byte] {.raises:[].} =
|
||||||
result = newSeqOfCap[byte](64)
|
result = newSeqOfCap[byte](64)
|
||||||
result.add(packetKind(T).ord)
|
result.add(packetKind(T).ord)
|
||||||
# result.add(rlp.encode(p))
|
|
||||||
|
|
||||||
const sz = numFields(T)
|
const sz = numFields(T)
|
||||||
var writer = initRlpList(sz + 1)
|
var writer = initRlpList(sz + 1)
|
||||||
|
@ -350,6 +363,3 @@ proc encodePacket*[T: SomePacket](p: T, reqId: RequestId): seq[byte] =
|
||||||
for k, v in fieldPairs(p):
|
for k, v in fieldPairs(p):
|
||||||
writer.append(v)
|
writer.append(v)
|
||||||
result.add(writer.finish())
|
result.add(writer.finish())
|
||||||
|
|
||||||
proc encodePacket*[T: SomePacket](p: T): seq[byte] =
|
|
||||||
encodePacket(p, newRequestId())
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import nimcrypto
|
import nimcrypto
|
||||||
|
|
||||||
proc hkdf*(HashType: typedesc, secret, salt, info: openarray[byte], output: var openarray[byte]) =
|
proc hkdf*(HashType: typedesc, ikm, salt, info: openarray[byte],
|
||||||
|
output: var openarray[byte]) =
|
||||||
var ctx: HMAC[HashType]
|
var ctx: HMAC[HashType]
|
||||||
ctx.init(salt)
|
ctx.init(salt)
|
||||||
ctx.update(secret)
|
ctx.update(ikm)
|
||||||
let prk = ctx.finish().data
|
let prk = ctx.finish().data
|
||||||
const hashLen = HashType.bits div 8
|
const hashLen = HashType.bits div 8
|
||||||
|
|
||||||
|
|
|
@ -120,6 +120,8 @@ type
|
||||||
node: Node
|
node: Node
|
||||||
packet: seq[byte]
|
packet: seq[byte]
|
||||||
|
|
||||||
|
RandomSourceDepleted* = object of CatchableError
|
||||||
|
|
||||||
proc addNode*(d: Protocol, node: Node) =
|
proc addNode*(d: Protocol, node: Node) =
|
||||||
discard d.routingTable.addNode(node)
|
discard d.routingTable.addNode(node)
|
||||||
|
|
||||||
|
@ -182,7 +184,9 @@ proc decodeWhoAreYou(d: Protocol, msg: openArray[byte]): Whoareyou =
|
||||||
proc sendWhoareyou(d: Protocol, address: Address, toNode: NodeId, authTag: AuthTag) =
|
proc sendWhoareyou(d: Protocol, address: Address, toNode: NodeId, authTag: AuthTag) =
|
||||||
trace "sending who are you", to = $toNode, toAddress = $address
|
trace "sending who are you", to = $toNode, toAddress = $address
|
||||||
let challenge = Whoareyou(authTag: authTag, recordSeq: 0)
|
let challenge = Whoareyou(authTag: authTag, recordSeq: 0)
|
||||||
encoding.randomBytes2(challenge.idNonce)
|
|
||||||
|
if randomBytes(challenge.idNonce) != challenge.idNonce.len:
|
||||||
|
raise newException(RandomSourceDepleted, "Could not randomize bytes")
|
||||||
# If there is already a handshake going on for this nodeid then we drop this
|
# If there is already a handshake going on for this nodeid then we drop this
|
||||||
# new one. Handshake will get cleaned up after `handshakeTimeout`.
|
# new one. Handshake will get cleaned up after `handshakeTimeout`.
|
||||||
# If instead overwriting the handshake would be allowed, the handshake timeout
|
# If instead overwriting the handshake would be allowed, the handshake timeout
|
||||||
|
@ -208,7 +212,7 @@ proc sendNodes(d: Protocol, toId: NodeId, toAddr: Address, reqId: RequestId,
|
||||||
proc sendNodes(d: Protocol, toId: NodeId, toAddr: Address,
|
proc sendNodes(d: Protocol, toId: NodeId, toAddr: Address,
|
||||||
packet: NodesPacket, reqId: RequestId) {.nimcall.} =
|
packet: NodesPacket, reqId: RequestId) {.nimcall.} =
|
||||||
let (data, _) = d.codec.encodeEncrypted(toId, toAddr,
|
let (data, _) = d.codec.encodeEncrypted(toId, toAddr,
|
||||||
encodePacket(packet, reqId), challenge = nil)
|
encodePacket(packet, reqId), challenge = nil).tryGet()
|
||||||
d.send(toAddr, data)
|
d.send(toAddr, data)
|
||||||
|
|
||||||
var packet: NodesPacket
|
var packet: NodesPacket
|
||||||
|
@ -234,7 +238,7 @@ proc handlePing(d: Protocol, fromId: NodeId, fromAddr: Address,
|
||||||
pong.port = a.udpPort.uint16
|
pong.port = a.udpPort.uint16
|
||||||
|
|
||||||
let (data, _) = d.codec.encodeEncrypted(fromId, fromAddr,
|
let (data, _) = d.codec.encodeEncrypted(fromId, fromAddr,
|
||||||
encodePacket(pong, reqId), challenge = nil)
|
encodePacket(pong, reqId), challenge = nil).tryGet()
|
||||||
d.send(fromAddr, data)
|
d.send(fromAddr, data)
|
||||||
|
|
||||||
proc handleFindNode(d: Protocol, fromId: NodeId, fromAddr: Address,
|
proc handleFindNode(d: Protocol, fromId: NodeId, fromAddr: Address,
|
||||||
|
@ -270,9 +274,9 @@ proc receive*(d: Protocol, a: Address, msg: openArray[byte]) {.gcsafe,
|
||||||
whoareyou.pubKey = toNode.node.pubkey # TODO: Yeah, rather ugly this.
|
whoareyou.pubKey = toNode.node.pubkey # TODO: Yeah, rather ugly this.
|
||||||
try:
|
try:
|
||||||
let (data, _) = d.codec.encodeEncrypted(toNode.id, toNode.address,
|
let (data, _) = d.codec.encodeEncrypted(toNode.id, toNode.address,
|
||||||
pr.packet, challenge = whoareyou)
|
pr.packet, challenge = whoareyou).tryGet()
|
||||||
d.send(toNode, data)
|
d.send(toNode, data)
|
||||||
except RandomSourceDepleted as err:
|
except RandomSourceDepleted:
|
||||||
debug "Failed to respond to a who-you-are msg " &
|
debug "Failed to respond to a who-you-are msg " &
|
||||||
"due to randomness source depletion."
|
"due to randomness source depletion."
|
||||||
|
|
||||||
|
@ -286,7 +290,7 @@ proc receive*(d: Protocol, a: Address, msg: openArray[byte]) {.gcsafe,
|
||||||
var node: Node
|
var node: Node
|
||||||
var packet: Packet
|
var packet: Packet
|
||||||
let decoded = d.codec.decodeEncrypted(sender, a, msg, authTag, node, packet)
|
let decoded = d.codec.decodeEncrypted(sender, a, msg, authTag, node, packet)
|
||||||
if decoded == DecodeStatus.Success:
|
if decoded.isOk:
|
||||||
if not node.isNil:
|
if not node.isNil:
|
||||||
# Not filling table with nodes without correct IP in the ENR
|
# Not filling table with nodes without correct IP in the ENR
|
||||||
if a.ip == node.address.ip:
|
if a.ip == node.address.ip:
|
||||||
|
@ -305,19 +309,20 @@ proc receive*(d: Protocol, a: Address, msg: openArray[byte]) {.gcsafe,
|
||||||
waiter.complete(packet.some)
|
waiter.complete(packet.some)
|
||||||
else:
|
else:
|
||||||
debug "TODO: handle packet: ", packet = packet.kind, origin = a
|
debug "TODO: handle packet: ", packet = packet.kind, origin = a
|
||||||
elif decoded == DecodeStatus.DecryptError:
|
elif decoded.error == DecodeError.DecryptError:
|
||||||
debug "Could not decrypt packet, respond with whoareyou",
|
debug "Could not decrypt packet, respond with whoareyou",
|
||||||
localNode = $d.localNode, address = a
|
localNode = $d.localNode, address = a
|
||||||
# only sendingWhoareyou in case it is a decryption failure
|
# only sendingWhoareyou in case it is a decryption failure
|
||||||
d.sendWhoareyou(a, sender, authTag)
|
d.sendWhoareyou(a, sender, authTag)
|
||||||
elif decoded == DecodeStatus.PacketError:
|
elif decoded.error == DecodeError.UnsupportedPacketType:
|
||||||
# Still adding the node in case there is a packet error (could be
|
# Still adding the node in case failure is because of unsupported packet.
|
||||||
# unsupported packet)
|
|
||||||
if not node.isNil:
|
if not node.isNil:
|
||||||
if a.ip == node.address.ip:
|
if a.ip == node.address.ip:
|
||||||
debug "Adding new node to routing table", node = $node,
|
debug "Adding new node to routing table", node = $node,
|
||||||
localNode = $d.localNode
|
localNode = $d.localNode
|
||||||
discard d.routingTable.addNode(node)
|
discard d.routingTable.addNode(node)
|
||||||
|
# elif decoded.error == DecodeError.PacketError:
|
||||||
|
# Not adding this node as from our perspective it is sending rubbish.
|
||||||
|
|
||||||
proc processClient(transp: DatagramTransport,
|
proc processClient(transp: DatagramTransport,
|
||||||
raddr: TransportAddress): Future[void] {.async, gcsafe.} =
|
raddr: TransportAddress): Future[void] {.async, gcsafe.} =
|
||||||
|
@ -389,11 +394,11 @@ proc waitNodes(d: Protocol, fromNode: Node, reqId: RequestId): Future[seq[Node]]
|
||||||
|
|
||||||
proc sendPing(d: Protocol, toNode: Node): RequestId =
|
proc sendPing(d: Protocol, toNode: Node): RequestId =
|
||||||
let
|
let
|
||||||
reqId = newRequestId()
|
reqId = newRequestId().tryGet()
|
||||||
ping = PingPacket(enrSeq: d.localNode.record.seqNum)
|
ping = PingPacket(enrSeq: d.localNode.record.seqNum)
|
||||||
packet = encodePacket(ping, reqId)
|
packet = encodePacket(ping, reqId)
|
||||||
(data, nonce) = d.codec.encodeEncrypted(toNode.id, toNode.address, packet,
|
(data, nonce) = d.codec.encodeEncrypted(toNode.id, toNode.address, packet,
|
||||||
challenge = nil)
|
challenge = nil).tryGet()
|
||||||
d.registerRequest(toNode, packet, nonce)
|
d.registerRequest(toNode, packet, nonce)
|
||||||
d.send(toNode, data)
|
d.send(toNode, data)
|
||||||
return reqId
|
return reqId
|
||||||
|
@ -406,10 +411,10 @@ proc ping*(d: Protocol, toNode: Node): Future[Option[PongPacket]] {.async.} =
|
||||||
return some(resp.get().pong)
|
return some(resp.get().pong)
|
||||||
|
|
||||||
proc sendFindNode(d: Protocol, toNode: Node, distance: uint32): RequestId =
|
proc sendFindNode(d: Protocol, toNode: Node, distance: uint32): RequestId =
|
||||||
let reqId = newRequestId()
|
let reqId = newRequestId().tryGet()
|
||||||
let packet = encodePacket(FindNodePacket(distance: distance), reqId)
|
let packet = encodePacket(FindNodePacket(distance: distance), reqId)
|
||||||
let (data, nonce) = d.codec.encodeEncrypted(toNode.id, toNode.address, packet,
|
let (data, nonce) = d.codec.encodeEncrypted(toNode.id, toNode.address, packet,
|
||||||
challenge = nil)
|
challenge = nil).tryGet()
|
||||||
d.registerRequest(toNode, packet, nonce)
|
d.registerRequest(toNode, packet, nonce)
|
||||||
|
|
||||||
d.send(toNode, data)
|
d.send(toNode, data)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import
|
import
|
||||||
unittest, chronos, sequtils, chronicles, tables, stint,
|
unittest, chronos, sequtils, chronicles, tables, stint, nimcrypto,
|
||||||
eth/[keys, rlp], eth/p2p/enode, eth/trie/db,
|
eth/[keys, rlp], eth/p2p/enode, eth/trie/db,
|
||||||
eth/p2p/discoveryv5/[discovery_db, enr, node, types, routing_table, encoding],
|
eth/p2p/discoveryv5/[discovery_db, enr, node, types, routing_table, encoding],
|
||||||
eth/p2p/discoveryv5/protocol as discv5_protocol,
|
eth/p2p/discoveryv5/protocol as discv5_protocol,
|
||||||
|
@ -26,8 +26,8 @@ proc randomPacket(tag: PacketTag): seq[byte] =
|
||||||
authTag: AuthTag
|
authTag: AuthTag
|
||||||
msg: array[44, byte]
|
msg: array[44, byte]
|
||||||
|
|
||||||
randomBytes2(authTag)
|
require randomBytes(authTag) == authTag.len
|
||||||
randomBytes2(msg)
|
require randomBytes(msg) == msg.len
|
||||||
result.add(tag)
|
result.add(tag)
|
||||||
result.add(rlp.encode(authTag))
|
result.add(rlp.encode(authTag))
|
||||||
result.add(msg)
|
result.add(msg)
|
||||||
|
@ -98,7 +98,7 @@ suite "Discovery v5 Tests":
|
||||||
let a = localAddress(20303)
|
let a = localAddress(20303)
|
||||||
|
|
||||||
for i in 0 ..< 5:
|
for i in 0 ..< 5:
|
||||||
randomBytes2(tag)
|
require randomBytes(tag) == tag.len
|
||||||
node.receive(a, randomPacket(tag))
|
node.receive(a, randomPacket(tag))
|
||||||
|
|
||||||
# Checking different nodeIds but same address
|
# Checking different nodeIds but same address
|
||||||
|
|
|
@ -166,10 +166,11 @@ suite "Discovery v5 Cryptographic Primitives":
|
||||||
idNonceSig = "0xc5036e702a79902ad8aa147dabfe3958b523fd6fa36cc78e2889b912d682d8d35fdea142e141f690736d86f50b39746ba2d2fc510b46f82ee08f08fd55d133a4"
|
idNonceSig = "0xc5036e702a79902ad8aa147dabfe3958b523fd6fa36cc78e2889b912d682d8d35fdea142e141f690736d86f50b39746ba2d2fc510b46f82ee08f08fd55d133a4"
|
||||||
|
|
||||||
let
|
let
|
||||||
c = Codec(privKey: PrivateKey.fromHex(localSecretKey)[])
|
privKey = PrivateKey.fromHex(localSecretKey)[]
|
||||||
signature = signIDNonce(c, hexToByteArray[idNonceSize](idNonce),
|
signature = signIDNonce(privKey, hexToByteArray[idNonceSize](idNonce),
|
||||||
hexToByteArray[64](ephemeralKey))
|
hexToByteArray[64](ephemeralKey))
|
||||||
check signature.toRaw() == hexToByteArray[64](idNonceSig)
|
require signature.isOK()
|
||||||
|
check signature[].toRaw() == hexToByteArray[64](idNonceSig)
|
||||||
|
|
||||||
test "Encryption/Decryption":
|
test "Encryption/Decryption":
|
||||||
const
|
const
|
||||||
|
|
Loading…
Reference in New Issue