feat(noise): WakuPairing

This commit is contained in:
Richard Ramos 2022-12-14 12:22:48 -04:00 committed by RichΛrd
parent 636e6b284e
commit f1fd8b354e
11 changed files with 651 additions and 53 deletions

View File

@ -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,

4
go.mod
View File

@ -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

4
go.sum
View File

@ -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=

15
waku/v2/noise/crypto.go Normal file
View File

@ -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[:]
}

View File

@ -1,10 +1,15 @@
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
@ -16,6 +21,7 @@ var (
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)
)
@ -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)
@ -38,6 +44,7 @@ func newHandshakeState(pattern n.HandshakePattern, initiator bool, staticKeypair
Pattern: pattern,
Initiator: initiator,
StaticKeypair: staticKeypair,
EphemeralKeypair: ephemeralKeyPair,
Prologue: prologue,
PresharedKey: presharedKey,
PeerStatic: peerStatic,
@ -56,6 +63,8 @@ type Handshake struct {
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)
}

View File

@ -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++
}
}
}

View File

@ -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)

View File

@ -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},
},
}

View File

@ -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

80
waku/v2/noise/qr.go Normal file
View File

@ -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
}

View File

@ -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)
}