657 lines
19 KiB
Nim
657 lines
19 KiB
Nim
# Nim-LibP2P
|
|
# Copyright (c) 2023-2024 Status Research & Development GmbH
|
|
# Licensed under either of
|
|
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
|
|
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
|
|
# at your option.
|
|
# This file may not be copied, modified, or distributed except according to
|
|
# those terms.
|
|
|
|
{.push raises: [].}
|
|
|
|
import std/strformat
|
|
import chronos
|
|
import chronicles
|
|
import bearssl/[rand, hash]
|
|
import stew/[endians2, byteutils]
|
|
import nimcrypto/[utils, sha2, hmac]
|
|
import ../../stream/[connection, streamseq]
|
|
import ../../peerid
|
|
import ../../peerinfo
|
|
import ../../protobuf/minprotobuf
|
|
import ../../utility
|
|
import ../../errors
|
|
|
|
import secure, ../../crypto/[crypto, chacha20poly1305, curve25519, hkdf]
|
|
|
|
when defined(libp2p_dump):
|
|
import ../../debugutils
|
|
|
|
logScope:
|
|
topics = "libp2p noise"
|
|
|
|
const
|
|
# https://godoc.org/github.com/libp2p/go-libp2p-noise#pkg-constants
|
|
NoiseCodec* = "/noise"
|
|
|
|
PayloadString = toBytes("noise-libp2p-static-key:")
|
|
|
|
ProtocolXXName = "Noise_XX_25519_ChaChaPoly_SHA256"
|
|
|
|
# Empty is a special value which indicates k has not yet been initialized.
|
|
EmptyKey = default(ChaChaPolyKey)
|
|
NonceMax = uint64.high - 1 # max is reserved
|
|
NoiseSize = 32
|
|
MaxPlainSize = int(uint16.high - NoiseSize - ChaChaPolyTag.len)
|
|
|
|
HandshakeTimeout = 1.minutes
|
|
|
|
type
|
|
KeyPair = object
|
|
privateKey: Curve25519Key
|
|
publicKey: Curve25519Key
|
|
|
|
# https://noiseprotocol.org/noise.html#the-cipherstate-object
|
|
CipherState = object
|
|
k: ChaChaPolyKey
|
|
n: uint64
|
|
|
|
# https://noiseprotocol.org/noise.html#the-symmetricstate-object
|
|
SymmetricState = object
|
|
cs: CipherState
|
|
ck: ChaChaPolyKey
|
|
h: MDigest[256]
|
|
|
|
# https://noiseprotocol.org/noise.html#the-handshakestate-object
|
|
HandshakeState = object
|
|
ss: SymmetricState
|
|
s: KeyPair
|
|
e: KeyPair
|
|
rs: Curve25519Key
|
|
re: Curve25519Key
|
|
|
|
HandshakeResult = object
|
|
cs1: CipherState
|
|
cs2: CipherState
|
|
remoteP2psecret: seq[byte]
|
|
rs: Curve25519Key
|
|
|
|
Noise* = ref object of Secure
|
|
rng: ref HmacDrbgContext
|
|
localPrivateKey: PrivateKey
|
|
localPublicKey: seq[byte]
|
|
noiseKeys: KeyPair
|
|
commonPrologue: seq[byte]
|
|
outgoing: bool
|
|
|
|
NoiseConnection* = ref object of SecureConn
|
|
readCs: CipherState
|
|
writeCs: CipherState
|
|
|
|
NoiseError* = object of LPStreamError
|
|
NoiseHandshakeError* = object of NoiseError
|
|
NoiseDecryptTagError* = object of NoiseError
|
|
NoiseOversizedPayloadError* = object of NoiseError
|
|
NoiseNonceMaxError* = object of NoiseError # drop connection on purpose
|
|
|
|
# Utility
|
|
|
|
func shortLog*(conn: NoiseConnection): auto =
|
|
try:
|
|
if conn == nil:
|
|
"NoiseConnection(nil)"
|
|
else:
|
|
&"{shortLog(conn.peerId)}:{conn.oid}"
|
|
except ValueError as exc:
|
|
raiseAssert(exc.msg)
|
|
|
|
chronicles.formatIt(NoiseConnection):
|
|
shortLog(it)
|
|
|
|
proc genKeyPair(rng: var HmacDrbgContext): KeyPair =
|
|
result.privateKey = Curve25519Key.random(rng)
|
|
result.publicKey = result.privateKey.public()
|
|
|
|
proc hashProtocol(name: string): MDigest[256] =
|
|
# If protocol_name is less than or equal to HASHLEN bytes in length,
|
|
# sets h to protocol_name with zero bytes appended to make HASHLEN bytes.
|
|
# Otherwise sets h = HASH(protocol_name).
|
|
|
|
if name.len <= 32:
|
|
result.data[0 .. name.high] = name.toBytes
|
|
else:
|
|
result = sha256.digest(name)
|
|
|
|
proc dh(priv: Curve25519Key, pub: Curve25519Key): Curve25519Key =
|
|
result = pub
|
|
Curve25519.mul(result, priv)
|
|
|
|
# Cipherstate
|
|
|
|
proc hasKey(cs: CipherState): bool =
|
|
cs.k != EmptyKey
|
|
|
|
proc encrypt(
|
|
state: var CipherState, data: var openArray[byte], ad: openArray[byte]
|
|
): ChaChaPolyTag {.noinit, raises: [NoiseNonceMaxError].} =
|
|
var nonce: ChaChaPolyNonce
|
|
nonce[4 ..< 12] = toBytesLE(state.n)
|
|
|
|
ChaChaPoly.encrypt(state.k, nonce, result, data, ad)
|
|
|
|
inc state.n
|
|
if state.n > NonceMax:
|
|
raise (ref NoiseNonceMaxError)(msg: "Noise max nonce value reached")
|
|
|
|
proc encryptWithAd(
|
|
state: var CipherState, ad, data: openArray[byte]
|
|
): seq[byte] {.raises: [NoiseNonceMaxError].} =
|
|
result = newSeqOfCap[byte](data.len + sizeof(ChaChaPolyTag))
|
|
result.add(data)
|
|
|
|
let tag = encrypt(state, result, ad)
|
|
|
|
result.add(tag)
|
|
|
|
trace "encryptWithAd",
|
|
tag = byteutils.toHex(tag), data = result.shortLog, nonce = state.n - 1
|
|
|
|
proc decryptWithAd(
|
|
state: var CipherState, ad, data: openArray[byte]
|
|
): seq[byte] {.raises: [NoiseDecryptTagError, NoiseNonceMaxError].} =
|
|
var
|
|
tagIn = data.toOpenArray(data.len - ChaChaPolyTag.len, data.high).intoChaChaPolyTag
|
|
tagOut: ChaChaPolyTag
|
|
nonce: ChaChaPolyNonce
|
|
nonce[4 ..< 12] = toBytesLE(state.n)
|
|
result = data[0 .. (data.high - ChaChaPolyTag.len)]
|
|
ChaChaPoly.decrypt(state.k, nonce, tagOut, result, ad)
|
|
trace "decryptWithAd",
|
|
tagIn = tagIn.shortLog, tagOut = tagOut.shortLog, nonce = state.n
|
|
if tagIn != tagOut:
|
|
debug "decryptWithAd failed", data = shortLog(data)
|
|
raise (ref NoiseDecryptTagError)(msg: "decryptWithAd failed tag authentication.")
|
|
inc state.n
|
|
if state.n > NonceMax:
|
|
raise (ref NoiseNonceMaxError)(msg: "Noise max nonce value reached")
|
|
|
|
# Symmetricstate
|
|
|
|
proc init(_: type[SymmetricState]): SymmetricState =
|
|
result.h = ProtocolXXName.hashProtocol
|
|
result.ck = result.h.data.intoChaChaPolyKey
|
|
result.cs = CipherState(k: EmptyKey)
|
|
|
|
proc mixKey(ss: var SymmetricState, ikm: ChaChaPolyKey) =
|
|
var temp_keys: array[2, ChaChaPolyKey]
|
|
sha256.hkdf(ss.ck, ikm, [], temp_keys)
|
|
ss.ck = temp_keys[0]
|
|
ss.cs = CipherState(k: temp_keys[1])
|
|
trace "mixKey", key = ss.cs.k.shortLog
|
|
|
|
proc mixHash(ss: var SymmetricState, data: openArray[byte]) =
|
|
var ctx: sha256
|
|
ctx.init()
|
|
ctx.update(ss.h.data)
|
|
ctx.update(data)
|
|
ss.h = ctx.finish()
|
|
trace "mixHash", hash = ss.h.data.shortLog
|
|
|
|
# We might use this for other handshake patterns/tokens
|
|
proc mixKeyAndHash(ss: var SymmetricState, ikm: openArray[byte]) {.used.} =
|
|
var temp_keys: array[3, ChaChaPolyKey]
|
|
sha256.hkdf(ss.ck, ikm, [], temp_keys)
|
|
ss.ck = temp_keys[0]
|
|
ss.mixHash(temp_keys[1])
|
|
ss.cs = CipherState(k: temp_keys[2])
|
|
|
|
proc encryptAndHash(
|
|
ss: var SymmetricState, data: openArray[byte]
|
|
): seq[byte] {.raises: [NoiseNonceMaxError].} =
|
|
# according to spec if key is empty leave plaintext
|
|
if ss.cs.hasKey:
|
|
result = ss.cs.encryptWithAd(ss.h.data, data)
|
|
else:
|
|
result = @data
|
|
ss.mixHash(result)
|
|
|
|
proc decryptAndHash(
|
|
ss: var SymmetricState, data: openArray[byte]
|
|
): seq[byte] {.raises: [NoiseDecryptTagError, NoiseNonceMaxError].} =
|
|
# according to spec if key is empty leave plaintext
|
|
if ss.cs.hasKey and data.len > ChaChaPolyTag.len:
|
|
result = ss.cs.decryptWithAd(ss.h.data, data)
|
|
else:
|
|
result = @data
|
|
ss.mixHash(data)
|
|
|
|
proc split(ss: var SymmetricState): tuple[cs1, cs2: CipherState] =
|
|
var temp_keys: array[2, ChaChaPolyKey]
|
|
sha256.hkdf(ss.ck, [], [], temp_keys)
|
|
return (CipherState(k: temp_keys[0]), CipherState(k: temp_keys[1]))
|
|
|
|
proc init(_: type[HandshakeState]): HandshakeState =
|
|
result.ss = SymmetricState.init()
|
|
|
|
template write_e(): untyped =
|
|
trace "noise write e"
|
|
# Sets e (which must be empty) to GENERATE_KEYPAIR().
|
|
# Appends e.public_key to the buffer. Calls MixHash(e.public_key).
|
|
hs.e = genKeyPair(p.rng[])
|
|
msg.add hs.e.publicKey
|
|
hs.ss.mixHash(hs.e.publicKey)
|
|
|
|
template write_s(): untyped =
|
|
trace "noise write s"
|
|
# Appends EncryptAndHash(s.public_key) to the buffer.
|
|
msg.add hs.ss.encryptAndHash(hs.s.publicKey)
|
|
|
|
template dh_ee(): untyped =
|
|
trace "noise dh ee"
|
|
# Calls MixKey(DH(e, re)).
|
|
hs.ss.mixKey(dh(hs.e.privateKey, hs.re))
|
|
|
|
template dh_es(): untyped =
|
|
trace "noise dh es"
|
|
# Calls MixKey(DH(e, rs)) if initiator, MixKey(DH(s, re)) if responder.
|
|
when initiator:
|
|
hs.ss.mixKey(dh(hs.e.privateKey, hs.rs))
|
|
else:
|
|
hs.ss.mixKey(dh(hs.s.privateKey, hs.re))
|
|
|
|
template dh_se(): untyped =
|
|
trace "noise dh se"
|
|
# Calls MixKey(DH(s, re)) if initiator, MixKey(DH(e, rs)) if responder.
|
|
when initiator:
|
|
hs.ss.mixKey(dh(hs.s.privateKey, hs.re))
|
|
else:
|
|
hs.ss.mixKey(dh(hs.e.privateKey, hs.rs))
|
|
|
|
# might be used for other token/handshakes
|
|
template dh_ss(): untyped {.used.} =
|
|
trace "noise dh ss"
|
|
# Calls MixKey(DH(s, rs)).
|
|
hs.ss.mixKey(dh(hs.s.privateKey, hs.rs))
|
|
|
|
template read_e(): untyped =
|
|
trace "noise read e", size = msg.len
|
|
|
|
if msg.len < Curve25519Key.len:
|
|
raise (ref NoiseHandshakeError)(msg: "Noise E, expected more data")
|
|
|
|
# Sets re (which must be empty) to the next DHLEN bytes from the message.
|
|
# Calls MixHash(re.public_key).
|
|
hs.re[0 .. Curve25519Key.high] = msg.toOpenArray(0, Curve25519Key.high)
|
|
msg.consume(Curve25519Key.len)
|
|
hs.ss.mixHash(hs.re)
|
|
|
|
template read_s(): untyped =
|
|
trace "noise read s", size = msg.len
|
|
# Sets temp to the next DHLEN + 16 bytes of the message if HasKey() == True,
|
|
# or to the next DHLEN bytes otherwise.
|
|
# Sets rs (which must be empty) to DecryptAndHash(temp).
|
|
let rsLen =
|
|
if hs.ss.cs.hasKey:
|
|
if msg.len < Curve25519Key.len + ChaChaPolyTag.len:
|
|
raise (ref NoiseHandshakeError)(msg: "Noise S, expected more data")
|
|
Curve25519Key.len + ChaChaPolyTag.len
|
|
else:
|
|
if msg.len < Curve25519Key.len:
|
|
raise (ref NoiseHandshakeError)(msg: "Noise S, expected more data")
|
|
Curve25519Key.len
|
|
hs.rs[0 .. Curve25519Key.high] = hs.ss.decryptAndHash(msg.toOpenArray(0, rsLen - 1))
|
|
|
|
msg.consume(rsLen)
|
|
|
|
proc readFrame(
|
|
sconn: Connection
|
|
): Future[seq[byte]] {.async: (raises: [CancelledError, LPStreamError]).} =
|
|
var besize {.noinit.}: array[2, byte]
|
|
await sconn.readExactly(addr besize[0], besize.len)
|
|
let size = uint16.fromBytesBE(besize).int
|
|
trace "readFrame", sconn, size
|
|
if size == 0:
|
|
return
|
|
|
|
var buffer = newSeqUninitialized[byte](size)
|
|
await sconn.readExactly(addr buffer[0], buffer.len)
|
|
return buffer
|
|
|
|
proc writeFrame(
|
|
sconn: Connection, buf: openArray[byte]
|
|
): Future[void] {.async: (raises: [CancelledError, LPStreamError], raw: true).} =
|
|
doAssert buf.len <= uint16.high.int
|
|
var
|
|
lesize = buf.len.uint16
|
|
besize = lesize.toBytesBE
|
|
outbuf = newSeqOfCap[byte](besize.len + buf.len)
|
|
trace "writeFrame", sconn, size = lesize, data = shortLog(buf)
|
|
outbuf &= besize
|
|
outbuf &= buf
|
|
sconn.write(outbuf)
|
|
|
|
proc receiveHSMessage(
|
|
sconn: Connection
|
|
): Future[seq[byte]] {.async: (raises: [CancelledError, LPStreamError], raw: true).} =
|
|
readFrame(sconn)
|
|
|
|
proc sendHSMessage(
|
|
sconn: Connection, buf: openArray[byte]
|
|
): Future[void] {.async: (raises: [CancelledError, LPStreamError], raw: true).} =
|
|
writeFrame(sconn, buf)
|
|
|
|
proc handshakeXXOutbound(
|
|
p: Noise, conn: Connection, p2pSecret: seq[byte]
|
|
): Future[HandshakeResult] {.async: (raises: [CancelledError, LPStreamError]).} =
|
|
const initiator = true
|
|
var hs = HandshakeState.init()
|
|
|
|
try:
|
|
hs.ss.mixHash(p.commonPrologue)
|
|
hs.s = p.noiseKeys
|
|
|
|
# -> e
|
|
var msg: StreamSeq
|
|
|
|
write_e()
|
|
|
|
# IK might use this btw!
|
|
msg.add hs.ss.encryptAndHash([])
|
|
|
|
await conn.sendHSMessage(msg.data)
|
|
|
|
# <- e, ee, s, es
|
|
|
|
msg.assign(await conn.receiveHSMessage())
|
|
|
|
read_e()
|
|
dh_ee()
|
|
read_s()
|
|
dh_es()
|
|
|
|
let remoteP2psecret = hs.ss.decryptAndHash(msg.data)
|
|
msg.clear()
|
|
|
|
# -> s, se
|
|
|
|
write_s()
|
|
dh_se()
|
|
|
|
# last payload must follow the encrypted way of sending
|
|
msg.add hs.ss.encryptAndHash(p2pSecret)
|
|
|
|
await conn.sendHSMessage(msg.data)
|
|
|
|
let (cs1, cs2) = hs.ss.split()
|
|
return
|
|
HandshakeResult(cs1: cs1, cs2: cs2, remoteP2psecret: remoteP2psecret, rs: hs.rs)
|
|
finally:
|
|
burnMem(hs)
|
|
|
|
proc handshakeXXInbound(
|
|
p: Noise, conn: Connection, p2pSecret: seq[byte]
|
|
): Future[HandshakeResult] {.async: (raises: [CancelledError, LPStreamError]).} =
|
|
const initiator = false
|
|
|
|
var hs = HandshakeState.init()
|
|
|
|
try:
|
|
hs.ss.mixHash(p.commonPrologue)
|
|
hs.s = p.noiseKeys
|
|
|
|
# -> e
|
|
|
|
var msg: StreamSeq
|
|
msg.add(await conn.receiveHSMessage())
|
|
|
|
read_e()
|
|
|
|
# we might use this early data one day, keeping it here for clarity
|
|
let earlyData {.used.} = hs.ss.decryptAndHash(msg.data)
|
|
|
|
# <- e, ee, s, es
|
|
|
|
msg.consume(msg.len)
|
|
|
|
write_e()
|
|
dh_ee()
|
|
write_s()
|
|
dh_es()
|
|
|
|
msg.add hs.ss.encryptAndHash(p2pSecret)
|
|
|
|
await conn.sendHSMessage(msg.data)
|
|
msg.clear()
|
|
|
|
# -> s, se
|
|
|
|
msg.add(await conn.receiveHSMessage())
|
|
|
|
read_s()
|
|
dh_se()
|
|
|
|
let
|
|
remoteP2psecret = hs.ss.decryptAndHash(msg.data)
|
|
(cs1, cs2) = hs.ss.split()
|
|
return
|
|
HandshakeResult(cs1: cs1, cs2: cs2, remoteP2psecret: remoteP2psecret, rs: hs.rs)
|
|
finally:
|
|
burnMem(hs)
|
|
|
|
method readMessage*(
|
|
sconn: NoiseConnection
|
|
): Future[seq[byte]] {.async: (raises: [CancelledError, LPStreamError]).} =
|
|
while true: # Discard 0-length payloads
|
|
let frame = await sconn.stream.readFrame()
|
|
sconn.activity = true
|
|
if frame.len > ChaChaPolyTag.len:
|
|
let res = sconn.readCs.decryptWithAd([], frame)
|
|
if res.len > 0:
|
|
when defined(libp2p_dump):
|
|
dumpMessage(sconn, FlowDirection.Incoming, res)
|
|
return res
|
|
|
|
when defined(libp2p_dump):
|
|
dumpMessage(sconn, FlowDirection.Incoming, [])
|
|
trace "Received 0-length message", sconn
|
|
|
|
proc encryptFrame(
|
|
sconn: NoiseConnection, cipherFrame: var openArray[byte], src: openArray[byte]
|
|
) {.raises: [NoiseNonceMaxError].} =
|
|
# Frame consists of length + cipher data + tag
|
|
doAssert src.len <= MaxPlainSize
|
|
doAssert cipherFrame.len == 2 + src.len + sizeof(ChaChaPolyTag)
|
|
|
|
cipherFrame[0 ..< 2] = toBytesBE(uint16(src.len + sizeof(ChaChaPolyTag)))
|
|
cipherFrame[2 ..< 2 + src.len()] = src
|
|
|
|
let tag = encrypt(sconn.writeCs, cipherFrame.toOpenArray(2, 2 + src.len() - 1), [])
|
|
|
|
cipherFrame[2 + src.len() ..< cipherFrame.len] = tag
|
|
|
|
method write*(
|
|
sconn: NoiseConnection, message: seq[byte]
|
|
): Future[void] {.async: (raises: [CancelledError, LPStreamError], raw: true).} =
|
|
# Fast path: `{.async.}` would introduce a copy of `message`
|
|
const FramingSize = 2 + sizeof(ChaChaPolyTag)
|
|
|
|
let frames = (message.len + MaxPlainSize - 1) div MaxPlainSize
|
|
|
|
var
|
|
cipherFrames = newSeqUninitialized[byte](message.len + frames * FramingSize)
|
|
left = message.len
|
|
offset = 0
|
|
woffset = 0
|
|
|
|
while left > 0:
|
|
let chunkSize = min(MaxPlainSize, left)
|
|
|
|
try:
|
|
encryptFrame(
|
|
sconn,
|
|
cipherFrames.toOpenArray(woffset, woffset + chunkSize + FramingSize - 1),
|
|
message.toOpenArray(offset, offset + chunkSize - 1),
|
|
)
|
|
except NoiseNonceMaxError as exc:
|
|
debug "Noise nonce exceeded"
|
|
let fut = newFuture[void]("noise.write.nonce")
|
|
fut.fail(exc)
|
|
return fut
|
|
|
|
when defined(libp2p_dump):
|
|
dumpMessage(
|
|
sconn,
|
|
FlowDirection.Outgoing,
|
|
message.toOpenArray(offset, offset + chunkSize - 1),
|
|
)
|
|
|
|
left = left - chunkSize
|
|
offset += chunkSize
|
|
woffset += chunkSize + FramingSize
|
|
|
|
sconn.activity = true
|
|
|
|
# Write all `cipherFrames` in a single write, to avoid interleaving /
|
|
# sequencing issues
|
|
sconn.stream.write(cipherFrames)
|
|
|
|
method handshake*(
|
|
p: Noise, conn: Connection, initiator: bool, peerId: Opt[PeerId]
|
|
): Future[SecureConn] {.async: (raises: [CancelledError, LPStreamError]).} =
|
|
trace "Starting Noise handshake", conn, initiator
|
|
|
|
let timeout = conn.timeout
|
|
conn.timeout = HandshakeTimeout
|
|
|
|
# https://github.com/libp2p/specs/tree/master/noise#libp2p-data-in-handshake-messages
|
|
let signedPayload =
|
|
p.localPrivateKey.sign(PayloadString & p.noiseKeys.publicKey.getBytes)
|
|
if signedPayload.isErr():
|
|
raise (ref NoiseHandshakeError)(
|
|
msg: "Failed to sign public key: " & $signedPayload.error()
|
|
)
|
|
|
|
var libp2pProof = initProtoBuffer()
|
|
libp2pProof.write(1, p.localPublicKey)
|
|
libp2pProof.write(2, signedPayload.get().getBytes())
|
|
# data field also there but not used!
|
|
libp2pProof.finish()
|
|
|
|
var handshakeRes =
|
|
if initiator:
|
|
await handshakeXXOutbound(p, conn, libp2pProof.buffer)
|
|
else:
|
|
await handshakeXXInbound(p, conn, libp2pProof.buffer)
|
|
|
|
var secure =
|
|
try:
|
|
var
|
|
remoteProof = initProtoBuffer(handshakeRes.remoteP2psecret)
|
|
remotePubKey: PublicKey
|
|
remotePubKeyBytes: seq[byte]
|
|
remoteSig: Signature
|
|
remoteSigBytes: seq[byte]
|
|
|
|
if not remoteProof.getField(1, remotePubKeyBytes).valueOr(false):
|
|
raise (ref NoiseHandshakeError)(
|
|
msg:
|
|
"Failed to deserialize remote public key bytes. (initiator: " & $initiator &
|
|
")"
|
|
)
|
|
if not remoteProof.getField(2, remoteSigBytes).valueOr(false):
|
|
raise (ref NoiseHandshakeError)(
|
|
msg:
|
|
"Failed to deserialize remote signature bytes. (initiator: " & $initiator &
|
|
")"
|
|
)
|
|
|
|
if not remotePubKey.init(remotePubKeyBytes):
|
|
raise (ref NoiseHandshakeError)(
|
|
msg: "Failed to decode remote public key. (initiator: " & $initiator & ")"
|
|
)
|
|
if not remoteSig.init(remoteSigBytes):
|
|
raise (ref NoiseHandshakeError)(
|
|
msg: "Failed to decode remote signature. (initiator: " & $initiator & ")"
|
|
)
|
|
|
|
let verifyPayload = PayloadString & handshakeRes.rs.getBytes
|
|
if not remoteSig.verify(verifyPayload, remotePubKey):
|
|
raise (ref NoiseHandshakeError)(msg: "Noise handshake signature verify failed.")
|
|
else:
|
|
trace "Remote signature verified", conn
|
|
|
|
let pid = PeerId.init(remotePubKey).valueOr:
|
|
raise (ref NoiseHandshakeError)(msg: "Invalid remote peer id: " & $error)
|
|
|
|
trace "Remote peer id", pid = $pid
|
|
|
|
peerId.withValue(targetPid):
|
|
if not targetPid.validate():
|
|
raise (ref NoiseHandshakeError)(msg: "Failed to validate expected peerId.")
|
|
|
|
if pid != targetPid:
|
|
var failedKey: PublicKey
|
|
discard extractPublicKey(targetPid, failedKey)
|
|
debug "Noise handshake, peer id doesn't match!",
|
|
initiator,
|
|
dealt_peer = conn,
|
|
dealt_key = $failedKey,
|
|
received_peer = $pid,
|
|
received_key = $remotePubKey
|
|
raise (ref NoiseHandshakeError)(
|
|
msg: "Noise handshake, peer id don't match! " & $pid & " != " & $targetPid
|
|
)
|
|
conn.peerId = pid
|
|
|
|
var tmp = NoiseConnection.new(conn, conn.peerId, conn.observedAddr)
|
|
if initiator:
|
|
tmp.readCs = handshakeRes.cs2
|
|
tmp.writeCs = handshakeRes.cs1
|
|
else:
|
|
tmp.readCs = handshakeRes.cs1
|
|
tmp.writeCs = handshakeRes.cs2
|
|
tmp
|
|
finally:
|
|
burnMem(handshakeRes)
|
|
|
|
trace "Noise handshake completed!", initiator, peer = shortLog(secure.peerId)
|
|
|
|
conn.timeout = timeout
|
|
|
|
return secure
|
|
|
|
method closeImpl*(s: NoiseConnection) {.async: (raises: []).} =
|
|
await procCall SecureConn(s).closeImpl()
|
|
|
|
burnMem(s.readCs)
|
|
burnMem(s.writeCs)
|
|
|
|
method init*(p: Noise) {.gcsafe.} =
|
|
procCall Secure(p).init()
|
|
p.codec = NoiseCodec
|
|
|
|
proc new*(
|
|
T: typedesc[Noise],
|
|
rng: ref HmacDrbgContext,
|
|
privateKey: PrivateKey,
|
|
outgoing: bool = true,
|
|
commonPrologue: seq[byte] = @[],
|
|
): T =
|
|
let pkBytes = privateKey
|
|
.getPublicKey()
|
|
.expect("Expected valid Private Key")
|
|
.getBytes()
|
|
.expect("Couldn't get public Key bytes")
|
|
|
|
var noise = Noise(
|
|
rng: rng,
|
|
outgoing: outgoing,
|
|
localPrivateKey: privateKey,
|
|
localPublicKey: pkBytes,
|
|
noiseKeys: genKeyPair(rng[]),
|
|
commonPrologue: commonPrologue,
|
|
)
|
|
|
|
noise.init()
|
|
noise
|