From f1fd8b354e8be77c3e54fcb1ffb58f80c3ed7e1a Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Wed, 14 Dec 2022 12:22:48 -0400 Subject: [PATCH] feat(noise): WakuPairing --- cmd/waku/flags.go | 4 +- go.mod | 4 +- go.sum | 4 +- waku/v2/noise/crypto.go | 15 +++ waku/v2/noise/handshake.go | 205 ++++++++++++++++++++++++----- waku/v2/noise/messagenametag.go | 132 +++++++++++++++++++ waku/v2/noise/noise_test.go | 26 ++-- waku/v2/noise/patterns.go | 10 ++ waku/v2/noise/payload.go | 15 ++- waku/v2/noise/qr.go | 80 ++++++++++++ waku/v2/noise/wakupairing_test.go | 209 ++++++++++++++++++++++++++++++ 11 files changed, 651 insertions(+), 53 deletions(-) create mode 100644 waku/v2/noise/crypto.go create mode 100644 waku/v2/noise/messagenametag.go create mode 100644 waku/v2/noise/qr.go create mode 100644 waku/v2/noise/wakupairing_test.go diff --git a/cmd/waku/flags.go b/cmd/waku/flags.go index ed8a65bc..2594046e 100644 --- a/cmd/waku/flags.go +++ b/cmd/waku/flags.go @@ -147,11 +147,11 @@ var ( Choices: []string{"DEBUG", "INFO", "WARN", "ERROR", "DPANIC", "PANIC", "FATAL"}, Value: &options.LogLevel, }, - Usage: "Define the logging level,", + Usage: "Define the logging level (allowed values: DEBUG, INFO, WARN, ERROR, DPANIC, PANIC, FATAL)", } LogEncoding = &cli.GenericFlag{ Name: "log-encoding", - Usage: "Define the encoding used for the logs", + Usage: "Define the encoding used for the logs (allowed values: console, nocolor, json)", Value: &cliutils.ChoiceValue{ Choices: []string{"console", "nocolor", "json"}, Value: &options.LogEncoding, diff --git a/go.mod b/go.mod index e77dd775..cb3b938e 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/gorilla/mux v1.8.0 github.com/waku-org/go-discover v0.0.0-20221209174356-61c833f34d98 github.com/waku-org/go-zerokit-rln v0.1.7-wakuorg - github.com/waku-org/noise v1.0.2 + github.com/waku-org/noise v1.0.3 golang.org/x/text v0.4.0 ) @@ -149,7 +149,7 @@ require ( github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect - golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect + golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b // indirect golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect golang.org/x/net v0.0.0-20220920183852-bf014ff85ad5 // indirect diff --git a/go.sum b/go.sum index 9a11a80d..96ff3877 100644 --- a/go.sum +++ b/go.sum @@ -1522,8 +1522,8 @@ github.com/waku-org/go-discover v0.0.0-20221209174356-61c833f34d98 h1:xwY0kW5XZF github.com/waku-org/go-discover v0.0.0-20221209174356-61c833f34d98/go.mod h1:eBHgM6T4EG0RZzxpxKy+rGz/6Dw2Nd8DWxS0lm9ESDw= github.com/waku-org/go-zerokit-rln v0.1.7-wakuorg h1:2vVIBCtBih2w1K9ll8YnToTDZvbxcgbsClsPlJS/kkg= github.com/waku-org/go-zerokit-rln v0.1.7-wakuorg/go.mod h1:GlyaVeEWNEBxVJrWC6jFTvb4LNb9d9qnjdS6EiWVUvk= -github.com/waku-org/noise v1.0.2 h1:7WmlhpJ0eliBzwzKz6SoTqQznaEU2IuebHF3oCekqqs= -github.com/waku-org/noise v1.0.2/go.mod h1:emThr8WZLeAtKqFW+/nXfHn9VucuXTh8aHap03UXP84= +github.com/waku-org/noise v1.0.3 h1:BIecnRG0J0JlZmqcZTHphQ8yUeqqwkIaUAVk2JNK9VQ= +github.com/waku-org/noise v1.0.3/go.mod h1:emThr8WZLeAtKqFW+/nXfHn9VucuXTh8aHap03UXP84= github.com/whyrusleeping/timecache v0.0.0-20160911033111-cfcb2f1abfee h1:lYbXeSvJi5zk5GLKVuid9TVjS9a0OmLIDKTfoZBL6Ow= github.com/whyrusleeping/timecache v0.0.0-20160911033111-cfcb2f1abfee/go.mod h1:m2aV4LZI4Aez7dP5PMyVKEHhUyEJ/RjmPEDOpDvudHg= github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= diff --git a/waku/v2/noise/crypto.go b/waku/v2/noise/crypto.go new file mode 100644 index 00000000..6612a36c --- /dev/null +++ b/waku/v2/noise/crypto.go @@ -0,0 +1,15 @@ +package noise + +import ( + "crypto/ed25519" + "crypto/sha256" +) + +// Commits a public key pk for randomness r as H(pk || s) +func CommitPublicKey(publicKey ed25519.PublicKey, r []byte) []byte { + input := []byte{} + input = append(input, []byte(publicKey)...) + input = append(input, r...) + res := sha256.Sum256(input) + return res[:] +} diff --git a/waku/v2/noise/handshake.go b/waku/v2/noise/handshake.go index 795a6b67..85abfeae 100644 --- a/waku/v2/noise/handshake.go +++ b/waku/v2/noise/handshake.go @@ -1,22 +1,28 @@ package noise import ( + "bytes" "errors" "fmt" + "hash" + "io" + "math/big" n "github.com/waku-org/noise" + "golang.org/x/crypto/hkdf" ) // WakuNoiseProtocolID indicates the protocol ID defined according to https://rfc.vac.dev/spec/35/#specification type WakuNoiseProtocolID = byte var ( - None = WakuNoiseProtocolID(0) - Noise_K1K1_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(10) - Noise_XK1_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(11) - Noise_XX_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(12) - Noise_XXpsk0_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(13) - ChaChaPoly = WakuNoiseProtocolID(30) + None = WakuNoiseProtocolID(0) + Noise_K1K1_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(10) + Noise_XK1_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(11) + Noise_XX_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(12) + Noise_XXpsk0_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(13) + Noise_WakuPairing_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(14) + ChaChaPoly = WakuNoiseProtocolID(30) ) const NoisePaddingBlockSize = 248 @@ -26,7 +32,7 @@ var ErrorHandshakeComplete = errors.New("handshake complete") // All protocols share same cipher suite var cipherSuite = n.NewCipherSuite(n.DH25519, n.CipherChaChaPoly, n.HashSHA256) -func newHandshakeState(pattern n.HandshakePattern, initiator bool, staticKeypair n.DHKey, prologue []byte, presharedKey []byte, peerStatic []byte, peerEphemeral []byte) (hs *n.HandshakeState, err error) { +func newHandshakeState(pattern n.HandshakePattern, initiator bool, staticKeypair n.DHKey, ephemeralKeyPair n.DHKey, prologue []byte, presharedKey []byte, peerStatic []byte, peerEphemeral []byte) (hs *n.HandshakeState, err error) { defer func() { if rerr := recover(); rerr != nil { err = fmt.Errorf("panic in Noise handshake: %s", rerr) @@ -34,14 +40,15 @@ func newHandshakeState(pattern n.HandshakePattern, initiator bool, staticKeypair }() cfg := n.Config{ - CipherSuite: cipherSuite, - Pattern: pattern, - Initiator: initiator, - StaticKeypair: staticKeypair, - Prologue: prologue, - PresharedKey: presharedKey, - PeerStatic: peerStatic, - PeerEphemeral: peerEphemeral, + CipherSuite: cipherSuite, + Pattern: pattern, + Initiator: initiator, + StaticKeypair: staticKeypair, + EphemeralKeypair: ephemeralKeyPair, + Prologue: prologue, + PresharedKey: presharedKey, + PeerStatic: peerStatic, + PeerEphemeral: peerEphemeral, } return n.NewHandshakeState(cfg) @@ -54,8 +61,10 @@ type Handshake struct { hsBuff []byte - enc *n.CipherState - dec *n.CipherState + enc *n.CipherState + dec *n.CipherState + nametagsInbound *MessageNametagBuffer + nametagsOutbound *MessageNametagBuffer initiator bool shouldWrite bool @@ -77,19 +86,21 @@ func getHandshakePattern(protocol WakuNoiseProtocolID) (n.HandshakePattern, erro return HandshakeXX, nil case Noise_XXpsk0_25519_ChaChaPoly_SHA256: return HandshakeXXpsk0, nil + case Noise_WakuPairing_25519_ChaChaPoly_SHA256: + return HandshakeWakuPairing, nil default: return n.HandshakePattern{}, errors.New("unsupported handshake pattern") } } // NewHandshake creates a new handshake using aa WakuNoiseProtocolID that is maped to a handshake pattern. -func NewHandshake(protocolID WakuNoiseProtocolID, initiator bool, staticKeypair n.DHKey, prologue []byte, presharedKey []byte, peerStatic []byte, peerEphemeral []byte) (*Handshake, error) { +func NewHandshake(protocolID WakuNoiseProtocolID, initiator bool, staticKeypair n.DHKey, ephemeralKeyPair n.DHKey, prologue []byte, presharedKey []byte, peerStatic []byte, peerEphemeral []byte) (*Handshake, error) { hsPattern, err := getHandshakePattern(protocolID) if err != nil { return nil, err } - hsState, err := newHandshakeState(hsPattern, initiator, staticKeypair, prologue, presharedKey, peerStatic, peerEphemeral) + hsState, err := newHandshakeState(hsPattern, initiator, staticKeypair, ephemeralKeyPair, prologue, presharedKey, peerStatic, peerEphemeral) if err != nil { return nil, err } @@ -103,11 +114,24 @@ func NewHandshake(protocolID WakuNoiseProtocolID, initiator bool, staticKeypair }, nil } +func (hs *Handshake) Hash() hash.Hash { + return hs.state.Hash() +} + +func (hs *Handshake) H() []byte { + return hs.state.H() +} + +func (hs *Handshake) RS() []byte { + return hs.state.RS() +} + // Step advances a step in the 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. -// TODO: this might be refactored into a separate `sendHandshakeMessage` and `receiveHandshakeMessage` -func (hs *Handshake) Step(readPayloadV2 *PayloadV2, transportMessage []byte) (*HandshakeStepResult, error) { +// TODO: this might be refactored into a separate `sendHandshakeMessage` and `receiveHandshakeMessage`. +// The messageNameTag is an optional value and can be used to identify missing messages +func (hs *Handshake) Step(readPayloadV2 *PayloadV2, transportMessage []byte, messageNametag *MessageNametag) (*HandshakeStepResult, error) { if hs.enc != nil || hs.dec != nil { return nil, ErrorHandshakeComplete } @@ -129,13 +153,22 @@ func (hs *Handshake) Step(readPayloadV2 *PayloadV2, transportMessage []byte) (*H return nil, err } - msg, noisePubKeys, cs1, cs2, err = hs.state.WriteMessageAndGetPK(hs.hsBuff, [][]byte{}, payload) + var mtag MessageNametag + if messageNametag != nil { + mtag = *messageNametag + } + + msg, noisePubKeys, cs1, cs2, err = hs.state.WriteMessageAndGetPK(hs.hsBuff, [][]byte{}, payload, mtag[:]) if err != nil { return nil, err } hs.shouldWrite = false + if messageNametag != nil { + result.Payload2.MessageNametag = *messageNametag + } + result.Payload2.TransportMessage = msg for _, npk := range noisePubKeys { result.Payload2.HandshakeMessage = append(result.Payload2.HandshakeMessage, byteToNoisePublicKey(npk)) @@ -146,10 +179,20 @@ func (hs *Handshake) Step(readPayloadV2 *PayloadV2, transportMessage []byte) (*H return nil, errors.New("readPayloadV2 is required") } + var mtag MessageNametag + if messageNametag != nil { + mtag = *messageNametag + if !bytes.Equal(readPayloadV2.MessageNametag[:], mtag[:]) { + return nil, ErrNametagNotExpected + } + } + readTMessage := readPayloadV2.TransportMessage + // We retrieve and store the (decrypted) received transport message by passing the messageNametag as extra additional data + // Since we only read, nothing meanigful (i.e. public keys) is returned. (hsBuffer is not affected) - msg, cs1, cs2, err = hs.state.ReadMessage(nil, readTMessage) + msg, cs1, cs2, err = hs.state.ReadMessage(nil, readTMessage, mtag[:]...) if err != nil { return nil, err } @@ -167,7 +210,10 @@ func (hs *Handshake) Step(readPayloadV2 *PayloadV2, transportMessage []byte) (*H } if cs1 != nil && cs2 != nil { - hs.setCipherStates(cs1, cs2) + err = hs.setCipherStates(cs1, cs2) + if err != nil { + return nil, err + } } return &result, nil @@ -179,18 +225,36 @@ func (hs *Handshake) HandshakeComplete() bool { } // This is called when the final handshake message is processed -func (hs *Handshake) setCipherStates(cs1, cs2 *n.CipherState) { +func (hs *Handshake) setCipherStates(cs1, cs2 *n.CipherState) error { + // Optional: We derive a secret for the nametag derivation + nms1, nms2, err := hs.messageNametagSecrets() + if err != nil { + return err + } + if hs.initiator { hs.enc = cs1 hs.dec = cs2 + // and nametags secrets + hs.nametagsInbound = NewMessageNametagBuffer(nms1) + hs.nametagsOutbound = NewMessageNametagBuffer(nms2) } else { hs.enc = cs2 hs.dec = cs1 + // and nametags secrets + hs.nametagsInbound = NewMessageNametagBuffer(nms2) + hs.nametagsOutbound = NewMessageNametagBuffer(nms1) } + + // We initialize the message nametags inbound/outbound buffers + hs.nametagsInbound.Init() + hs.nametagsOutbound.Init() + + return nil } // Encrypt calls the cipher's encryption. It encrypts the provided plaintext and returns a PayloadV2 -func (hs *Handshake) Encrypt(plaintext []byte) (*PayloadV2, error) { +func (hs *Handshake) Encrypt(plaintext []byte, outboundMessageNametagBuffer ...*MessageNametagBuffer) (*PayloadV2, error) { if hs.enc == nil { return nil, errors.New("cannot encrypt, handshake incomplete") } @@ -204,7 +268,15 @@ func (hs *Handshake) Encrypt(plaintext []byte) (*PayloadV2, error) { return nil, err } - cyphertext, err := hs.enc.Encrypt(nil, nil, paddedTransportMessage) + // We set the message nametag using the input buffer + var messageNametag MessageNametag + if len(outboundMessageNametagBuffer) != 0 { + messageNametag = outboundMessageNametagBuffer[0].Pop() + } else { + messageNametag = hs.nametagsOutbound.Pop() + } + + cyphertext, err := hs.enc.Encrypt(nil, messageNametag[:], paddedTransportMessage) if err != nil { return nil, err } @@ -214,11 +286,12 @@ func (hs *Handshake) Encrypt(plaintext []byte) (*PayloadV2, error) { return &PayloadV2{ ProtocolId: None, TransportMessage: cyphertext, + MessageNametag: messageNametag, }, nil } // Decrypt calls the cipher's decryption. It decrypts the provided payload and returns the message in plaintext -func (hs *Handshake) Decrypt(payload *PayloadV2) ([]byte, error) { +func (hs *Handshake) Decrypt(payload *PayloadV2, inboundMessageNametagBuffer ...*MessageNametagBuffer) ([]byte, error) { if hs.dec == nil { return nil, errors.New("cannot decrypt, handshake incomplete") } @@ -227,34 +300,94 @@ func (hs *Handshake) Decrypt(payload *PayloadV2) ([]byte, error) { return nil, errors.New("no payload to decrypt") } + // 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.) + if len(inboundMessageNametagBuffer) != 0 { + err := inboundMessageNametagBuffer[0].CheckNametag(payload.MessageNametag) + if err != nil { + return nil, err + } + } else { + err := hs.nametagsInbound.CheckNametag(payload.MessageNametag) + if err != nil { + return nil, err + } + } + if len(payload.TransportMessage) == 0 { return nil, errors.New("tried to decrypt empty ciphertext") } - paddedMessage, err := hs.dec.Decrypt(nil, nil, payload.TransportMessage) + // Decryption is done with messageNametag as associated data + paddedMessage, err := hs.dec.Decrypt(nil, payload.MessageNametag[:], payload.TransportMessage) if err != nil { return nil, err } + // The message successfully decrypted, we can delete the first element of the inbound Message Nametag Buffer + hs.nametagsInbound.Delete(1) + return PKCS7_Unpad(paddedMessage, NoisePaddingBlockSize) } +func getHKDF(h func() hash.Hash, ck []byte, ikm []byte, numBytes int) ([]byte, error) { + hkdf := hkdf.New(h, ikm, ck, nil) + result := make([]byte, numBytes) + if _, err := io.ReadFull(hkdf, result); err != nil { + return nil, err + } + return result, nil +} + +// Generates an 8 decimal digits authorization code using HKDF and the handshake state +func (hs *Handshake) Authcode() (string, error) { + output0, err := getHKDF(hs.Hash, hs.H(), nil, 8) + if err != nil { + return "", err + } + bn := new(big.Int) + bn.SetBytes(output0) + code := new(big.Int) + code.Mod(bn, big.NewInt(100_000_000)) + return fmt.Sprintf("'%8s'", code.String()), nil +} + +func (hs *Handshake) messageNametagSecrets() (nms1 []byte, nms2 []byte, err error) { + output, err := getHKDF(hs.Hash, hs.H(), nil, 64) + if err != nil { + return nil, nil, err + } + nms1 = output[0:32] + nms2 = output[32:] + return +} + +// 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 +func (hs *Handshake) ToMessageNametag() (MessageNametag, error) { + output, err := getHKDF(hs.Hash, hs.H(), nil, 32) + if err != nil { + return [16]byte{}, err + } + return BytesToMessageNametag(output), nil +} + // NewHandshake_XX_25519_ChaChaPoly_SHA256 creates a handshake where the initiator and receiver are not aware of each other static keys func NewHandshake_XX_25519_ChaChaPoly_SHA256(staticKeypair n.DHKey, initiator bool, prologue []byte) (*Handshake, error) { - return NewHandshake(Noise_XX_25519_ChaChaPoly_SHA256, initiator, staticKeypair, prologue, nil, nil, nil) + return NewHandshake(Noise_XX_25519_ChaChaPoly_SHA256, initiator, staticKeypair, n.DHKey{}, prologue, nil, nil, nil) } // NewHandshake_XXpsk0_25519_ChaChaPoly_SHA256 creates a handshake where the initiator and receiver are not aware of each other static keys // and use a preshared secret to strengthen their mutual authentication func NewHandshake_XXpsk0_25519_ChaChaPoly_SHA256(staticKeypair n.DHKey, initiator bool, presharedKey []byte, prologue []byte) (*Handshake, error) { - return NewHandshake(Noise_XXpsk0_25519_ChaChaPoly_SHA256, initiator, staticKeypair, prologue, presharedKey, nil, nil) + return NewHandshake(Noise_XXpsk0_25519_ChaChaPoly_SHA256, initiator, staticKeypair, n.DHKey{}, prologue, presharedKey, nil, nil) } // NewHandshake_K1K1_25519_ChaChaPoly_SHA256 creates a handshake where both initiator and recever know each other handshake. Only ephemeral keys // are exchanged. This handshake is useful in case the initiator needs to instantiate a new separate encrypted communication // channel with the receiver func NewHandshake_K1K1_25519_ChaChaPoly_SHA256(staticKeypair n.DHKey, initiator bool, peerStaticKey []byte, prologue []byte) (*Handshake, error) { - return NewHandshake(Noise_K1K1_25519_ChaChaPoly_SHA256, initiator, staticKeypair, prologue, nil, peerStaticKey, nil) + return NewHandshake(Noise_K1K1_25519_ChaChaPoly_SHA256, initiator, staticKeypair, n.DHKey{}, prologue, nil, peerStaticKey, nil) } // NewHandshake_XK1_25519_ChaChaPoly_SHA256 creates a handshake where the initiator knows the receiver public static key. Within this handshake, @@ -265,5 +398,11 @@ func NewHandshake_XK1_25519_ChaChaPoly_SHA256(staticKeypair n.DHKey, initiator b if !initiator && len(peerStaticKey) != 0 { return nil, errors.New("recipient shouldnt know initiator key") } - return NewHandshake(Noise_XK1_25519_ChaChaPoly_SHA256, initiator, staticKeypair, prologue, nil, peerStaticKey, nil) + return NewHandshake(Noise_XK1_25519_ChaChaPoly_SHA256, initiator, staticKeypair, n.DHKey{}, prologue, nil, peerStaticKey, nil) +} + +// NewHandshake_WakuPairing_25519_ChaChaPoly_SHA256 +func NewHandshake_WakuPairing_25519_ChaChaPoly_SHA256(staticKeypair n.DHKey, ephemeralKeyPair n.DHKey, initiator bool, prologue []byte, presharedKey []byte) (*Handshake, error) { + peerEphemeral := presharedKey[0:32] + return NewHandshake(Noise_WakuPairing_25519_ChaChaPoly_SHA256, initiator, staticKeypair, ephemeralKeyPair, prologue, presharedKey, nil, peerEphemeral) } diff --git a/waku/v2/noise/messagenametag.go b/waku/v2/noise/messagenametag.go new file mode 100644 index 00000000..b554ea4e --- /dev/null +++ b/waku/v2/noise/messagenametag.go @@ -0,0 +1,132 @@ +package noise + +import ( + "bytes" + "crypto/sha256" + "encoding/binary" + "errors" +) + +type MessageNametag = [MessageNametagLength]byte + +const MessageNametagLength = 16 +const MessageNametagBufferSize = 50 + +var ( + ErrNametagNotFound = errors.New("message nametag not found in buffer") + ErrNametagNotExpected = errors.New("message nametag is present in buffer but is not the next expected nametag. One or more messages were probably lost") +) + +// Converts a sequence or array (arbitrary size) to a MessageNametag +func BytesToMessageNametag(input []byte) MessageNametag { + var result MessageNametag + copy(result[:], input) + return result +} + +type MessageNametagBuffer struct { + buffer []MessageNametag + counter uint64 + secret []byte +} + +func NewMessageNametagBuffer(secret []byte) *MessageNametagBuffer { + return &MessageNametagBuffer{ + secret: secret, + } +} + +// Initializes the empty Message nametag buffer. The n-th nametag is equal to HKDF( secret || n ) +func (m *MessageNametagBuffer) Init() { + // We default the counter and buffer fields + m.counter = 0 + m.buffer = make([]MessageNametag, MessageNametagBufferSize) + if len(m.secret) != 0 { + for i := range m.buffer { + counterBytesLE := make([]byte, 8) + binary.LittleEndian.PutUint64(counterBytesLE, m.counter) + toHash := []byte{} + toHash = append(toHash, m.secret...) + toHash = append(toHash, counterBytesLE...) + d := sha256.Sum256(toHash) + m.buffer[i] = BytesToMessageNametag(d[:]) + m.counter++ + } + } +} + +func (m *MessageNametagBuffer) Pop() MessageNametag { + // Note that if the input MessageNametagBuffer is set to default, an all 0 messageNametag is returned + if len(m.buffer) == 0 { + var m MessageNametag + return m + } else { + messageNametag := m.buffer[0] + m.Delete(1) + return messageNametag + } +} + +// Checks if the input messageNametag is contained in the input MessageNametagBuffer +func (m *MessageNametagBuffer) CheckNametag(messageNametag MessageNametag) error { + if len(m.buffer) != MessageNametagBufferSize { + return nil + } + + index := -1 + for i, x := range m.buffer { + if bytes.Equal(x[:], messageNametag[:]) { + index = i + break + } + } + + if index == -1 { + return ErrNametagNotFound + } else if index > 0 { + return ErrNametagNotExpected + } + + // index is 0, hence the read message tag is the next expected one + return nil +} + +func rotateLeft(elems []MessageNametag, k int) []MessageNametag { + if k < 0 || len(elems) == 0 { + return elems + } + r := len(elems) - k%len(elems) + + result := elems[r:] + result = append(result, elems[:r]...) + + return result +} + +// Deletes the first n elements in buffer and appends n new ones +func (m *MessageNametagBuffer) Delete(n int) { + if n <= 0 { + return + } + + // We ensure n is at most MessageNametagBufferSize (the buffer will be fully replaced) + if n > MessageNametagBufferSize { + 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 len(m.secret) != 0 { + m.buffer = rotateLeft(m.buffer, n) + for i := 0; i < n; i++ { + counterBytesLE := make([]byte, 8) + binary.LittleEndian.PutUint64(counterBytesLE, m.counter) + toHash := []byte{} + toHash = append(toHash, m.secret...) + toHash = append(toHash, counterBytesLE...) + d := sha256.Sum256(toHash) + m.buffer[len(m.buffer)-n+i] = BytesToMessageNametag(d[:]) + m.counter++ + } + } +} diff --git a/waku/v2/noise/noise_test.go b/waku/v2/noise/noise_test.go index 347384d1..5587b460 100644 --- a/waku/v2/noise/noise_test.go +++ b/waku/v2/noise/noise_test.go @@ -48,11 +48,11 @@ func handshakeTest(t *testing.T, hsAlice *Handshake, hsBob *Handshake) { // By being the handshake initiator, Alice writes a Waku2 payload v2 containing her handshake message // and the (encrypted) transport message sentTransportMessage := generateRandomBytes(t, 32) - aliceStep, err := hsAlice.Step(nil, sentTransportMessage) + aliceStep, err := hsAlice.Step(nil, sentTransportMessage, nil) require.NoError(t, err) // Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him - bobStep, err := hsBob.Step(&aliceStep.Payload2, nil) + bobStep, err := hsBob.Step(&aliceStep.Payload2, nil, nil) require.NoError(t, err) // check: @@ -64,11 +64,11 @@ func handshakeTest(t *testing.T, hsAlice *Handshake, hsBob *Handshake) { // At this step, Bob writes and returns a payload sentTransportMessage = generateRandomBytes(t, 32) - bobStep, err = hsBob.Step(nil, sentTransportMessage) + bobStep, err = hsBob.Step(nil, sentTransportMessage, nil) require.NoError(t, err) // While Alice reads and returns the (decrypted) transport message - aliceStep, err = hsAlice.Step(&bobStep.Payload2, nil) + aliceStep, err = hsAlice.Step(&bobStep.Payload2, nil, nil) require.NoError(t, err) // check: @@ -80,11 +80,11 @@ func handshakeTest(t *testing.T, hsAlice *Handshake, hsBob *Handshake) { // Similarly as in first step, Alice writes a Waku2 payload containing the handshake message and the (encrypted) transport message sentTransportMessage = generateRandomBytes(t, 32) - aliceStep, err = hsAlice.Step(nil, sentTransportMessage) + aliceStep, err = hsAlice.Step(nil, sentTransportMessage, nil) require.NoError(t, err) // Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him - bobStep, err = hsBob.Step(&aliceStep.Payload2, nil) + bobStep, err = hsBob.Step(&aliceStep.Payload2, nil, nil) require.NoError(t, err) // check: @@ -95,10 +95,10 @@ func handshakeTest(t *testing.T, hsAlice *Handshake, hsBob *Handshake) { require.True(t, hsAlice.HandshakeComplete()) require.True(t, hsBob.HandshakeComplete()) - _, err = hsAlice.Step(nil, generateRandomBytes(t, 32)) + _, err = hsAlice.Step(nil, generateRandomBytes(t, 32), nil) require.ErrorIs(t, err, ErrorHandshakeComplete) - _, err = hsBob.Step(nil, generateRandomBytes(t, 32)) + _, err = hsBob.Step(nil, generateRandomBytes(t, 32), nil) require.ErrorIs(t, err, ErrorHandshakeComplete) // ######################### @@ -107,14 +107,16 @@ func handshakeTest(t *testing.T, hsAlice *Handshake, hsBob *Handshake) { // We test read/write of random messages exchanged between Alice and Bob + defaultMessageNametagBuffer := NewMessageNametagBuffer(nil) + for i := 0; i < 10; i++ { // Alice writes to Bob message := generateRandomBytes(t, 32) - encryptedPayload, err := hsAlice.Encrypt(message) + encryptedPayload, err := hsAlice.Encrypt(message, defaultMessageNametagBuffer) require.NoError(t, err) - plaintext, err := hsBob.Decrypt(encryptedPayload) + plaintext, err := hsBob.Decrypt(encryptedPayload, defaultMessageNametagBuffer) require.NoError(t, err) require.Equal(t, message, plaintext) @@ -122,10 +124,10 @@ func handshakeTest(t *testing.T, hsAlice *Handshake, hsBob *Handshake) { // Bob writes to Alice message = generateRandomBytes(t, 32) - encryptedPayload, err = hsBob.Encrypt(message) + encryptedPayload, err = hsBob.Encrypt(message, defaultMessageNametagBuffer) require.NoError(t, err) - plaintext, err = hsAlice.Decrypt(encryptedPayload) + plaintext, err = hsAlice.Decrypt(encryptedPayload, defaultMessageNametagBuffer) require.NoError(t, err) require.Equal(t, message, plaintext) diff --git a/waku/v2/noise/patterns.go b/waku/v2/noise/patterns.go index d1ab15a6..c41cdffb 100644 --- a/waku/v2/noise/patterns.go +++ b/waku/v2/noise/patterns.go @@ -75,3 +75,13 @@ var HandshakeXXpsk0 = n.HandshakePattern{ {n.MessagePatternS, n.MessagePatternDHSE}, }, } + +var HandshakeWakuPairing = n.HandshakePattern{ + Name: "WakuPairing", + ResponderPreMessages: []n.MessagePattern{n.MessagePatternE}, + Messages: [][]n.MessagePattern{ + {n.MessagePatternE, n.MessagePatternDHEE}, + {n.MessagePatternS, n.MessagePatternDHES}, + {n.MessagePatternS, n.MessagePatternDHSE, n.MessagePatternDHSS}, + }, +} diff --git a/waku/v2/noise/payload.go b/waku/v2/noise/payload.go index 40bc0438..725da9ef 100644 --- a/waku/v2/noise/payload.go +++ b/waku/v2/noise/payload.go @@ -116,6 +116,7 @@ type PayloadV2 struct { ProtocolId byte HandshakeMessage []*NoisePublicKey TransportMessage []byte + MessageNametag MessageNametag } // Checks equality between two PayloadsV2 objects @@ -165,7 +166,8 @@ func (p *PayloadV2) Serialize() ([]byte, error) { // payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage) // We declare it as a byte sequence of length accordingly to the PayloadV2 information read - payload := make([]byte, 0, 1+ // 1 byte for protocol ID + payload := make([]byte, 0, MessageNametagLength+ + 1+ // 1 byte for protocol ID 1+ // 1 byte for length of serializedHandshakeMessage field serializedHandshakeMessageLen+ // serializedHandshakeMessageLen bytes for serializedHandshakeMessage 8+ // 8 bytes for transportMessageLen @@ -174,6 +176,10 @@ func (p *PayloadV2) Serialize() ([]byte, error) { payloadBuf := bytes.NewBuffer(payload) + if _, err := payloadBuf.Write(p.MessageNametag[:]); err != nil { + return nil, err + } + // 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 if err := payloadBuf.WriteByte(p.ProtocolId); err != nil { return nil, err @@ -215,7 +221,12 @@ func DeserializePayloadV2(payload []byte) (*PayloadV2, error) { result := &PayloadV2{} - // We start reading the Protocol ID + // We start by reading the messageNametag + if err := binary.Read(payloadBuf, binary.BigEndian, &result.MessageNametag); err != nil { + return nil, err + } + + // We read the Protocol ID // TODO: when the list of supported protocol ID is defined, check if read protocol ID is supported if err := binary.Read(payloadBuf, binary.BigEndian, &result.ProtocolId); err != nil { return nil, err diff --git a/waku/v2/noise/qr.go b/waku/v2/noise/qr.go new file mode 100644 index 00000000..bcd8aa46 --- /dev/null +++ b/waku/v2/noise/qr.go @@ -0,0 +1,80 @@ +package noise + +import ( + "crypto/ed25519" + b64 "encoding/base64" + "errors" + "strings" +) + +type QR struct { + applicationName string + applicationVersion string + shardId string + ephemeralKey ed25519.PublicKey + committedStaticKey []byte +} + +func NewQR(applicationName, applicationVersion, shardId string, ephemeralKey ed25519.PublicKey, committedStaticKey []byte) QR { + return QR{ + applicationName: applicationName, + applicationVersion: applicationVersion, + shardId: shardId, + ephemeralKey: ephemeralKey, + committedStaticKey: committedStaticKey, + } +} + +// Serializes input parameters to a base64 string for exposure through QR code (used by WakuPairing) +func (qr QR) String() string { + return b64.StdEncoding.EncodeToString([]byte(qr.applicationName)) + ":" + + b64.StdEncoding.EncodeToString([]byte(qr.applicationVersion)) + ":" + + b64.StdEncoding.EncodeToString([]byte(qr.shardId)) + ":" + + b64.StdEncoding.EncodeToString(qr.ephemeralKey) + ":" + + b64.StdEncoding.EncodeToString(qr.committedStaticKey[:]) +} + +func (qr QR) Bytes() []byte { + return []byte(qr.String()) +} + +// Deserializes input string in base64 to the corresponding (applicationName, applicationVersion, shardId, ephemeralKey, committedStaticKey) +func StringToQR(qrString string) (QR, error) { + values := strings.Split(qrString, ":") + if len(values) != 5 { + return QR{}, errors.New("invalid qr string") + } + + applicationName, err := b64.StdEncoding.DecodeString(values[0]) + if err != nil { + return QR{}, err + } + + applicationVersion, err := b64.StdEncoding.DecodeString(values[1]) + if err != nil { + return QR{}, err + } + + shardId, err := b64.StdEncoding.DecodeString(values[2]) + if err != nil { + return QR{}, err + } + + ephemeralKey, err := b64.StdEncoding.DecodeString(values[3]) + if err != nil { + return QR{}, err + } + + committedStaticKey, err := b64.StdEncoding.DecodeString(values[4]) + if err != nil { + return QR{}, err + } + + return QR{ + applicationName: string(applicationName), + applicationVersion: string(applicationVersion), + shardId: string(shardId), + ephemeralKey: ephemeralKey, + committedStaticKey: committedStaticKey, + }, nil +} diff --git a/waku/v2/noise/wakupairing_test.go b/waku/v2/noise/wakupairing_test.go new file mode 100644 index 00000000..7e492112 --- /dev/null +++ b/waku/v2/noise/wakupairing_test.go @@ -0,0 +1,209 @@ +package noise + +import ( + "bytes" + "crypto/rand" + "testing" + + "github.com/stretchr/testify/require" + n "github.com/waku-org/noise" +) + +func TestWakuPairing(t *testing.T) { + // Pairing Phase + // ========== + + // Alice static/ephemeral key initialization and commitment + aliceStaticKey, _ := n.DH25519.GenerateKeypair(rand.Reader) + aliceEphemeralKey, _ := n.DH25519.GenerateKeypair(rand.Reader) + s := generateRandomBytes(t, 32) + aliceCommittedStaticKey := CommitPublicKey(aliceStaticKey.Public, s) + + // Bob static/ephemeral key initialization and commitment + bobStaticKey, _ := n.DH25519.GenerateKeypair(rand.Reader) + bobEphemeralKey, _ := n.DH25519.GenerateKeypair(rand.Reader) + r := generateRandomBytes(t, 32) + bobCommittedStaticKey := CommitPublicKey(bobStaticKey.Public, r) + + // Content topic information + applicationName := "waku-noise-sessions" + applicationVersion := "0.1" + shardId := "10" + qrMessageNameTag := BytesToMessageNametag(generateRandomBytes(t, MessageNametagLength)) + + // Out-of-band Communication + + // Bob prepares the QR and sends it out-of-band to Alice + qr := NewQR(applicationName, applicationVersion, shardId, bobEphemeralKey.Public, bobCommittedStaticKey) + + // Alice deserializes the QR code + readQR, err := StringToQR(qr.String()) + require.NoError(t, err) + + // We check if QR serialization/deserialization works + require.Equal(t, applicationName, readQR.applicationName) + require.Equal(t, applicationVersion, readQR.applicationVersion) + require.Equal(t, shardId, readQR.shardId) + require.True(t, bytes.Equal(bobEphemeralKey.Public, readQR.ephemeralKey)) + require.True(t, bytes.Equal(bobCommittedStaticKey[:], readQR.committedStaticKey[:])) + + // Pre-handshake message + // <- eB {H(sB||r), contentTopicParams, messageNametag} + preMessagePKs := bobEphemeralKey.Public + + // We initialize the Handshake states. + // Note that we pass the whole qr serialization as prologue information + + aliceHS, err := NewHandshake_WakuPairing_25519_ChaChaPoly_SHA256(aliceStaticKey, aliceEphemeralKey, true, qr.Bytes(), preMessagePKs) + require.NoError(t, err) + + bobHS, err := NewHandshake_WakuPairing_25519_ChaChaPoly_SHA256(bobStaticKey, bobEphemeralKey, false, qr.Bytes(), preMessagePKs) + require.NoError(t, err) + + // Pairing Handshake + // ========== + + // 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 := 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, err := aliceHS.Step(nil, sentTransportMessage, &qrMessageNameTag) + require.NoError(t, err) + + // 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, err := bobHS.Step(&aliceStep.Payload2, nil, &qrMessageNameTag) + require.NoError(t, err) + + require.True(t, bytes.Equal(bobStep.TransportMessage, sentTransportMessage)) + + // We generate an authorization code using the handshake state + aliceAuthcode, err := aliceHS.Authcode() + require.NoError(t, err) + + bobAuthcode, err := bobHS.Authcode() + require.NoError(t, err) + + // We check that they are equal. Note that this check has to be confirmed with a user interaction. + require.Equal(t, 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, err := aliceHS.ToMessageNametag() + require.NoError(t, err) + + bobMessageNametag, err := bobHS.ToMessageNametag() + require.NoError(t, err) + + // We set as a transport message the commitment randomness r + sentTransportMessage = r + + // At this step, Bob writes and returns a payload + bobStep, err = bobHS.Step(nil, sentTransportMessage, &bobMessageNametag) + require.NoError(t, err) + + // While Alice reads and returns the (decrypted) transport message + aliceStep, err = aliceHS.Step(&bobStep.Payload2, nil, &aliceMessageNametag) + require.NoError(t, err) + require.Equal(t, aliceStep.TransportMessage, sentTransportMessage) + + // Alice further checks if Bob's commitment opens to Bob's static key she just received + expectedBobCommittedStaticKey := CommitPublicKey(aliceHS.RS(), aliceStep.TransportMessage) + require.True(t, bytes.Equal(expectedBobCommittedStaticKey, bobCommittedStaticKey)) + + // 3rd step + // -> sA, sAeB, sAsB {s} + + // Alice and Bob update their local next messageNametag using the available handshake information + aliceMessageNametag, err = aliceHS.ToMessageNametag() + require.NoError(t, err) + + bobMessageNametag, err = bobHS.ToMessageNametag() + require.NoError(t, err) + + // 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, err = aliceHS.Step(nil, sentTransportMessage, &aliceMessageNametag) + require.NoError(t, err) + + // Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him + bobStep, err = bobHS.Step(&aliceStep.Payload2, nil, &bobMessageNametag) + require.NoError(t, err) + require.True(t, bytes.Equal(bobStep.TransportMessage, sentTransportMessage)) + + // Bob further checks if Alice's commitment opens to Alice's static key he just received + expectedAliceCommittedStaticKey := CommitPublicKey(bobHS.RS(), bobStep.TransportMessage) + + require.True(t, bytes.Equal(expectedAliceCommittedStaticKey, aliceCommittedStaticKey)) + + // Secure Transfer Phase + // ========== + + // We test read/write of random messages exchanged between Alice and Bob + // 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 := 0; i < 10*MessageNametagBufferSize; i++ { + // Alice writes to Bob + message := generateRandomBytes(t, 32) + payload, err := aliceHS.Encrypt(message) + require.NoError(t, err) + + readMessage, err := bobHS.Decrypt(payload) + require.NoError(t, err) + require.True(t, bytes.Equal(message, readMessage)) + + // Bob writes to Alice + message = generateRandomBytes(t, 32) + payload, err = bobHS.Encrypt(message) + require.NoError(t, err) + + readMessage, err = aliceHS.Decrypt(payload) + require.NoError(t, err) + require.True(t, bytes.Equal(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 := generateRandomBytes(t, 32) + _, err = aliceHS.Encrypt(message) + require.NoError(t, err) + + message = generateRandomBytes(t, 32) + payload2, err := aliceHS.Encrypt(message) + require.NoError(t, err) + + _, err = bobHS.Decrypt(payload2) + require.Error(t, err) + require.ErrorIs(t, err, ErrNametagNotExpected) + + // We adjust bob nametag buffer for next test (i.e. the missed message is correctly recovered) + bobHS.nametagsInbound.Delete(2) + message = generateRandomBytes(t, 32) + payload2, err = bobHS.Encrypt(message) + require.NoError(t, err) + readMessage, err := aliceHS.Decrypt(payload2) + require.NoError(t, err) + require.True(t, bytes.Equal(message, readMessage)) + + // We test if a missing nametag is correctly detected + message = generateRandomBytes(t, 32) + payload2, err = aliceHS.Encrypt(message) + require.NoError(t, err) + bobHS.nametagsInbound.Delete(1) + _, err = bobHS.Decrypt(payload2) + require.ErrorIs(t, err, ErrNametagNotFound) +}