mirror of https://github.com/status-im/go-waku.git
feat(noise): WakuPairing
This commit is contained in:
parent
636e6b284e
commit
f1fd8b354e
|
@ -147,11 +147,11 @@ var (
|
||||||
Choices: []string{"DEBUG", "INFO", "WARN", "ERROR", "DPANIC", "PANIC", "FATAL"},
|
Choices: []string{"DEBUG", "INFO", "WARN", "ERROR", "DPANIC", "PANIC", "FATAL"},
|
||||||
Value: &options.LogLevel,
|
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{
|
LogEncoding = &cli.GenericFlag{
|
||||||
Name: "log-encoding",
|
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{
|
Value: &cliutils.ChoiceValue{
|
||||||
Choices: []string{"console", "nocolor", "json"},
|
Choices: []string{"console", "nocolor", "json"},
|
||||||
Value: &options.LogEncoding,
|
Value: &options.LogEncoding,
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -33,7 +33,7 @@ require (
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/waku-org/go-discover v0.0.0-20221209174356-61c833f34d98
|
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/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
|
golang.org/x/text v0.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ require (
|
||||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
go.uber.org/atomic v1.10.0 // indirect
|
go.uber.org/atomic v1.10.0 // indirect
|
||||||
go.uber.org/multierr v1.8.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/exp v0.0.0-20220916125017-b168a2c6b86b // indirect
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||||
golang.org/x/net v0.0.0-20220920183852-bf014ff85ad5 // indirect
|
golang.org/x/net v0.0.0-20220920183852-bf014ff85ad5 // indirect
|
||||||
|
|
4
go.sum
4
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-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 h1:2vVIBCtBih2w1K9ll8YnToTDZvbxcgbsClsPlJS/kkg=
|
||||||
github.com/waku-org/go-zerokit-rln v0.1.7-wakuorg/go.mod h1:GlyaVeEWNEBxVJrWC6jFTvb4LNb9d9qnjdS6EiWVUvk=
|
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.3 h1:BIecnRG0J0JlZmqcZTHphQ8yUeqqwkIaUAVk2JNK9VQ=
|
||||||
github.com/waku-org/noise v1.0.2/go.mod h1:emThr8WZLeAtKqFW+/nXfHn9VucuXTh8aHap03UXP84=
|
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 h1:lYbXeSvJi5zk5GLKVuid9TVjS9a0OmLIDKTfoZBL6Ow=
|
||||||
github.com/whyrusleeping/timecache v0.0.0-20160911033111-cfcb2f1abfee/go.mod h1:m2aV4LZI4Aez7dP5PMyVKEHhUyEJ/RjmPEDOpDvudHg=
|
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=
|
github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||||
|
|
|
@ -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[:]
|
||||||
|
}
|
|
@ -1,22 +1,28 @@
|
||||||
package noise
|
package noise
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"io"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
n "github.com/waku-org/noise"
|
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
|
// WakuNoiseProtocolID indicates the protocol ID defined according to https://rfc.vac.dev/spec/35/#specification
|
||||||
type WakuNoiseProtocolID = byte
|
type WakuNoiseProtocolID = byte
|
||||||
|
|
||||||
var (
|
var (
|
||||||
None = WakuNoiseProtocolID(0)
|
None = WakuNoiseProtocolID(0)
|
||||||
Noise_K1K1_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(10)
|
Noise_K1K1_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(10)
|
||||||
Noise_XK1_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(11)
|
Noise_XK1_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(11)
|
||||||
Noise_XX_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(12)
|
Noise_XX_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(12)
|
||||||
Noise_XXpsk0_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(13)
|
Noise_XXpsk0_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(13)
|
||||||
ChaChaPoly = WakuNoiseProtocolID(30)
|
Noise_WakuPairing_25519_ChaChaPoly_SHA256 = WakuNoiseProtocolID(14)
|
||||||
|
ChaChaPoly = WakuNoiseProtocolID(30)
|
||||||
)
|
)
|
||||||
|
|
||||||
const NoisePaddingBlockSize = 248
|
const NoisePaddingBlockSize = 248
|
||||||
|
@ -26,7 +32,7 @@ var ErrorHandshakeComplete = errors.New("handshake complete")
|
||||||
// All protocols share same cipher suite
|
// All protocols share same cipher suite
|
||||||
var cipherSuite = n.NewCipherSuite(n.DH25519, n.CipherChaChaPoly, n.HashSHA256)
|
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() {
|
defer func() {
|
||||||
if rerr := recover(); rerr != nil {
|
if rerr := recover(); rerr != nil {
|
||||||
err = fmt.Errorf("panic in Noise handshake: %s", rerr)
|
err = fmt.Errorf("panic in Noise handshake: %s", rerr)
|
||||||
|
@ -34,14 +40,15 @@ func newHandshakeState(pattern n.HandshakePattern, initiator bool, staticKeypair
|
||||||
}()
|
}()
|
||||||
|
|
||||||
cfg := n.Config{
|
cfg := n.Config{
|
||||||
CipherSuite: cipherSuite,
|
CipherSuite: cipherSuite,
|
||||||
Pattern: pattern,
|
Pattern: pattern,
|
||||||
Initiator: initiator,
|
Initiator: initiator,
|
||||||
StaticKeypair: staticKeypair,
|
StaticKeypair: staticKeypair,
|
||||||
Prologue: prologue,
|
EphemeralKeypair: ephemeralKeyPair,
|
||||||
PresharedKey: presharedKey,
|
Prologue: prologue,
|
||||||
PeerStatic: peerStatic,
|
PresharedKey: presharedKey,
|
||||||
PeerEphemeral: peerEphemeral,
|
PeerStatic: peerStatic,
|
||||||
|
PeerEphemeral: peerEphemeral,
|
||||||
}
|
}
|
||||||
|
|
||||||
return n.NewHandshakeState(cfg)
|
return n.NewHandshakeState(cfg)
|
||||||
|
@ -54,8 +61,10 @@ type Handshake struct {
|
||||||
|
|
||||||
hsBuff []byte
|
hsBuff []byte
|
||||||
|
|
||||||
enc *n.CipherState
|
enc *n.CipherState
|
||||||
dec *n.CipherState
|
dec *n.CipherState
|
||||||
|
nametagsInbound *MessageNametagBuffer
|
||||||
|
nametagsOutbound *MessageNametagBuffer
|
||||||
|
|
||||||
initiator bool
|
initiator bool
|
||||||
shouldWrite bool
|
shouldWrite bool
|
||||||
|
@ -77,19 +86,21 @@ func getHandshakePattern(protocol WakuNoiseProtocolID) (n.HandshakePattern, erro
|
||||||
return HandshakeXX, nil
|
return HandshakeXX, nil
|
||||||
case Noise_XXpsk0_25519_ChaChaPoly_SHA256:
|
case Noise_XXpsk0_25519_ChaChaPoly_SHA256:
|
||||||
return HandshakeXXpsk0, nil
|
return HandshakeXXpsk0, nil
|
||||||
|
case Noise_WakuPairing_25519_ChaChaPoly_SHA256:
|
||||||
|
return HandshakeWakuPairing, nil
|
||||||
default:
|
default:
|
||||||
return n.HandshakePattern{}, errors.New("unsupported handshake pattern")
|
return n.HandshakePattern{}, errors.New("unsupported handshake pattern")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandshake creates a new handshake using aa WakuNoiseProtocolID that is maped to a 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)
|
hsPattern, err := getHandshakePattern(protocolID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -103,11 +114,24 @@ func NewHandshake(protocolID WakuNoiseProtocolID, initiator bool, staticKeypair
|
||||||
}, nil
|
}, 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.
|
// 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
|
// 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.
|
// 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`
|
// TODO: this might be refactored into a separate `sendHandshakeMessage` and `receiveHandshakeMessage`.
|
||||||
func (hs *Handshake) Step(readPayloadV2 *PayloadV2, transportMessage []byte) (*HandshakeStepResult, error) {
|
// 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 {
|
if hs.enc != nil || hs.dec != nil {
|
||||||
return nil, ErrorHandshakeComplete
|
return nil, ErrorHandshakeComplete
|
||||||
}
|
}
|
||||||
|
@ -129,13 +153,22 @@ func (hs *Handshake) Step(readPayloadV2 *PayloadV2, transportMessage []byte) (*H
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
hs.shouldWrite = false
|
hs.shouldWrite = false
|
||||||
|
|
||||||
|
if messageNametag != nil {
|
||||||
|
result.Payload2.MessageNametag = *messageNametag
|
||||||
|
}
|
||||||
|
|
||||||
result.Payload2.TransportMessage = msg
|
result.Payload2.TransportMessage = msg
|
||||||
for _, npk := range noisePubKeys {
|
for _, npk := range noisePubKeys {
|
||||||
result.Payload2.HandshakeMessage = append(result.Payload2.HandshakeMessage, byteToNoisePublicKey(npk))
|
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")
|
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
|
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)
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -167,7 +210,10 @@ func (hs *Handshake) Step(readPayloadV2 *PayloadV2, transportMessage []byte) (*H
|
||||||
}
|
}
|
||||||
|
|
||||||
if cs1 != nil && cs2 != nil {
|
if cs1 != nil && cs2 != nil {
|
||||||
hs.setCipherStates(cs1, cs2)
|
err = hs.setCipherStates(cs1, cs2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &result, nil
|
return &result, nil
|
||||||
|
@ -179,18 +225,36 @@ func (hs *Handshake) HandshakeComplete() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is called when the final handshake message is processed
|
// 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 {
|
if hs.initiator {
|
||||||
hs.enc = cs1
|
hs.enc = cs1
|
||||||
hs.dec = cs2
|
hs.dec = cs2
|
||||||
|
// and nametags secrets
|
||||||
|
hs.nametagsInbound = NewMessageNametagBuffer(nms1)
|
||||||
|
hs.nametagsOutbound = NewMessageNametagBuffer(nms2)
|
||||||
} else {
|
} else {
|
||||||
hs.enc = cs2
|
hs.enc = cs2
|
||||||
hs.dec = cs1
|
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
|
// 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 {
|
if hs.enc == nil {
|
||||||
return nil, errors.New("cannot encrypt, handshake incomplete")
|
return nil, errors.New("cannot encrypt, handshake incomplete")
|
||||||
}
|
}
|
||||||
|
@ -204,7 +268,15 @@ func (hs *Handshake) Encrypt(plaintext []byte) (*PayloadV2, error) {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -214,11 +286,12 @@ func (hs *Handshake) Encrypt(plaintext []byte) (*PayloadV2, error) {
|
||||||
return &PayloadV2{
|
return &PayloadV2{
|
||||||
ProtocolId: None,
|
ProtocolId: None,
|
||||||
TransportMessage: cyphertext,
|
TransportMessage: cyphertext,
|
||||||
|
MessageNametag: messageNametag,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decrypt calls the cipher's decryption. It decrypts the provided payload and returns the message in plaintext
|
// 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 {
|
if hs.dec == nil {
|
||||||
return nil, errors.New("cannot decrypt, handshake incomplete")
|
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")
|
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 {
|
if len(payload.TransportMessage) == 0 {
|
||||||
return nil, errors.New("tried to decrypt empty ciphertext")
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
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
|
// 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) {
|
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
|
// 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
|
// 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) {
|
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
|
// 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
|
// are exchanged. This handshake is useful in case the initiator needs to instantiate a new separate encrypted communication
|
||||||
// channel with the receiver
|
// channel with the receiver
|
||||||
func NewHandshake_K1K1_25519_ChaChaPoly_SHA256(staticKeypair n.DHKey, initiator bool, peerStaticKey []byte, prologue []byte) (*Handshake, error) {
|
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,
|
// 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 {
|
if !initiator && len(peerStaticKey) != 0 {
|
||||||
return nil, errors.New("recipient shouldnt know initiator key")
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
// By being the handshake initiator, Alice writes a Waku2 payload v2 containing her handshake message
|
||||||
// and the (encrypted) transport message
|
// and the (encrypted) transport message
|
||||||
sentTransportMessage := generateRandomBytes(t, 32)
|
sentTransportMessage := generateRandomBytes(t, 32)
|
||||||
aliceStep, err := hsAlice.Step(nil, sentTransportMessage)
|
aliceStep, err := hsAlice.Step(nil, sentTransportMessage, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// check:
|
// check:
|
||||||
|
@ -64,11 +64,11 @@ func handshakeTest(t *testing.T, hsAlice *Handshake, hsBob *Handshake) {
|
||||||
|
|
||||||
// At this step, Bob writes and returns a payload
|
// At this step, Bob writes and returns a payload
|
||||||
sentTransportMessage = generateRandomBytes(t, 32)
|
sentTransportMessage = generateRandomBytes(t, 32)
|
||||||
bobStep, err = hsBob.Step(nil, sentTransportMessage)
|
bobStep, err = hsBob.Step(nil, sentTransportMessage, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// While Alice reads and returns the (decrypted) transport message
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// check:
|
// 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
|
// Similarly as in first step, Alice writes a Waku2 payload containing the handshake message and the (encrypted) transport message
|
||||||
sentTransportMessage = generateRandomBytes(t, 32)
|
sentTransportMessage = generateRandomBytes(t, 32)
|
||||||
aliceStep, err = hsAlice.Step(nil, sentTransportMessage)
|
aliceStep, err = hsAlice.Step(nil, sentTransportMessage, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
|
// 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)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// check:
|
// check:
|
||||||
|
@ -95,10 +95,10 @@ func handshakeTest(t *testing.T, hsAlice *Handshake, hsBob *Handshake) {
|
||||||
require.True(t, hsAlice.HandshakeComplete())
|
require.True(t, hsAlice.HandshakeComplete())
|
||||||
require.True(t, hsBob.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)
|
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)
|
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
|
// We test read/write of random messages exchanged between Alice and Bob
|
||||||
|
|
||||||
|
defaultMessageNametagBuffer := NewMessageNametagBuffer(nil)
|
||||||
|
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
// Alice writes to Bob
|
// Alice writes to Bob
|
||||||
message := generateRandomBytes(t, 32)
|
message := generateRandomBytes(t, 32)
|
||||||
|
|
||||||
encryptedPayload, err := hsAlice.Encrypt(message)
|
encryptedPayload, err := hsAlice.Encrypt(message, defaultMessageNametagBuffer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
plaintext, err := hsBob.Decrypt(encryptedPayload)
|
plaintext, err := hsBob.Decrypt(encryptedPayload, defaultMessageNametagBuffer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, message, plaintext)
|
require.Equal(t, message, plaintext)
|
||||||
|
@ -122,10 +124,10 @@ func handshakeTest(t *testing.T, hsAlice *Handshake, hsBob *Handshake) {
|
||||||
// Bob writes to Alice
|
// Bob writes to Alice
|
||||||
message = generateRandomBytes(t, 32)
|
message = generateRandomBytes(t, 32)
|
||||||
|
|
||||||
encryptedPayload, err = hsBob.Encrypt(message)
|
encryptedPayload, err = hsBob.Encrypt(message, defaultMessageNametagBuffer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
plaintext, err = hsAlice.Decrypt(encryptedPayload)
|
plaintext, err = hsAlice.Decrypt(encryptedPayload, defaultMessageNametagBuffer)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
require.Equal(t, message, plaintext)
|
require.Equal(t, message, plaintext)
|
||||||
|
|
|
@ -75,3 +75,13 @@ var HandshakeXXpsk0 = n.HandshakePattern{
|
||||||
{n.MessagePatternS, n.MessagePatternDHSE},
|
{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},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
|
@ -116,6 +116,7 @@ type PayloadV2 struct {
|
||||||
ProtocolId byte
|
ProtocolId byte
|
||||||
HandshakeMessage []*NoisePublicKey
|
HandshakeMessage []*NoisePublicKey
|
||||||
TransportMessage []byte
|
TransportMessage []byte
|
||||||
|
MessageNametag MessageNametag
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks equality between two PayloadsV2 objects
|
// Checks equality between two PayloadsV2 objects
|
||||||
|
@ -165,7 +166,8 @@ func (p *PayloadV2) Serialize() ([]byte, error) {
|
||||||
// payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
|
// payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
|
||||||
|
|
||||||
// We declare it as a byte sequence of length accordingly to the PayloadV2 information read
|
// 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
|
1+ // 1 byte for length of serializedHandshakeMessage field
|
||||||
serializedHandshakeMessageLen+ // serializedHandshakeMessageLen bytes for serializedHandshakeMessage
|
serializedHandshakeMessageLen+ // serializedHandshakeMessageLen bytes for serializedHandshakeMessage
|
||||||
8+ // 8 bytes for transportMessageLen
|
8+ // 8 bytes for transportMessageLen
|
||||||
|
@ -174,6 +176,10 @@ func (p *PayloadV2) Serialize() ([]byte, error) {
|
||||||
|
|
||||||
payloadBuf := bytes.NewBuffer(payload)
|
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
|
// 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 {
|
if err := payloadBuf.WriteByte(p.ProtocolId); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -215,7 +221,12 @@ func DeserializePayloadV2(payload []byte) (*PayloadV2, error) {
|
||||||
|
|
||||||
result := &PayloadV2{}
|
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
|
// 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 {
|
if err := binary.Read(payloadBuf, binary.BigEndian, &result.ProtocolId); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue