mirror of https://github.com/waku-org/nwaku.git
PoC implementation of Waku Pairing and Secure Transfer (#1117)
* feat(noise): add PoC implementation for WakuPairing and Secure Transfer
This commit is contained in:
parent
797c82b030
commit
01cd201f58
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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 = @[]
|
||||
|
|
|
@ -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()
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue