From 2cfeb75e187828540e712bcc348c247d782d63b8 Mon Sep 17 00:00:00 2001 From: G <28568419+s1fr0@users.noreply.github.com> Date: Mon, 12 Sep 2022 02:23:14 +0200 Subject: [PATCH] PoC implementation of Waku Pairing and Secure Transfer (#1117) * feat(noise): add PoC implementation for WakuPairing and Secure Transfer --- tests/all_tests_v2.nim | 3 +- tests/v2/test_waku_noise.nim | 37 +- tests/v2/test_waku_noise_sessions.nim | 328 ++++++++++++++++++ waku/v2/node/waku_payload.nim | 4 +- waku/v2/protocol/waku_noise/noise.nim | 13 +- .../waku_noise/noise_handshake_processing.nim | 72 ++-- waku/v2/protocol/waku_noise/noise_types.nim | 102 +++--- waku/v2/protocol/waku_noise/noise_utils.nim | 176 +++++++++- 8 files changed, 645 insertions(+), 90 deletions(-) create mode 100644 tests/v2/test_waku_noise_sessions.nim diff --git a/tests/all_tests_v2.nim b/tests/all_tests_v2.nim index 0dd1c672c..c89317c7d 100644 --- a/tests/all_tests_v2.nim +++ b/tests/all_tests_v2.nim @@ -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 diff --git a/tests/v2/test_waku_noise.nim b/tests/v2/test_waku_noise.nim index f1bf2abbc..4aab39891 100644 --- a/tests/v2/test_waku_noise.nim +++ b/tests/v2/test_waku_noise.nim @@ -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 diff --git a/tests/v2/test_waku_noise_sessions.nim b/tests/v2/test_waku_noise_sessions.nim new file mode 100644 index 000000000..6bfa47175 --- /dev/null +++ b/tests/v2/test_waku_noise_sessions.nim @@ -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() + diff --git a/waku/v2/node/waku_payload.nim b/waku/v2/node/waku_payload.nim index 8a849de94..e8fe2e886 100644 --- a/waku/v2/node/waku_payload.nim +++ b/waku/v2/node/waku_payload.nim @@ -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) \ No newline at end of file diff --git a/waku/v2/protocol/waku_noise/noise.nim b/waku/v2/protocol/waku_noise/noise.nim index ca722f3f1..94a0f9e7c 100644 --- a/waku/v2/protocol/waku_noise/noise.nim +++ b/waku/v2/protocol/waku_noise/noise.nim @@ -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 diff --git a/waku/v2/protocol/waku_noise/noise_handshake_processing.nim b/waku/v2/protocol/waku_noise/noise_handshake_processing.nim index da1606d0b..500ad55a1 100644 --- a/waku/v2/protocol/waku_noise/noise_handshake_processing.nim +++ b/waku/v2/protocol/waku_noise/noise_handshake_processing.nim @@ -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 = @[] diff --git a/waku/v2/protocol/waku_noise/noise_types.nim b/waku/v2/protocol/waku_noise/noise_types.nim index 0f75899f3..7c1dee6ac 100644 --- a/waku/v2/protocol/waku_noise/noise_types.nim +++ b/waku/v2/protocol/waku_noise/noise_types.nim @@ -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 \ No newline at end of file + }.toTable() \ No newline at end of file diff --git a/waku/v2/protocol/waku_noise/noise_utils.nim b/waku/v2/protocol/waku_noise/noise_utils.nim index ed7334aaf..91fbb3c43 100644 --- a/waku/v2/protocol/waku_noise/noise_utils.nim +++ b/waku/v2/protocol/waku_noise/noise_utils.nim @@ -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.. 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..