From dec00e69addfe55674238da1ed3908a353d64f6c Mon Sep 17 00:00:00 2001 From: Richard Ramos Date: Tue, 9 Aug 2022 09:55:08 -0400 Subject: [PATCH] feat: noise (#258) --- go.mod | 9 +- go.sum | 10 +- waku/v2/node/waku_payload.go | 24 +++ waku/v2/noise/handshake.go | 248 ++++++++++++++++++++++++++++ waku/v2/noise/noise_test.go | 188 +++++++++++++++++++++ waku/v2/noise/patterns.go | 73 +++++++++ waku/v2/noise/payload.go | 306 +++++++++++++++++++++++++++++++++++ 7 files changed, 847 insertions(+), 11 deletions(-) create mode 100644 waku/v2/noise/handshake.go create mode 100644 waku/v2/noise/noise_test.go create mode 100644 waku/v2/noise/patterns.go create mode 100644 waku/v2/noise/payload.go diff --git a/go.mod b/go.mod index 5c7ae949..0f532644 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ replace github.com/raulk/go-watchdog v1.2.0 => github.com/status-im/go-watchdog replace github.com/ethereum/go-ethereum v1.10.18 => github.com/status-im/go-ethereum v1.10.4-status.2 +replace github.com/flynn/noise v1.0.0 => github.com/status-im/noise v1.0.1-handshakeMessages + require ( contrib.go.opencensus.io/exporter/prometheus v0.4.1 github.com/btcsuite/btcd/btcec/v2 v2.1.3 @@ -36,7 +38,10 @@ require ( golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 ) -require golang.org/x/text v0.3.7 +require ( + github.com/flynn/noise v1.0.0 + golang.org/x/text v0.3.7 +) require ( github.com/benbjohnson/clock v1.3.0 // indirect @@ -55,7 +60,6 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/elastic/gosigar v0.14.1 // indirect - github.com/flynn/noise v1.0.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/go-kit/log v0.1.0 // indirect @@ -90,7 +94,6 @@ require ( github.com/libp2p/go-libp2p-blankhost v0.3.0 // indirect github.com/libp2p/go-libp2p-connmgr v0.3.1 // indirect github.com/libp2p/go-libp2p-discovery v0.6.0 // indirect - github.com/libp2p/go-libp2p-quic-transport v0.17.0 // indirect github.com/libp2p/go-libp2p-resource-manager v0.3.0 // indirect github.com/libp2p/go-libp2p-swarm v0.10.2 // indirect github.com/libp2p/go-libp2p-tls v0.3.1 // indirect diff --git a/go.sum b/go.sum index f6d69080..a7c060ec 100644 --- a/go.sum +++ b/go.sum @@ -541,8 +541,6 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= -github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= @@ -1115,18 +1113,12 @@ github.com/libp2p/go-libp2p-mplex v0.5.0/go.mod h1:eLImPJLkj3iG5t5lq68w3Vm5NAQ5B github.com/libp2p/go-libp2p-peerstore v0.4.0/go.mod h1:rDJUFyzEWPpXpEwywkcTYYzDHlwza8riYMaUzaN6hX0= github.com/libp2p/go-libp2p-peerstore v0.6.0 h1:HJminhQSGISBIRb93N6WK3t6Fa8OOTnHd/VBjL4mY5A= github.com/libp2p/go-libp2p-peerstore v0.6.0/go.mod h1:DGEmKdXrcYpK9Jha3sS7MhqYdInxJy84bIPtSu65bKc= -github.com/libp2p/go-libp2p-peerstore v0.7.0 h1:2iIUwok3vtmnWJTZeTeLgnBO6GbkXcwSRwgZHEKrQZs= -github.com/libp2p/go-libp2p-peerstore v0.7.0/go.mod h1:cdUWTHro83vpg6unCpGUr8qJoX3e93Vy8o97u5ppIM0= github.com/libp2p/go-libp2p-pnet v0.2.0 h1:J6htxttBipJujEjz1y0a5+eYoiPcFHhSYHH6na5f0/k= github.com/libp2p/go-libp2p-pnet v0.2.0/go.mod h1:Qqvq6JH/oMZGwqs3N1Fqhv8NVhrdYcO0BW4wssv21LA= -github.com/libp2p/go-libp2p-pubsub v0.6.1 h1:wycbV+f4rreCoVY61Do6g/BUk0RIrbNRcYVbn+QkjGk= -github.com/libp2p/go-libp2p-pubsub v0.6.1/go.mod h1:nJv87QM2cU0w45KPR1rZicq+FmFIOD16zmT+ep1nOmg= github.com/libp2p/go-libp2p-pubsub v0.7.1 h1:e2CPBP5uxvDkE0FiS0obZGZPzt+xuBOc6PpG+50pIAo= github.com/libp2p/go-libp2p-pubsub v0.7.1/go.mod h1:EuyBJFtF8qF67IEA98biwK8Xnw5MNJpJ/Z+8iWCMFwc= github.com/libp2p/go-libp2p-quic-transport v0.13.0/go.mod h1:39/ZWJ1TW/jx1iFkKzzUg00W6tDJh73FC0xYudjr7Hc= github.com/libp2p/go-libp2p-quic-transport v0.16.0/go.mod h1:1BXjVMzr+w7EkPfiHkKnwsWjPjtfaNT0q8RS3tGDvEQ= -github.com/libp2p/go-libp2p-quic-transport v0.16.1 h1:N/XqYXHurphPLDfXYhll8NyqzdZYQqAF4GIr7+SmLV8= -github.com/libp2p/go-libp2p-quic-transport v0.16.1/go.mod h1:1BXjVMzr+w7EkPfiHkKnwsWjPjtfaNT0q8RS3tGDvEQ= github.com/libp2p/go-libp2p-quic-transport v0.17.0 h1:yFh4Gf5MlToAYLuw/dRvuzYd1EnE2pX3Lq1N6KDiWRQ= github.com/libp2p/go-libp2p-quic-transport v0.17.0/go.mod h1:x4pw61P3/GRCcSLypcQJE/Q2+E9f4X+5aRcZLXf20LM= github.com/libp2p/go-libp2p-resource-manager v0.3.0 h1:2+cYxUNi33tcydsVLt6K5Fv2E3OTiVeafltecAj15E0= @@ -1689,6 +1681,8 @@ github.com/status-im/go-waku-rendezvous v0.0.0-20211018070416-a93f3b70c432/go.mo github.com/status-im/go-watchdog v1.2.0-ios-nolibproc h1:BJwZEF7OVKaXc2zErBUAolFSGzwrTBbWnN8e/6MER5E= github.com/status-im/go-watchdog v1.2.0-ios-nolibproc/go.mod h1:lzSbAl5sh4rtI8tYHU01BWIDzgzqaQLj6RcA1i4mlqI= github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= +github.com/status-im/noise v1.0.1-handshakeMessages h1:mj1btE58Qk2pS0qz+BHE22HYIOhZoEFNTnRpQeMfHYk= +github.com/status-im/noise v1.0.1-handshakeMessages/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= github.com/status-im/status-go/extkeys v1.1.2 h1:FSjARgDathJ3rIapJt851LsIXP9Oyuu2M2jPJKuzloU= github.com/status-im/status-go/extkeys v1.1.2/go.mod h1:hCmFzb2jiiVF2voZKYbzuhOQiHHCmyLJsZJXrFFg7BY= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= diff --git a/waku/v2/node/waku_payload.go b/waku/v2/node/waku_payload.go index 5708d6e5..3e8ca221 100644 --- a/waku/v2/node/waku_payload.go +++ b/waku/v2/node/waku_payload.go @@ -14,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/ecies" + "github.com/status-im/go-waku/waku/v2/noise" "github.com/status-im/go-waku/waku/v2/protocol/pb" ) @@ -449,3 +450,26 @@ func bytesToUintLittleEndian(b []byte) (res uint64) { } return res } + +// Decodes a WakuMessage to a PayloadV2 +// Currently, this is just a wrapper over deserializePayloadV2 and encryption/decryption is done on top (no KeyInfo) +func DecodePayloadV2(message *pb.WakuMessage) (*noise.PayloadV2, error) { + if message.Version != 2 { + return nil, errors.New("wrong message version while decoding payload") + } + return noise.DeserializePayloadV2(message.Payload) +} + +// Encodes a PayloadV2 to a WakuMessage +// Currently, this is just a wrapper over serializePayloadV2 and encryption/decryption is done on top (no KeyInfo) +func EncodePayloadV2(payload2 *noise.PayloadV2) (*pb.WakuMessage, error) { + serializedPayload2, err := payload2.Serialize() + if err != nil { + return nil, err + } + + return &pb.WakuMessage{ + Payload: serializedPayload2, + Version: 2, + }, nil +} diff --git a/waku/v2/noise/handshake.go b/waku/v2/noise/handshake.go new file mode 100644 index 00000000..438a68e7 --- /dev/null +++ b/waku/v2/noise/handshake.go @@ -0,0 +1,248 @@ +package noise + +import ( + "errors" + "fmt" + + n "github.com/flynn/noise" +) + +// 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) +) + +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) { + defer func() { + if rerr := recover(); rerr != nil { + err = fmt.Errorf("panic in Noise handshake: %s", rerr) + } + }() + + cfg := n.Config{ + CipherSuite: cipherSuite, + Pattern: pattern, + Initiator: initiator, + StaticKeypair: staticKeypair, + Prologue: prologue, + PresharedKey: presharedKey, + PeerStatic: peerStatic, + PeerEphemeral: peerEphemeral, + } + + return n.NewHandshakeState(cfg) +} + +type Handshake struct { + protocolID WakuNoiseProtocolID + pattern n.HandshakePattern + state *n.HandshakeState + + hsBuff []byte + + enc *n.CipherState + dec *n.CipherState + + initiator bool + shouldWrite bool +} + +// HandshakeStepResult stores the intermediate result of processing messages patterns +type HandshakeStepResult struct { + Payload2 PayloadV2 + TransportMessage []byte +} + +func getHandshakePattern(protocol WakuNoiseProtocolID) (n.HandshakePattern, error) { + switch protocol { + case Noise_K1K1_25519_ChaChaPoly_SHA256: + return HandshakeK1K1, nil + case Noise_XK1_25519_ChaChaPoly_SHA256: + return HandshakeXK1, nil + case Noise_XX_25519_ChaChaPoly_SHA256: + return HandshakeXX, nil + case Noise_XXpsk0_25519_ChaChaPoly_SHA256: + return HandshakeXXpsk0, 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) { + hsPattern, err := getHandshakePattern(protocolID) + if err != nil { + return nil, err + } + + hsState, err := newHandshakeState(hsPattern, initiator, staticKeypair, prologue, presharedKey, peerStatic, peerEphemeral) + if err != nil { + return nil, err + } + + return &Handshake{ + protocolID: protocolID, + pattern: hsPattern, + initiator: initiator, + shouldWrite: initiator, + state: hsState, + }, nil +} + +// 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) { + if hs.enc != nil || hs.dec != nil { + return nil, ErrorHandshakeComplete + } + + var cs1 *n.CipherState + var cs2 *n.CipherState + var err error + var msg []byte + + result := HandshakeStepResult{} + + if hs.shouldWrite { + // We initialize a payload v2 and we set proper protocol ID (if supported) + result.Payload2.ProtocolId = hs.protocolID + + var noisePubKeys [][]byte + msg, cs1, cs2, err = hs.state.WriteMessageAndGetPK(hs.hsBuff, &noisePubKeys, transportMessage) + if err != nil { + return nil, err + } + + hs.shouldWrite = false + + result.Payload2.TransportMessage = msg + for _, npk := range noisePubKeys { + result.Payload2.HandshakeMessage = append(result.Payload2.HandshakeMessage, byteToNoisePublicKey(npk)) + } + + } else { + if readPayloadV2 == nil { + return nil, errors.New("readPayloadV2 is required") + } + + readTMessage := readPayloadV2.TransportMessage + + // 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) + if err != nil { + return nil, err + } + + hs.shouldWrite = true + + // We retrieve and store the (decrypted) received transport message + result.TransportMessage = msg + } + + if cs1 != nil && cs2 != nil { + hs.setCipherStates(cs1, cs2) + } + + return &result, nil +} + +// HandshakeComplete indicates whether the handshake process is complete or not +func (hs *Handshake) HandshakeComplete() bool { + return hs.enc != nil && hs.dec != nil +} + +// This is called when the final handshake message is processed +func (hs *Handshake) setCipherStates(cs1, cs2 *n.CipherState) { + if hs.initiator { + hs.enc = cs1 + hs.dec = cs2 + } else { + hs.enc = cs2 + hs.dec = cs1 + } +} + +// Encrypt calls the cipher's encryption. It encrypts the provided plaintext and returns a PayloadV2 +func (hs *Handshake) Encrypt(plaintext []byte) (*PayloadV2, error) { + if hs.enc == nil { + return nil, errors.New("cannot encrypt, handshake incomplete") + } + + if len(plaintext) == 0 { + return nil, errors.New("tried to encrypt empty plaintext") + } + + // TODO: add padding (?) + + cyphertext, err := hs.enc.Encrypt(nil, nil, plaintext) + if err != nil { + return nil, err + } + + // According to 35/WAKU2-NOISE RFC, no Handshake protocol information is sent when exchanging messages + // This correspond to setting protocol-id to 0 (None) + return &PayloadV2{ + ProtocolId: None, + TransportMessage: cyphertext, + }, 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) { + if hs.dec == nil { + return nil, errors.New("cannot decrypt, handshake incomplete") + } + + if payload == nil { + return nil, errors.New("no payload to decrypt") + } + + if len(payload.TransportMessage) == 0 { + return nil, errors.New("tried to decrypt empty ciphertext") + } + + return hs.dec.Decrypt(nil, nil, payload.TransportMessage) +} + +// 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) +} + +// 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) +} + +// 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) +} + +// NewHandshake_XK1_25519_ChaChaPoly_SHA256 creates a handshake where the initiator knows the receiver public static key. Within this handshake, +// the initiator and receiver reciprocally authenticate their static keys using ephemeral keys. We note that while the receiver's +// static key is assumed to be known to Alice (and hence is not transmitted), The initiator static key is sent to the +// receiver encrypted with a key derived from both parties ephemeral keys and the receiver's static key. +func NewHandshake_XK1_25519_ChaChaPoly_SHA256(staticKeypair n.DHKey, initiator bool, peerStaticKey []byte, prologue []byte) (*Handshake, error) { + 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) +} diff --git a/waku/v2/noise/noise_test.go b/waku/v2/noise/noise_test.go new file mode 100644 index 00000000..0f3a62bc --- /dev/null +++ b/waku/v2/noise/noise_test.go @@ -0,0 +1,188 @@ +package noise + +import ( + "crypto/ed25519" + "crypto/rand" + "testing" + + "github.com/flynn/noise" + "github.com/stretchr/testify/require" +) + +func generateRandomBytes(t *testing.T, n int) []byte { + b := make([]byte, n) + _, err := rand.Read(b) + require.NoError(t, err) + return b +} + +func TestSerialization(t *testing.T) { + handshakeMessages := make([]*NoisePublicKey, 2) + + pk1, _, _ := ed25519.GenerateKey(rand.Reader) + + pk2, _, _ := ed25519.GenerateKey(rand.Reader) + + handshakeMessages[0] = Ed25519PubKeyToNoisePublicKey(pk1) + handshakeMessages[1] = Ed25519PubKeyToNoisePublicKey(pk2) + + p1 := &PayloadV2{ + ProtocolId: Noise_K1K1_25519_ChaChaPoly_SHA256, + HandshakeMessage: handshakeMessages, + TransportMessage: []byte{9, 8, 7, 6, 5, 4, 3, 2, 1}, + } + + serializedPayload, err := p1.Serialize() + require.NoError(t, err) + + deserializedPayload, err := DeserializePayloadV2(serializedPayload) + require.NoError(t, err) + require.Equal(t, p1, deserializedPayload) +} + +func handshakeTest(t *testing.T, hsAlice *Handshake, hsBob *Handshake) { + // ############### + // # 1st step + // ############### + + // 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) + 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) + require.NoError(t, err) + + // check: + require.Equal(t, sentTransportMessage, bobStep.TransportMessage) + + // ############### + // # 2nd step + // ############### + + // At this step, Bob writes and returns a payload + sentTransportMessage = generateRandomBytes(t, 32) + bobStep, err = hsBob.Step(nil, sentTransportMessage) + require.NoError(t, err) + + // While Alice reads and returns the (decrypted) transport message + aliceStep, err = hsAlice.Step(&bobStep.Payload2, nil) + require.NoError(t, err) + + // check: + require.Equal(t, sentTransportMessage, aliceStep.TransportMessage) + + // ############### + // # 3rd step + // ############### + + // 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) + 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) + require.NoError(t, err) + + // check: + require.Equal(t, sentTransportMessage, bobStep.TransportMessage) + + // Note that for this handshake pattern, no more message patterns are left for processing + // We test that extra calls to stepHandshake do not affect parties' handshake states + require.True(t, hsAlice.HandshakeComplete()) + require.True(t, hsBob.HandshakeComplete()) + + _, err = hsAlice.Step(nil, generateRandomBytes(t, 32)) + require.ErrorIs(t, err, ErrorHandshakeComplete) + + _, err = hsBob.Step(nil, generateRandomBytes(t, 32)) + require.ErrorIs(t, err, ErrorHandshakeComplete) + + // ######################### + // After Handshake + // ######################### + + // We test read/write of random messages exchanged between Alice and Bob + + for i := 0; i < 10; i++ { + // Alice writes to Bob + message := generateRandomBytes(t, 32) + + encryptedPayload, err := hsAlice.Encrypt(message) + require.NoError(t, err) + + plaintext, err := hsBob.Decrypt(encryptedPayload) + require.NoError(t, err) + + require.Equal(t, message, plaintext) + + // Bob writes to Alice + message = generateRandomBytes(t, 32) + + encryptedPayload, err = hsBob.Encrypt(message) + require.NoError(t, err) + + plaintext, err = hsAlice.Decrypt(encryptedPayload) + require.NoError(t, err) + + require.Equal(t, message, plaintext) + } +} + +func TestNoiseXXHandshakeRoundtrip(t *testing.T) { + aliceKP, _ := noise.DH25519.GenerateKeypair(rand.Reader) + bobKP, _ := noise.DH25519.GenerateKeypair(rand.Reader) + + hsAlice, err := NewHandshake_XX_25519_ChaChaPoly_SHA256(aliceKP, true, nil) + require.NoError(t, err) + + hsBob, err := NewHandshake_XX_25519_ChaChaPoly_SHA256(bobKP, false, nil) + require.NoError(t, err) + + handshakeTest(t, hsAlice, hsBob) +} + +func TestNoiseXXpsk0HandshakeRoundtrip(t *testing.T) { + aliceKP, _ := noise.DH25519.GenerateKeypair(rand.Reader) + bobKP, _ := noise.DH25519.GenerateKeypair(rand.Reader) + + // We generate a random psk + psk := generateRandomBytes(t, 32) + + hsAlice, err := NewHandshake_XXpsk0_25519_ChaChaPoly_SHA256(aliceKP, true, psk, nil) + require.NoError(t, err) + + hsBob, err := NewHandshake_XXpsk0_25519_ChaChaPoly_SHA256(bobKP, false, psk, nil) + require.NoError(t, err) + + handshakeTest(t, hsAlice, hsBob) +} + +func TestNoiseK1K1HandshakeRoundtrip(t *testing.T) { + aliceKP, _ := noise.DH25519.GenerateKeypair(rand.Reader) + bobKP, _ := noise.DH25519.GenerateKeypair(rand.Reader) + + hsAlice, err := NewHandshake_K1K1_25519_ChaChaPoly_SHA256(aliceKP, true, bobKP.Public, nil) + require.NoError(t, err) + + hsBob, err := NewHandshake_K1K1_25519_ChaChaPoly_SHA256(bobKP, false, aliceKP.Public, nil) + require.NoError(t, err) + + handshakeTest(t, hsAlice, hsBob) +} + +func TestNoiseXK1HandshakeRoundtrip(t *testing.T) { + aliceKP, _ := noise.DH25519.GenerateKeypair(rand.Reader) + bobKP, _ := noise.DH25519.GenerateKeypair(rand.Reader) + + hsAlice, err := NewHandshake_XK1_25519_ChaChaPoly_SHA256(aliceKP, true, bobKP.Public, nil) + require.NoError(t, err) + + hsBob, err := NewHandshake_XK1_25519_ChaChaPoly_SHA256(bobKP, false, nil, nil) + require.NoError(t, err) + + handshakeTest(t, hsAlice, hsBob) +} diff --git a/waku/v2/noise/patterns.go b/waku/v2/noise/patterns.go new file mode 100644 index 00000000..1b454b1d --- /dev/null +++ b/waku/v2/noise/patterns.go @@ -0,0 +1,73 @@ +package noise + +import ( + n "github.com/flynn/noise" +) + +/* + K1K1: + -> s + <- s + ... + -> e + <- e, ee, es + -> se +*/ +var HandshakeK1K1 = n.HandshakePattern{ + Name: "K1K1", + InitiatorPreMessages: []n.MessagePattern{n.MessagePatternS}, + ResponderPreMessages: []n.MessagePattern{n.MessagePatternS}, + Messages: [][]n.MessagePattern{ + {n.MessagePatternE}, + {n.MessagePatternE, n.MessagePatternDHEE, n.MessagePatternDHES}, + {n.MessagePatternDHSE}, + }, +} + +/* + XK1: + <- s + ... + -> e + <- e, ee, es + -> s, se +*/ +var HandshakeXK1 = n.HandshakePattern{ + Name: "XK1", + ResponderPreMessages: []n.MessagePattern{n.MessagePatternS}, + Messages: [][]n.MessagePattern{ + {n.MessagePatternE}, + {n.MessagePatternE, n.MessagePatternDHEE, n.MessagePatternDHES}, + {n.MessagePatternS, n.MessagePatternDHSE}, + }, +} + +/* +XX: + -> e + <- e, ee, s, es + -> s, se +*/ +var HandshakeXX = n.HandshakePattern{ + Name: "XX", + Messages: [][]n.MessagePattern{ + {n.MessagePatternE}, + {n.MessagePatternE, n.MessagePatternDHEE, n.MessagePatternS, n.MessagePatternDHES}, + {n.MessagePatternS, n.MessagePatternDHSE}, + }, +} + +/* + XXpsk0: + -> psk, e + <- e, ee, s, es + -> s, se +*/ +var HandshakeXXpsk0 = n.HandshakePattern{ + Name: "XXpsk0", + Messages: [][]n.MessagePattern{ + {n.MessagePatternPSK, n.MessagePatternE}, + {n.MessagePatternE, n.MessagePatternDHEE, n.MessagePatternS, n.MessagePatternDHES}, + {n.MessagePatternS, n.MessagePatternDHSE}, + }, +} diff --git a/waku/v2/noise/payload.go b/waku/v2/noise/payload.go new file mode 100644 index 00000000..23d7065d --- /dev/null +++ b/waku/v2/noise/payload.go @@ -0,0 +1,306 @@ +package noise + +import ( + "bytes" + "crypto/ed25519" + "encoding/binary" + "errors" + + n "github.com/flynn/noise" +) + +const MaxUint8 = 1<<8 - 1 + +// 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 +type NoisePublicKey struct { + Flag byte + PubKey []byte +} + +func byteToNoisePublicKey(input []byte) *NoisePublicKey { + flag := byte(0) + if len(input) > n.DH25519.DHLen() { + flag = 1 + } + + return &NoisePublicKey{ + Flag: flag, + PubKey: input, + } +} + +// EcdsaPubKeyToNoisePublicKey converts a Elliptic Curve public key +// to an unencrypted Noise public key +func Ed25519PubKeyToNoisePublicKey(pk ed25519.PublicKey) *NoisePublicKey { + return &NoisePublicKey{ + Flag: 0, + PubKey: pk, + } +} + +// Equals checks equality between two Noise public keys +func (pk *NoisePublicKey) Equals(pk2 *NoisePublicKey) bool { + return pk.Flag == pk2.Flag && bytes.Equal(pk.PubKey, pk2.PubKey) +} + +type SerializedNoisePublicKey []byte + +// Serialize converts a Noise public key to a stream of bytes as in +// https://rfc.vac.dev/spec/35/#public-keys-serialization +func (pk *NoisePublicKey) Serialize() SerializedNoisePublicKey { + // 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 + serializedPK := make([]byte, len(pk.PubKey)+1) + serializedPK[0] = pk.Flag + copy(serializedPK[1:], pk.PubKey) + + return serializedPK +} + +// Unserialize converts a serialized Noise public key to a NoisePublicKey object as in +// https://rfc.vac.dev/spec/35/#public-keys-serialization +func (s SerializedNoisePublicKey) Unserialize() (*NoisePublicKey, error) { + if len(s) <= 1 { + return nil, errors.New("invalid serialized public key length") + } + + pubk := &NoisePublicKey{} + pubk.Flag = s[0] + if !(pubk.Flag == 0 || pubk.Flag == 1) { + return nil, errors.New("invalid flag in serialized public key") + } + + pubk.PubKey = s[1:] + + return pubk, nil +} + +// Encrypt encrypts a Noise public key using a Cipher State +func (pk *NoisePublicKey) Encrypt(state *n.CipherState) error { + if pk.Flag == 0 { + // Authorization tag is appended to output + encPk, err := state.Encrypt(nil, nil, pk.PubKey) + if err != nil { + return err + } + pk.Flag = 1 + pk.PubKey = encPk + } + + return nil +} + +// Decrypts decrypts a Noise public key using a Cipher State +func (pk *NoisePublicKey) Decrypt(state *n.CipherState) error { + if pk.Flag == 1 { + decPk, err := state.Decrypt(nil, nil, pk.PubKey) // encrypted pk should contain the auth tag + if err != nil { + return err + } + pk.Flag = 0 + pk.PubKey = decPk + } + + return nil +} + +// 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) +type PayloadV2 struct { + ProtocolId byte + HandshakeMessage []*NoisePublicKey + TransportMessage []byte +} + +// Checks equality between two PayloadsV2 objects +func (p *PayloadV2) Equals(p2 *PayloadV2) bool { + if p.ProtocolId != p2.ProtocolId || !bytes.Equal(p.TransportMessage, p2.TransportMessage) { + return false + } + + for _, p1 := range p.HandshakeMessage { + for _, p2 := range p2.HandshakeMessage { + if !p1.Equals(p2) { + return false + } + } + } + + return true +} + +// Serializes a PayloadV2 object to a byte sequences according to https://rfc.vac.dev/spec/35/ +// The output serialized payload concatenates the input PayloadV2 object fields as +// payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage) +// The output can be then passed to the payload field of a WakuMessage https://rfc.vac.dev/spec/14/ +func (p *PayloadV2) Serialize() ([]byte, error) { + // We collect public keys contained in the handshake message + + // According to https://rfc.vac.dev/spec/35/, the maximum size for the handshake message is 256 bytes, that is + // the handshake message length can be represented with 1 byte only. (its length can be stored in 1 byte) + // However, to ease public keys length addition operation, we declare it as int and later cast to uit8 + serializedHandshakeMessageLen := 0 + // This variables will store the concatenation of the serializations of all public keys in the handshake message + serializedHandshakeMessage := make([]byte, 0, 256) + serializedHandshakeMessageBuffer := bytes.NewBuffer(serializedHandshakeMessage) + + for _, pk := range p.HandshakeMessage { + serializedPK := pk.Serialize() + serializedHandshakeMessageLen += len(serializedPK) + if _, err := serializedHandshakeMessageBuffer.Write(serializedPK); err != nil { + return nil, err + } + if serializedHandshakeMessageLen > MaxUint8 { + return nil, errors.New("too many public keys in handshake message") + } + } + + // 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 + payload := make([]byte, 0, 1+ // 1 byte for protocol ID + 1+ // 1 byte for length of serializedHandshakeMessage field + serializedHandshakeMessageLen+ // serializedHandshakeMessageLen bytes for serializedHandshakeMessage + 8+ // 8 bytes for transportMessageLen + len(p.TransportMessage), // transportMessageLen bytes for transportMessage + ) + + payloadBuf := bytes.NewBuffer(payload) + + // 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 + } + + if err := payloadBuf.WriteByte(byte(serializedHandshakeMessageLen)); err != nil { + return nil, err + } + + if _, err := payloadBuf.Write(serializedHandshakeMessageBuffer.Bytes()); err != nil { + return nil, err + } + + TransportMessageLen := uint64(len(p.TransportMessage)) + if err := binary.Write(payloadBuf, binary.LittleEndian, TransportMessageLen); err != nil { + return nil, err + } + + if _, err := payloadBuf.Write(p.TransportMessage); err != nil { + return nil, err + } + + return payloadBuf.Bytes(), nil +} + +func isProtocolIDSupported(protocolID WakuNoiseProtocolID) bool { + return protocolID == Noise_K1K1_25519_ChaChaPoly_SHA256 || protocolID == Noise_XK1_25519_ChaChaPoly_SHA256 || + protocolID == Noise_XX_25519_ChaChaPoly_SHA256 || protocolID == Noise_XXpsk0_25519_ChaChaPoly_SHA256 || + protocolID == ChaChaPoly || protocolID == None +} + +const ChaChaPolyTagSize = byte(16) + +// 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) +func DeserializePayloadV2(payload []byte) (*PayloadV2, error) { + payloadBuf := bytes.NewBuffer(payload) + + result := &PayloadV2{} + + // We start reading 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 + } + + if !isProtocolIDSupported(result.ProtocolId) { + return nil, errors.New("unsupported protocol") + } + + // We read the Handshake Message length (1 byte) + var handshakeMessageLen byte + if err := binary.Read(payloadBuf, binary.BigEndian, &handshakeMessageLen); err != nil { + return nil, err + } + if handshakeMessageLen > MaxUint8 { + return nil, errors.New("too many public keys in handshake message") + } + + written := byte(0) + var handshakeMessages []*NoisePublicKey + for written < handshakeMessageLen { + // We obtain the current Noise Public key encryption flag + flag, err := payloadBuf.ReadByte() + if err != nil { + return nil, err + } + + if flag == 0 { + // If the key is unencrypted, we only read the X coordinate of the EC public key and we deserialize into a Noise Public Key + pkLen := ed25519.PublicKeySize + var pkBytes SerializedNoisePublicKey = make([]byte, pkLen) + if err := binary.Read(payloadBuf, binary.BigEndian, &pkBytes); err != nil { + return nil, err + } + + serializedPK := SerializedNoisePublicKey(make([]byte, ed25519.PublicKeySize+1)) + serializedPK[0] = flag + copy(serializedPK[1:], pkBytes) + + pk, err := serializedPK.Unserialize() + if err != nil { + return nil, err + } + + handshakeMessages = append(handshakeMessages, pk) + written += uint8(len(serializedPK)) + + } else if flag == 1 { + // If the key is encrypted, we only read the encrypted X coordinate and the authorization tag, and we deserialize into a Noise Public Key + pkLen := ed25519.PublicKeySize + ChaChaPolyTagSize + // TODO: duplicated code: ============== + + var pkBytes SerializedNoisePublicKey = make([]byte, pkLen) + if err := binary.Read(payloadBuf, binary.BigEndian, &pkBytes); err != nil { + return nil, err + } + + serializedPK := SerializedNoisePublicKey(make([]byte, ed25519.PublicKeySize+1)) + serializedPK[0] = flag + copy(serializedPK[1:], pkBytes) + + pk, err := serializedPK.Unserialize() + if err != nil { + return nil, err + } + + handshakeMessages = append(handshakeMessages, pk) + written += uint8(len(serializedPK)) + // TODO: duplicated + } else { + return nil, errors.New("invalid flag for Noise public key") + } + } + + result.HandshakeMessage = handshakeMessages + + var TransportMessageLen uint64 + if err := binary.Read(payloadBuf, binary.LittleEndian, &TransportMessageLen); err != nil { + return nil, err + } + + result.TransportMessage = make([]byte, TransportMessageLen) + if err := binary.Read(payloadBuf, binary.BigEndian, &result.TransportMessage); err != nil { + return nil, err + } + + return result, nil +}