From 32f91cb26b725f686e52ac165ff3de7e032763a7 Mon Sep 17 00:00:00 2001 From: G <28568419+s1fr0@users.noreply.github.com> Date: Thu, 4 Aug 2022 10:47:00 +0200 Subject: [PATCH] Noise: split Noise submodule in smaller submodules (#979) * refactor(noise): split Noise submodule in smaller submodules --- tests/v2/test_waku_noise.nim | 3 + waku/v2/node/waku_payload.nim | 3 +- waku/v2/protocol/waku_noise/noise.nim | 1156 +---------------- .../waku_noise/noise_handshake_processing.nim | 586 +++++++++ waku/v2/protocol/waku_noise/noise_types.nim | 249 ++++ waku/v2/protocol/waku_noise/noise_utils.nim | 386 ++++++ 6 files changed, 1232 insertions(+), 1151 deletions(-) create mode 100644 waku/v2/protocol/waku_noise/noise_handshake_processing.nim create mode 100644 waku/v2/protocol/waku_noise/noise_types.nim create mode 100644 waku/v2/protocol/waku_noise/noise_utils.nim diff --git a/tests/v2/test_waku_noise.nim b/tests/v2/test_waku_noise.nim index b2af86d4a..8c10c1922 100644 --- a/tests/v2/test_waku_noise.nim +++ b/tests/v2/test_waku_noise.nim @@ -6,7 +6,10 @@ import std/tables, stew/byteutils, ../../waku/v2/node/waku_payload, + ../../waku/v2/protocol/waku_noise/noise_types, + ../../waku/v2/protocol/waku_noise/noise_utils, ../../waku/v2/protocol/waku_noise/noise, + ../../waku/v2/protocol/waku_noise/noise_handshake_processing, ../../waku/v2/protocol/waku_message, ../test_helpers, libp2p/crypto/chacha20poly1305, diff --git a/waku/v2/node/waku_payload.nim b/waku/v2/node/waku_payload.nim index 07e71a65e..941d39a19 100644 --- a/waku/v2/node/waku_payload.nim +++ b/waku/v2/node/waku_payload.nim @@ -5,7 +5,8 @@ import eth/keys, ../../whisper/whisper_types, ../protocol/waku_message, - ../protocol/waku_noise/noise + ../protocol/waku_noise/noise_types, + ../protocol/waku_noise/noise_utils export whisper_types, keys, options diff --git a/waku/v2/protocol/waku_noise/noise.nim b/waku/v2/protocol/waku_noise/noise.nim index fa57ad021..db47771cd 100644 --- a/waku/v2/protocol/waku_noise/noise.nim +++ b/waku/v2/protocol/waku_noise/noise.nim @@ -1,4 +1,5 @@ # 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 ## @@ -11,344 +12,21 @@ import std/[oids, options, strutils, tables] import chronos import chronicles import bearssl -import stew/[results, endians2, byteutils] +import stew/[results, byteutils, endians2] import nimcrypto/[utils, sha2, hmac] import libp2p/utility import libp2p/errors -import libp2p/crypto/[crypto, chacha20poly1305, curve25519, hkdf] +import libp2p/crypto/[crypto, chacha20poly1305, hkdf] import libp2p/protocols/secure/secure +import ./noise_types logScope: topics = "wakunoise" ################################################################# -# 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 - -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 = "se" - 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: - 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 protocol ID field, the handshake message (for Noise handshakes) and - # a transport message (for Noise handshakes and ChaChaPoly encryptions) - PayloadV2* = object - 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 - - -################################# -# 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])] - ) - }.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, - "ChaChaPoly": 30.uint8 - - }.toTable() - - -################################################################# - -################################# -# Utilities -################################# - -# Generates random byte sequences of given size -proc randomSeqByte*(rng: var BrHmacDrbgContext, size: int): seq[byte] = - var output = newSeq[byte](size.uint32) - brHmacDrbgGenerate(rng, output) - return output - -# Generate random (public, private) Elliptic Curve key pairs -proc genKeyPair*(rng: var BrHmacDrbgContext): 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: - 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 - -# 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 - -################################################################# - # Noise state machine primitives # Overview : @@ -367,7 +45,7 @@ proc dh*(private: EllipticCurveKey, public: EllipticCurveKey): EllipticCurveKey ################################# # Checks if a Cipher State has an encryption key set -proc hasKey(cs: CipherState): bool = +proc hasKey*(cs: CipherState): bool = return (cs.k != EmptyKey) # Encrypts a plaintext using key material in a Noise Cipher State @@ -666,826 +344,4 @@ proc decrypt*( if tagIn != tagOut: debug "decrypt failed", plaintext = shortLog(plaintext) raise newException(NoiseDecryptTagError, "decrypt tag authentication failed.") - return plaintext - -# Generates a random ChaChaPolyKey for testing encryption/decryption -proc randomChaChaPolyKey*(rng: var BrHmacDrbgContext): ChaChaPolyKey = - var key: ChaChaPolyKey - brHmacDrbgGenerate(rng, key) - return key - -# Generates a random ChaChaPoly Cipher State for testing encryption/decryption -proc randomChaChaPolyCipherState*(rng: var BrHmacDrbgContext): ChaChaPolyCipherState = - var randomCipherState: ChaChaPolyCipherState - randomCipherState.k = randomChaChaPolyKey(rng) - brHmacDrbgGenerate(rng, randomCipherState.nonce) - randomCipherState.ad = newSeq[byte](32) - brHmacDrbgGenerate(rng, randomCipherState.ad) - return randomCipherState - - -################################################################# - -################################# -# Noise Public keys -################################# - -# 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 BrHmacDrbgContext): 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.. uint8.high.int: - debug "PayloadV2 malformed: too many public keys contained in the handshake message" - return err("Too many public keys in handshake message") - - - # We get the transport message byte length - let transportMessageLen = self.transportMessage.len - - # The output payload as in https://rfc.vac.dev/spec/35/. We concatenate all the PayloadV2 fields as - # payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage) - # We declare it as a byte sequence of length accordingly to the PayloadV2 information read - var payload = newSeqOfCap[byte](1 + # 1 byte for protocol ID - 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.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 = ( 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 reading 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: - debug "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.. - 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 -proc processMessagePatternPayload(hs: var HandshakeState, transportMessage: seq[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) - elif writing: - payload = hs.ss.encryptAndHash(transportMessage) - - 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 BrHmacDrbgContext, 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 hs.s == default(KeyPair): - 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 hs.e == default(KeyPair) or hs.re == default(Curve25519Key): - 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 hs.e == default(KeyPair) or hs.rs == default(Curve25519Key): - raise newException(NoisePublicKeyError, "Local or remote ephemeral/static key not set") - hs.ss.mixKey(dh(hs.e.privateKey, hs.rs)) - else: - if hs.re == default(Curve25519Key) or hs.s == default(KeyPair): - 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 hs.s == default(KeyPair) or hs.re == default(Curve25519Key): - raise newException(NoiseMalformedHandshake, "Local or remote ephemeral/static key not set") - hs.ss.mixKey(dh(hs.s.privateKey, hs.re)) - else: - if hs.rs == default(Curve25519Key) or hs.e == default(KeyPair): - 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 hs.s == default(KeyPair) or hs.rs == default(Curve25519Key): - 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) has to be passed to transportMessage and readPayloadV2 can be left to its default value -# It the user is reading the handshake message, the read payload v2 has to be passed to readPayloadV2 and the transportMessage can be left to its default values. -proc stepHandshake*(rng: var BrHmacDrbgContext, hs: var HandshakeState, readPayloadV2: PayloadV2 = default(PayloadV2), transportMessage: seq[byte] = @[]): Result[HandshakeStepResult, cstring] - {.raises: [Defect, NoiseHandshakeError, 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): - debug "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: - raise newException(NoiseMalformedHandshake, "Handshake Pattern not supported") - - # We set the handshake and transport message - hsStepResult.payload2.handshakeMessage = processMessagePatternTokens(rng, hs).get() - hsStepResult.payload2.transportMessage = processMessagePatternPayload(hs, transportMessage) - - # If we read an answer during this handshake step - elif reading: - # 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 - hsStepResult.transportMessage = processMessagePatternPayload(hs, readTransportMessage) - - 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() - - # We assign the proper Cipher States - if hs.initiator: - hsResult.csOutbound = cs1 - hsResult.csInbound = cs2 - else: - hsResult.csOutbound = cs2 - hsResult.csInbound = cs1 - - # 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]): PayloadV2 - {.raises: [Defect, NoiseNonceMaxError].} = - - var payload2: PayloadV2 - - # 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 - # Encryption is done with zero-length associated data as per specification - payload2.transportMessage = encryptWithAd(hsr.csOutbound, @[], transportMessage) - - return payload2 - -# Reads an encrypted message using the proper Cipher State -# Associated data ad for encryption is optional, since the latter is out of scope for Noise -proc readMessage*(hsr: var HandshakeResult, readPayload2: PayloadV2): Result[seq[byte], cstring] - {.raises: [Defect, NoiseDecryptTagError, NoiseNonceMaxError].} = - - # The output decrypted message - var message: seq[byte] - - # 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 - # (this because an attacker may flood the content topic on which messages are exchanged) - try: - # Decryption is done with zero-length associated data as per specification - message = decryptWithAd(hsr.csInbound, @[], readPayload2.transportMessage) - except NoiseDecryptTagError: - debug "A read message failed decryption. Returning empty message as plaintext." - message = @[] - - return ok(message) \ No newline at end of file + return plaintext \ No newline at end of file diff --git a/waku/v2/protocol/waku_noise/noise_handshake_processing.nim b/waku/v2/protocol/waku_noise/noise_handshake_processing.nim new file mode 100644 index 000000000..d2ea73c23 --- /dev/null +++ b/waku/v2/protocol/waku_noise/noise_handshake_processing.nim @@ -0,0 +1,586 @@ +# 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: [Defect].} + +import std/[oids, options, strutils, tables] +import chronos +import chronicles +import bearssl +import stew/[results, endians2] +import nimcrypto/[utils, sha2, hmac] + +import libp2p/errors +import libp2p/crypto/[chacha20poly1305, curve25519] + +import ./noise_types +import ./noise +import ./noise_utils + +logScope: + topics = "wakunoise" + +################################################################# + +# 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 +proc processMessagePatternPayload(hs: var HandshakeState, transportMessage: seq[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) + elif writing: + payload = hs.ss.encryptAndHash(transportMessage) + + 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 BrHmacDrbgContext, 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 hs.s == default(KeyPair): + 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 hs.e == default(KeyPair) or hs.re == default(Curve25519Key): + 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 hs.e == default(KeyPair) or hs.rs == default(Curve25519Key): + raise newException(NoisePublicKeyError, "Local or remote ephemeral/static key not set") + hs.ss.mixKey(dh(hs.e.privateKey, hs.rs)) + else: + if hs.re == default(Curve25519Key) or hs.s == default(KeyPair): + 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 hs.s == default(KeyPair) or hs.re == default(Curve25519Key): + raise newException(NoiseMalformedHandshake, "Local or remote ephemeral/static key not set") + hs.ss.mixKey(dh(hs.s.privateKey, hs.re)) + else: + if hs.rs == default(Curve25519Key) or hs.e == default(KeyPair): + 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 hs.s == default(KeyPair) or hs.rs == default(Curve25519Key): + 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) has to be passed to transportMessage and readPayloadV2 can be left to its default value +# It the user is reading the handshake message, the read payload v2 has to be passed to readPayloadV2 and the transportMessage can be left to its default values. +proc stepHandshake*(rng: var BrHmacDrbgContext, hs: var HandshakeState, readPayloadV2: PayloadV2 = default(PayloadV2), transportMessage: seq[byte] = @[]): Result[HandshakeStepResult, cstring] + {.raises: [Defect, NoiseHandshakeError, 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): + debug "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: + raise newException(NoiseMalformedHandshake, "Handshake Pattern not supported") + + # We set the handshake and transport message + hsStepResult.payload2.handshakeMessage = processMessagePatternTokens(rng, hs).get() + hsStepResult.payload2.transportMessage = processMessagePatternPayload(hs, transportMessage) + + # If we read an answer during this handshake step + elif reading: + # 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 + hsStepResult.transportMessage = processMessagePatternPayload(hs, readTransportMessage) + + 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() + + # We assign the proper Cipher States + if hs.initiator: + hsResult.csOutbound = cs1 + hsResult.csInbound = cs2 + else: + hsResult.csOutbound = cs2 + hsResult.csInbound = cs1 + + # 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]): PayloadV2 + {.raises: [Defect, NoiseNonceMaxError].} = + + var payload2: PayloadV2 + + # 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 + # Encryption is done with zero-length associated data as per specification + payload2.transportMessage = encryptWithAd(hsr.csOutbound, @[], transportMessage) + + return payload2 + +# Reads an encrypted message using the proper Cipher State +# Associated data ad for encryption is optional, since the latter is out of scope for Noise +proc readMessage*(hsr: var HandshakeResult, readPayload2: PayloadV2): Result[seq[byte], cstring] + {.raises: [Defect, NoiseDecryptTagError, NoiseNonceMaxError].} = + + # The output decrypted message + var message: seq[byte] + + # 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 + # (this because an attacker may flood the content topic on which messages are exchanged) + try: + # Decryption is done with zero-length associated data as per specification + message = decryptWithAd(hsr.csInbound, @[], readPayload2.transportMessage) + except NoiseDecryptTagError: + debug "A read message failed decryption. Returning empty message as plaintext." + message = @[] + + return ok(message) \ No newline at end of file diff --git a/waku/v2/protocol/waku_noise/noise_types.nim b/waku/v2/protocol/waku_noise/noise_types.nim new file mode 100644 index 000000000..7300ef46b --- /dev/null +++ b/waku/v2/protocol/waku_noise/noise_types.nim @@ -0,0 +1,249 @@ +# 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: [Defect].} + +import std/[options, strutils, tables] +import chronos +import chronicles +import bearssl +import stew/[results, endians2] +import nimcrypto/[utils, sha2, hmac] + +import libp2p/errors +import libp2p/crypto/[crypto, chacha20poly1305, curve25519] + +logScope: + topics = "wakunoise" + +################################################################# + +# 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 + +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 = "se" + 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: + 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 protocol ID field, the handshake message (for Noise handshakes) and + # a transport message (for Noise handshakes and ChaChaPoly encryptions) + PayloadV2* = object + 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 + + +################################# +# 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])] + ) + }.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, + "ChaChaPoly": 30.uint8 + + }.toTable() diff --git a/waku/v2/protocol/waku_noise/noise_utils.nim b/waku/v2/protocol/waku_noise/noise_utils.nim new file mode 100644 index 000000000..9573a0478 --- /dev/null +++ b/waku/v2/protocol/waku_noise/noise_utils.nim @@ -0,0 +1,386 @@ +# 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: [Defect].} + +import std/[oids, options, strutils, tables] +import chronos +import chronicles +import bearssl +import stew/[results, endians2, byteutils] +import nimcrypto/[utils, sha2, hmac] + +import libp2p/errors +import libp2p/crypto/[chacha20poly1305, curve25519] + +import ./noise_types +import ./noise + +logScope: + topics = "wakunoise" + +################################################################# + +################################# +# Generic Utilities +################################# + +# Generates random byte sequences of given size +proc randomSeqByte*(rng: var BrHmacDrbgContext, size: int): seq[byte] = + var output = newSeq[byte](size.uint32) + brHmacDrbgGenerate(rng, output) + return output + + +################################################################# + +################################# +# Noise Handhshake Utilities +################################# + +# Generate random (public, private) Elliptic Curve key pairs +proc genKeyPair*(rng: var BrHmacDrbgContext): 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: + 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 + +# 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 BrHmacDrbgContext): ChaChaPolyKey = + var key: ChaChaPolyKey + brHmacDrbgGenerate(rng, key) + return key + +# Generates a random ChaChaPoly Cipher State for testing encryption/decryption +proc randomChaChaPolyCipherState*(rng: var BrHmacDrbgContext): ChaChaPolyCipherState = + var randomCipherState: ChaChaPolyCipherState + randomCipherState.k = randomChaChaPolyKey(rng) + brHmacDrbgGenerate(rng, randomCipherState.nonce) + randomCipherState.ad = newSeq[byte](32) + brHmacDrbgGenerate(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 BrHmacDrbgContext): 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.. uint8.high.int: + debug "PayloadV2 malformed: too many public keys contained in the handshake message" + return err("Too many public keys in handshake message") + + + # We get the transport message byte length + let transportMessageLen = self.transportMessage.len + + # The output payload as in https://rfc.vac.dev/spec/35/. We concatenate all the PayloadV2 fields as + # payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage) + # We declare it as a byte sequence of length accordingly to the PayloadV2 information read + var payload = newSeqOfCap[byte](1 + # 1 byte for protocol ID + 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.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 = ( 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 reading 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: + debug "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..