nim-eth/eth/p2p/discoveryv5/encoding.nim

356 lines
11 KiB
Nim
Raw Normal View History

import
std/[tables, options], nimcrypto, stint, chronicles,
types, node, enr, hkdf, ../enode, eth/[rlp, keys]
2019-12-16 19:38:45 +00:00
export keys
2019-12-16 19:38:45 +00:00
const
idNoncePrefix = "discovery-id-nonce"
keyAgreementPrefix = "discovery v5 key agreement"
2020-02-17 17:04:29 +00:00
authSchemeName* = "gcm"
gcmNonceSize* = 12
gcmTagSize* = 16
tagSize* = 32 ## size of the tag where each message (except whoareyou) starts
## with
2019-12-16 19:38:45 +00:00
type
PacketTag* = array[tagSize, byte]
2019-12-16 19:38:45 +00:00
AuthResponse = object
version: int
signature: array[64, byte]
record: Record
Codec* = object
localNode*: Node
privKey*: PrivateKey
db*: Database
handshakes*: Table[HandShakeKey, Whoareyou]
2019-12-16 19:38:45 +00:00
HandshakeSecrets = object
writeKey: AesKey
readKey: AesKey
authRespKey: AesKey
2019-12-16 19:38:45 +00:00
2020-02-17 17:04:29 +00:00
AuthHeader* = object
auth*: AuthTag
idNonce*: IdNonce
2020-02-17 17:04:29 +00:00
scheme*: string
ephemeralKey*: array[64, byte]
response*: seq[byte]
2019-12-16 19:38:45 +00:00
RandomSourceDepleted* = object of CatchableError
2019-12-16 19:38:45 +00:00
DecodeStatus* = enum
Success,
HandshakeError,
PacketError,
DecryptError
proc randomBytes2*(v: var openarray[byte]) =
# TODO if this is called randomBytes it breaks calling the real randomBytes
# 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")
2019-12-16 19:38:45 +00:00
proc idNonceHash(nonce, ephkey: openarray[byte]): MDigest[256] =
2019-12-16 19:38:45 +00:00
var ctx: sha256
ctx.init()
ctx.update(idNoncePrefix)
ctx.update(nonce)
ctx.update(ephkey)
ctx.finish()
2019-12-16 19:38:45 +00:00
proc signIDNonce*(c: Codec, idNonce, ephKey: openarray[byte]): SignatureNR =
let sig = signNR(c.privKey, idNonceHash(idNonce, ephKey))
if sig.isErr:
raise newException(CatchableError, "Could not sign idNonce")
sig[]
2019-12-16 19:38:45 +00:00
proc deriveKeys(n1, n2: NodeID, priv: PrivateKey, pub: PublicKey,
idNonce: openarray[byte], result: var HandshakeSecrets) =
let eph = ecdhRawFull(priv, pub)
if eph.isErr:
raise newException(CatchableError, "ecdhRawFull failed")
2019-12-16 19:38:45 +00:00
# TODO: Unneeded allocation here
var info = newSeqOfCap[byte](idNoncePrefix.len + 32 * 2)
for i, c in keyAgreementPrefix: info.add(byte(c))
info.add(n1.toByteArrayBE())
info.add(n2.toByteArrayBE())
# echo "EPH: ", eph.data.toHex, " idNonce: ", challenge.idNonce.toHex, "info: ", info.toHex
static: assert(sizeof(result) == aesKeySize * 3)
2019-12-16 19:38:45 +00:00
var res = cast[ptr UncheckedArray[byte]](addr result)
hkdf(sha256, eph[].data, idNonce, info, toOpenArray(res, 0, sizeof(result) - 1))
2019-12-16 19:38:45 +00:00
proc encryptGCM*(key, nonce, pt, authData: openarray[byte]): seq[byte] =
2019-12-16 19:38:45 +00:00
var ectx: GCM[aes128]
ectx.init(key, nonce, authData)
result = newSeq[byte](pt.len + gcmTagSize)
ectx.encrypt(pt, result)
ectx.getTag(result.toOpenArray(pt.len, result.high))
ectx.clear()
proc makeAuthHeader(c: Codec, toId: NodeID, nonce: array[gcmNonceSize, byte],
handshakeSecrets: var HandshakeSecrets,
challenge: Whoareyou): seq[byte] =
2019-12-16 19:38:45 +00:00
var resp = AuthResponse(version: 5)
let ln = c.localNode
if challenge.recordSeq < ln.record.seqNum:
2019-12-16 19:38:45 +00:00
resp.record = ln.record
let ephKeys = KeyPair.random().tryGet()
2019-12-16 19:38:45 +00:00
resp.signature = c.signIDNonce(challenge.idNonce, ephKeys.pubkey.toRaw).toRaw
2019-12-16 19:38:45 +00:00
deriveKeys(ln.id, toId, ephKeys.seckey, challenge.pubKey, challenge.idNonce,
handshakeSecrets)
2019-12-16 19:38:45 +00:00
let respRlp = rlp.encode(resp)
var zeroNonce: array[gcmNonceSize, byte]
let respEnc = encryptGCM(handshakeSecrets.authRespKey, zeroNonce, respRLP, [])
2019-12-16 19:38:45 +00:00
let header = AuthHeader(auth: nonce, idNonce: challenge.idNonce,
scheme: authSchemeName, ephemeralKey: ephKeys.pubkey.toRaw, response: respEnc)
2019-12-16 19:38:45 +00:00
rlp.encode(header)
proc `xor`[N: static[int], T](a, b: array[N, T]): array[N, T] =
for i in 0 .. a.high:
result[i] = a[i] xor b[i]
proc packetTag(destNode, srcNode: NodeID): PacketTag =
2019-12-16 19:38:45 +00:00
let destId = destNode.toByteArrayBE()
let srcId = srcNode.toByteArrayBE()
let destidHash = sha256.digest(destId)
result = srcId xor destidHash.data
proc encodeEncrypted*(c: Codec,
toId: NodeID,
toAddr: Address,
packetData: seq[byte],
challenge: Whoareyou):
(seq[byte], array[gcmNonceSize, byte]) =
2019-12-16 19:38:45 +00:00
var nonce: array[gcmNonceSize, byte]
randomBytes2(nonce)
2019-12-16 19:38:45 +00:00
var headEnc: seq[byte]
var writeKey: AesKey
2019-12-16 19:38:45 +00:00
if challenge.isNil:
headEnc = rlp.encode(nonce)
var readKey: AesKey
2019-12-16 19:38:45 +00:00
# We might not have the node's keys if the handshake hasn't been performed
# yet. That's fine, we will be responded with whoareyou.
discard c.db.loadKeys(toId, toAddr, readKey, writeKey)
2019-12-16 19:38:45 +00:00
else:
var sec: HandshakeSecrets
headEnc = c.makeAuthHeader(toId, nonce, sec, challenge)
2019-12-16 19:38:45 +00:00
writeKey = sec.writeKey
# TODO: is it safe to ignore the error here?
discard c.db.storeKeys(toId, toAddr, sec.readKey, sec.writeKey)
2019-12-16 19:38:45 +00:00
var body = packetData
let tag = packetTag(toId, c.localNode.id)
2019-12-16 19:38:45 +00:00
var headBuf = newSeqOfCap[byte](tag.len + headEnc.len)
headBuf.add(tag)
headBuf.add(headEnc)
headBuf.add(encryptGCM(writeKey, nonce, body, tag))
return (headBuf, nonce)
proc decryptGCM*(key: AesKey, nonce, ct, authData: openarray[byte]):
Option[seq[byte]] =
if ct.len <= gcmTagSize:
debug "cipher is missing tag", len = ct.len
return
2019-12-16 19:38:45 +00:00
var dctx: GCM[aes128]
dctx.init(key, nonce, authData)
var res = newSeq[byte](ct.len - gcmTagSize)
2019-12-16 19:38:45 +00:00
var tag: array[gcmTagSize, byte]
dctx.decrypt(ct.toOpenArray(0, ct.high - gcmTagSize), res)
2019-12-16 19:38:45 +00:00
dctx.getTag(tag)
dctx.clear()
if tag != ct.toOpenArray(ct.len - gcmTagSize, ct.high):
return
return some(res)
2020-02-27 18:09:05 +00:00
type
DecodePacketResult = enum
decodingSuccessful
invalidPacketPayload
invalidPacketType
unsupportedPacketType
proc decodePacketBody(typ: byte,
body: openarray[byte],
res: var Packet): DecodePacketResult =
if typ < PacketKind.low.byte or typ > PacketKind.high.byte:
return invalidPacketType
let kind = cast[PacketKind](typ)
res = Packet(kind: kind)
var rlp = rlpFromBytes(@body.toRange)
if rlp.enterList:
2019-12-16 19:38:45 +00:00
res.reqId = rlp.read(RequestId)
proc decode[T](rlp: var Rlp, v: var T) {.inline, nimcall.} =
for k, v in v.fieldPairs:
v = rlp.read(typeof(v))
2020-02-27 18:09:05 +00:00
case kind
of unused: return invalidPacketPayload
of ping: rlp.decode(res.ping)
of pong: rlp.decode(res.pong)
of findNode: rlp.decode(res.findNode)
of nodes: rlp.decode(res.nodes)
of regtopic, ticket, regconfirmation, topicquery:
# TODO Implement these packet types
return unsupportedPacketType
return decodingSuccessful
2019-12-16 19:38:45 +00:00
else:
2020-02-27 18:09:05 +00:00
return invalidPacketPayload
2019-12-16 19:38:45 +00:00
proc decodeAuthResp(c: Codec, fromId: NodeId, head: AuthHeader,
challenge: Whoareyou, secrets: var HandshakeSecrets, newNode: var Node): bool =
2019-12-16 19:38:45 +00:00
if head.scheme != authSchemeName:
warn "Unknown auth scheme"
2019-12-16 19:38:45 +00:00
return false
var ephKey = PublicKey.fromRaw(head.ephemeralKey)
if ephKey.isErr:
2019-12-16 19:38:45 +00:00
return false
deriveKeys(fromId, c.localNode.id, c.privKey, ephKey[], challenge.idNonce,
secrets)
2019-12-16 19:38:45 +00:00
var zeroNonce: array[gcmNonceSize, byte]
let respData = decryptGCM(secrets.authRespKey, zeroNonce, head.response, [])
if respData.isNone():
return false
let authResp = rlp.decode(respData.get(), AuthResponse)
# TODO:
# 1. Should allow for not having an ENR included, solved for now by sending
# whoareyou with always recordSeq of 0
# 2. Should verify ENR and check for correct id in case an ENR is included
# 3. Should verify id nonce signature
2019-12-16 19:38:45 +00:00
# More TODO:
# 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
2019-12-16 19:38:45 +00:00
newNode = newNode(authResp.record)
return true
2020-02-22 18:49:14 +00:00
proc decodeEncrypted*(c: var Codec,
fromId: NodeID,
fromAddr: Address,
input: seq[byte],
authTag: var AuthTag,
2020-02-22 18:49:14 +00:00
newNode: var Node,
packet: var Packet): DecodeStatus =
2019-12-16 19:38:45 +00:00
let input = input.toRange
var r = rlpFromBytes(input[tagSize .. ^1])
2019-12-16 19:38:45 +00:00
var auth: AuthHeader
var readKey: AesKey
2020-02-27 18:09:05 +00:00
logScope: sender = $fromAddr
2019-12-16 19:38:45 +00:00
if r.isList:
# Handshake - rlp list indicates auth-header
2019-12-16 19:38:45 +00:00
auth = r.read(AuthHeader)
authTag = auth.auth
let key = HandShakeKey(nodeId: fromId, address: $fromAddr)
let challenge = c.handshakes.getOrDefault(key)
2019-12-16 19:38:45 +00:00
if challenge.isNil:
2020-02-22 18:49:14 +00:00
trace "Decoding failed (no challenge)"
return HandshakeError
2019-12-16 19:38:45 +00:00
if auth.idNonce != challenge.idNonce:
2020-02-22 18:49:14 +00:00
trace "Decoding failed (different nonce)"
return HandshakeError
2019-12-16 19:38:45 +00:00
var sec: HandshakeSecrets
if not c.decodeAuthResp(fromId, auth, challenge, sec, newNode):
2020-02-22 18:49:14 +00:00
trace "Decoding failed (bad auth)"
return HandshakeError
c.handshakes.del(key)
2019-12-16 19:38:45 +00:00
# For an incoming handshake, we are not sure the address in the ENR is there
# and if it is the real external IP, so we use the one we know from the
# UDP packet.
updateEndpoint(newNode, fromAddr)
2019-12-16 19:38:45 +00:00
# Swap keys to match remote
swap(sec.readKey, sec.writeKey)
# TODO: is it safe to ignore the error here?
discard c.db.storeKeys(fromId, fromAddr, sec.readKey, sec.writeKey)
2019-12-16 19:38:45 +00:00
readKey = sec.readKey
else:
# Message packet or random packet - rlp bytes (size 12) indicates auth-tag
authTag = r.read(AuthTag)
2019-12-16 19:38:45 +00:00
auth.auth = authTag
var writeKey: AesKey
2019-12-16 19:38:45 +00:00
if not c.db.loadKeys(fromId, fromAddr, readKey, writeKey):
2020-02-22 18:49:14 +00:00
trace "Decoding failed (no keys)"
return DecryptError
2019-12-16 19:38:45 +00:00
# doAssert(false, "TODO: HANDLE ME!")
let headSize = tagSize + r.position
2019-12-16 19:38:45 +00:00
let bodyEnc = input[headSize .. ^1]
let body = decryptGCM(readKey, auth.auth, bodyEnc.toOpenArray,
input[0 .. tagSize - 1].toOpenArray)
if body.isNone():
discard c.db.deleteKeys(fromId, fromAddr)
return DecryptError
let packetData = body.get()
if packetData.len > 1:
let status = decodePacketBody(packetData[0],
packetData.toOpenArray(1, packetData.high), packet)
2020-02-27 18:09:05 +00:00
if status == decodingSuccessful:
return Success
2020-02-27 18:09:05 +00:00
else:
debug "Failed to decode discovery packet", reason = status
return PacketError
else:
return PacketError
2019-12-16 19:38:45 +00:00
proc newRequestId*(): RequestId =
2020-01-08 13:25:27 +00:00
if randomBytes(addr result, sizeof(result)) != sizeof(result):
raise newException(RandomSourceDepleted, "Could not randomize bytes")
2019-12-16 19:38:45 +00:00
proc numFields(T: typedesc): int =
for k, v in fieldPairs(default(T)): inc result
proc encodePacket*[T: SomePacket](p: T, reqId: RequestId): seq[byte] =
result = newSeqOfCap[byte](64)
result.add(packetKind(T).ord)
# result.add(rlp.encode(p))
const sz = numFields(T)
var writer = initRlpList(sz + 1)
writer.append(reqId)
for k, v in fieldPairs(p):
writer.append(v)
result.add(writer.finish())
proc encodePacket*[T: SomePacket](p: T): seq[byte] =
encodePacket(p, newRequestId())