mirror of
https://github.com/logos-messaging/logos-messaging-nim.git
synced 2026-06-11 12:09:32 +00:00
clean waku_noise because it is not used in prod code (#3934)
This commit is contained in:
parent
faa6741311
commit
c7350abb58
@ -1,36 +0,0 @@
|
||||
{.push raises: [].}
|
||||
|
||||
import results
|
||||
import ../waku_core, ../waku_noise/noise_types, ../waku_noise/noise_utils
|
||||
|
||||
# Decodes a WakuMessage to a PayloadV2
|
||||
# Currently, this is just a wrapper over deserializePayloadV2 and encryption/decryption is done on top (no KeyInfo)
|
||||
proc decodePayloadV2*(
|
||||
message: WakuMessage
|
||||
): Result[PayloadV2, cstring] {.raises: [NoiseMalformedHandshake, NoisePublicKeyError].} =
|
||||
# We check message version (only 2 is supported in this proc)
|
||||
case message.version
|
||||
of 2:
|
||||
# We attempt to decode the WakuMessage payload
|
||||
let deserializedPayload2 = deserializePayloadV2(message.payload).valueOr:
|
||||
return err("Failed to decode WakuMessage")
|
||||
return ok(deserializedPayload2)
|
||||
else:
|
||||
return err("Wrong message version while decoding payload")
|
||||
|
||||
# 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, contentTopic: ContentTopic = default(ContentTopic)
|
||||
): Result[WakuMessage, cstring] {.
|
||||
raises: [NoiseMalformedHandshake, NoisePublicKeyError]
|
||||
.} =
|
||||
# We attempt to encode the PayloadV2
|
||||
let serializedPayload2 = serializePayloadV2(payload2).valueOr:
|
||||
return err("Failed to encode PayloadV2")
|
||||
|
||||
# If successful, we create and return a WakuMessage
|
||||
let msg =
|
||||
WakuMessage(payload: serializedPayload2, version: 2, contentTopic: contentTopic)
|
||||
|
||||
return ok(msg)
|
||||
@ -1,364 +0,0 @@
|
||||
# Waku Noise Protocols for Waku Payload Encryption
|
||||
# Noise module implementing the Noise State Objects and ChaChaPoly encryption/decryption primitives
|
||||
## See spec for more details:
|
||||
## https://github.com/vacp2p/rfc/tree/master/content/docs/rfcs/35
|
||||
##
|
||||
## Implementation partially inspired by noise-libp2p:
|
||||
## https://github.com/status-im/nim-libp2p/blob/master/libp2p/protocols/secure/noise.nim
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[options, strutils]
|
||||
import stew/byteutils
|
||||
import chronos
|
||||
import chronicles
|
||||
import bearssl/rand
|
||||
import stew/endians2
|
||||
import nimcrypto/[sha2, hmac]
|
||||
|
||||
import libp2p/utility
|
||||
import libp2p/crypto/[crypto, chacha20poly1305, hkdf]
|
||||
import libp2p/protocols/secure/secure
|
||||
|
||||
import ./noise_types
|
||||
|
||||
logScope:
|
||||
topics = "waku noise"
|
||||
|
||||
#################################################################
|
||||
|
||||
# Noise state machine primitives
|
||||
|
||||
# Overview :
|
||||
# - Alice and Bob process (i.e. read and write, based on their role) each token appearing in a handshake pattern, consisting of pre-message and message patterns;
|
||||
# - Both users initialize and update according to processed tokens a Handshake State, a Symmetric State and a Cipher State;
|
||||
# - A preshared key psk is processed by calling MixKeyAndHash(psk);
|
||||
# - When an ephemeral public key e is read or written, the handshake hash value h is updated by calling mixHash(e); If the handshake expects a psk, MixKey(e) is further called
|
||||
# - When an encrypted static public key s or a payload message m is read, it is decrypted with decryptAndHash;
|
||||
# - When a static public key s or a payload message is writted, it is encrypted with encryptAndHash;
|
||||
# - When any Diffie-Hellman token ee, es, se, ss is read or written, the chaining key ck is updated by calling MixKey on the computed secret;
|
||||
# - If all tokens are processed, users compute two new Cipher States by calling Split;
|
||||
# - The two Cipher States obtained from Split are used to encrypt/decrypt outbound/inbound messages.
|
||||
|
||||
#################################
|
||||
# Cipher State Primitives
|
||||
#################################
|
||||
|
||||
# Checks if a Cipher State has an encryption key set
|
||||
proc hasKey*(cs: CipherState): bool =
|
||||
return (cs.k != EmptyKey)
|
||||
|
||||
# Encrypts a plaintext using key material in a Noise Cipher State
|
||||
# The CipherState is updated increasing the nonce (used as a counter in Noise) by one
|
||||
proc encryptWithAd*(
|
||||
state: var CipherState, ad, plaintext: openArray[byte]
|
||||
): seq[byte] {.raises: [Defect, NoiseNonceMaxError].} =
|
||||
# We raise an error if encryption is called using a Cipher State with nonce greater than MaxNonce
|
||||
if state.n > NonceMax:
|
||||
raise newException(NoiseNonceMaxError, "Noise max nonce value reached")
|
||||
|
||||
var ciphertext: seq[byte]
|
||||
|
||||
# If an encryption key is set in the Cipher state, we proceed with encryption
|
||||
if state.hasKey:
|
||||
# The output is the concatenation of the ciphertext and authorization tag
|
||||
# We define its length accordingly
|
||||
ciphertext = newSeqOfCap[byte](plaintext.len + sizeof(ChaChaPolyTag))
|
||||
|
||||
# Since ChaChaPoly encryption primitive overwrites the input with the output,
|
||||
# we copy the plaintext in the output ciphertext variable and we pass it to encryption
|
||||
ciphertext.add(plaintext)
|
||||
|
||||
# The nonce is read from the input CipherState
|
||||
# By Noise specification the nonce is 8 bytes long out of the 12 bytes supported by ChaChaPoly
|
||||
var nonce: ChaChaPolyNonce
|
||||
nonce[4 ..< 12] = toBytesLE(state.n)
|
||||
|
||||
# We perform encryption and we store the authorization tag
|
||||
var authorizationTag: ChaChaPolyTag
|
||||
ChaChaPoly.encrypt(state.k, nonce, authorizationTag, ciphertext, ad)
|
||||
|
||||
# We append the authorization tag to ciphertext
|
||||
ciphertext.add(authorizationTag)
|
||||
|
||||
# We increase the Cipher state nonce
|
||||
inc state.n
|
||||
# If the nonce is greater than the maximum allowed nonce, we raise an exception
|
||||
if state.n > NonceMax:
|
||||
raise newException(NoiseNonceMaxError, "Noise max nonce value reached")
|
||||
|
||||
trace "encryptWithAd",
|
||||
authorizationTag = byteutils.toHex(authorizationTag),
|
||||
ciphertext = ciphertext,
|
||||
nonce = state.n - 1
|
||||
|
||||
# Otherwise we return the input plaintext according to specification http://www.noiseprotocol.org/noise.html#the-cipherstate-object
|
||||
else:
|
||||
ciphertext = @plaintext
|
||||
info "encryptWithAd called with no encryption key set. Returning plaintext."
|
||||
|
||||
return ciphertext
|
||||
|
||||
# Decrypts a ciphertext using key material in a Noise Cipher State
|
||||
# The CipherState is updated increasing the nonce (used as a counter in Noise) by one
|
||||
proc decryptWithAd*(
|
||||
state: var CipherState, ad, ciphertext: openArray[byte]
|
||||
): seq[byte] {.raises: [Defect, NoiseDecryptTagError, NoiseNonceMaxError].} =
|
||||
# We raise an error if encryption is called using a Cipher State with nonce greater than MaxNonce
|
||||
if state.n > NonceMax:
|
||||
raise newException(NoiseNonceMaxError, "Noise max nonce value reached")
|
||||
|
||||
var plaintext: seq[byte]
|
||||
|
||||
# If an encryption key is set in the Cipher state, we proceed with decryption
|
||||
if state.hasKey:
|
||||
# We read the authorization appendend at the end of a ciphertext
|
||||
let inputAuthorizationTag = ciphertext.toOpenArray(
|
||||
ciphertext.len - ChaChaPolyTag.len, ciphertext.high
|
||||
).intoChaChaPolyTag
|
||||
|
||||
var
|
||||
authorizationTag: ChaChaPolyTag
|
||||
nonce: ChaChaPolyNonce
|
||||
|
||||
# The nonce is read from the input CipherState
|
||||
# By Noise specification the nonce is 8 bytes long out of the 12 bytes supported by ChaChaPoly
|
||||
nonce[4 ..< 12] = toBytesLE(state.n)
|
||||
|
||||
# Since ChaChaPoly decryption primitive overwrites the input with the output,
|
||||
# we copy the ciphertext (authorization tag excluded) in the output plaintext variable and we pass it to decryption
|
||||
plaintext = ciphertext[0 .. (ciphertext.high - ChaChaPolyTag.len)]
|
||||
|
||||
ChaChaPoly.decrypt(state.k, nonce, authorizationTag, plaintext, ad)
|
||||
|
||||
# We check if the input authorization tag matches the decryption authorization tag
|
||||
if inputAuthorizationTag != authorizationTag:
|
||||
info "decryptWithAd failed",
|
||||
plaintext = plaintext,
|
||||
ciphertext = ciphertext,
|
||||
inputAuthorizationTag = inputAuthorizationTag,
|
||||
authorizationTag = authorizationTag
|
||||
raise
|
||||
newException(NoiseDecryptTagError, "decryptWithAd failed tag authentication.")
|
||||
|
||||
# We increase the Cipher state nonce
|
||||
inc state.n
|
||||
# If the nonce is greater than the maximum allowed nonce, we raise an exception
|
||||
if state.n > NonceMax:
|
||||
raise newException(NoiseNonceMaxError, "Noise max nonce value reached")
|
||||
|
||||
trace "decryptWithAd",
|
||||
inputAuthorizationTag = inputAuthorizationTag,
|
||||
authorizationTag = authorizationTag,
|
||||
nonce = state.n
|
||||
|
||||
# Otherwise we return the input ciphertext according to specification http://www.noiseprotocol.org/noise.html#the-cipherstate-object
|
||||
else:
|
||||
plaintext = @ciphertext
|
||||
info "decryptWithAd called with no encryption key set. Returning ciphertext."
|
||||
|
||||
return plaintext
|
||||
|
||||
# Sets the nonce of a Cipher State
|
||||
proc setNonce*(cs: var CipherState, nonce: uint64) =
|
||||
cs.n = nonce
|
||||
|
||||
# Sets the key of a Cipher State
|
||||
proc setCipherStateKey*(cs: var CipherState, key: ChaChaPolyKey) =
|
||||
cs.k = key
|
||||
|
||||
# Generates a random Symmetric Cipher State for test purposes
|
||||
proc randomCipherState*(rng: var HmacDrbgContext, nonce: uint64 = 0): CipherState =
|
||||
var randomCipherState: CipherState
|
||||
hmacDrbgGenerate(rng, randomCipherState.k)
|
||||
setNonce(randomCipherState, nonce)
|
||||
return randomCipherState
|
||||
|
||||
# Gets the key of a Cipher State
|
||||
proc getKey*(cs: CipherState): ChaChaPolyKey =
|
||||
return cs.k
|
||||
|
||||
# Gets the nonce of a Cipher State
|
||||
proc getNonce*(cs: CipherState): uint64 =
|
||||
return cs.n
|
||||
|
||||
#################################
|
||||
# Symmetric State primitives
|
||||
#################################
|
||||
|
||||
# Initializes a Symmetric State
|
||||
proc init*(_: type[SymmetricState], hsPattern: HandshakePattern): SymmetricState =
|
||||
var ss: SymmetricState
|
||||
# We compute the hash of the protocol name
|
||||
ss.h = hsPattern.name.hashProtocol
|
||||
# We initialize the chaining key ck
|
||||
ss.ck = ss.h.data.intoChaChaPolyKey
|
||||
# We initialize the Cipher state
|
||||
ss.cs = CipherState(k: EmptyKey)
|
||||
return ss
|
||||
|
||||
# MixKey as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||
# Updates a Symmetric state chaining key and symmetric state
|
||||
proc mixKey*(ss: var SymmetricState, inputKeyMaterial: openArray[byte]) =
|
||||
# We derive two keys using HKDF
|
||||
var tempKeys: array[2, ChaChaPolyKey]
|
||||
sha256.hkdf(ss.ck, inputKeyMaterial, [], tempKeys)
|
||||
# We update ck and the Cipher state's key k using the output of HDKF
|
||||
ss.ck = tempKeys[0]
|
||||
ss.cs = CipherState(k: tempKeys[1])
|
||||
trace "mixKey", ck = ss.ck, k = ss.cs.k
|
||||
|
||||
# MixHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||
# Hashes data into a Symmetric State's handshake hash value h
|
||||
proc mixHash*(ss: var SymmetricState, data: openArray[byte]) =
|
||||
# We prepare the hash context
|
||||
var ctx: sha256
|
||||
ctx.init()
|
||||
# We add the previous handshake hash
|
||||
ctx.update(ss.h.data)
|
||||
# We append the input data
|
||||
ctx.update(data)
|
||||
# We hash and store the result in the Symmetric State's handshake hash value
|
||||
ss.h = ctx.finish()
|
||||
trace "mixHash", hash = ss.h.data
|
||||
|
||||
# mixKeyAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||
# Combines MixKey and MixHash
|
||||
proc mixKeyAndHash*(
|
||||
ss: var SymmetricState, inputKeyMaterial: openArray[byte]
|
||||
) {.used.} =
|
||||
var tempKeys: array[3, ChaChaPolyKey]
|
||||
# Derives 3 keys using HKDF, the chaining key and the input key material
|
||||
sha256.hkdf(ss.ck, inputKeyMaterial, [], tempKeys)
|
||||
# Sets the chaining key
|
||||
ss.ck = tempKeys[0]
|
||||
# Updates the handshake hash value
|
||||
ss.mixHash(tempKeys[1])
|
||||
# Updates the Cipher state's key
|
||||
# Note for later support of 512 bits hash functions: "If HASHLEN is 64, then truncates tempKeys[2] to 32 bytes."
|
||||
ss.cs = CipherState(k: tempKeys[2])
|
||||
|
||||
# EncryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||
# Combines encryptWithAd and mixHash
|
||||
# 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(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], 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(ad, ciphertext)
|
||||
# According to specification, the ciphertext enters mixHash (and not the plaintext)
|
||||
ss.mixHash(ciphertext)
|
||||
return plaintext
|
||||
|
||||
# Split as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||
# Once a handshake is complete, returns two Cipher States to encrypt/decrypt outbound/inbound messages
|
||||
proc split*(ss: var SymmetricState): tuple[cs1, cs2: CipherState] =
|
||||
# Derives 2 keys using HKDF and the chaining key
|
||||
var tempKeys: array[2, ChaChaPolyKey]
|
||||
sha256.hkdf(ss.ck, [], [], tempKeys)
|
||||
# Returns a tuple of two Cipher States initialized with the derived keys
|
||||
return (CipherState(k: tempKeys[0]), CipherState(k: tempKeys[1]))
|
||||
|
||||
# Gets the chaining key field of a Symmetric State
|
||||
proc getChainingKey*(ss: SymmetricState): ChaChaPolyKey =
|
||||
return ss.ck
|
||||
|
||||
# Gets the handshake hash field of a Symmetric State
|
||||
proc getHandshakeHash*(ss: SymmetricState): MDigest[256] =
|
||||
return ss.h
|
||||
|
||||
# Gets the Cipher State field of a Symmetric State
|
||||
proc getCipherState*(ss: SymmetricState): CipherState =
|
||||
return ss.cs
|
||||
|
||||
#################################
|
||||
# Handshake State primitives
|
||||
#################################
|
||||
|
||||
# Initializes a Handshake State
|
||||
proc init*(
|
||||
_: type[HandshakeState], hsPattern: HandshakePattern, psk: seq[byte] = @[]
|
||||
): HandshakeState =
|
||||
# The output Handshake State
|
||||
var hs: HandshakeState
|
||||
# By default the Handshake State initiator flag is set to false
|
||||
# Will be set to true when the user associated to the handshake state starts an handshake
|
||||
hs.initiator = false
|
||||
# We copy the information on the handshake pattern for which the state is initialized (protocol name, handshake pattern, psk)
|
||||
hs.handshakePattern = hsPattern
|
||||
hs.psk = psk
|
||||
# We initialize the Symmetric State
|
||||
hs.ss = SymmetricState.init(hsPattern)
|
||||
return hs
|
||||
|
||||
#################################################################
|
||||
|
||||
#################################
|
||||
# ChaChaPoly Symmetric Cipher
|
||||
#################################
|
||||
|
||||
# ChaChaPoly encryption
|
||||
# It takes a Cipher State (with key, nonce, and associated data) and encrypts a plaintext
|
||||
# The cipher state in not changed
|
||||
proc encrypt*(
|
||||
state: ChaChaPolyCipherState, plaintext: openArray[byte]
|
||||
): ChaChaPolyCiphertext {.noinit, raises: [Defect, NoiseEmptyChaChaPolyInput].} =
|
||||
# If plaintext is empty, we raise an error
|
||||
if plaintext == @[]:
|
||||
raise newException(NoiseEmptyChaChaPolyInput, "Tried to encrypt empty plaintext")
|
||||
var ciphertext: ChaChaPolyCiphertext
|
||||
# Since ChaChaPoly's library "encrypt" primitive directly changes the input plaintext to the ciphertext,
|
||||
# we copy the plaintext into the ciphertext variable and we pass the latter to encrypt
|
||||
ciphertext.data.add plaintext
|
||||
# TODO: add padding
|
||||
# ChaChaPoly.encrypt takes as input: the key (k), the nonce (nonce), a data structure for storing the computed authorization tag (tag),
|
||||
# the plaintext (overwritten to ciphertext) (data), the associated data (ad)
|
||||
ChaChaPoly.encrypt(state.k, state.nonce, ciphertext.tag, ciphertext.data, state.ad)
|
||||
return ciphertext
|
||||
|
||||
# ChaChaPoly decryption
|
||||
# It takes a Cipher State (with key, nonce, and associated data) and decrypts a ciphertext
|
||||
# The cipher state is not changed
|
||||
proc decrypt*(
|
||||
state: ChaChaPolyCipherState, ciphertext: ChaChaPolyCiphertext
|
||||
): seq[byte] {.raises: [Defect, NoiseEmptyChaChaPolyInput, NoiseDecryptTagError].} =
|
||||
# If ciphertext is empty, we raise an error
|
||||
if ciphertext.data == @[]:
|
||||
raise newException(NoiseEmptyChaChaPolyInput, "Tried to decrypt empty ciphertext")
|
||||
var
|
||||
# The input authorization tag
|
||||
tagIn = ciphertext.tag
|
||||
# The authorization tag computed during decryption
|
||||
tagOut: ChaChaPolyTag
|
||||
# Since ChaChaPoly's library "decrypt" primitive directly changes the input ciphertext to the plaintext,
|
||||
# we copy the ciphertext into the plaintext variable and we pass the latter to decrypt
|
||||
var plaintext = ciphertext.data
|
||||
# ChaChaPoly.decrypt takes as input: the key (k), the nonce (nonce), a data structure for storing the computed authorization tag (tag),
|
||||
# the ciphertext (overwritten to plaintext) (data), the associated data (ad)
|
||||
ChaChaPoly.decrypt(state.k, state.nonce, tagOut, plaintext, state.ad)
|
||||
# TODO: add unpadding
|
||||
trace "decrypt", tagIn = tagIn, tagOut = tagOut, nonce = state.nonce
|
||||
# We check if the authorization tag computed while decrypting is the same as the input tag
|
||||
if tagIn != tagOut:
|
||||
info "decrypt failed", plaintext = shortLog(plaintext)
|
||||
raise newException(NoiseDecryptTagError, "decrypt tag authentication failed.")
|
||||
return plaintext
|
||||
@ -1,668 +0,0 @@
|
||||
# Waku Noise Protocols for Waku Payload Encryption
|
||||
## See spec for more details:
|
||||
## https://github.com/vacp2p/rfc/tree/master/content/docs/rfcs/35
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[options, strutils, tables]
|
||||
import chronos
|
||||
import chronicles
|
||||
import bearssl/rand
|
||||
import results
|
||||
|
||||
import libp2p/crypto/[chacha20poly1305, curve25519]
|
||||
|
||||
import ./noise_types
|
||||
import ./noise
|
||||
import ./noise_utils
|
||||
|
||||
logScope:
|
||||
topics = "waku noise"
|
||||
|
||||
#################################################################
|
||||
|
||||
# Handshake Processing
|
||||
|
||||
#################################
|
||||
## Utilities
|
||||
#################################
|
||||
|
||||
# Based on the message handshake direction and if the user is or not the initiator, returns a boolean tuple telling if the user
|
||||
# has to read or write the next handshake message
|
||||
proc getReadingWritingState(
|
||||
hs: HandshakeState, direction: MessageDirection
|
||||
): (bool, bool) =
|
||||
var reading, writing: bool
|
||||
|
||||
if hs.initiator and direction == D_r:
|
||||
# I'm Alice and direction is ->
|
||||
reading = false
|
||||
writing = true
|
||||
elif hs.initiator and direction == D_l:
|
||||
# I'm Alice and direction is <-
|
||||
reading = true
|
||||
writing = false
|
||||
elif not hs.initiator and direction == D_r:
|
||||
# I'm Bob and direction is ->
|
||||
reading = true
|
||||
writing = false
|
||||
elif not hs.initiator and direction == D_l:
|
||||
# I'm Bob and direction is <-
|
||||
reading = false
|
||||
writing = true
|
||||
|
||||
return (reading, writing)
|
||||
|
||||
# Checks if a pre-message is valid according to Noise specifications
|
||||
# http://www.noiseprotocol.org/noise.html#handshake-patterns
|
||||
proc isValid(msg: seq[PreMessagePattern]): bool =
|
||||
var isValid: bool = true
|
||||
|
||||
# Non-empty pre-messages can only have patterns "e", "s", "e,s" in each direction
|
||||
let allowedPatterns: seq[PreMessagePattern] = @[
|
||||
PreMessagePattern(direction: D_r, tokens: @[T_s]),
|
||||
PreMessagePattern(direction: D_r, tokens: @[T_e]),
|
||||
PreMessagePattern(direction: D_r, tokens: @[T_e, T_s]),
|
||||
PreMessagePattern(direction: D_l, tokens: @[T_s]),
|
||||
PreMessagePattern(direction: D_l, tokens: @[T_e]),
|
||||
PreMessagePattern(direction: D_l, tokens: @[T_e, T_s]),
|
||||
]
|
||||
|
||||
# We check if pre message patterns are allowed
|
||||
for pattern in msg:
|
||||
if not (pattern in allowedPatterns):
|
||||
isValid = false
|
||||
break
|
||||
|
||||
return isValid
|
||||
|
||||
#################################
|
||||
# Handshake messages processing procedures
|
||||
#################################
|
||||
|
||||
# Processes pre-message patterns
|
||||
proc processPreMessagePatternTokens(
|
||||
hs: var HandshakeState, inPreMessagePKs: seq[NoisePublicKey] = @[]
|
||||
) {.
|
||||
raises: [Defect, NoiseMalformedHandshake, NoiseHandshakeError, NoisePublicKeyError]
|
||||
.} =
|
||||
var
|
||||
# I make a copy of the input pre-message public keys, so that I can easily delete processed ones without using iterators/counters
|
||||
preMessagePKs = inPreMessagePKs
|
||||
# Here we store currently processed pre message public key
|
||||
currPK: NoisePublicKey
|
||||
|
||||
# We retrieve the pre-message patterns to process, if any
|
||||
# If none, there's nothing to do
|
||||
if hs.handshakePattern.preMessagePatterns == EmptyPreMessage:
|
||||
return
|
||||
|
||||
# If not empty, we check that pre-message is valid according to Noise specifications
|
||||
if isValid(hs.handshakePattern.preMessagePatterns) == false:
|
||||
raise newException(NoiseMalformedHandshake, "Invalid pre-message in handshake")
|
||||
|
||||
# We iterate over each pattern contained in the pre-message
|
||||
for messagePattern in hs.handshakePattern.preMessagePatterns:
|
||||
let
|
||||
direction = messagePattern.direction
|
||||
tokens = messagePattern.tokens
|
||||
|
||||
# We get if the user is reading or writing the current pre-message pattern
|
||||
var (reading, writing) = getReadingWritingState(hs, direction)
|
||||
|
||||
# We process each message pattern token
|
||||
for token in tokens:
|
||||
# We process the pattern token
|
||||
case token
|
||||
of T_e:
|
||||
# We expect an ephemeral key, so we attempt to read it (next PK to process will always be at index 0 of preMessagePKs)
|
||||
if preMessagePKs.len > 0:
|
||||
currPK = preMessagePKs[0]
|
||||
else:
|
||||
raise newException(
|
||||
NoiseHandshakeError, "Noise pre-message read e, expected a public key"
|
||||
)
|
||||
|
||||
# If user is reading the "e" token
|
||||
if reading:
|
||||
trace "noise pre-message read e"
|
||||
|
||||
# We check if current key is encrypted or not. We assume pre-message public keys are all unencrypted on users' end
|
||||
if currPK.flag == 0.uint8:
|
||||
# Sets re and calls MixHash(re.public_key).
|
||||
hs.re = intoCurve25519Key(currPK.pk)
|
||||
hs.ss.mixHash(hs.re)
|
||||
else:
|
||||
raise newException(
|
||||
NoisePublicKeyError,
|
||||
"Noise read e, incorrect encryption flag for pre-message public key",
|
||||
)
|
||||
|
||||
# If user is writing the "e" token
|
||||
elif writing:
|
||||
trace "noise pre-message write e"
|
||||
|
||||
# When writing, the user is sending a public key,
|
||||
# We check that the public part corresponds to the set local key and we call MixHash(e.public_key).
|
||||
if hs.e.publicKey == intoCurve25519Key(currPK.pk):
|
||||
hs.ss.mixHash(hs.e.publicKey)
|
||||
else:
|
||||
raise newException(
|
||||
NoisePublicKeyError,
|
||||
"Noise pre-message e key doesn't correspond to locally set e key pair",
|
||||
)
|
||||
|
||||
# Noise specification: section 9.2
|
||||
# In non-PSK handshakes, the "e" token in a pre-message pattern or message pattern always results
|
||||
# in a call to MixHash(e.public_key).
|
||||
# In a PSK handshake, all of these calls are followed by MixKey(e.public_key).
|
||||
if "psk" in hs.handshakePattern.name:
|
||||
hs.ss.mixKey(currPK.pk)
|
||||
|
||||
# We delete processed public key
|
||||
preMessagePKs.delete(0)
|
||||
of T_s:
|
||||
# We expect a static key, so we attempt to read it (next PK to process will always be at index of preMessagePKs)
|
||||
if preMessagePKs.len > 0:
|
||||
currPK = preMessagePKs[0]
|
||||
else:
|
||||
raise newException(
|
||||
NoiseHandshakeError, "Noise pre-message read s, expected a public key"
|
||||
)
|
||||
|
||||
# If user is reading the "s" token
|
||||
if reading:
|
||||
trace "noise pre-message read s"
|
||||
|
||||
# We check if current key is encrypted or not. We assume pre-message public keys are all unencrypted on users' end
|
||||
if currPK.flag == 0.uint8:
|
||||
# Sets re and calls MixHash(re.public_key).
|
||||
hs.rs = intoCurve25519Key(currPK.pk)
|
||||
hs.ss.mixHash(hs.rs)
|
||||
else:
|
||||
raise newException(
|
||||
NoisePublicKeyError,
|
||||
"Noise read s, incorrect encryption flag for pre-message public key",
|
||||
)
|
||||
|
||||
# If user is writing the "s" token
|
||||
elif writing:
|
||||
trace "noise pre-message write s"
|
||||
|
||||
# If writing, it means that the user is sending a public key,
|
||||
# We check that the public part corresponds to the set local key and we call MixHash(s.public_key).
|
||||
if hs.s.publicKey == intoCurve25519Key(currPK.pk):
|
||||
hs.ss.mixHash(hs.s.publicKey)
|
||||
else:
|
||||
raise newException(
|
||||
NoisePublicKeyError,
|
||||
"Noise pre-message s key doesn't correspond to locally set s key pair",
|
||||
)
|
||||
|
||||
# Noise specification: section 9.2
|
||||
# In non-PSK handshakes, the "e" token in a pre-message pattern or message pattern always results
|
||||
# in a call to MixHash(e.public_key).
|
||||
# In a PSK handshake, all of these calls are followed by MixKey(e.public_key).
|
||||
if "psk" in hs.handshakePattern.name:
|
||||
hs.ss.mixKey(currPK.pk)
|
||||
|
||||
# We delete processed public key
|
||||
preMessagePKs.delete(0)
|
||||
else:
|
||||
raise
|
||||
newException(NoiseMalformedHandshake, "Invalid Token for pre-message pattern")
|
||||
|
||||
# This procedure encrypts/decrypts the implicit payload attached at the end of every message pattern
|
||||
# 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]
|
||||
|
||||
# We retrieve current message pattern (direction + tokens) to process
|
||||
let direction = hs.handshakePattern.messagePatterns[hs.msgPatternIdx].direction
|
||||
|
||||
# We get if the user is reading or writing the input handshake message
|
||||
var (reading, writing) = getReadingWritingState(hs, direction)
|
||||
|
||||
# We decrypt the transportMessage, if any
|
||||
if reading:
|
||||
payload = hs.ss.decryptAndHash(transportMessage, extraAd)
|
||||
payload = pkcs7_unpad(payload, NoisePaddingBlockSize)
|
||||
elif writing:
|
||||
payload = pkcs7_pad(transportMessage, NoisePaddingBlockSize)
|
||||
payload = hs.ss.encryptAndHash(payload, extraAd)
|
||||
|
||||
return payload
|
||||
|
||||
# We process an input handshake message according to current handshake state and we return the next handshake step's handshake message
|
||||
proc processMessagePatternTokens(
|
||||
rng: var rand.HmacDrbgContext,
|
||||
hs: var HandshakeState,
|
||||
inputHandshakeMessage: seq[NoisePublicKey] = @[],
|
||||
): Result[seq[NoisePublicKey], cstring] {.
|
||||
raises: [
|
||||
Defect, NoiseHandshakeError, NoiseMalformedHandshake, NoisePublicKeyError,
|
||||
NoiseDecryptTagError, NoiseNonceMaxError,
|
||||
]
|
||||
.} =
|
||||
# We retrieve current message pattern (direction + tokens) to process
|
||||
let
|
||||
messagePattern = hs.handshakePattern.messagePatterns[hs.msgPatternIdx]
|
||||
direction = messagePattern.direction
|
||||
tokens = messagePattern.tokens
|
||||
|
||||
# We get if the user is reading or writing the input handshake message
|
||||
var (reading, writing) = getReadingWritingState(hs, direction)
|
||||
|
||||
# I make a copy of the handshake message so that I can easily delete processed PKs without using iterators/counters
|
||||
# (Possibly) non-empty if reading
|
||||
var inHandshakeMessage = inputHandshakeMessage
|
||||
|
||||
# The party's output public keys
|
||||
# (Possibly) non-empty if writing
|
||||
var outHandshakeMessage: seq[NoisePublicKey] = @[]
|
||||
|
||||
# In currPK we store the currently processed public key from the handshake message
|
||||
var currPK: NoisePublicKey
|
||||
|
||||
# We process each message pattern token
|
||||
for token in tokens:
|
||||
case token
|
||||
of T_e:
|
||||
# If user is reading the "s" token
|
||||
if reading:
|
||||
trace "noise read e"
|
||||
|
||||
# We expect an ephemeral key, so we attempt to read it (next PK to process will always be at index 0 of preMessagePKs)
|
||||
if inHandshakeMessage.len > 0:
|
||||
currPK = inHandshakeMessage[0]
|
||||
else:
|
||||
raise newException(NoiseHandshakeError, "Noise read e, expected a public key")
|
||||
|
||||
# We check if current key is encrypted or not
|
||||
# Note: by specification, ephemeral keys should always be unencrypted. But we support encrypted ones.
|
||||
if currPK.flag == 0.uint8:
|
||||
# Unencrypted Public Key
|
||||
# Sets re and calls MixHash(re.public_key).
|
||||
hs.re = intoCurve25519Key(currPK.pk)
|
||||
hs.ss.mixHash(hs.re)
|
||||
|
||||
# The following is out of specification: we call decryptAndHash for encrypted ephemeral keys, similarly as happens for (encrypted) static keys
|
||||
elif currPK.flag == 1.uint8:
|
||||
# Encrypted public key
|
||||
# Decrypts re, sets re and calls MixHash(re.public_key).
|
||||
hs.re = intoCurve25519Key(hs.ss.decryptAndHash(currPK.pk))
|
||||
else:
|
||||
raise newException(
|
||||
NoisePublicKeyError,
|
||||
"Noise read e, incorrect encryption flag for public key",
|
||||
)
|
||||
|
||||
# Noise specification: section 9.2
|
||||
# In non-PSK handshakes, the "e" token in a pre-message pattern or message pattern always results
|
||||
# in a call to MixHash(e.public_key).
|
||||
# In a PSK handshake, all of these calls are followed by MixKey(e.public_key).
|
||||
if "psk" in hs.handshakePattern.name:
|
||||
hs.ss.mixKey(hs.re)
|
||||
|
||||
# We delete processed public key
|
||||
inHandshakeMessage.delete(0)
|
||||
|
||||
# If user is writing the "e" token
|
||||
elif writing:
|
||||
trace "noise write e"
|
||||
|
||||
# We generate a new ephemeral keypair
|
||||
hs.e = genKeyPair(rng)
|
||||
|
||||
# We update the state
|
||||
hs.ss.mixHash(hs.e.publicKey)
|
||||
|
||||
# Noise specification: section 9.2
|
||||
# In non-PSK handshakes, the "e" token in a pre-message pattern or message pattern always results
|
||||
# in a call to MixHash(e.public_key).
|
||||
# In a PSK handshake, all of these calls are followed by MixKey(e.public_key).
|
||||
if "psk" in hs.handshakePattern.name:
|
||||
hs.ss.mixKey(hs.e.publicKey)
|
||||
|
||||
# We add the ephemeral public key to the Waku payload
|
||||
outHandshakeMessage.add toNoisePublicKey(getPublicKey(hs.e))
|
||||
of T_s:
|
||||
# If user is reading the "s" token
|
||||
if reading:
|
||||
trace "noise read s"
|
||||
|
||||
# We expect a static key, so we attempt to read it (next PK to process will always be at index 0 of preMessagePKs)
|
||||
if inHandshakeMessage.len > 0:
|
||||
currPK = inHandshakeMessage[0]
|
||||
else:
|
||||
raise newException(NoiseHandshakeError, "Noise read s, expected a public key")
|
||||
|
||||
# We check if current key is encrypted or not
|
||||
if currPK.flag == 0.uint8:
|
||||
# Unencrypted Public Key
|
||||
# Sets re and calls MixHash(re.public_key).
|
||||
hs.rs = intoCurve25519Key(currPK.pk)
|
||||
hs.ss.mixHash(hs.rs)
|
||||
elif currPK.flag == 1.uint8:
|
||||
# Encrypted public key
|
||||
# Decrypts rs, sets rs and calls MixHash(rs.public_key).
|
||||
hs.rs = intoCurve25519Key(hs.ss.decryptAndHash(currPK.pk))
|
||||
else:
|
||||
raise newException(
|
||||
NoisePublicKeyError,
|
||||
"Noise read s, incorrect encryption flag for public key",
|
||||
)
|
||||
|
||||
# We delete processed public key
|
||||
inHandshakeMessage.delete(0)
|
||||
|
||||
# If user is writing the "s" token
|
||||
elif writing:
|
||||
trace "noise write s"
|
||||
|
||||
# If the local static key is not set (the handshake state was not properly initialized), we raise an error
|
||||
if isDefault(hs.s):
|
||||
raise newException(NoisePublicKeyError, "Static key not set")
|
||||
|
||||
# We encrypt the public part of the static key in case a key is set in the Cipher State
|
||||
# That is, encS may either be an encrypted or unencrypted static key.
|
||||
let encS = hs.ss.encryptAndHash(hs.s.publicKey)
|
||||
|
||||
# We add the (encrypted) static public key to the Waku payload
|
||||
# Note that encS = (Enc(s) || tag) if encryption key is set, otherwise encS = s.
|
||||
# We distinguish these two cases by checking length of encryption and we set the proper encryption flag
|
||||
if encS.len > Curve25519Key.len:
|
||||
outHandshakeMessage.add NoisePublicKey(flag: 1, pk: encS)
|
||||
else:
|
||||
outHandshakeMessage.add NoisePublicKey(flag: 0, pk: encS)
|
||||
of T_psk:
|
||||
# If user is reading the "psk" token
|
||||
|
||||
trace "noise psk"
|
||||
|
||||
# Calls MixKeyAndHash(psk)
|
||||
hs.ss.mixKeyAndHash(hs.psk)
|
||||
of T_ee:
|
||||
# If user is reading the "ee" token
|
||||
|
||||
trace "noise dh ee"
|
||||
|
||||
# If local and/or remote ephemeral keys are not set, we raise an error
|
||||
if isDefault(hs.e) or isDefault(hs.re):
|
||||
raise newException(NoisePublicKeyError, "Local or remote ephemeral key not set")
|
||||
|
||||
# Calls MixKey(DH(e, re)).
|
||||
hs.ss.mixKey(dh(hs.e.privateKey, hs.re))
|
||||
of T_es:
|
||||
# If user is reading the "es" token
|
||||
|
||||
trace "noise dh es"
|
||||
|
||||
# We check if keys are correctly set.
|
||||
# If both present, we call MixKey(DH(e, rs)) if initiator, MixKey(DH(s, re)) if responder.
|
||||
if hs.initiator:
|
||||
if isDefault(hs.e) or isDefault(hs.rs):
|
||||
raise newException(
|
||||
NoisePublicKeyError, "Local or remote ephemeral/static key not set"
|
||||
)
|
||||
hs.ss.mixKey(dh(hs.e.privateKey, hs.rs))
|
||||
else:
|
||||
if isDefault(hs.re) or isDefault(hs.s):
|
||||
raise newException(
|
||||
NoisePublicKeyError, "Local or remote ephemeral/static key not set"
|
||||
)
|
||||
hs.ss.mixKey(dh(hs.s.privateKey, hs.re))
|
||||
of T_se:
|
||||
# If user is reading the "se" token
|
||||
|
||||
trace "noise dh se"
|
||||
|
||||
# We check if keys are correctly set.
|
||||
# If both present, call MixKey(DH(s, re)) if initiator, MixKey(DH(e, rs)) if responder.
|
||||
if hs.initiator:
|
||||
if isDefault(hs.s) or isDefault(hs.re):
|
||||
raise newException(
|
||||
NoiseMalformedHandshake, "Local or remote ephemeral/static key not set"
|
||||
)
|
||||
hs.ss.mixKey(dh(hs.s.privateKey, hs.re))
|
||||
else:
|
||||
if isDefault(hs.rs) or isDefault(hs.e):
|
||||
raise newException(
|
||||
NoiseMalformedHandshake, "Local or remote ephemeral/static key not set"
|
||||
)
|
||||
hs.ss.mixKey(dh(hs.e.privateKey, hs.rs))
|
||||
of T_ss:
|
||||
# If user is reading the "ss" token
|
||||
|
||||
trace "noise dh ss"
|
||||
|
||||
# If local and/or remote static keys are not set, we raise an error
|
||||
if isDefault(hs.s) or isDefault(hs.rs):
|
||||
raise
|
||||
newException(NoiseMalformedHandshake, "Local or remote static key not set")
|
||||
|
||||
# Calls MixKey(DH(s, rs)).
|
||||
hs.ss.mixKey(dh(hs.s.privateKey, hs.rs))
|
||||
|
||||
return ok(outHandshakeMessage)
|
||||
|
||||
#################################
|
||||
## Procedures to progress handshakes between users
|
||||
#################################
|
||||
|
||||
# Initializes a Handshake State
|
||||
proc initialize*(
|
||||
hsPattern: HandshakePattern,
|
||||
ephemeralKey: KeyPair = default(KeyPair),
|
||||
staticKey: KeyPair = default(KeyPair),
|
||||
prologue: seq[byte] = @[],
|
||||
psk: seq[byte] = @[],
|
||||
preMessagePKs: seq[NoisePublicKey] = @[],
|
||||
initiator: bool = false,
|
||||
): HandshakeState {.
|
||||
raises: [Defect, NoiseMalformedHandshake, NoiseHandshakeError, NoisePublicKeyError]
|
||||
.} =
|
||||
var hs = HandshakeState.init(hsPattern)
|
||||
hs.ss.mixHash(prologue)
|
||||
hs.e = ephemeralKey
|
||||
hs.s = staticKey
|
||||
hs.psk = psk
|
||||
hs.msgPatternIdx = 0
|
||||
hs.initiator = initiator
|
||||
# We process any eventual handshake pre-message pattern by processing pre-message public keys
|
||||
processPreMessagePatternTokens(hs, preMessagePKs)
|
||||
return hs
|
||||
|
||||
# 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) 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
|
||||
|
||||
# If there are no more message patterns left for processing
|
||||
# we return an empty HandshakeStepResult
|
||||
if hs.msgPatternIdx > uint8(hs.handshakePattern.messagePatterns.len - 1):
|
||||
info "stepHandshake called more times than the number of message patterns present in handshake"
|
||||
return ok(hsStepResult)
|
||||
|
||||
# We process the next handshake message pattern
|
||||
|
||||
# We get if the user is reading or writing the input handshake message
|
||||
let direction = hs.handshakePattern.messagePatterns[hs.msgPatternIdx].direction
|
||||
var (reading, writing) = getReadingWritingState(hs, direction)
|
||||
|
||||
# If we write an answer at this handshake step
|
||||
if writing:
|
||||
# We initialize a payload v2 and we set proper protocol ID (if supported)
|
||||
try:
|
||||
hsStepResult.payload2.protocolId = PayloadV2ProtocolIDs[hs.handshakePattern.name]
|
||||
except CatchableError:
|
||||
raise newException(NoiseMalformedHandshake, "Handshake Pattern not supported")
|
||||
|
||||
# We set the messageNametag and the handshake and transport messages
|
||||
hsStepResult.payload2.messageNametag = toMessageNametag(messageNametag)
|
||||
hsStepResult.payload2.handshakeMessage = processMessagePatternTokens(rng, hs).get()
|
||||
# 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
|
||||
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 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"
|
||||
)
|
||||
|
||||
# We increase the handshake state message pattern index to progress to next step
|
||||
hs.msgPatternIdx += 1
|
||||
|
||||
return ok(hsStepResult)
|
||||
|
||||
# Finalizes the handshake by calling Split and assigning the proper Cipher States to users
|
||||
proc finalizeHandshake*(hs: var HandshakeState): HandshakeResult =
|
||||
var hsResult: HandshakeResult
|
||||
|
||||
## Noise specification, Section 5:
|
||||
## Processing the final handshake message returns two CipherState objects,
|
||||
## the first for encrypting transport messages from initiator to responder,
|
||||
## and the second for messages in the other direction.
|
||||
|
||||
# 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
|
||||
hsResult.h = hs.ss.h
|
||||
|
||||
return hsResult
|
||||
|
||||
#################################
|
||||
# After-handshake procedures
|
||||
#################################
|
||||
|
||||
## Noise specification, Section 5:
|
||||
## Transport messages are then encrypted and decrypted by calling EncryptWithAd()
|
||||
## and DecryptWithAd() on the relevant CipherState with zero-length associated data.
|
||||
## If DecryptWithAd() signals an error due to DECRYPT() failure, then the input message is discarded.
|
||||
## The application may choose to delete the CipherState and terminate the session on such an error,
|
||||
## or may continue to attempt communications. If EncryptWithAd() or DecryptWithAd() signal an error
|
||||
## 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],
|
||||
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, ad = @(payload2.messageNametag), plaintext = paddedTransportMessage
|
||||
)
|
||||
|
||||
return payload2
|
||||
|
||||
# Reads an encrypted message using the proper Cipher State
|
||||
# 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:
|
||||
# On application level we decide to discard messages which fail decryption, without raising an error
|
||||
try:
|
||||
# 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:
|
||||
info "A read message failed decryption. Returning empty message as plaintext."
|
||||
message = @[]
|
||||
|
||||
return ok(message)
|
||||
@ -1,284 +0,0 @@
|
||||
# Waku Noise Protocols for Waku Payload Encryption
|
||||
## See spec for more details:
|
||||
## https://github.com/vacp2p/rfc/tree/master/content/docs/rfcs/35
|
||||
##
|
||||
## Implementation partially inspired by noise-libp2p:
|
||||
## https://github.com/status-im/nim-libp2p/blob/master/libp2p/protocols/secure/noise.nim
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[options, tables]
|
||||
import chronos
|
||||
import chronicles
|
||||
import bearssl
|
||||
import nimcrypto/sha2
|
||||
|
||||
import libp2p/errors
|
||||
import libp2p/crypto/[crypto, chacha20poly1305, curve25519]
|
||||
|
||||
logScope:
|
||||
topics = "waku noise"
|
||||
|
||||
#################################################################
|
||||
|
||||
# Constants and data structures
|
||||
|
||||
const
|
||||
# EmptyKey represents a non-initialized ChaChaPolyKey
|
||||
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
|
||||
|
||||
#################################
|
||||
# Elliptic Curve arithemtic
|
||||
#################################
|
||||
|
||||
# Default underlying elliptic curve arithmetic (useful for switching to multiple ECs)
|
||||
# Current default is Curve25519
|
||||
EllipticCurve* = Curve25519
|
||||
EllipticCurveKey* = Curve25519Key
|
||||
|
||||
# An EllipticCurveKey (public, private) key pair
|
||||
KeyPair* = object
|
||||
privateKey*: EllipticCurveKey
|
||||
publicKey*: EllipticCurveKey
|
||||
|
||||
#################################
|
||||
# Noise Public Keys
|
||||
#################################
|
||||
|
||||
# A Noise public key is a public key exchanged during Noise handshakes (no private part)
|
||||
# This follows https://rfc.vac.dev/spec/35/#public-keys-serialization
|
||||
# pk contains the X coordinate of the public key, if unencrypted (this implies flag = 0)
|
||||
# or the encryption of the X coordinate concatenated with the authorization tag, if encrypted (this implies flag = 1)
|
||||
# Note: besides encryption, flag can be used to distinguish among multiple supported Elliptic Curves
|
||||
NoisePublicKey* = object
|
||||
flag*: uint8
|
||||
pk*: seq[byte]
|
||||
|
||||
#################################
|
||||
# ChaChaPoly Encryption
|
||||
#################################
|
||||
|
||||
# A ChaChaPoly ciphertext (data) + authorization tag (tag)
|
||||
ChaChaPolyCiphertext* = object
|
||||
data*: seq[byte]
|
||||
tag*: ChaChaPolyTag
|
||||
|
||||
# A ChaChaPoly Cipher State containing key (k), nonce (nonce) and associated data (ad)
|
||||
ChaChaPolyCipherState* = object
|
||||
k*: ChaChaPolyKey
|
||||
nonce*: ChaChaPolyNonce
|
||||
ad*: seq[byte]
|
||||
|
||||
#################################
|
||||
# Noise handshake patterns
|
||||
#################################
|
||||
|
||||
# The Noise tokens appearing in Noise (pre)message patterns
|
||||
# as in http://www.noiseprotocol.org/noise.html#handshake-pattern-basics
|
||||
NoiseTokens* = enum
|
||||
T_e = "e"
|
||||
T_s = "s"
|
||||
T_es = "es"
|
||||
T_ee = "ee"
|
||||
T_se = "se"
|
||||
T_ss = "ss"
|
||||
T_psk = "psk"
|
||||
|
||||
# The direction of a (pre)message pattern in canonical form (i.e. Alice-initiated form)
|
||||
# as in http://www.noiseprotocol.org/noise.html#alice-and-bob
|
||||
MessageDirection* = enum
|
||||
D_r = "->"
|
||||
D_l = "<-"
|
||||
|
||||
# The pre message pattern consisting of a message direction and some Noise tokens, if any.
|
||||
# (if non empty, only tokens e and s are allowed: http://www.noiseprotocol.org/noise.html#handshake-pattern-basics)
|
||||
PreMessagePattern* = object
|
||||
direction*: MessageDirection
|
||||
tokens*: seq[NoiseTokens]
|
||||
|
||||
# The message pattern consisting of a message direction and some Noise tokens
|
||||
# All Noise tokens are allowed
|
||||
MessagePattern* = object
|
||||
direction*: MessageDirection
|
||||
tokens*: seq[NoiseTokens]
|
||||
|
||||
# The handshake pattern object. It stores the handshake protocol name, the handshake pre message patterns and the handshake message patterns
|
||||
HandshakePattern* = object
|
||||
name*: string
|
||||
preMessagePatterns*: seq[PreMessagePattern]
|
||||
messagePatterns*: seq[MessagePattern]
|
||||
|
||||
#################################
|
||||
# Noise state machine
|
||||
#################################
|
||||
|
||||
# The Cipher State as in https://noiseprotocol.org/noise.html#the-cipherstate-object
|
||||
# Contains an encryption key k and a nonce n (used in Noise as a counter)
|
||||
CipherState* = object
|
||||
k*: ChaChaPolyKey
|
||||
n*: uint64
|
||||
|
||||
# The Symmetric State as in https://noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||
# Contains a Cipher State cs, the chaining key ck and the handshake hash value h
|
||||
SymmetricState* = object
|
||||
cs*: CipherState
|
||||
ck*: ChaChaPolyKey
|
||||
h*: MDigest[256]
|
||||
|
||||
# The Handshake State as in https://noiseprotocol.org/noise.html#the-handshakestate-object
|
||||
# Contains
|
||||
# - the local and remote ephemeral/static keys e,s,re,rs (if any)
|
||||
# - the initiator flag (true if the user creating the state is the handshake initiator, false otherwise)
|
||||
# - the handshakePattern (containing the handshake protocol name, and (pre)message patterns)
|
||||
# This object is futher extended from specifications by storing:
|
||||
# - a message pattern index msgPatternIdx indicating the next handshake message pattern to process
|
||||
# - the user's preshared psk, if any
|
||||
HandshakeState* = object
|
||||
s*: KeyPair
|
||||
e*: KeyPair
|
||||
rs*: EllipticCurveKey
|
||||
re*: EllipticCurveKey
|
||||
ss*: SymmetricState
|
||||
initiator*: bool
|
||||
handshakePattern*: HandshakePattern
|
||||
msgPatternIdx*: uint8
|
||||
psk*: seq[byte]
|
||||
|
||||
# While processing messages patterns, users either:
|
||||
# - read (decrypt) the other party's (encrypted) transport message
|
||||
# - write (encrypt) a message, sent through a PayloadV2
|
||||
# These two intermediate results are stored in the HandshakeStepResult data structure
|
||||
HandshakeStepResult* = object
|
||||
payload2*: PayloadV2
|
||||
transportMessage*: seq[byte]
|
||||
|
||||
# When a handshake is complete, the HandhshakeResult will contain the two
|
||||
# Cipher States used to encrypt/decrypt outbound/inbound messages
|
||||
# The recipient static key rs and handshake hash values h are stored to address some possible future applications (channel-binding, session management, etc.).
|
||||
# However, are not required by Noise specifications and are thus optional
|
||||
HandshakeResult* = object
|
||||
csOutbound*: CipherState
|
||||
csInbound*: CipherState
|
||||
# Optional fields:
|
||||
nametagsInbound*: MessageNametagBuffer
|
||||
nametagsOutbound*: MessageNametagBuffer
|
||||
rs*: EllipticCurveKey
|
||||
h*: MDigest[256]
|
||||
|
||||
#################################
|
||||
# Waku Payload V2
|
||||
#################################
|
||||
|
||||
# PayloadV2 defines an object for Waku payloads with version 2 as in
|
||||
# https://rfc.vac.dev/spec/35/#public-keys-serialization
|
||||
# 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]
|
||||
|
||||
#################################
|
||||
# Some useful error types
|
||||
#################################
|
||||
NoiseError* = object of LPError
|
||||
NoiseHandshakeError* = object of NoiseError
|
||||
NoiseEmptyChaChaPolyInput* = object of NoiseError
|
||||
NoiseDecryptTagError* = object of NoiseError
|
||||
NoiseNonceMaxError* = object of NoiseError
|
||||
NoisePublicKeyError* = object of NoiseError
|
||||
NoiseMalformedHandshake* = object of NoiseError
|
||||
NoiseMessageNametagError* = object of NoiseError
|
||||
NoiseSomeMessagesWereLost* = object of NoiseError
|
||||
|
||||
#################################
|
||||
# Constants (supported protocols)
|
||||
#################################
|
||||
const
|
||||
|
||||
# The empty pre message patterns
|
||||
EmptyPreMessage*: seq[PreMessagePattern] = @[]
|
||||
|
||||
# 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]),
|
||||
],
|
||||
),
|
||||
"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]),
|
||||
],
|
||||
),
|
||||
"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]),
|
||||
],
|
||||
),
|
||||
}.toTable()
|
||||
|
||||
# Supported Protocol ID for PayloadV2 objects
|
||||
# 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,
|
||||
"Noise_WakuPairing_25519_ChaChaPoly_SHA256": 14.uint8,
|
||||
"ChaChaPoly": 30.uint8,
|
||||
}.toTable()
|
||||
@ -1,588 +0,0 @@
|
||||
# Waku Noise Protocols for Waku Payload Encryption
|
||||
# Noise utilities module
|
||||
## See spec for more details:
|
||||
## https://github.com/vacp2p/rfc/tree/master/content/docs/rfcs/35
|
||||
|
||||
{.push raises: [].}
|
||||
|
||||
import std/[algorithm, base64, oids, options, strutils, tables, sequtils]
|
||||
import chronos
|
||||
import chronicles
|
||||
import bearssl/rand
|
||||
import results
|
||||
import stew/[endians2, byteutils]
|
||||
import nimcrypto/sha2
|
||||
|
||||
import libp2p/crypto/[chacha20poly1305, curve25519, hkdf]
|
||||
|
||||
import ./noise_types
|
||||
import ./noise
|
||||
|
||||
logScope:
|
||||
topics = "waku noise"
|
||||
|
||||
#################################################################
|
||||
|
||||
#################################
|
||||
# Generic Utilities
|
||||
#################################
|
||||
|
||||
# Generates random byte sequences of given size
|
||||
proc randomSeqByte*(rng: var HmacDrbgContext, size: int): seq[byte] =
|
||||
var output = newSeq[byte](size.uint32)
|
||||
hmacDrbgGenerate(rng, output)
|
||||
return output
|
||||
|
||||
# Pads a payload according to PKCS#7 as per RFC 5652 https://datatracker.ietf.org/doc/html/rfc5652#section-6.3
|
||||
proc pkcs7_pad*(payload: seq[byte], paddingSize: int): seq[byte] =
|
||||
assert(paddingSize < 256)
|
||||
|
||||
let k = paddingSize - (payload.len mod paddingSize)
|
||||
|
||||
var padding: seq[byte]
|
||||
|
||||
if k != 0:
|
||||
padding = newSeqWith(k, k.byte)
|
||||
else:
|
||||
padding = newSeqWith(paddingSize, paddingSize.byte)
|
||||
|
||||
let padded = concat(payload, padding)
|
||||
|
||||
return padded
|
||||
|
||||
# Unpads a payload according to PKCS#7 as per RFC 5652 https://datatracker.ietf.org/doc/html/rfc5652#section-6.3
|
||||
proc pkcs7_unpad*(payload: seq[byte], paddingSize: int): seq[byte] =
|
||||
let k = payload[payload.high]
|
||||
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, safe = true) & ":"
|
||||
qr.add encode(applicationVersion, safe = true) & ":"
|
||||
qr.add encode(shardId, safe = true) & ":"
|
||||
qr.add encode(ephemeralKey, safe = true) & ":"
|
||||
qr.add encode(committedStaticKey.data, safe = true)
|
||||
|
||||
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 =
|
||||
value == static(default(T))
|
||||
|
||||
#################################################################
|
||||
|
||||
#################################
|
||||
# Noise Handhshake Utilities
|
||||
#################################
|
||||
|
||||
# Generate random (public, private) Elliptic Curve key pairs
|
||||
proc genKeyPair*(rng: var HmacDrbgContext): KeyPair =
|
||||
var keyPair: KeyPair
|
||||
keyPair.privateKey = EllipticCurveKey.random(rng)
|
||||
keyPair.publicKey = keyPair.privateKey.public()
|
||||
return keyPair
|
||||
|
||||
# Gets private key from a key pair
|
||||
proc getPrivateKey*(keypair: KeyPair): EllipticCurveKey =
|
||||
return keypair.privateKey
|
||||
|
||||
# Gets public key from a key pair
|
||||
proc getPublicKey*(keypair: KeyPair): EllipticCurveKey =
|
||||
return keypair.publicKey
|
||||
|
||||
# Prints Handshake Patterns using Noise pattern layout
|
||||
proc print*(self: HandshakePattern) {.raises: [IOError, NoiseMalformedHandshake].} =
|
||||
try:
|
||||
if self.name != "":
|
||||
stdout.write self.name, ":\n"
|
||||
stdout.flushFile()
|
||||
# We iterate over pre message patterns, if any
|
||||
if self.preMessagePatterns != EmptyPreMessage:
|
||||
for pattern in self.preMessagePatterns:
|
||||
stdout.write " ", pattern.direction
|
||||
var first = true
|
||||
for token in pattern.tokens:
|
||||
if first:
|
||||
stdout.write " ", token
|
||||
first = false
|
||||
else:
|
||||
stdout.write ", ", token
|
||||
stdout.write "\n"
|
||||
stdout.flushFile()
|
||||
stdout.write " ...\n"
|
||||
stdout.flushFile()
|
||||
# We iterate over message patterns
|
||||
for pattern in self.messagePatterns:
|
||||
stdout.write " ", pattern.direction
|
||||
var first = true
|
||||
for token in pattern.tokens:
|
||||
if first:
|
||||
stdout.write " ", token
|
||||
first = false
|
||||
else:
|
||||
stdout.write ", ", token
|
||||
stdout.write "\n"
|
||||
stdout.flushFile()
|
||||
except CatchableError:
|
||||
raise newException(NoiseMalformedHandshake, "HandshakePattern malformed")
|
||||
|
||||
# Hashes a Noise protocol name using SHA256
|
||||
proc hashProtocol*(protocolName: string): MDigest[256] =
|
||||
# The output hash value
|
||||
var hash: MDigest[256]
|
||||
|
||||
# From Noise specification: Section 5.2
|
||||
# http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||
# If protocol_name is less than or equal to HASHLEN bytes in length,
|
||||
# sets h equal to protocol_name with zero bytes appended to make HASHLEN bytes.
|
||||
# Otherwise sets h = HASH(protocol_name).
|
||||
if protocolName.len <= 32:
|
||||
hash.data[0 .. protocolName.high] = protocolName.toBytes
|
||||
else:
|
||||
hash = sha256.digest(protocolName)
|
||||
|
||||
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
|
||||
info "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
|
||||
info "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 =
|
||||
# The output result of the Diffie-Hellman operation
|
||||
var output: EllipticCurveKey
|
||||
|
||||
# Since the EC multiplication writes the result to the input, we copy the input to the output variable
|
||||
output = public
|
||||
# We execute the DH operation
|
||||
EllipticCurve.mul(output, private)
|
||||
|
||||
return output
|
||||
|
||||
#################################################################
|
||||
|
||||
#################################
|
||||
# ChaChaPoly Cipher utilities
|
||||
#################################
|
||||
|
||||
# Generates a random ChaChaPolyKey for testing encryption/decryption
|
||||
proc randomChaChaPolyKey*(rng: var HmacDrbgContext): ChaChaPolyKey =
|
||||
var key: ChaChaPolyKey
|
||||
hmacDrbgGenerate(rng, key)
|
||||
return key
|
||||
|
||||
# Generates a random ChaChaPoly Cipher State for testing encryption/decryption
|
||||
proc randomChaChaPolyCipherState*(rng: var HmacDrbgContext): ChaChaPolyCipherState =
|
||||
var randomCipherState: ChaChaPolyCipherState
|
||||
randomCipherState.k = randomChaChaPolyKey(rng)
|
||||
hmacDrbgGenerate(rng, randomCipherState.nonce)
|
||||
randomCipherState.ad = newSeq[byte](32)
|
||||
hmacDrbgGenerate(rng, randomCipherState.ad)
|
||||
return randomCipherState
|
||||
|
||||
#################################################################
|
||||
|
||||
#################################
|
||||
# Noise Public keys utilities
|
||||
#################################
|
||||
|
||||
# Checks equality between two Noise public keys
|
||||
proc `==`*(k1, k2: NoisePublicKey): bool =
|
||||
return (k1.flag == k2.flag) and (k1.pk == k2.pk)
|
||||
|
||||
# Converts a public Elliptic Curve key to an unencrypted Noise public key
|
||||
proc toNoisePublicKey*(publicKey: EllipticCurveKey): NoisePublicKey =
|
||||
var noisePublicKey: NoisePublicKey
|
||||
noisePublicKey.flag = 0
|
||||
noisePublicKey.pk = getBytes(publicKey)
|
||||
return noisePublicKey
|
||||
|
||||
# Generates a random Noise public key
|
||||
proc genNoisePublicKey*(rng: var HmacDrbgContext): NoisePublicKey =
|
||||
var noisePublicKey: NoisePublicKey
|
||||
# We generate a random key pair
|
||||
let keyPair: KeyPair = genKeyPair(rng)
|
||||
# Since it is unencrypted, flag is 0
|
||||
noisePublicKey.flag = 0
|
||||
# We copy the public X coordinate of the key pair to the output Noise public key
|
||||
noisePublicKey.pk = getBytes(keyPair.publicKey)
|
||||
return noisePublicKey
|
||||
|
||||
# Converts a Noise public key to a stream of bytes as in
|
||||
# https://rfc.vac.dev/spec/35/#public-keys-serialization
|
||||
proc serializeNoisePublicKey*(noisePublicKey: NoisePublicKey): seq[byte] =
|
||||
var serializedNoisePublicKey: seq[byte]
|
||||
# Public key is serialized as (flag || pk)
|
||||
# Note that pk contains the X coordinate of the public key if unencrypted
|
||||
# or the encryption concatenated with the authorization tag if encrypted
|
||||
serializedNoisePublicKey.add noisePublicKey.flag
|
||||
serializedNoisePublicKey.add noisePublicKey.pk
|
||||
return serializedNoisePublicKey
|
||||
|
||||
# Converts a serialized Noise public key to a NoisePublicKey object as in
|
||||
# https://rfc.vac.dev/spec/35/#public-keys-serialization
|
||||
proc intoNoisePublicKey*(
|
||||
serializedNoisePublicKey: seq[byte]
|
||||
): NoisePublicKey {.raises: [Defect, NoisePublicKeyError].} =
|
||||
var noisePublicKey: NoisePublicKey
|
||||
# We retrieve the encryption flag
|
||||
noisePublicKey.flag = serializedNoisePublicKey[0]
|
||||
# If not 0 or 1 we raise a new exception
|
||||
if not (noisePublicKey.flag == 0 or noisePublicKey.flag == 1):
|
||||
raise newException(NoisePublicKeyError, "Invalid flag in serialized public key")
|
||||
# We set the remaining sequence to the pk value (this may be an encrypted or not encrypted X coordinate)
|
||||
noisePublicKey.pk = serializedNoisePublicKey[1 ..< serializedNoisePublicKey.len]
|
||||
return noisePublicKey
|
||||
|
||||
# Encrypts a Noise public key using a ChaChaPoly Cipher State
|
||||
proc encryptNoisePublicKey*(
|
||||
cs: ChaChaPolyCipherState, noisePublicKey: NoisePublicKey
|
||||
): NoisePublicKey {.raises: [Defect, NoiseEmptyChaChaPolyInput, NoiseNonceMaxError].} =
|
||||
var encryptedNoisePublicKey: NoisePublicKey
|
||||
# We proceed with encryption only if
|
||||
# - a key is set in the cipher state
|
||||
# - the public key is unencrypted
|
||||
if cs.k != EmptyKey and noisePublicKey.flag == 0:
|
||||
let encPk = encrypt(cs, noisePublicKey.pk)
|
||||
# We set the flag to 1, since encrypted
|
||||
encryptedNoisePublicKey.flag = 1
|
||||
# Authorization tag is appendend to the ciphertext
|
||||
encryptedNoisePublicKey.pk = encPk.data
|
||||
encryptedNoisePublicKey.pk.add encPk.tag
|
||||
# Otherwise we return the public key as it is
|
||||
else:
|
||||
encryptedNoisePublicKey = noisePublicKey
|
||||
return encryptedNoisePublicKey
|
||||
|
||||
# Decrypts a Noise public key using a ChaChaPoly Cipher State
|
||||
proc decryptNoisePublicKey*(
|
||||
cs: ChaChaPolyCipherState, noisePublicKey: NoisePublicKey
|
||||
): NoisePublicKey {.raises: [Defect, NoiseEmptyChaChaPolyInput, NoiseDecryptTagError].} =
|
||||
var decryptedNoisePublicKey: NoisePublicKey
|
||||
# We proceed with decryption only if
|
||||
# - a key is set in the cipher state
|
||||
# - the public key is encrypted
|
||||
if cs.k != EmptyKey and noisePublicKey.flag == 1:
|
||||
# Since the pk field would contain an encryption + tag, we retrieve the ciphertext length
|
||||
let pkLen = noisePublicKey.pk.len - ChaChaPolyTag.len
|
||||
# We isolate the ciphertext and the authorization tag
|
||||
let pk = noisePublicKey.pk[0 ..< pkLen]
|
||||
let pkAuth =
|
||||
intoChaChaPolyTag(noisePublicKey.pk[pkLen ..< pkLen + ChaChaPolyTag.len])
|
||||
# We convert it to a ChaChaPolyCiphertext
|
||||
let ciphertext = ChaChaPolyCiphertext(data: pk, tag: pkAuth)
|
||||
# We run decryption and store its value to a non-encrypted Noise public key (flag = 0)
|
||||
decryptedNoisePublicKey.pk = decrypt(cs, ciphertext)
|
||||
decryptedNoisePublicKey.flag = 0
|
||||
# Otherwise we return the public key as it is
|
||||
else:
|
||||
decryptedNoisePublicKey = noisePublicKey
|
||||
return decryptedNoisePublicKey
|
||||
|
||||
#################################################################
|
||||
|
||||
#################################
|
||||
# Payload encoding/decoding procedures
|
||||
#################################
|
||||
|
||||
# Checks equality between two PayloadsV2 objects
|
||||
proc `==`*(p1, p2: PayloadV2): bool =
|
||||
return
|
||||
(p1.messageNametag == p2.messageNametag) and (p1.protocolId == p2.protocolId) and
|
||||
(p1.handshakeMessage == p2.handshakeMessage) and
|
||||
(p1.transportMessage == p2.transportMessage)
|
||||
|
||||
# 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
|
||||
payload2.handshakeMessage =
|
||||
@[genNoisePublicKey(rng), genNoisePublicKey(rng), genNoisePublicKey(rng)]
|
||||
# We set the transport message to a random 128-bytes long sequence
|
||||
payload2.transportMessage = randomSeqByte(rng, 128)
|
||||
return payload2
|
||||
|
||||
# Serializes a PayloadV2 object to a byte sequences according to https://rfc.vac.dev/spec/35/.
|
||||
# The output serialized payload concatenates the input PayloadV2 object fields as
|
||||
# payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
|
||||
# The output can be then passed to the payload field of a WakuMessage https://rfc.vac.dev/spec/14/
|
||||
proc serializePayloadV2*(self: PayloadV2): Result[seq[byte], cstring] =
|
||||
# We collect public keys contained in the handshake message
|
||||
var
|
||||
# According to https://rfc.vac.dev/spec/35/, the maximum size for the handshake message is 256 bytes, that is
|
||||
# the handshake message length can be represented with 1 byte only. (its length can be stored in 1 byte)
|
||||
# However, to ease public keys length addition operation, we declare it as int and later cast to uit8
|
||||
serializedHandshakeMessageLen: int = 0
|
||||
# This variables will store the concatenation of the serializations of all public keys in the handshake message
|
||||
serializedHandshakeMessage = newSeqOfCap[byte](256)
|
||||
# A variable to store the currently processed public key serialization
|
||||
serializedPk: seq[byte]
|
||||
# For each public key in the handshake message
|
||||
for pk in self.handshakeMessage:
|
||||
# We serialize the public key
|
||||
serializedPk = serializeNoisePublicKey(pk)
|
||||
# We sum its serialized length to the total
|
||||
serializedHandshakeMessageLen += serializedPk.len
|
||||
# We add its serialization to the concatenation of all serialized public keys in the handshake message
|
||||
serializedHandshakeMessage.add serializedPk
|
||||
# If we are processing more than 256 byte, we return an error
|
||||
if serializedHandshakeMessageLen > uint8.high.int:
|
||||
info "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](
|
||||
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
|
||||
transportMessageLen # transportMessageLen bytes for transportMessage
|
||||
)
|
||||
|
||||
# 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
|
||||
# The transport message length is converted from uint64 to bytes in Little-Endian
|
||||
payload.add toBytesLE(transportMessageLen.uint64)
|
||||
payload.add self.transportMessage
|
||||
|
||||
return ok(payload)
|
||||
|
||||
# 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 = ( messageNametag || protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
|
||||
proc deserializePayloadV2*(
|
||||
payload: seq[byte]
|
||||
): Result[PayloadV2, cstring] {.raises: [Defect, NoisePublicKeyError].} =
|
||||
# The output PayloadV2
|
||||
var payload2: PayloadV2
|
||||
|
||||
# i is the read input buffer position index
|
||||
var i: uint64 = 0
|
||||
|
||||
# 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
|
||||
|
||||
# We read the Handshake Message lenght (1 byte)
|
||||
var handshakeMessageLen = payload[i].uint64
|
||||
if handshakeMessageLen > uint8.high.uint64:
|
||||
info "Payload malformed: too many public keys contained in the handshake message"
|
||||
return err("Too many public keys in handshake message")
|
||||
|
||||
i += 1
|
||||
|
||||
# We now read for handshakeMessageLen bytes the buffer and we deserialize each (encrypted/unencrypted) public key read
|
||||
var
|
||||
# In handshakeMessage we accumulate the read deserialized Noise Public keys
|
||||
handshakeMessage: seq[NoisePublicKey]
|
||||
flag: byte
|
||||
pkLen: uint64
|
||||
written: uint64 = 0
|
||||
|
||||
# We read the buffer until handshakeMessageLen are read
|
||||
while written != handshakeMessageLen:
|
||||
# We obtain the current Noise Public key encryption flag
|
||||
flag = payload[i]
|
||||
# If the key is unencrypted, we only read the X coordinate of the EC public key and we deserialize into a Noise Public Key
|
||||
if flag == 0:
|
||||
pkLen = 1 + EllipticCurveKey.len
|
||||
handshakeMessage.add intoNoisePublicKey(payload[i ..< i + pkLen])
|
||||
i += pkLen
|
||||
written += pkLen
|
||||
# If the key is encrypted, we only read the encrypted X coordinate and the authorization tag, and we deserialize into a Noise Public Key
|
||||
elif flag == 1:
|
||||
pkLen = 1 + EllipticCurveKey.len + ChaChaPolyTag.len
|
||||
handshakeMessage.add intoNoisePublicKey(payload[i ..< i + pkLen])
|
||||
i += pkLen
|
||||
written += pkLen
|
||||
else:
|
||||
return err("Invalid flag for Noise public key")
|
||||
|
||||
# We save in the output PayloadV2 the read handshake message
|
||||
payload2.handshakeMessage = handshakeMessage
|
||||
|
||||
# We read the transport message length (8 bytes) and we convert to uint64 in Little Endian
|
||||
let transportMessageLen = fromBytesLE(uint64, payload[i .. (i + 8 - 1)])
|
||||
i += 8
|
||||
|
||||
# We read the transport message (handshakeMessage bytes)
|
||||
payload2.transportMessage = payload[i .. i + transportMessageLen - 1]
|
||||
i += transportMessageLen
|
||||
|
||||
return ok(payload2)
|
||||
@ -64,8 +64,6 @@ import
|
||||
./test_waku_enr,
|
||||
./test_waku_dnsdisc,
|
||||
./test_relay_peer_exchange,
|
||||
./test_waku_noise,
|
||||
./test_waku_noise_sessions,
|
||||
./test_waku_netconfig,
|
||||
./test_waku_switch,
|
||||
./test_waku_rendezvous,
|
||||
|
||||
@ -28,8 +28,6 @@ import
|
||||
../resources/payloads,
|
||||
../waku_rln_relay/[utils_static, utils_onchain]
|
||||
|
||||
from ../../logos_delivery/waku/waku_noise/noise_utils import randomSeqByte
|
||||
|
||||
proc buildRandomIdentityCredentials(): IdentityCredential =
|
||||
# We generate a random identity credential (inter-value constrains are not enforced, otherwise we need to load e.g. zerokit RLN keygen)
|
||||
let
|
||||
|
||||
@ -3,8 +3,6 @@
|
||||
import std/[os, json], chronos, testutils/unittests
|
||||
import logos_delivery/waku/waku_keystore, ./testlib/common
|
||||
|
||||
from logos_delivery/waku/waku_noise/noise_utils import randomSeqByte
|
||||
|
||||
procSuite "Credentials test suite":
|
||||
let testAppInfo = AppInfo(application: "test", appIdentifier: "1234", version: "0.1")
|
||||
|
||||
|
||||
@ -3,8 +3,6 @@
|
||||
import std/[json, os], stew/byteutils, testutils/unittests, chronos, eth/keys
|
||||
import logos_delivery/waku/waku_keystore, ./testlib/common
|
||||
|
||||
from logos_delivery/waku/waku_noise/noise_utils import randomSeqByte
|
||||
|
||||
suite "KeyFile test suite":
|
||||
test "Create/Save/Load single keyfile":
|
||||
# The password we use to encrypt our secret
|
||||
|
||||
@ -1,901 +0,0 @@
|
||||
{.used.}
|
||||
|
||||
import
|
||||
testutils/unittests,
|
||||
std/random,
|
||||
std/tables,
|
||||
stew/byteutils,
|
||||
libp2p/crypto/chacha20poly1305,
|
||||
libp2p/protobuf/minprotobuf,
|
||||
stew/endians2
|
||||
import
|
||||
logos_delivery/waku/[
|
||||
utils/noise as waku_message_utils,
|
||||
waku_noise/noise_types,
|
||||
waku_noise/noise_utils,
|
||||
waku_noise/noise,
|
||||
waku_noise/noise_handshake_processing,
|
||||
waku_core,
|
||||
],
|
||||
./testlib/common
|
||||
|
||||
procSuite "Waku Noise":
|
||||
common.randomize()
|
||||
|
||||
test "PKCS#7 Padding/Unpadding":
|
||||
# We test padding for different message lengths
|
||||
let maxMessageLength = 3 * NoisePaddingBlockSize
|
||||
for messageLen in 0 .. maxMessageLength:
|
||||
let
|
||||
message = randomSeqByte(rng[], messageLen)
|
||||
padded = pkcs7_pad(message, NoisePaddingBlockSize)
|
||||
unpadded = pkcs7_unpad(padded, NoisePaddingBlockSize)
|
||||
|
||||
check:
|
||||
padded.len != 0
|
||||
padded.len mod NoisePaddingBlockSize == 0
|
||||
message == unpadded
|
||||
|
||||
test "ChaChaPoly Encryption/Decryption: random byte sequences":
|
||||
let cipherState = randomChaChaPolyCipherState(rng[])
|
||||
|
||||
# We encrypt/decrypt random byte sequences
|
||||
let
|
||||
plaintext: seq[byte] = randomSeqByte(rng[], rand(1 .. 128))
|
||||
ciphertext: ChaChaPolyCiphertext = encrypt(cipherState, plaintext)
|
||||
decryptedCiphertext: seq[byte] = decrypt(cipherState, ciphertext)
|
||||
|
||||
check:
|
||||
plaintext == decryptedCiphertext
|
||||
|
||||
test "ChaChaPoly Encryption/Decryption: random strings":
|
||||
let cipherState = randomChaChaPolyCipherState(rng[])
|
||||
|
||||
# We encrypt/decrypt random strings
|
||||
var plaintext: string
|
||||
for _ in 1 .. rand(1 .. 128):
|
||||
add(plaintext, char(rand(int('A') .. int('z'))))
|
||||
|
||||
let
|
||||
ciphertext: ChaChaPolyCiphertext = encrypt(cipherState, plaintext.toBytes())
|
||||
decryptedCiphertext: seq[byte] = decrypt(cipherState, ciphertext)
|
||||
|
||||
check:
|
||||
plaintext.toBytes() == decryptedCiphertext
|
||||
|
||||
test "Noise public keys: encrypt and decrypt a public key":
|
||||
let noisePublicKey: NoisePublicKey = genNoisePublicKey(rng[])
|
||||
|
||||
let
|
||||
cs: ChaChaPolyCipherState = randomChaChaPolyCipherState(rng[])
|
||||
encryptedPk: NoisePublicKey = encryptNoisePublicKey(cs, noisePublicKey)
|
||||
decryptedPk: NoisePublicKey = decryptNoisePublicKey(cs, encryptedPk)
|
||||
|
||||
check:
|
||||
noisePublicKey == decryptedPk
|
||||
|
||||
test "Noise public keys: decrypt an unencrypted public key":
|
||||
let noisePublicKey: NoisePublicKey = genNoisePublicKey(rng[])
|
||||
|
||||
let
|
||||
cs: ChaChaPolyCipherState = randomChaChaPolyCipherState(rng[])
|
||||
decryptedPk: NoisePublicKey = decryptNoisePublicKey(cs, noisePublicKey)
|
||||
|
||||
check:
|
||||
noisePublicKey == decryptedPk
|
||||
|
||||
test "Noise public keys: encrypt an encrypted public key":
|
||||
let noisePublicKey: NoisePublicKey = genNoisePublicKey(rng[])
|
||||
|
||||
let
|
||||
cs: ChaChaPolyCipherState = randomChaChaPolyCipherState(rng[])
|
||||
encryptedPk: NoisePublicKey = encryptNoisePublicKey(cs, noisePublicKey)
|
||||
encryptedPk2: NoisePublicKey = encryptNoisePublicKey(cs, encryptedPk)
|
||||
|
||||
check:
|
||||
encryptedPk == encryptedPk2
|
||||
|
||||
test "Noise public keys: encrypt, decrypt and decrypt a public key":
|
||||
let noisePublicKey: NoisePublicKey = genNoisePublicKey(rng[])
|
||||
|
||||
let
|
||||
cs: ChaChaPolyCipherState = randomChaChaPolyCipherState(rng[])
|
||||
encryptedPk: NoisePublicKey = encryptNoisePublicKey(cs, noisePublicKey)
|
||||
decryptedPk: NoisePublicKey = decryptNoisePublicKey(cs, encryptedPk)
|
||||
decryptedPk2: NoisePublicKey = decryptNoisePublicKey(cs, decryptedPk)
|
||||
|
||||
check:
|
||||
decryptedPk == decryptedPk2
|
||||
|
||||
test "Noise public keys: serialize and deserialize an unencrypted public key":
|
||||
let
|
||||
noisePublicKey: NoisePublicKey = genNoisePublicKey(rng[])
|
||||
serializedNoisePublicKey: seq[byte] = serializeNoisePublicKey(noisePublicKey)
|
||||
deserializedNoisePublicKey: NoisePublicKey =
|
||||
intoNoisePublicKey(serializedNoisePublicKey)
|
||||
|
||||
check:
|
||||
noisePublicKey == deserializedNoisePublicKey
|
||||
|
||||
test "Noise public keys: encrypt, serialize, deserialize and decrypt a public key":
|
||||
let noisePublicKey: NoisePublicKey = genNoisePublicKey(rng[])
|
||||
|
||||
let
|
||||
cs: ChaChaPolyCipherState = randomChaChaPolyCipherState(rng[])
|
||||
encryptedPk: NoisePublicKey = encryptNoisePublicKey(cs, noisePublicKey)
|
||||
serializedNoisePublicKey: seq[byte] = serializeNoisePublicKey(encryptedPk)
|
||||
deserializedNoisePublicKey: NoisePublicKey =
|
||||
intoNoisePublicKey(serializedNoisePublicKey)
|
||||
decryptedPk: NoisePublicKey =
|
||||
decryptNoisePublicKey(cs, deserializedNoisePublicKey)
|
||||
|
||||
check:
|
||||
noisePublicKey == decryptedPk
|
||||
|
||||
test "PayloadV2: serialize/deserialize PayloadV2 to byte sequence":
|
||||
let
|
||||
payload2: PayloadV2 = randomPayloadV2(rng[])
|
||||
serializedPayload = serializePayloadV2(payload2)
|
||||
|
||||
check:
|
||||
serializedPayload.isOk()
|
||||
|
||||
let deserializedPayload = deserializePayloadV2(serializedPayload.get())
|
||||
|
||||
check:
|
||||
deserializedPayload.isOk()
|
||||
payload2 == deserializedPayload.get()
|
||||
|
||||
test "PayloadV2: Encode/Decode a Waku Message (version 2) to a PayloadV2":
|
||||
# We encode to a WakuMessage a random PayloadV2
|
||||
let
|
||||
payload2 = randomPayloadV2(rng[])
|
||||
msg = encodePayloadV2(payload2)
|
||||
|
||||
check:
|
||||
msg.isOk()
|
||||
|
||||
# We create ProtoBuffer from WakuMessage
|
||||
let pb = msg.get().encode()
|
||||
|
||||
# We decode the WakuMessage from the ProtoBuffer
|
||||
let msgFromPb = WakuMessage.decode(pb.buffer)
|
||||
|
||||
check:
|
||||
msgFromPb.isOk()
|
||||
|
||||
let decoded = decodePayloadV2(msgFromPb.get())
|
||||
|
||||
check:
|
||||
decoded.isOk()
|
||||
payload2 == decoded.get()
|
||||
|
||||
test "Noise State Machine: Diffie-Hellman operation":
|
||||
#We generate random keypairs
|
||||
let
|
||||
aliceKey = genKeyPair(rng[])
|
||||
bobKey = genKeyPair(rng[])
|
||||
|
||||
# A Diffie-Hellman operation between Alice's private key and Bob's public key must be equal to
|
||||
# a Diffie-hellman operation between Alice's public key and Bob's private key
|
||||
let
|
||||
dh1 = dh(getPrivateKey(aliceKey), getPublicKey(bobKey))
|
||||
dh2 = dh(getPrivateKey(bobKey), getPublicKey(aliceKey))
|
||||
|
||||
check:
|
||||
dh1 == dh2
|
||||
|
||||
test "Noise State Machine: Cipher State primitives":
|
||||
# We generate a random Cipher State, associated data ad and plaintext
|
||||
var
|
||||
cipherState: CipherState = randomCipherState(rng[])
|
||||
nonce: uint64 = uint64(rand(0 .. int.high))
|
||||
ad: seq[byte] = randomSeqByte(rng[], rand(1 .. 128))
|
||||
plaintext: seq[byte] = randomSeqByte(rng[], rand(1 .. 128))
|
||||
|
||||
# We set the random nonce generated in the cipher state
|
||||
setNonce(cipherState, nonce)
|
||||
|
||||
# We perform encryption
|
||||
var ciphertext: seq[byte] = encryptWithAd(cipherState, ad, plaintext)
|
||||
|
||||
# After any encryption/decryption operation, the Cipher State's nonce increases by 1
|
||||
check:
|
||||
getNonce(cipherState) == nonce + 1
|
||||
|
||||
# We set the nonce back to its original value for decryption
|
||||
setNonce(cipherState, nonce)
|
||||
|
||||
# We decrypt (using the original nonce)
|
||||
var decrypted: seq[byte] = decryptWithAd(cipherState, ad, ciphertext)
|
||||
|
||||
# We check if encryption and decryption are correct and that nonce correctly increased after decryption
|
||||
check:
|
||||
getNonce(cipherState) == nonce + 1
|
||||
plaintext == decrypted
|
||||
|
||||
# If a Cipher State has no key set, encryptWithAd should return the plaintext without increasing the nonce
|
||||
setCipherStateKey(cipherState, EmptyKey)
|
||||
nonce = getNonce(cipherState)
|
||||
|
||||
plaintext = randomSeqByte(rng[], rand(1 .. 128))
|
||||
ciphertext = encryptWithAd(cipherState, ad, plaintext)
|
||||
|
||||
check:
|
||||
ciphertext == plaintext
|
||||
getNonce(cipherState) == nonce
|
||||
|
||||
# If a Cipher State has no key set, decryptWithAd should return the ciphertext without increasing the nonce
|
||||
setCipherStateKey(cipherState, EmptyKey)
|
||||
nonce = getNonce(cipherState)
|
||||
|
||||
# Note that we set ciphertext minimum length to 16 to not trigger checks on authentication tag length
|
||||
ciphertext = randomSeqByte(rng[], rand(16 .. 128))
|
||||
plaintext = decryptWithAd(cipherState, ad, ciphertext)
|
||||
|
||||
check:
|
||||
ciphertext == plaintext
|
||||
getNonce(cipherState) == nonce
|
||||
|
||||
# A Cipher State cannot have a nonce greater or equal 2^64-1
|
||||
# Note that NonceMax is uint64.high - 1 = 2^64-1-1 and that nonce is increased after each encryption and decryption operation
|
||||
|
||||
# We generate a test Cipher State with nonce set to MaxNonce
|
||||
cipherState = randomCipherState(rng[])
|
||||
setNonce(cipherState, NonceMax)
|
||||
plaintext = randomSeqByte(rng[], rand(1 .. 128))
|
||||
|
||||
# We test if encryption fails with a NoiseNonceMaxError error. Any subsequent encryption call over the Cipher State should fail similarly and leave the nonce unchanged
|
||||
for _ in [1 .. 5]:
|
||||
expect NoiseNonceMaxError:
|
||||
ciphertext = encryptWithAd(cipherState, ad, plaintext)
|
||||
|
||||
check:
|
||||
getNonce(cipherState) == NonceMax + 1
|
||||
|
||||
# We generate a test Cipher State
|
||||
# Since nonce is increased after decryption as well, we need to generate a proper ciphertext in order to test MaxNonceError error handling
|
||||
# We cannot call encryptWithAd to encrypt a plaintext using a nonce equal MaxNonce, since this will trigger a MaxNonceError.
|
||||
# To perform such test, we then need to encrypt a test plaintext using directly ChaChaPoly primitive
|
||||
cipherState = randomCipherState(rng[])
|
||||
setNonce(cipherState, NonceMax)
|
||||
plaintext = randomSeqByte(rng[], rand(1 .. 128))
|
||||
|
||||
# We perform encryption using the Cipher State key, NonceMax and ad
|
||||
# By Noise specification the nonce is 8 bytes long out of the 12 bytes supported by ChaChaPoly, thus we copy the Little endian conversion of the nonce to a ChaChaPolyNonce
|
||||
var
|
||||
encNonce: ChaChaPolyNonce
|
||||
authorizationTag: ChaChaPolyTag
|
||||
encNonce[4 ..< 12] = toBytesLE(NonceMax)
|
||||
ChaChaPoly.encrypt(getKey(cipherState), encNonce, authorizationTag, plaintext, ad)
|
||||
|
||||
# The output ciphertext is stored in the plaintext variable after ChaChaPoly.encrypt is called: we copy it along with the authorization tag.
|
||||
ciphertext = @[]
|
||||
ciphertext.add(plaintext)
|
||||
ciphertext.add(authorizationTag)
|
||||
|
||||
# At this point ciphertext is a proper encryption of the original plaintext obtained with nonce equal to NonceMax
|
||||
# We can now test if decryption fails with a NoiseNonceMaxError error. Any subsequent decryption call over the Cipher State should fail similarly and leave the nonce unchanged
|
||||
# Note that decryptWithAd doesn't fail in decrypting the ciphertext (otherwise a NoiseDecryptTagError would have been triggered)
|
||||
for _ in [1 .. 5]:
|
||||
expect NoiseNonceMaxError:
|
||||
plaintext = decryptWithAd(cipherState, ad, ciphertext)
|
||||
|
||||
check:
|
||||
getNonce(cipherState) == NonceMax + 1
|
||||
|
||||
test "Noise State Machine: Symmetric State primitives":
|
||||
# We select one supported handshake pattern and we initialize a symmetric state
|
||||
var
|
||||
hsPattern = NoiseHandshakePatterns["XX"]
|
||||
symmetricState: SymmetricState = SymmetricState.init(hsPattern)
|
||||
|
||||
# We get all the Symmetric State field
|
||||
# cs : Cipher State
|
||||
# ck : chaining key
|
||||
# h : handshake hash
|
||||
var
|
||||
cs = getCipherState(symmetricState)
|
||||
ck = getChainingKey(symmetricState)
|
||||
h = getHandshakeHash(symmetricState)
|
||||
|
||||
# When a Symmetric state is initialized, handshake hash and chaining key are (byte-wise) equal
|
||||
check:
|
||||
h.data.intoChaChaPolyKey == ck
|
||||
|
||||
########################################
|
||||
# mixHash
|
||||
########################################
|
||||
|
||||
# We generate a random byte sequence and execute a mixHash over it
|
||||
mixHash(symmetricState, randomSeqByte(rng[], rand(1 .. 128)))
|
||||
|
||||
# mixHash changes only the handshake hash value of the Symmetric state
|
||||
check:
|
||||
cs == getCipherState(symmetricState)
|
||||
ck == getChainingKey(symmetricState)
|
||||
h != getHandshakeHash(symmetricState)
|
||||
|
||||
# We update test values
|
||||
h = getHandshakeHash(symmetricState)
|
||||
|
||||
########################################
|
||||
# mixKey
|
||||
########################################
|
||||
|
||||
# We generate random input key material and we execute mixKey
|
||||
var inputKeyMaterial = randomSeqByte(rng[], rand(1 .. 128))
|
||||
mixKey(symmetricState, inputKeyMaterial)
|
||||
|
||||
# mixKey changes the Symmetric State's chaining key and encryption key of the embedded Cipher State
|
||||
# It further sets to 0 the nonce of the embedded Cipher State
|
||||
check:
|
||||
getKey(cs) != getKey(getCipherState(symmetricState))
|
||||
getNonce(getCipherState(symmetricState)) == 0.uint64
|
||||
cs != getCipherState(symmetricState)
|
||||
ck != getChainingKey(symmetricState)
|
||||
h == getHandshakeHash(symmetricState)
|
||||
|
||||
# We update test values
|
||||
cs = getCipherState(symmetricState)
|
||||
ck = getChainingKey(symmetricState)
|
||||
|
||||
########################################
|
||||
# mixKeyAndHash
|
||||
########################################
|
||||
|
||||
# We generate random input key material and we execute mixKeyAndHash
|
||||
inputKeyMaterial = randomSeqByte(rng[], rand(1 .. 128))
|
||||
mixKeyAndHash(symmetricState, inputKeyMaterial)
|
||||
|
||||
# mixKeyAndHash executes a mixKey and a mixHash using the input key material
|
||||
# All Symmetric State's fields are updated
|
||||
check:
|
||||
cs != getCipherState(symmetricState)
|
||||
ck != getChainingKey(symmetricState)
|
||||
h != getHandshakeHash(symmetricState)
|
||||
|
||||
# We update test values
|
||||
cs = getCipherState(symmetricState)
|
||||
ck = getChainingKey(symmetricState)
|
||||
h = getHandshakeHash(symmetricState)
|
||||
|
||||
########################################
|
||||
# encryptAndHash and decryptAndHash
|
||||
########################################
|
||||
|
||||
# We store the initial symmetricState in order to correctly perform decryption
|
||||
var initialSymmetricState = symmetricState
|
||||
|
||||
# We generate random plaintext and we execute encryptAndHash
|
||||
var plaintext = randomChaChaPolyKey(rng[])
|
||||
var nonce = getNonce(getCipherState(symmetricState))
|
||||
var ciphertext = encryptAndHash(symmetricState, plaintext)
|
||||
|
||||
# encryptAndHash combines encryptWithAd and mixHash over the ciphertext (encryption increases the nonce of the embedded Cipher State but does not change its key)
|
||||
# We check if only the handshake hash value and the Symmetric State changed accordingly
|
||||
check:
|
||||
cs != getCipherState(symmetricState)
|
||||
getKey(cs) == getKey(getCipherState(symmetricState))
|
||||
getNonce(getCipherState(symmetricState)) == nonce + 1
|
||||
ck == getChainingKey(symmetricState)
|
||||
h != getHandshakeHash(symmetricState)
|
||||
|
||||
# We restore the symmetric State to its initial value to test decryption
|
||||
symmetricState = initialSymmetricState
|
||||
|
||||
# We execute decryptAndHash over the ciphertext
|
||||
var decrypted = decryptAndHash(symmetricState, ciphertext)
|
||||
|
||||
# decryptAndHash combines decryptWithAd and mixHash over the ciphertext (encryption increases the nonce of the embedded Cipher State but does not change its key)
|
||||
# We check if only the handshake hash value and the Symmetric State changed accordingly
|
||||
# We further check if decryption corresponds to the original plaintext
|
||||
check:
|
||||
cs != getCipherState(symmetricState)
|
||||
getKey(cs) == getKey(getCipherState(symmetricState))
|
||||
getNonce(getCipherState(symmetricState)) == nonce + 1
|
||||
ck == getChainingKey(symmetricState)
|
||||
h != getHandshakeHash(symmetricState)
|
||||
decrypted == plaintext
|
||||
|
||||
########################################
|
||||
# split
|
||||
########################################
|
||||
|
||||
# If at least one mixKey is executed (as above), ck is non-empty
|
||||
check:
|
||||
getChainingKey(symmetricState) != EmptyKey
|
||||
|
||||
# When a Symmetric State's ck is non-empty, we can execute split, which creates two distinct Cipher States cs1 and cs2
|
||||
# with non-empty encryption keys and nonce set to 0
|
||||
var (cs1, cs2) = split(symmetricState)
|
||||
|
||||
check:
|
||||
getKey(cs1) != EmptyKey
|
||||
getKey(cs2) != EmptyKey
|
||||
getNonce(cs1) == 0.uint64
|
||||
getNonce(cs2) == 0.uint64
|
||||
getKey(cs1) != getKey(cs2)
|
||||
|
||||
test "Noise XX Handhshake and message encryption (extended test)":
|
||||
let hsPattern = NoiseHandshakePatterns["XX"]
|
||||
|
||||
# We initialize Alice's and Bob's Handshake State
|
||||
let aliceStaticKey = genKeyPair(rng[])
|
||||
var aliceHS =
|
||||
initialize(hsPattern = hsPattern, staticKey = aliceStaticKey, initiator = true)
|
||||
|
||||
let bobStaticKey = genKeyPair(rng[])
|
||||
var bobHS = initialize(hsPattern = hsPattern, staticKey = bobStaticKey)
|
||||
|
||||
var
|
||||
sentTransportMessage: seq[byte]
|
||||
aliceStep, bobStep: HandshakeStepResult
|
||||
|
||||
# Here the handshake starts
|
||||
# Write and read calls alternate between Alice and Bob: the handhshake progresses by alternatively calling stepHandshake for each user
|
||||
|
||||
###############
|
||||
# 1st step
|
||||
###############
|
||||
|
||||
# We generate a random transport message
|
||||
sentTransportMessage = randomSeqByte(rng[], 32)
|
||||
|
||||
# By being the handshake initiator, Alice writes a Waku2 payload v2 containing her handshake message
|
||||
# and the (encrypted) transport message
|
||||
aliceStep =
|
||||
stepHandshake(rng[], aliceHS, transportMessage = sentTransportMessage).get()
|
||||
|
||||
# Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
|
||||
bobStep = stepHandshake(rng[], bobHS, readPayloadV2 = aliceStep.payload2).get()
|
||||
|
||||
check:
|
||||
bobStep.transportMessage == sentTransportMessage
|
||||
|
||||
###############
|
||||
# 2nd step
|
||||
###############
|
||||
|
||||
# We generate a random transport message
|
||||
sentTransportMessage = randomSeqByte(rng[], 32)
|
||||
|
||||
# At this step, Bob writes and returns a payload
|
||||
bobStep = stepHandshake(rng[], bobHS, transportMessage = sentTransportMessage).get()
|
||||
|
||||
# While Alice reads and returns the (decrypted) transport message
|
||||
aliceStep = stepHandshake(rng[], aliceHS, readPayloadV2 = bobStep.payload2).get()
|
||||
|
||||
check:
|
||||
aliceStep.transportMessage == sentTransportMessage
|
||||
|
||||
###############
|
||||
# 3rd step
|
||||
###############
|
||||
|
||||
# We generate a random transport message
|
||||
sentTransportMessage = randomSeqByte(rng[], 32)
|
||||
|
||||
# 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).get()
|
||||
|
||||
# Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
|
||||
bobStep = stepHandshake(rng[], bobHS, readPayloadV2 = aliceStep.payload2).get()
|
||||
|
||||
check:
|
||||
bobStep.transportMessage == sentTransportMessage
|
||||
|
||||
# Note that for this handshake pattern, no more message patterns are left for processing
|
||||
# Another call to stepHandshake would return an empty HandshakeStepResult
|
||||
# We test that extra calls to stepHandshake do not affect parties' handshake states
|
||||
# and that the intermediate HandshakeStepResult are empty
|
||||
let prevAliceHS = aliceHS
|
||||
let prevBobHS = bobHS
|
||||
|
||||
let bobStep1 =
|
||||
stepHandshake(rng[], bobHS, transportMessage = sentTransportMessage).get()
|
||||
let aliceStep1 =
|
||||
stepHandshake(rng[], aliceHS, readPayloadV2 = bobStep1.payload2).get()
|
||||
let aliceStep2 =
|
||||
stepHandshake(rng[], aliceHS, transportMessage = sentTransportMessage).get()
|
||||
let bobStep2 =
|
||||
stepHandshake(rng[], bobHS, readPayloadV2 = aliceStep2.payload2).get()
|
||||
|
||||
check:
|
||||
aliceStep1 == default(HandshakeStepResult)
|
||||
aliceStep2 == default(HandshakeStepResult)
|
||||
bobStep1 == default(HandshakeStepResult)
|
||||
bobStep2 == default(HandshakeStepResult)
|
||||
aliceHS == prevAliceHS
|
||||
bobHS == prevBobHS
|
||||
|
||||
#########################
|
||||
# After Handshake
|
||||
#########################
|
||||
|
||||
# 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]
|
||||
defaultMessageNametagBuffer: MessageNametagBuffer
|
||||
|
||||
for _ in 0 .. 10:
|
||||
# Alice writes to Bob
|
||||
message = randomSeqByte(rng[], 32)
|
||||
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, defaultMessageNametagBuffer)
|
||||
readMessage =
|
||||
readMessage(aliceHSResult, payload2, defaultMessageNametagBuffer).get()
|
||||
|
||||
check:
|
||||
message == readMessage
|
||||
|
||||
test "Noise XXpsk0 Handhshake and message encryption (short test)":
|
||||
let hsPattern = NoiseHandshakePatterns["XXpsk0"]
|
||||
|
||||
# We generate a random psk
|
||||
let psk = randomSeqByte(rng[], 32)
|
||||
|
||||
# We initialize Alice's and Bob's Handshake State
|
||||
let aliceStaticKey = genKeyPair(rng[])
|
||||
var aliceHS = initialize(
|
||||
hsPattern = hsPattern, staticKey = aliceStaticKey, psk = psk, initiator = true
|
||||
)
|
||||
|
||||
let bobStaticKey = genKeyPair(rng[])
|
||||
var bobHS = initialize(hsPattern = hsPattern, staticKey = bobStaticKey, psk = psk)
|
||||
|
||||
var
|
||||
sentTransportMessage: seq[byte]
|
||||
aliceStep, bobStep: HandshakeStepResult
|
||||
|
||||
# Here the handshake starts
|
||||
# Write and read calls alternate between Alice and Bob: the handhshake progresses by alternatively calling stepHandshake for each user
|
||||
|
||||
###############
|
||||
# 1st step
|
||||
###############
|
||||
|
||||
# We generate a random transport message
|
||||
sentTransportMessage = randomSeqByte(rng[], 32)
|
||||
|
||||
# By being the handshake initiator, Alice writes a Waku2 payload v2 containing her handshake message
|
||||
# and the (encrypted) transport message
|
||||
aliceStep =
|
||||
stepHandshake(rng[], aliceHS, transportMessage = sentTransportMessage).get()
|
||||
|
||||
# Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
|
||||
bobStep = stepHandshake(rng[], bobHS, readPayloadV2 = aliceStep.payload2).get()
|
||||
|
||||
check:
|
||||
bobStep.transportMessage == sentTransportMessage
|
||||
|
||||
###############
|
||||
# 2nd step
|
||||
###############
|
||||
|
||||
# We generate a random transport message
|
||||
sentTransportMessage = randomSeqByte(rng[], 32)
|
||||
|
||||
# At this step, Bob writes and returns a payload
|
||||
bobStep = stepHandshake(rng[], bobHS, transportMessage = sentTransportMessage).get()
|
||||
|
||||
# While Alice reads and returns the (decrypted) transport message
|
||||
aliceStep = stepHandshake(rng[], aliceHS, readPayloadV2 = bobStep.payload2).get()
|
||||
|
||||
check:
|
||||
aliceStep.transportMessage == sentTransportMessage
|
||||
|
||||
###############
|
||||
# 3rd step
|
||||
###############
|
||||
|
||||
# We generate a random transport message
|
||||
sentTransportMessage = randomSeqByte(rng[], 32)
|
||||
|
||||
# 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).get()
|
||||
|
||||
# Bob reads Alice's payloads, and returns the (decrypted) transportMessage alice sent to him
|
||||
bobStep = stepHandshake(rng[], bobHS, readPayloadV2 = aliceStep.payload2).get()
|
||||
|
||||
check:
|
||||
bobStep.transportMessage == sentTransportMessage
|
||||
|
||||
# Note that for this handshake pattern, no more message patterns are left for processing
|
||||
|
||||
#########################
|
||||
# After Handshake
|
||||
#########################
|
||||
|
||||
# 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]
|
||||
defaultMessageNametagBuffer: MessageNametagBuffer
|
||||
|
||||
for _ in 0 .. 10:
|
||||
# Alice writes to Bob
|
||||
message = randomSeqByte(rng[], 32)
|
||||
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, defaultMessageNametagBuffer)
|
||||
readMessage =
|
||||
readMessage(aliceHSResult, payload2, defaultMessageNametagBuffer).get()
|
||||
|
||||
check:
|
||||
message == readMessage
|
||||
|
||||
test "Noise K1K1 Handhshake and message encryption (short test)":
|
||||
let hsPattern = NoiseHandshakePatterns["K1K1"]
|
||||
|
||||
# We initialize Alice's and Bob's Handshake State
|
||||
let aliceStaticKey = genKeyPair(rng[])
|
||||
let bobStaticKey = genKeyPair(rng[])
|
||||
|
||||
# This handshake has the following pre-message pattern:
|
||||
# -> s
|
||||
# <- s
|
||||
# ...
|
||||
# So we define accordingly the sequence of the pre-message public keys
|
||||
let preMessagePKs: seq[NoisePublicKey] = @[
|
||||
toNoisePublicKey(getPublicKey(aliceStaticKey)),
|
||||
toNoisePublicKey(getPublicKey(bobStaticKey)),
|
||||
]
|
||||
|
||||
var aliceHS = initialize(
|
||||
hsPattern = hsPattern,
|
||||
staticKey = aliceStaticKey,
|
||||
preMessagePKs = preMessagePKs,
|
||||
initiator = true,
|
||||
)
|
||||
var bobHS = initialize(
|
||||
hsPattern = hsPattern, staticKey = bobStaticKey, preMessagePKs = preMessagePKs
|
||||
)
|
||||
|
||||
var
|
||||
sentTransportMessage: seq[byte]
|
||||
aliceStep, bobStep: HandshakeStepResult
|
||||
|
||||
# Here the handshake starts
|
||||
# Write and read calls alternate between Alice and Bob: the handhshake progresses by alternatively calling stepHandshake for each user
|
||||
|
||||
###############
|
||||
# 1st step
|
||||
###############
|
||||
|
||||
# We generate a random transport message
|
||||
sentTransportMessage = randomSeqByte(rng[], 32)
|
||||
|
||||
# By being the handshake initiator, Alice writes a Waku2 payload v2 containing her handshake message
|
||||
# and the (encrypted) transport message
|
||||
aliceStep =
|
||||
stepHandshake(rng[], aliceHS, transportMessage = sentTransportMessage).get()
|
||||
|
||||
# Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
|
||||
bobStep = stepHandshake(rng[], bobHS, readPayloadV2 = aliceStep.payload2).get()
|
||||
|
||||
check:
|
||||
bobStep.transportMessage == sentTransportMessage
|
||||
|
||||
###############
|
||||
# 2nd step
|
||||
###############
|
||||
|
||||
# We generate a random transport message
|
||||
sentTransportMessage = randomSeqByte(rng[], 32)
|
||||
|
||||
# At this step, Bob writes and returns a payload
|
||||
bobStep = stepHandshake(rng[], bobHS, transportMessage = sentTransportMessage).get()
|
||||
|
||||
# While Alice reads and returns the (decrypted) transport message
|
||||
aliceStep = stepHandshake(rng[], aliceHS, readPayloadV2 = bobStep.payload2).get()
|
||||
|
||||
check:
|
||||
aliceStep.transportMessage == sentTransportMessage
|
||||
|
||||
###############
|
||||
# 3rd step
|
||||
###############
|
||||
|
||||
# We generate a random transport message
|
||||
sentTransportMessage = randomSeqByte(rng[], 32)
|
||||
|
||||
# Similarly as in first step, Alice writes a Waku2 payload containing the handshake_message and the (encrypted) transportMessage
|
||||
aliceStep =
|
||||
stepHandshake(rng[], aliceHS, transportMessage = sentTransportMessage).get()
|
||||
|
||||
# Bob reads Alice's payloads, and returns the (decrypted) transportMessage alice sent to him
|
||||
bobStep = stepHandshake(rng[], bobHS, readPayloadV2 = aliceStep.payload2).get()
|
||||
|
||||
check:
|
||||
bobStep.transportMessage == sentTransportMessage
|
||||
|
||||
# Note that for this handshake pattern, no more message patterns are left for processing
|
||||
|
||||
#########################
|
||||
# After Handshake
|
||||
#########################
|
||||
|
||||
# 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 between Alice and Bob
|
||||
var
|
||||
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, defaultMessageNametagBuffer)
|
||||
readMessage =
|
||||
readMessage(bobHSResult, payload2, defaultMessageNametagBuffer).get()
|
||||
|
||||
check:
|
||||
message == readMessage
|
||||
|
||||
# Bob writes to Alice
|
||||
message = randomSeqByte(rng[], 32)
|
||||
payload2 = writeMessage(bobHSResult, message, defaultMessageNametagBuffer)
|
||||
readMessage =
|
||||
readMessage(aliceHSResult, payload2, defaultMessageNametagBuffer).get()
|
||||
|
||||
check:
|
||||
message == readMessage
|
||||
|
||||
test "Noise XK1 Handhshake and message encryption (short test)":
|
||||
let hsPattern = NoiseHandshakePatterns["XK1"]
|
||||
|
||||
# We initialize Alice's and Bob's Handshake State
|
||||
let aliceStaticKey = genKeyPair(rng[])
|
||||
let bobStaticKey = genKeyPair(rng[])
|
||||
|
||||
# This handshake has the following pre-message pattern:
|
||||
# <- s
|
||||
# ...
|
||||
# So we define accordingly the sequence of the pre-message public keys
|
||||
let preMessagePKs: seq[NoisePublicKey] =
|
||||
@[toNoisePublicKey(getPublicKey(bobStaticKey))]
|
||||
|
||||
var aliceHS = initialize(
|
||||
hsPattern = hsPattern,
|
||||
staticKey = aliceStaticKey,
|
||||
preMessagePKs = preMessagePKs,
|
||||
initiator = true,
|
||||
)
|
||||
var bobHS = initialize(
|
||||
hsPattern = hsPattern, staticKey = bobStaticKey, preMessagePKs = preMessagePKs
|
||||
)
|
||||
|
||||
var
|
||||
sentTransportMessage: seq[byte]
|
||||
aliceStep, bobStep: HandshakeStepResult
|
||||
|
||||
# Here the handshake starts
|
||||
# Write and read calls alternate between Alice and Bob: the handhshake progresses by alternatively calling stepHandshake for each user
|
||||
|
||||
###############
|
||||
# 1st step
|
||||
###############
|
||||
|
||||
# We generate a random transport message
|
||||
sentTransportMessage = randomSeqByte(rng[], 32)
|
||||
|
||||
# By being the handshake initiator, Alice writes a Waku2 payload v2 containing her handshake message
|
||||
# and the (encrypted) transport message
|
||||
aliceStep =
|
||||
stepHandshake(rng[], aliceHS, transportMessage = sentTransportMessage).get()
|
||||
|
||||
# Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
|
||||
bobStep = stepHandshake(rng[], bobHS, readPayloadV2 = aliceStep.payload2).get()
|
||||
|
||||
check:
|
||||
bobStep.transportMessage == sentTransportMessage
|
||||
|
||||
###############
|
||||
# 2nd step
|
||||
###############
|
||||
|
||||
# We generate a random transport message
|
||||
sentTransportMessage = randomSeqByte(rng[], 32)
|
||||
|
||||
# At this step, Bob writes and returns a payload
|
||||
bobStep = stepHandshake(rng[], bobHS, transportMessage = sentTransportMessage).get()
|
||||
|
||||
# While Alice reads and returns the (decrypted) transport message
|
||||
aliceStep = stepHandshake(rng[], aliceHS, readPayloadV2 = bobStep.payload2).get()
|
||||
|
||||
check:
|
||||
aliceStep.transportMessage == sentTransportMessage
|
||||
|
||||
###############
|
||||
# 3rd step
|
||||
###############
|
||||
|
||||
# We generate a random transport message
|
||||
sentTransportMessage = randomSeqByte(rng[], 32)
|
||||
|
||||
# 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).get()
|
||||
|
||||
# Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
|
||||
bobStep = stepHandshake(rng[], bobHS, readPayloadV2 = aliceStep.payload2).get()
|
||||
|
||||
check:
|
||||
bobStep.transportMessage == sentTransportMessage
|
||||
|
||||
# Note that for this handshake pattern, no more message patterns are left for processing
|
||||
|
||||
#########################
|
||||
# After Handshake
|
||||
#########################
|
||||
|
||||
# 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]
|
||||
defaultMessageNametagBuffer: MessageNametagBuffer
|
||||
|
||||
for _ in 0 .. 10:
|
||||
# Alice writes to Bob
|
||||
message = randomSeqByte(rng[], 32)
|
||||
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, defaultMessageNametagBuffer)
|
||||
readMessage =
|
||||
readMessage(aliceHSResult, payload2, defaultMessageNametagBuffer).get()
|
||||
|
||||
check:
|
||||
message == readMessage
|
||||
@ -1,421 +0,0 @@
|
||||
{.used.}
|
||||
|
||||
import std/tables, results, stew/byteutils, testutils/unittests
|
||||
import
|
||||
logos_delivery/waku/[
|
||||
common/protobuf,
|
||||
utils/noise as waku_message_utils,
|
||||
waku_noise/noise_types,
|
||||
waku_noise/noise_utils,
|
||||
waku_noise/noise_handshake_processing,
|
||||
waku_core,
|
||||
],
|
||||
./testlib/common
|
||||
|
||||
procSuite "Waku Noise Sessions":
|
||||
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: ProtobufResult[WakuMessage]
|
||||
wakuMsg: Result[WakuMessage, cstring]
|
||||
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.decode(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.decode(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.decode(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()
|
||||
@ -30,3 +30,8 @@ proc getRng(): ref HmacDrbgContext =
|
||||
|
||||
template rng*(): ref HmacDrbgContext =
|
||||
getRng()
|
||||
|
||||
proc randomSeqByte*(rng: var HmacDrbgContext, size: int): seq[byte] =
|
||||
var output = newSeq[byte](size.uint32)
|
||||
hmacDrbgGenerate(rng, output)
|
||||
return output
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user