PoC implementation of Waku Pairing and Secure Transfer (#1117)

* feat(noise): add PoC implementation for WakuPairing and Secure Transfer
This commit is contained in:
G 2022-09-12 02:23:14 +02:00 committed by GitHub
parent 797c82b030
commit 01cd201f58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 645 additions and 90 deletions

View File

@ -35,7 +35,8 @@ import
./v2/test_waku_discv5,
./v2/test_enr_utils,
./v2/test_peer_exchange,
./v2/test_waku_noise
./v2/test_waku_noise,
./v2/test_waku_noise_sessions
when defined(rln) or defined(rlnzerokit):
import

View File

@ -13,6 +13,7 @@ import
../../waku/v2/protocol/waku_message,
../test_helpers,
libp2p/crypto/chacha20poly1305,
libp2p/protobuf/minprotobuf,
stew/endians2
@ -534,21 +535,22 @@ procSuite "Waku Noise":
payload2: PayloadV2
message: seq[byte]
readMessage: seq[byte]
defaultMessageNametagBuffer: MessageNametagBuffer
for _ in 0..10:
# Alice writes to Bob
message = randomSeqByte(rng[], 32)
payload2 = writeMessage(aliceHSResult, message)
readMessage = readMessage(bobHSResult, payload2).get()
payload2 = writeMessage(aliceHSResult, message, defaultMessageNametagBuffer)
readMessage = readMessage(bobHSResult, payload2, defaultMessageNametagBuffer).get()
check:
message == readMessage
# Bob writes to Alice
message = randomSeqByte(rng[], 32)
payload2 = writeMessage(bobHSResult, message)
readMessage = readMessage(aliceHSResult, payload2).get()
payload2 = writeMessage(bobHSResult, message, defaultMessageNametagBuffer)
readMessage = readMessage(aliceHSResult, payload2, defaultMessageNametagBuffer).get()
check:
message == readMessage
@ -640,21 +642,22 @@ procSuite "Waku Noise":
payload2: PayloadV2
message: seq[byte]
readMessage: seq[byte]
defaultMessageNametagBuffer: MessageNametagBuffer
for _ in 0..10:
# Alice writes to Bob
message = randomSeqByte(rng[], 32)
payload2 = writeMessage(aliceHSResult, message)
readMessage = readMessage(bobHSResult, payload2).get()
payload2 = writeMessage(aliceHSResult, message, defaultMessageNametagBuffer)
readMessage = readMessage(bobHSResult, payload2, defaultMessageNametagBuffer).get()
check:
message == readMessage
# Bob writes to Alice
message = randomSeqByte(rng[], 32)
payload2 = writeMessage(bobHSResult, message)
readMessage = readMessage(aliceHSResult, payload2).get()
payload2 = writeMessage(bobHSResult, message, defaultMessageNametagBuffer)
readMessage = readMessage(aliceHSResult, payload2, defaultMessageNametagBuffer).get()
check:
message == readMessage
@ -750,21 +753,22 @@ procSuite "Waku Noise":
payload2: PayloadV2
message: seq[byte]
readMessage: seq[byte]
defaultMessageNametagBuffer: MessageNametagBuffer
for _ in 0..10:
# Alice writes to Bob
message = randomSeqByte(rng[], 32)
payload2 = writeMessage(aliceHSResult, message)
readMessage = readMessage(bobHSResult, payload2).get()
payload2 = writeMessage(aliceHSResult, message, defaultMessageNametagBuffer)
readMessage = readMessage(bobHSResult, payload2, defaultMessageNametagBuffer).get()
check:
message == readMessage
# Bob writes to Alice
message = randomSeqByte(rng[], 32)
payload2 = writeMessage(bobHSResult, message)
readMessage = readMessage(aliceHSResult, payload2).get()
payload2 = writeMessage(bobHSResult, message, defaultMessageNametagBuffer)
readMessage = readMessage(aliceHSResult, payload2, defaultMessageNametagBuffer).get()
check:
message == readMessage
@ -860,21 +864,22 @@ procSuite "Waku Noise":
payload2: PayloadV2
message: seq[byte]
readMessage: seq[byte]
defaultMessageNametagBuffer: MessageNametagBuffer
for _ in 0..10:
# Alice writes to Bob
message = randomSeqByte(rng[], 32)
payload2 = writeMessage(aliceHSResult, message)
readMessage = readMessage(bobHSResult, payload2).get()
payload2 = writeMessage(aliceHSResult, message, defaultMessageNametagBuffer)
readMessage = readMessage(bobHSResult, payload2, defaultMessageNametagBuffer).get()
check:
message == readMessage
# Bob writes to Alice
message = randomSeqByte(rng[], 32)
payload2 = writeMessage(bobHSResult, message)
readMessage = readMessage(aliceHSResult, payload2).get()
payload2 = writeMessage(bobHSResult, message, defaultMessageNametagBuffer)
readMessage = readMessage(aliceHSResult, payload2, defaultMessageNametagBuffer).get()
check:
message == readMessage

View File

@ -0,0 +1,328 @@
{.used.}
import
testutils/unittests,
std/random,
std/strutils,
std/tables,
stew/byteutils,
../../waku/v2/node/waku_payload,
../../waku/v2/protocol/waku_noise/noise_types,
../../waku/v2/protocol/waku_noise/noise_utils,
../../waku/v2/protocol/waku_noise/noise,
../../waku/v2/protocol/waku_noise/noise_handshake_processing,
../../waku/v2/protocol/waku_message,
libp2p/protobuf/minprotobuf,
../test_helpers,
stew/endians2
procSuite "Waku Noise Sessions":
# We initialize the RNG in test_helpers
let rng = rng()
# We initialize the RNG in std/random
randomize()
# This test implements the Device pairing and Secure Transfers with Noise
# detailed in the 43/WAKU2-DEVICE-PAIRING RFC https://rfc.vac.dev/spec/43/
test "Noise Waku Pairing Handhshake and Secure transfer":
#########################
# Pairing Phase
#########################
let hsPattern = NoiseHandshakePatterns["WakuPairing"]
# Alice static/ephemeral key initialization and commitment
let aliceStaticKey = genKeyPair(rng[])
let aliceEphemeralKey = genKeyPair(rng[])
let s = randomSeqByte(rng[], 32)
let aliceCommittedStaticKey = commitPublicKey(getPublicKey(aliceStaticKey), s)
# Bob static/ephemeral key initialization and commitment
let bobStaticKey = genKeyPair(rng[])
let bobEphemeralKey = genKeyPair(rng[])
let r = randomSeqByte(rng[], 32)
let bobCommittedStaticKey = commitPublicKey(getPublicKey(bobStaticKey), r)
# Content Topic information
let applicationName = "waku-noise-sessions"
let applicationVersion = "0.1"
let shardId = "10"
let qrMessageNametag = randomSeqByte(rng[], MessageNametagLength)
# Out-of-band Communication
# Bob prepares the QR and sends it out-of-band to Alice
let qr = toQr(applicationName, applicationVersion, shardId, getPublicKey(bobEphemeralKey), bobCommittedStaticKey)
# Alice deserializes the QR code
let (readApplicationName, readApplicationVersion, readShardId, readEphemeralKey, readCommittedStaticKey) = fromQr(qr)
# We check if QR serialization/deserialization works
check:
applicationName == readApplicationName
applicationVersion == readApplicationVersion
shardId == readShardId
getPublicKey(bobEphemeralKey) == readEphemeralKey
bobCommittedStaticKey == readCommittedStaticKey
# We set the contentTopic from the content topic parameters exchanged in the QR
let contentTopic: ContentTopic = "/" & applicationName & "/" & applicationVersion & "/wakunoise/1/sessions_shard-" & shardId & "/proto"
###############
# Pre-handshake message
#
# <- eB {H(sB||r), contentTopicParams, messageNametag}
###############
let preMessagePKs: seq[NoisePublicKey] = @[toNoisePublicKey(getPublicKey(bobEphemeralKey))]
# We initialize the Handshake states.
# Note that we pass the whole qr serialization as prologue information
var aliceHS = initialize(hsPattern = hsPattern, ephemeralKey = aliceEphemeralKey, staticKey = aliceStaticKey, prologue = qr.toBytes, preMessagePKs = preMessagePKs, initiator = true)
var bobHS = initialize(hsPattern = hsPattern, ephemeralKey = bobEphemeralKey, staticKey = bobStaticKey, prologue = qr.toBytes, preMessagePKs = preMessagePKs)
###############
# Pairing Handshake
###############
var
sentTransportMessage: seq[byte]
aliceStep, bobStep: HandshakeStepResult
msgFromPb: ProtoResult[WakuMessage]
wakuMsg: WakuResult[WakuMessage]
pb: ProtoBuffer
readPayloadV2: PayloadV2
aliceMessageNametag, bobMessageNametag: MessageNametag
# Write and read calls alternate between Alice and Bob: the handhshake progresses by alternatively calling stepHandshake for each user
###############
# 1st step
#
# -> eA, eAeB {H(sA||s)} [authcode]
###############
# The messageNametag for the first handshake message is randomly generated and exchanged out-of-band
# and corresponds to qrMessageNametag
# We set the transport message to be H(sA||s)
sentTransportMessage = digestToSeq(aliceCommittedStaticKey)
# We ensure that digestToSeq and its inverse seqToDigest256 are correct
check:
seqToDigest256(sentTransportMessage) == aliceCommittedStaticKey
# By being the handshake initiator, Alice writes a Waku2 payload v2 containing her handshake message
# and the (encrypted) transport message
# The message is sent with a messageNametag equal to the one received through the QR code
aliceStep = stepHandshake(rng[], aliceHS, transportMessage = sentTransportMessage, messageNametag = qrMessageNametag).get()
###############################################
# We prepare a Waku message from Alice's payload2
wakuMsg = encodePayloadV2(aliceStep.payload2, contentTopic)
check:
wakuMsg.isOk()
wakuMsg.get().contentTopic == contentTopic
# At this point wakuMsg is sent over the Waku network and is received
# We simulate this by creating the ProtoBuffer from wakuMsg
pb = wakuMsg.get().encode()
# We decode the WakuMessage from the ProtoBuffer
msgFromPb = WakuMessage.init(pb.buffer)
check:
msgFromPb.isOk()
# We decode the payloadV2 from the WakuMessage
readPayloadV2 = decodePayloadV2(msgFromPb.get()).get()
check:
readPayloadV2 == aliceStep.payload2
###############################################
# Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
# Note that Bob verifies if the received payloadv2 has the expected messageNametag set
bobStep = stepHandshake(rng[], bobHS, readPayloadV2 = readPayloadV2, messageNametag = qrMessageNametag).get()
check:
bobStep.transportMessage == sentTransportMessage
# We generate an authorization code using the handshake state
let aliceAuthcode = genAuthcode(aliceHS)
let bobAuthcode = genAuthcode(bobHS)
# We check that they are equal. Note that this check has to be confirmed with a user interaction.
check:
aliceAuthcode == bobAuthcode
###############
# 2nd step
#
# <- sB, eAsB {r}
###############
# Alice and Bob update their local next messageNametag using the available handshake information
# During the handshake, messageNametag = HKDF(h), where h is the handshake hash value at the end of the last processed message
aliceMessageNametag = toMessageNametag(aliceHS)
bobMessageNametag = toMessageNametag(bobHS)
# We set as a transport message the commitment randomness r
sentTransportMessage = r
# At this step, Bob writes and returns a payload
bobStep = stepHandshake(rng[], bobHS, transportMessage = sentTransportMessage, messageNametag = bobMessageNametag).get()
###############################################
# We prepare a Waku message from Bob's payload2
wakuMsg = encodePayloadV2(bobStep.payload2, contentTopic)
check:
wakuMsg.isOk()
wakuMsg.get().contentTopic == contentTopic
# At this point wakuMsg is sent over the Waku network and is received
# We simulate this by creating the ProtoBuffer from wakuMsg
pb = wakuMsg.get().encode()
# We decode the WakuMessage from the ProtoBuffer
msgFromPb = WakuMessage.init(pb.buffer)
check:
msgFromPb.isOk()
# We decode the payloadV2 from the WakuMessage
readPayloadV2 = decodePayloadV2(msgFromPb.get()).get()
check:
readPayloadV2 == bobStep.payload2
###############################################
# While Alice reads and returns the (decrypted) transport message
aliceStep = stepHandshake(rng[], aliceHS, readPayloadV2 = readPayloadV2, messageNametag = aliceMessageNametag).get()
check:
aliceStep.transportMessage == sentTransportMessage
# Alice further checks if Bob's commitment opens to Bob's static key she just received
let expectedBobCommittedStaticKey = commitPublicKey(aliceHS.rs, aliceStep.transportMessage)
check:
expectedBobCommittedStaticKey == bobCommittedStaticKey
###############
# 3rd step
#
# -> sA, sAeB, sAsB {s}
###############
# Alice and Bob update their local next messageNametag using the available handshake information
aliceMessageNametag = toMessageNametag(aliceHS)
bobMessageNametag = toMessageNametag(bobHS)
# We set as a transport message the commitment randomness s
sentTransportMessage = s
# Similarly as in first step, Alice writes a Waku2 payload containing the handshake message and the (encrypted) transport message
aliceStep = stepHandshake(rng[], aliceHS, transportMessage = sentTransportMessage, messageNametag = aliceMessageNametag).get()
###############################################
# We prepare a Waku message from Bob's payload2
wakuMsg = encodePayloadV2(aliceStep.payload2, contentTopic)
check:
wakuMsg.isOk()
wakuMsg.get().contentTopic == contentTopic
# At this point wakuMsg is sent over the Waku network and is received
# We simulate this by creating the ProtoBuffer from wakuMsg
pb = wakuMsg.get().encode()
# We decode the WakuMessage from the ProtoBuffer
msgFromPb = WakuMessage.init(pb.buffer)
check:
msgFromPb.isOk()
# We decode the payloadV2 from the WakuMessage
readPayloadV2 = decodePayloadV2(msgFromPb.get()).get()
check:
readPayloadV2 == aliceStep.payload2
###############################################
# Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
bobStep = stepHandshake(rng[], bobHS, readPayloadV2 = readPayloadV2, messageNametag = bobMessageNametag).get()
check:
bobStep.transportMessage == sentTransportMessage
# Bob further checks if Alice's commitment opens to Alice's static key he just received
let expectedAliceCommittedStaticKey = commitPublicKey(bobHS.rs, bobStep.transportMessage)
check:
expectedAliceCommittedStaticKey == aliceCommittedStaticKey
#########################
# Secure Transfer Phase
#########################
# We finalize the handshake to retrieve the Inbound/Outbound Symmetric States
var aliceHSResult, bobHSResult: HandshakeResult
aliceHSResult = finalizeHandshake(aliceHS)
bobHSResult = finalizeHandshake(bobHS)
# We test read/write of random messages exchanged between Alice and Bob
var
payload2: PayloadV2
message: seq[byte]
readMessage: seq[byte]
# We test message exchange
# Note that we exchange more than the number of messages contained in the nametag buffer to test if they are filled correctly as the communication proceeds
for i in 0 .. 10 * MessageNametagBufferSize:
# Alice writes to Bob
message = randomSeqByte(rng[], 32)
payload2 = writeMessage(aliceHSResult, message, outboundMessageNametagBuffer = aliceHSResult.nametagsOutbound)
readMessage = readMessage(bobHSResult, payload2, inboundMessageNametagBuffer = bobHSResult.nametagsInbound).get()
check:
message == readMessage
# Bob writes to Alice
message = randomSeqByte(rng[], 32)
payload2 = writeMessage(bobHSResult, message, outboundMessageNametagBuffer = bobHSResult.nametagsOutbound)
readMessage = readMessage(aliceHSResult, payload2, inboundMessageNametagBuffer = aliceHSResult.nametagsInbound).get()
check:
message == readMessage
# We test how nametag buffers help in detecting lost messages
# Alice writes two messages to Bob, but only the second is received
message = randomSeqByte(rng[], 32)
payload2 = writeMessage(aliceHSResult, message, outboundMessageNametagBuffer = aliceHSResult.nametagsOutbound)
message = randomSeqByte(rng[], 32)
payload2 = writeMessage(aliceHSResult, message, outboundMessageNametagBuffer = aliceHSResult.nametagsOutbound)
expect NoiseSomeMessagesWereLost:
readMessage = readMessage(bobHSResult, payload2, inboundMessageNametagBuffer = bobHSResult.nametagsInbound).get()
# We adjust bob nametag buffer for next test (i.e. the missed message is correctly recovered)
delete(bobHSResult.nametagsInbound, 2)
message = randomSeqByte(rng[], 32)
payload2 = writeMessage(bobHSResult, message, outboundMessageNametagBuffer = bobHSResult.nametagsOutbound)
readMessage = readMessage(aliceHSResult, payload2, inboundMessageNametagBuffer = aliceHSResult.nametagsInbound).get()
check:
message == readMessage
# We test if a missing nametag is correctly detected
message = randomSeqByte(rng[], 32)
payload2 = writeMessage(aliceHSResult, message, outboundMessageNametagBuffer = aliceHSResult.nametagsOutbound)
delete(bobHSResult.nametagsInbound, 1)
expect NoiseMessageNametagError:
readMessage = readMessage(bobHSResult, payload2, inboundMessageNametagBuffer = bobHSResult.nametagsInbound).get()

View File

@ -100,7 +100,7 @@ proc decodePayloadV2*(message: WakuMessage): WakuResult[PayloadV2]
# Encodes a PayloadV2 to a WakuMessage
# Currently, this is just a wrapper over serializePayloadV2 and encryption/decryption is done on top (no KeyInfo)
proc encodePayloadV2*(payload2: PayloadV2): WakuResult[WakuMessage]
proc encodePayloadV2*(payload2: PayloadV2, contentTopic: ContentTopic = default(ContentTopic)): WakuResult[WakuMessage]
{.raises: [Defect, NoiseMalformedHandshake, NoisePublicKeyError].} =
# We attempt to encode the PayloadV2
@ -109,6 +109,6 @@ proc encodePayloadV2*(payload2: PayloadV2): WakuResult[WakuMessage]
return err("Failed to encode PayloadV2")
# If successful, we create and return a WakuMessage
let msg = WakuMessage(payload: serializedPayload2.get(), version: 2)
let msg = WakuMessage(payload: serializedPayload2.get(), version: 2, contentTopic: contentTopic)
return ok(msg)

View File

@ -230,24 +230,29 @@ proc mixKeyAndHash*(ss: var SymmetricState, inputKeyMaterial: openArray[byte]) {
# EncryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
# Combines encryptWithAd and mixHash
proc encryptAndHash*(ss: var SymmetricState, plaintext: openArray[byte]): seq[byte]
# Note that by setting extraAd, it is possible to pass extra additional data that will be concatenated to the ad specified by Noise (can be used to authenticate messageNametag)
proc encryptAndHash*(ss: var SymmetricState, plaintext: openArray[byte], extraAd: openArray[byte] = []): seq[byte]
{.raises: [Defect, NoiseNonceMaxError].} =
# The output ciphertext
var ciphertext: seq[byte]
# The additional data
let ad = @(ss.h.data) & @(extraAd)
# Note that if an encryption key is not set yet in the Cipher state, ciphertext will be equal to plaintex
ciphertext = ss.cs.encryptWithAd(ss.h.data, plaintext)
ciphertext = ss.cs.encryptWithAd(ad, plaintext)
# We call mixHash over the result
ss.mixHash(ciphertext)
return ciphertext
# DecryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
# Combines decryptWithAd and mixHash
proc decryptAndHash*(ss: var SymmetricState, ciphertext: openArray[byte]): seq[byte]
proc decryptAndHash*(ss: var SymmetricState, ciphertext: openArray[byte], extraAd: openArray[byte] = []): seq[byte]
{.raises: [Defect, NoiseDecryptTagError, NoiseNonceMaxError].} =
# The output plaintext
var plaintext: seq[byte]
# The additional data
let ad = @(ss.h.data) & @(extraAd)
# Note that if an encryption key is not set yet in the Cipher state, plaintext will be equal to ciphertext
plaintext = ss.cs.decryptWithAd(ss.h.data, ciphertext)
plaintext = ss.cs.decryptWithAd(ad, ciphertext)
# According to specification, the ciphertext enters mixHash (and not the plaintext)
ss.mixHash(ciphertext)
return plaintext

View File

@ -207,7 +207,8 @@ proc processPreMessagePatternTokens(hs: var HandshakeState, inPreMessagePKs: seq
raise newException(NoiseMalformedHandshake, "Invalid Token for pre-message pattern")
# This procedure encrypts/decrypts the implicit payload attached at the end of every message pattern
proc processMessagePatternPayload(hs: var HandshakeState, transportMessage: seq[byte]): seq[byte]
# An optional extraAd to pass extra additional data in encryption/decryption can be set (useful to authenticate messageNametag)
proc processMessagePatternPayload(hs: var HandshakeState, transportMessage: seq[byte], extraAd: openArray[byte] = []): seq[byte]
{.raises: [Defect, NoiseDecryptTagError, NoiseNonceMaxError].} =
var payload: seq[byte]
@ -220,11 +221,11 @@ proc processMessagePatternPayload(hs: var HandshakeState, transportMessage: seq[
# We decrypt the transportMessage, if any
if reading:
payload = hs.ss.decryptAndHash(transportMessage)
payload = hs.ss.decryptAndHash(transportMessage, extraAd)
payload = pkcs7_unpad(payload, NoisePaddingBlockSize)
elif writing:
payload = pkcs7_pad(transportMessage, NoisePaddingBlockSize)
payload = hs.ss.encryptAndHash(payload)
payload = hs.ss.encryptAndHash(payload, extraAd)
return payload
@ -461,10 +462,10 @@ proc initialize*(hsPattern: HandshakePattern, ephemeralKey: KeyPair = default(Ke
# Advances 1 step in handshake
# Each user in a handshake alternates writing and reading of handshake messages.
# If the user is writing the handshake message, the transport message (if not empty) has to be passed to transportMessage and readPayloadV2 can be left to its default value
# It the user is reading the handshake message, the read payload v2 has to be passed to readPayloadV2 and the transportMessage can be left to its default values.
proc stepHandshake*(rng: var rand.HmacDrbgContext, hs: var HandshakeState, readPayloadV2: PayloadV2 = default(PayloadV2), transportMessage: seq[byte] = @[]): Result[HandshakeStepResult, cstring]
{.raises: [Defect, NoiseHandshakeError, NoiseMalformedHandshake, NoisePublicKeyError, NoiseDecryptTagError, NoiseNonceMaxError].} =
# If the user is writing the handshake message, the transport message (if not empty) and eventually a non-empty message nametag has to be passed to transportMessage and messageNametag and readPayloadV2 can be left to its default value
# It the user is reading the handshake message, the read payload v2 has to be passed to readPayloadV2 and the transportMessage can be left to its default values. Decryption is skipped if the payloadv2 read doesn't have a message nametag equal to messageNametag (empty input nametags are converted to all-0 MessageNametagLength bytes arrays)
proc stepHandshake*(rng: var rand.HmacDrbgContext, hs: var HandshakeState, readPayloadV2: PayloadV2 = default(PayloadV2), transportMessage: seq[byte] = @[], messageNametag: openArray[byte] = []): Result[HandshakeStepResult, cstring]
{.raises: [Defect, NoiseHandshakeError, NoiseMessageNametagError, NoiseMalformedHandshake, NoisePublicKeyError, NoiseDecryptTagError, NoiseNonceMaxError].} =
var hsStepResult: HandshakeStepResult
@ -488,21 +489,27 @@ proc stepHandshake*(rng: var rand.HmacDrbgContext, hs: var HandshakeState, readP
except:
raise newException(NoiseMalformedHandshake, "Handshake Pattern not supported")
# We set the handshake and transport message
# We set the messageNametag and the handshake and transport messages
hsStepResult.payload2.messageNametag = toMessageNametag(messageNametag)
hsStepResult.payload2.handshakeMessage = processMessagePatternTokens(rng, hs).get()
hsStepResult.payload2.transportMessage = processMessagePatternPayload(hs, transportMessage)
# We write the payload by passing the messageNametag as extra additional data
hsStepResult.payload2.transportMessage = processMessagePatternPayload(hs, transportMessage, extraAd = hsStepResult.payload2.messageNametag)
# If we read an answer during this handshake step
elif reading:
# If the read message nametag doesn't match the expected input one we raise an error
if readPayloadV2.messageNametag != toMessageNametag(messageNametag):
raise newException(NoiseMessageNametagError, "The message nametag of the read message doesn't match the expected one")
# We process the read public keys and (eventually decrypt) the read transport message
let
let
readHandshakeMessage = readPayloadV2.handshakeMessage
readTransportMessage = readPayloadV2.transportMessage
# Since we only read, nothing meanigful (i.e. public keys) is returned
discard processMessagePatternTokens(rng, hs, readHandshakeMessage)
# We retrieve and store the (decrypted) received transport message
hsStepResult.transportMessage = processMessagePatternPayload(hs, readTransportMessage)
# We retrieve and store the (decrypted) received transport message by passing the messageNametag as extra additional data
hsStepResult.transportMessage = processMessagePatternPayload(hs, readTransportMessage, extraAd = readPayloadV2.messageNametag)
else:
raise newException(NoiseHandshakeError, "Handshake Error: neither writing or reading user")
@ -525,13 +532,26 @@ proc finalizeHandshake*(hs: var HandshakeState): HandshakeResult =
# We call Split()
let (cs1, cs2) = hs.ss.split()
# Optional: We derive a secret for the nametag derivation
let (nms1, nms2) = genMessageNametagSecrets(hs)
# We assign the proper Cipher States
if hs.initiator:
hsResult.csOutbound = cs1
hsResult.csInbound = cs2
# and nametags secrets
hsResult.nametagsInbound.secret = some(nms1)
hsResult.nametagsOutbound.secret = some(nms2)
else:
hsResult.csOutbound = cs2
hsResult.csInbound = cs1
# and nametags secrets
hsResult.nametagsInbound.secret = some(nms2)
hsResult.nametagsOutbound.secret = some(nms1)
# We initialize the message nametags inbound/outbound buffers
hsResult.nametagsInbound.initNametagsBuffer
hsResult.nametagsOutbound.initNametagsBuffer
# We store the optional fields rs and h
hsResult.rs = hs.rs
@ -552,39 +572,49 @@ proc finalizeHandshake*(hs: var HandshakeState): HandshakeResult =
## due to nonce exhaustion, then the application must delete the CipherState and terminate the session.
# Writes an encrypted message using the proper Cipher State
proc writeMessage*(hsr: var HandshakeResult, transportMessage: seq[byte]): PayloadV2
proc writeMessage*(hsr: var HandshakeResult, transportMessage: seq[byte], outboundMessageNametagBuffer: var MessageNametagBuffer): PayloadV2
{.raises: [Defect, NoiseNonceMaxError].} =
var payload2: PayloadV2
# We set the message nametag using the input buffer
payload2.messageNametag = pop(outboundMessageNametagBuffer)
# According to 35/WAKU2-NOISE RFC, no Handshake protocol information is sent when exchanging messages
# This correspond to setting protocol-id to 0
payload2.protocolId = 0.uint8
# We pad the transport message
let paddedTransportMessage = pkcs7_pad(transportMessage, NoisePaddingBlockSize)
# Encryption is done with zero-length associated data as per specification
payload2.transportMessage = encryptWithAd(hsr.csOutbound, @[], paddedTransportMessage)
payload2.transportMessage = encryptWithAd(hsr.csOutbound, ad = @(payload2.messageNametag), plaintext = paddedTransportMessage)
return payload2
# Reads an encrypted message using the proper Cipher State
# Associated data ad for encryption is optional, since the latter is out of scope for Noise
proc readMessage*(hsr: var HandshakeResult, readPayload2: PayloadV2): Result[seq[byte], cstring]
{.raises: [Defect, NoiseDecryptTagError, NoiseNonceMaxError].} =
# Decryption is attempted only if the input PayloadV2 has a messageNametag equal to the one expected
proc readMessage*(hsr: var HandshakeResult, readPayload2: PayloadV2, inboundMessageNametagBuffer: var MessageNametagBuffer): Result[seq[byte], cstring]
{.raises: [Defect, NoiseDecryptTagError, NoiseMessageNametagError, NoiseNonceMaxError, NoiseSomeMessagesWereLost].} =
# The output decrypted message
var message: seq[byte]
# If the message nametag does not correspond to the nametag expected in the inbound message nametag buffer
# an error is raised (to be handled externally, i.e. re-request lost messages, discard, etc.)
let nametagIsOk = checkNametag(readPayload2.messageNametag, inboundMessageNametagBuffer).isOk
assert(nametagIsOk)
# At this point the messageNametag matches the expected nametag.
# According to 35/WAKU2-NOISE RFC, no Handshake protocol information is sent when exchanging messages
if readPayload2.protocolId == 0.uint8:
if readPayload2.protocolId == 0.uint8:
# On application level we decide to discard messages which fail decryption, without raising an error
# (this because an attacker may flood the content topic on which messages are exchanged)
try:
# Decryption is done with zero-length associated data as per specification
let paddedMessage = decryptWithAd(hsr.csInbound, @[], readPayload2.transportMessage)
# Decryption is done with messageNametag as associated data
let paddedMessage = decryptWithAd(hsr.csInbound, ad = @(readPayload2.messageNametag), ciphertext = readPayload2.transportMessage)
# We unpdad the decrypted message
message = pkcs7_unpad(paddedMessage, NoisePaddingBlockSize)
# The message successfully decrypted, we can delete the first element of the inbound Message Nametag Buffer
delete(inboundMessageNametagBuffer, 1)
except NoiseDecryptTagError:
debug "A read message failed decryption. Returning empty message as plaintext."
message = @[]

View File

@ -29,6 +29,14 @@ const
EmptyKey* = default(ChaChaPolyKey)
# The maximum ChaChaPoly allowed nonce in Noise Handshakes
NonceMax* = uint64.high - 1
# The padding blocksize of a transport message
NoisePaddingBlockSize* = 248
# The default length of a message nametag
MessageNametagLength* = 16
# The default length of the secret to generate Inbound/Outbound nametags buffer
MessageNametagSecretLength* = 32
# The default size of an Inbound/outbound MessageNametagBuffer
MessageNametagBufferSize* = 50
type
@ -165,6 +173,8 @@ type
csOutbound*: CipherState
csInbound*: CipherState
# Optional fields:
nametagsInbound*: MessageNametagBuffer
nametagsOutbound*: MessageNametagBuffer
rs*: EllipticCurveKey
h*: MDigest[256]
@ -174,9 +184,17 @@ type
# PayloadV2 defines an object for Waku payloads with version 2 as in
# https://rfc.vac.dev/spec/35/#public-keys-serialization
# It contains a protocol ID field, the handshake message (for Noise handshakes) and
# It contains a message nametag, protocol ID field, the handshake message (for Noise handshakes) and
# a transport message (for Noise handshakes and ChaChaPoly encryptions)
MessageNametag* = array[MessageNametagLength, byte]
MessageNametagBuffer* = object
buffer*: array[MessageNametagBufferSize, MessageNametag]
counter*: uint64
secret*: Option[array[MessageNametagSecretLength, byte]]
PayloadV2* = object
messageNametag*: MessageNametag
protocolId*: uint8
handshakeMessage*: seq[NoisePublicKey]
transportMessage*: seq[byte]
@ -192,7 +210,8 @@ type
NoiseNonceMaxError* = object of NoiseError
NoisePublicKeyError* = object of NoiseError
NoiseMalformedHandshake* = object of NoiseError
NoiseMessageNametagError* = object of NoiseError
NoiseSomeMessagesWereLost* = object of NoiseError
#################################
# Constants (supported protocols)
@ -204,34 +223,42 @@ const
# Supported Noise handshake patterns as defined in https://rfc.vac.dev/spec/35/#specification
NoiseHandshakePatterns* = {
"K1K1": HandshakePattern(name: "Noise_K1K1_25519_ChaChaPoly_SHA256",
preMessagePatterns: @[PreMessagePattern(direction: D_r, tokens: @[T_s]),
PreMessagePattern(direction: D_l, tokens: @[T_s])],
messagePatterns: @[ MessagePattern(direction: D_r, tokens: @[T_e]),
MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_es]),
MessagePattern(direction: D_r, tokens: @[T_se])]
),
"K1K1": HandshakePattern(name: "Noise_K1K1_25519_ChaChaPoly_SHA256",
preMessagePatterns: @[PreMessagePattern(direction: D_r, tokens: @[T_s]),
PreMessagePattern(direction: D_l, tokens: @[T_s])],
messagePatterns: @[ MessagePattern(direction: D_r, tokens: @[T_e]),
MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_es]),
MessagePattern(direction: D_r, tokens: @[T_se])]
),
"XK1": HandshakePattern(name: "Noise_XK1_25519_ChaChaPoly_SHA256",
preMessagePatterns: @[PreMessagePattern(direction: D_l, tokens: @[T_s])],
messagePatterns: @[ MessagePattern(direction: D_r, tokens: @[T_e]),
MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_es]),
MessagePattern(direction: D_r, tokens: @[T_s, T_se])]
),
"XX": HandshakePattern(name: "Noise_XX_25519_ChaChaPoly_SHA256",
preMessagePatterns: EmptyPreMessage,
messagePatterns: @[ MessagePattern(direction: D_r, tokens: @[T_e]),
MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_s, T_es]),
MessagePattern(direction: D_r, tokens: @[T_s, T_se])]
),
"XXpsk0": HandshakePattern(name: "Noise_XXpsk0_25519_ChaChaPoly_SHA256",
preMessagePatterns: EmptyPreMessage,
messagePatterns: @[ MessagePattern(direction: D_r, tokens: @[T_psk, T_e]),
MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_s, T_es]),
MessagePattern(direction: D_r, tokens: @[T_s, T_se])]
),
"XK1": HandshakePattern(name: "Noise_XK1_25519_ChaChaPoly_SHA256",
preMessagePatterns: @[PreMessagePattern(direction: D_l, tokens: @[T_s])],
messagePatterns: @[ MessagePattern(direction: D_r, tokens: @[T_e]),
MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_es]),
MessagePattern(direction: D_r, tokens: @[T_s, T_se])]
),
"WakuPairing": HandshakePattern(name: "Noise_WakuPairing_25519_ChaChaPoly_SHA256",
preMessagePatterns: @[PreMessagePattern(direction: D_l, tokens: @[T_e])],
messagePatterns: @[ MessagePattern(direction: D_r, tokens: @[T_e, T_ee]),
MessagePattern(direction: D_l, tokens: @[T_s, T_es]),
MessagePattern(direction: D_r, tokens: @[T_s, T_se, T_ss])]
)
"XX": HandshakePattern(name: "Noise_XX_25519_ChaChaPoly_SHA256",
preMessagePatterns: EmptyPreMessage,
messagePatterns: @[ MessagePattern(direction: D_r, tokens: @[T_e]),
MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_s, T_es]),
MessagePattern(direction: D_r, tokens: @[T_s, T_se])]
),
"XXpsk0": HandshakePattern(name: "Noise_XXpsk0_25519_ChaChaPoly_SHA256",
preMessagePatterns: EmptyPreMessage,
messagePatterns: @[ MessagePattern(direction: D_r, tokens: @[T_psk, T_e]),
MessagePattern(direction: D_l, tokens: @[T_e, T_ee, T_s, T_es]),
MessagePattern(direction: D_r, tokens: @[T_s, T_se])]
)
}.toTable()
@ -239,15 +266,12 @@ const
# Protocol IDs are defined according to https://rfc.vac.dev/spec/35/#specification
PayloadV2ProtocolIDs* = {
"": 0.uint8,
"Noise_K1K1_25519_ChaChaPoly_SHA256": 10.uint8,
"Noise_XK1_25519_ChaChaPoly_SHA256": 11.uint8,
"Noise_XX_25519_ChaChaPoly_SHA256": 12.uint8,
"Noise_XXpsk0_25519_ChaChaPoly_SHA256": 13.uint8,
"ChaChaPoly": 30.uint8
"": 0.uint8,
"Noise_K1K1_25519_ChaChaPoly_SHA256": 10.uint8,
"Noise_XK1_25519_ChaChaPoly_SHA256": 11.uint8,
"Noise_XX_25519_ChaChaPoly_SHA256": 12.uint8,
"Noise_XXpsk0_25519_ChaChaPoly_SHA256": 13.uint8,
"Noise_WakuPairing_25519_ChaChaPoly_SHA256": 14.uint8,
"ChaChaPoly": 30.uint8
}.toTable()
# Other constants
const
NoisePaddingBlockSize* = 248
}.toTable()

View File

@ -5,7 +5,7 @@
{.push raises: [Defect].}
import std/[oids, options, strutils, tables, sequtils]
import std/[algorithm, base64, oids, options, strutils, tables, sequtils]
import chronos
import chronicles
import bearssl/rand
@ -13,7 +13,7 @@ import stew/[results, endians2, byteutils]
import nimcrypto/[utils, sha2, hmac]
import libp2p/errors
import libp2p/crypto/[chacha20poly1305, curve25519]
import libp2p/crypto/[chacha20poly1305, curve25519, hkdf]
import ./noise_types
import ./noise
@ -57,6 +57,76 @@ proc pkcs7_unpad*(payload: seq[byte], paddingSize: int): seq[byte] =
let unpadded = payload[0..payload.high-k.int]
return unpadded
proc seqToDigest256*(sequence: seq[byte]): MDigest[256] =
var digest: MDigest[256]
for i in 0..<digest.data.len:
digest.data[i] = sequence[i]
return digest
proc digestToSeq*[T](digest: MDigest[T]): seq[byte] =
var sequence: seq[byte]
for i in 0..<digest.data.len:
sequence.add digest.data[i]
return sequence
# Serializes input parameters to a base64 string for exposure through QR code (used by WakuPairing)
proc toQr*(applicationName: string, applicationVersion: string, shardId: string, ephemeralKey: EllipticCurveKey, committedStaticKey: MDigest[256]): string =
var qr: string
qr.add encode(applicationName) & ":"
qr.add encode(applicationVersion) & ":"
qr.add encode(shardId) & ":"
qr.add encode(ephemeralKey) & ":"
qr.add encode(committedStaticKey.data)
return qr
# Deserializes input string in base64 to the corresponding (applicationName, applicationVersion, shardId, ephemeralKey, committedStaticKey)
proc fromQr*(qr: string): (string, string, string, EllipticCurveKey, MDigest[256]) {.raises: [Defect, ValueError].} =
let values = qr.split(":")
assert(values.len == 5)
let applicationName: string = decode(values[0])
let applicationVersion: string = decode(values[1])
let shardId: string = decode(values[2])
let decodedEphemeralKey = decode(values[3]).toBytes
var ephemeralKey: EllipticCurveKey
for i in 0..<ephemeralKey.len:
ephemeralKey[i] = decodedEphemeralKey[i]
let committedStaticKey = seqToDigest256(decode(values[4]).toBytes)
return (applicationName, applicationVersion, shardId, ephemeralKey, committedStaticKey)
# Converts a sequence or array (arbitrary size) to a MessageNametag
proc toMessageNametag*(input: openArray[byte]): MessageNametag =
var byte_seq: seq[byte] = @input
# We set its length to the default message nametag length (will be truncated or 0-padded)
byte_seq.setLen(MessageNametagLength)
# We copy it to a MessageNametag
var messageNametag: MessageNametag
for i in 0..<MessageNametagLength:
messageNametag[i] = byte_seq[i]
return messageNametag
# Uses the cryptographic information stored in the input handshake state to generate a random message nametag
# In current implementation the messageNametag = HKDF(handshake hash value), but other derivation mechanisms can be implemented
proc toMessageNametag*(hs: HandshakeState): MessageNametag =
var output: array[1, array[MessageNametagLength, byte]]
sha256.hkdf(hs.ss.h.data, [], [], output)
return output[0]
proc genMessageNametagSecrets*(hs: HandshakeState): (array[MessageNametagSecretLength, byte], array[MessageNametagSecretLength, byte]) =
var output: array[2, array[MessageNametagSecretLength, byte]]
sha256.hkdf(hs.ss.h.data, [], [], output)
return (output[0], output[1])
# Simple utility that checks if the given variable is "default",
# Therefore, it has not been initialized
proc isDefault*[T](value: T): bool =
@ -138,6 +208,87 @@ proc hashProtocol*(protocolName: string): MDigest[256] =
return hash
# Commits a public key pk for randomness r as H(pk || s)
proc commitPublicKey*(publicKey: EllipticCurveKey, r: seq[byte]): MDigest[256] =
var hashInput: seq[byte]
hashInput.add getBytes(publicKey)
hashInput.add r
# The output hash value
var hash: MDigest[256]
hash = sha256.digest(hashInput)
return hash
# Generates an 8 decimal digits authorization code using HKDF and the handshake state
proc genAuthcode*(hs: HandshakeState): string =
var output: array[1, array[8, byte]]
sha256.hkdf(hs.ss.h.data, [], [], output)
let code = cast[uint64](output[0]) mod 100_000_000
return $code
# Initializes the empty Message nametag buffer. The n-th nametag is equal to HKDF( secret || n )
proc initNametagsBuffer*(mntb: var MessageNametagBuffer) =
# We default the counter and buffer fields
mntb.counter = 0
mntb.buffer = default(array[MessageNametagBufferSize, MessageNametag])
if mntb.secret.isSome:
for i in 0..<mntb.buffer.len:
mntb.buffer[i] = toMessageNametag(sha256.digest(@(mntb.secret.get()) & @(toBytesLE(mntb.counter))).data)
mntb.counter += 1
else:
# We warn users if no secret is set
debug "The message nametags buffer has not a secret set"
# Deletes the first n elements in buffer and appends n new ones
proc delete*(mntb: var MessageNametagBuffer, n: int) =
if n <= 0:
return
# We ensure n is at most MessageNametagBufferSize (the buffer will be fully replaced)
let n = min(n, MessageNametagBufferSize)
# We update the last n values in the array if a secret is set
# Note that if the input MessageNametagBuffer is set to default, nothing is done here
if mntb.secret.isSome:
# We rotate left the array by n
mntb.buffer.rotateLeft(n)
for i in 0..<n:
mntb.buffer[mntb.buffer.len-n+i] = toMessageNametag(sha256.digest(@(mntb.secret.get()) & @(toBytesLE(mntb.counter))).data)
mntb.counter += 1
else:
# We warn users that no secret is set
debug "The message nametags buffer has no secret set"
# Checks if the input messageNametag is contained in the input MessageNametagBuffer
proc checkNametag*(messageNametag: MessageNametag, mntb: var MessageNametagBuffer): Result[bool, cstring]
{.raises: [Defect, NoiseMessageNametagError, NoiseSomeMessagesWereLost].} =
let index = mntb.buffer.find(messageNametag)
if index == -1:
raise newException(NoiseMessageNametagError, "Message nametag not found in buffer")
elif index > 0:
raise newException(NoiseSomeMessagesWereLost, "Message nametag is present in buffer but is not the next expected nametag. One or more messages were probably lost")
# index is 0, hence the read message tag is the next expected one
return ok(true)
# Deletes the first n elements in buffer and appends n new ones
proc pop*(mntb: var MessageNametagBuffer): MessageNametag =
# Note that if the input MessageNametagBuffer is set to default, an all 0 messageNametag is returned
let messageNametag = mntb.buffer[0]
delete(mntb, 1)
return messageNametag
# Performs a Diffie-Hellman operation between two elliptic curve keys (one private, one public)
proc dh*(private: EllipticCurveKey, public: EllipticCurveKey): EllipticCurveKey =
@ -275,7 +426,8 @@ proc decryptNoisePublicKey*(cs: ChaChaPolyCipherState, noisePublicKey: NoisePubl
# Checks equality between two PayloadsV2 objects
proc `==`*(p1, p2: PayloadV2): bool =
return (p1.protocolId == p2.protocolId) and
return (p1.messageNametag == p2.messageNametag) and
(p1.protocolId == p2.protocolId) and
(p1.handshakeMessage == p2.handshakeMessage) and
(p1.transportMessage == p2.transportMessage)
@ -283,6 +435,10 @@ proc `==`*(p1, p2: PayloadV2): bool =
# Generates a random PayloadV2
proc randomPayloadV2*(rng: var HmacDrbgContext): PayloadV2 =
var payload2: PayloadV2
# We set a random messageNametag
let randMessageNametag = randomSeqByte(rng, MessageNametagLength)
for i in 0..<MessageNametagLength:
payload2.messageNametag[i] = randMessageNametag[i]
# To generate a random protocol id, we generate a random 1-byte long sequence, and we convert the first element to uint8
payload2.protocolId = randomSeqByte(rng, 1)[0].uint8
# We set the handshake message to three unencrypted random Noise Public Keys
@ -321,14 +477,14 @@ proc serializePayloadV2*(self: PayloadV2): Result[seq[byte], cstring] =
debug "PayloadV2 malformed: too many public keys contained in the handshake message"
return err("Too many public keys in handshake message")
# We get the transport message byte length
let transportMessageLen = self.transportMessage.len
# The output payload as in https://rfc.vac.dev/spec/35/. We concatenate all the PayloadV2 fields as
# payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
# We declare it as a byte sequence of length accordingly to the PayloadV2 information read
var payload = newSeqOfCap[byte](1 + # 1 byte for protocol ID
var payload = newSeqOfCap[byte](MessageNametagLength + #MessageNametagLength bytes for messageNametag
1 + # 1 byte for protocol ID
1 + # 1 byte for length of serializedHandshakeMessage field
serializedHandshakeMessageLen + # serializedHandshakeMessageLen bytes for serializedHandshakeMessage
8 + # 8 bytes for transportMessageLen
@ -337,6 +493,7 @@ proc serializePayloadV2*(self: PayloadV2): Result[seq[byte], cstring] =
# We concatenate all the data
# The protocol ID (1 byte) and handshake message length (1 byte) can be directly casted to byte to allow direct copy to the payload byte sequence
payload.add @(self.messageNametag)
payload.add self.protocolId.byte
payload.add serializedHandshakeMessageLen.byte
payload.add serializedHandshakeMessage
@ -349,7 +506,7 @@ proc serializePayloadV2*(self: PayloadV2): Result[seq[byte], cstring] =
# Deserializes a byte sequence to a PayloadV2 object according to https://rfc.vac.dev/spec/35/.
# The input serialized payload concatenates the output PayloadV2 object fields as
# payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
# payload = ( messageNametag || protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
proc deserializePayloadV2*(payload: seq[byte]): Result[PayloadV2, cstring]
{.raises: [Defect, NoisePublicKeyError].} =
@ -359,7 +516,12 @@ proc deserializePayloadV2*(payload: seq[byte]): Result[PayloadV2, cstring]
# i is the read input buffer position index
var i: uint64 = 0
# We start reading the Protocol ID
# We start by reading the messageNametag
for j in 0..<MessageNametagLength:
payload2.messageNametag[j] = payload[i+j.uint64]
i += MessageNametagLength
# We read the Protocol ID
# TODO: when the list of supported protocol ID is defined, check if read protocol ID is supported
payload2.protocolId = payload[i].uint8
i += 1