feat: initial commit

This commit is contained in:
Richard Ramos 2022-12-20 21:33:09 -04:00
commit aedffc51a8
No known key found for this signature in database
GPG Key ID: BD36D48BC9FFC88C
17 changed files with 2681 additions and 0 deletions

205
LICENSE-APACHEv2 Normal file
View File

@ -0,0 +1,205 @@
nim-waku is licensed under the Apache License version 2
Copyright (c) 2018 Status Research & Development GmbH
-----------------------------------------------------
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2018 Status Research & Development GmbH
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

25
LICENSE-MIT Normal file
View File

@ -0,0 +1,25 @@
nim-waku is licensed under the MIT License
Copyright (c) 2018 Status Research & Development GmbH
-----------------------------------------------------
The MIT License (MIT)
Copyright (c) 2018 Status Research & Development GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

4
README.md Normal file
View File

@ -0,0 +1,4 @@
# go-noise
A Go implementation for Waku Noise ([35/WAKU2-NOISE](https://rfc.vac.dev/spec/35/), [37/WAKU2-NOISE-SESSIONS](https://rfc.vac.dev/spec/37/), [43/WAKU2-NOISE-PAIRING](https://rfc.vac.dev/spec/43/)).

46
cipher.go Normal file
View File

@ -0,0 +1,46 @@
package noise
import (
"bytes"
"hash"
"io"
"golang.org/x/crypto/hkdf"
)
type DHKey interface {
GenerateKeypair() (Keypair, error)
DH(privkey, pubkey []byte) ([]byte, error)
DHLen() int
}
type Keypair struct {
Private []byte
Public []byte
}
func (k Keypair) IsDefault() bool {
return k.Equals(Keypair{})
}
func (k Keypair) Equals(b Keypair) bool {
return bytes.Equal(k.Private, b.Private) && bytes.Equal(k.Public, b.Public)
}
func getHKDF(h func() hash.Hash, ck []byte, ikm []byte, numBytes int) []byte {
hkdf := hkdf.New(h, ikm, ck, nil)
result := make([]byte, numBytes)
_, _ = io.ReadFull(hkdf, result)
return result
}
// CommitPublicKey commits a public key pk for randomness r as H(pk || s)
func CommitPublicKey(h func() hash.Hash, publicKey []byte, r []byte) []byte {
input := []byte{}
input = append(input, []byte(publicKey)...)
input = append(input, r...)
hash := h()
hash.Write(input)
return hash.Sum(nil)
}

36
curve25519.go Normal file
View File

@ -0,0 +1,36 @@
package noise
import (
"crypto/rand"
"io"
"golang.org/x/crypto/curve25519"
)
type dh25519 struct {
DHKey
}
func (d dh25519) GenerateKeypair() (Keypair, error) {
privkey := make([]byte, DH25519.DHLen())
if _, err := io.ReadFull(rand.Reader, privkey); err != nil {
return Keypair{}, err
}
return d.GenerateKeyPairFromPrivateKey(privkey)
}
func (d dh25519) DH(privkey, pubkey []byte) ([]byte, error) {
return curve25519.X25519(privkey, pubkey)
}
func (d dh25519) DHLen() int { return 32 }
func (d dh25519) GenerateKeyPairFromPrivateKey(privkey []byte) (Keypair, error) {
pubkey, err := curve25519.X25519(privkey, curve25519.Basepoint)
if err != nil {
return Keypair{}, err
}
return Keypair{Private: privkey, Public: pubkey}, nil
}
var DH25519 = dh25519{}

15
go.mod Normal file
View File

@ -0,0 +1,15 @@
module github.com/waku-org/go-noise
go 1.18
require (
github.com/stretchr/testify v1.8.1
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

25
go.sum Normal file
View File

@ -0,0 +1,25 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

289
handshake.go Normal file
View File

@ -0,0 +1,289 @@
package noise
import (
"bytes"
"errors"
"fmt"
"math/big"
)
// Noise state machine
var ErrUnexpectedMessageNametag = errors.New("the message nametag of the read message doesn't match the expected one")
var ErrHandshakeComplete = errors.New("handshake complete")
// While processing messages patterns, users either:
// - read (decrypt) the other party's (encrypted) transport message
// - write (encrypt) a message, sent through a PayloadV2
// These two intermediate results are stored in the HandshakeStepResult data structure
// HandshakeStepResult stores the intermediate result of processing messages patterns
type HandshakeStepResult struct {
PayloadV2 *PayloadV2
TransportMessage []byte
}
// When a handshake is complete, the HandshakeResult will contain the two
// Cipher States used to encrypt/decrypt outbound/inbound messages
// The recipient static key rs and handshake hash values h are stored to address some possible future applications (channel-binding, session management, etc.).
// However, are not required by Noise specifications and are thus optional
type HandshakeResult struct {
csOutbound *CipherState
csInbound *CipherState
// Optional fields:
nametagsInbound MessageNametagBuffer
nametagsOutbound MessageNametagBuffer
rs []byte
h []byte
}
func NewHandshakeResult(csOutbound *CipherState, csInbound *CipherState) *HandshakeResult {
return &HandshakeResult{
csInbound: csInbound,
csOutbound: csOutbound,
}
}
// Noise specification, Section 5:
// Transport messages are then encrypted and decrypted by calling EncryptWithAd()
// and DecryptWithAd() on the relevant CipherState with zero-length associated data.
// If DecryptWithAd() signals an error due to DECRYPT() failure, then the input message is discarded.
// The application may choose to delete the CipherState and terminate the session on such an error,
// or may continue to attempt communications. If EncryptWithAd() or DecryptWithAd() signal an error
// due to nonce exhaustion, then the application must delete the CipherState and terminate the session.
// Writes an encrypted message using the proper Cipher State
func (hr *HandshakeResult) WriteMessage(transportMessage []byte, outboundMessageNametagBuffer *MessageNametagBuffer) (*PayloadV2, error) {
payload2 := &PayloadV2{}
// We set the message nametag using the input buffer
if outboundMessageNametagBuffer != nil {
payload2.MessageNametag = outboundMessageNametagBuffer.Pop()
} else {
payload2.MessageNametag = hr.nametagsOutbound.Pop()
}
// According to 35/WAKU2-NOISE RFC, no Handshake protocol information is sent when exchanging messages
// This correspond to setting protocol-id to 0
payload2.ProtocolId = 0
// We pad the transport message
paddedTransportMessage, err := PKCS7_Pad(transportMessage, NoisePaddingBlockSize)
if err != nil {
return nil, err
}
// Encryption is done with zero-length associated data as per specification
transportMessage, err = hr.csOutbound.encryptWithAd(payload2.MessageNametag[:], paddedTransportMessage)
if err != nil {
return nil, err
}
payload2.TransportMessage = transportMessage
return payload2, nil
}
// Reads an encrypted message using the proper Cipher State
// Decryption is attempted only if the input PayloadV2 has a messageNametag equal to the one expected
func (hr *HandshakeResult) ReadMessage(readPayload2 *PayloadV2, inboundMessageNametagBuffer *MessageNametagBuffer) ([]byte, error) {
// The output decrypted message
var message []byte
// 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 inboundMessageNametagBuffer != nil {
err := inboundMessageNametagBuffer.CheckNametag(readPayload2.MessageNametag)
if err != nil {
return nil, err
}
} else {
err := hr.nametagsInbound.CheckNametag(readPayload2.MessageNametag)
if err != nil {
return nil, err
}
}
// At this point the messageNametag matches the expected nametag.
// According to 35/WAKU2-NOISE RFC, no Handshake protocol information is sent when exchanging messages
if readPayload2.ProtocolId == 0 {
// Decryption is done with messageNametag as associated data
paddedMessage, err := hr.csInbound.decryptWithAd(readPayload2.MessageNametag[:], readPayload2.TransportMessage)
if err != nil {
return nil, err
}
// We unpad the decrypted message
message, err = PKCS7_Unpad(paddedMessage, NoisePaddingBlockSize)
if err != nil {
return nil, err
}
// The message successfully decrypted, we can delete the first element of the inbound Message Nametag Buffer
hr.nametagsInbound.Delete(1)
}
return message, nil
}
type Handshake struct {
hs *HandshakeState
hsResult *HandshakeResult
}
func NewHandshake(hsPattern HandshakePattern, staticKey Keypair, ephemeralKey Keypair, prologue []byte, psk []byte, preMessagePKs []*NoisePublicKey, initiator bool) (*Handshake, error) {
result := &Handshake{}
result.hs = NewHandshakeState(hsPattern, psk)
result.hs.ss.mixHash(prologue)
result.hs.e = ephemeralKey
result.hs.s = staticKey
result.hs.psk = psk
result.hs.msgPatternIdx = 0
result.hs.initiator = initiator
// We process any eventual handshake pre-message pattern by processing pre-message public keys
err := result.hs.processPreMessagePatternTokens(preMessagePKs)
if err != nil {
return nil, err
}
return result, nil
}
func (h *Handshake) Equals(b *Handshake) bool {
return h.hs.Equals(*b.hs)
}
// 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 := getHKDF(hs.hs.handshakePattern.hashFn, hs.hs.ss.h, nil, 16)
return BytesToMessageNametag(output), nil
}
// Generates an 8 decimal digits authorization code using HKDF and the handshake state
func (h *Handshake) Authcode() (string, error) {
output0 := getHKDF(h.hs.handshakePattern.hashFn, h.hs.ss.h, nil, 8)
bn := new(big.Int)
bn.SetBytes(output0)
code := new(big.Int)
code.Mod(bn, big.NewInt(100_000_000))
return fmt.Sprintf("'%08s'", code.String()), nil
}
// Advances 1 step in handshake
// Each user in a handshake alternates writing and reading of handshake messages.
// If the user is writing the handshake message, the transport message (if not empty) and eventually a non-empty message nametag has to be passed to transportMessage and messageNametag 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. Decryption is skipped if the PayloadV2 read doesn't have a message nametag equal to messageNametag (empty input nametags are converted to all-0 MessageNametagLength bytes arrays)
func (h *Handshake) Step(readPayloadV2 *PayloadV2, transportMessage []byte, messageNametag MessageNametag) (*HandshakeStepResult, error) {
hsStepResult := &HandshakeStepResult{}
if h.IsComplete() {
return nil, ErrHandshakeComplete
}
// We process the next handshake message pattern
// We get if the user is reading or writing the input handshake message
direction := h.hs.handshakePattern.messagePatterns[h.hs.msgPatternIdx].direction
reading, writing := h.hs.getReadingWritingState(direction)
var err error
if writing { // If we write an answer at this handshake step
hsStepResult.PayloadV2 = &PayloadV2{}
hsStepResult.PayloadV2.ProtocolId = h.hs.handshakePattern.protocolID
// We set the messageNametag and the handshake and transport messages
hsStepResult.PayloadV2.MessageNametag = messageNametag
hsStepResult.PayloadV2.HandshakeMessage, err = h.hs.processMessagePatternTokens(nil)
if err != nil {
return nil, err
}
// We write the payload by passing the messageNametag as extra additional data
hsStepResult.PayloadV2.TransportMessage, err = h.hs.processMessagePatternPayload(transportMessage, hsStepResult.PayloadV2.MessageNametag[:])
if err != nil {
return nil, err
}
} else if reading { // If we read an answer during this handshake step
// If the read message nametag doesn't match the expected input one we raise an error
expectedNametag := messageNametag
if !bytes.Equal(readPayloadV2.MessageNametag[:], expectedNametag[:]) {
return nil, ErrUnexpectedMessageNametag
}
// We process the read public keys and (eventually decrypt) the read transport message
// Since we only read, nothing meaningful (i.e. public keys) is returned
_, err := h.hs.processMessagePatternTokens(readPayloadV2.HandshakeMessage)
if err != nil {
return nil, err
}
// We retrieve and store the (decrypted) received transport message by passing the messageNametag as extra additional data
hsStepResult.TransportMessage, err = h.hs.processMessagePatternPayload(readPayloadV2.TransportMessage, readPayloadV2.MessageNametag[:])
if err != nil {
return nil, err
}
} else {
return nil, errors.New("handshake Error: neither writing or reading user")
}
// We increase the handshake state message pattern index to progress to next step
h.hs.msgPatternIdx += 1
return hsStepResult, nil
}
// Finalizes the handshake by calling Split and assigning the proper Cipher States to users
func (h *Handshake) FinalizeHandshake() (*HandshakeResult, error) {
if h.IsComplete() {
return h.hsResult, nil
}
var hsResult *HandshakeResult
// Noise specification, Section 5:
// Processing the final handshake message returns two CipherState objects,
// the first for encrypting transport messages from initiator to responder,
// and the second for messages in the other direction.
// We call Split()
cs1, cs2 := h.hs.ss.split()
// Optional: We derive a secret for the nametag derivation
nms1, nms2 := h.hs.genMessageNametagSecrets()
// We assign the proper Cipher States
if h.hs.initiator {
hsResult = NewHandshakeResult(cs1, cs2)
// and nametags secrets
hsResult.nametagsInbound.secret = nms1
hsResult.nametagsOutbound.secret = nms2
} else {
hsResult = NewHandshakeResult(cs2, cs1)
// and nametags secrets
hsResult.nametagsInbound.secret = nms2
hsResult.nametagsOutbound.secret = nms1
}
// We initialize the message nametags inbound/outbound buffers
hsResult.nametagsInbound.Init()
hsResult.nametagsOutbound.Init()
if len(h.hs.rs) == 0 {
return nil, errors.New("invalid handshake state")
}
// We store the optional fields rs and h
copy(hsResult.rs[:], h.hs.rs)
copy(hsResult.h[:], h.hs.ss.h)
h.hsResult = hsResult
return hsResult, nil
}
// HandshakeComplete indicates whether the handshake process is complete or not
func (hs *Handshake) IsComplete() bool {
return hs.hsResult != nil
}

543
handshake_state.go Normal file
View File

@ -0,0 +1,543 @@
package noise
import (
"bytes"
"errors"
"strings"
)
// The padding blocksize of a transport message
const NoisePaddingBlockSize = 248
// The Handshake State as in https://noiseprotocol.org/noise.html#the-handshakestate-object
// Contains
// - the local and remote ephemeral/static keys e,s,re,rs (if any)
// - the initiator flag (true if the user creating the state is the handshake initiator, false otherwise)
// - the handshakePattern (containing the handshake protocol name, and (pre)message patterns)
// This object is further extended from specifications by storing:
// - a message pattern index msgPatternIdx indicating the next handshake message pattern to process
// - the user's preshared psk, if any
type HandshakeState struct {
s Keypair
e Keypair
rs []byte
re []byte
ss *SymmetricState
initiator bool
handshakePattern HandshakePattern
msgPatternIdx int
psk []byte
}
func NewHandshakeState(hsPattern HandshakePattern, psk []byte) *HandshakeState {
return &HandshakeState{
// By default the Handshake State initiator flag is set to false
// Will be set to true when the user associated to the handshake state starts an handshake
initiator: false,
handshakePattern: hsPattern,
psk: psk,
ss: NewSymmetricState(hsPattern),
msgPatternIdx: 0,
}
}
func (h *HandshakeState) Equals(b HandshakeState) bool {
if !bytes.Equal(h.s.Private, b.s.Private) {
return false
}
if !bytes.Equal(h.s.Public, b.s.Public) {
return false
}
if !bytes.Equal(h.e.Private, b.e.Private) {
return false
}
if !bytes.Equal(h.e.Public, b.e.Public) {
return false
}
if !bytes.Equal(h.rs, b.rs) {
return false
}
if !bytes.Equal(h.re, b.re) {
return false
}
if !h.ss.Equals(b.ss) {
return false
}
if h.initiator != b.initiator {
return false
}
if !h.handshakePattern.Equals(b.handshakePattern) {
return false
}
if h.msgPatternIdx != b.msgPatternIdx {
return false
}
if !bytes.Equal(h.psk, b.psk) {
return false
}
return true
}
func (h *HandshakeState) genMessageNametagSecrets() (nms1 []byte, nms2 []byte) {
keyLen := h.handshakePattern.hashFn().Size()
output := getHKDF(h.handshakePattern.hashFn, h.ss.h, []byte{}, keyLen*2)
nms1 = output[:keyLen]
nms2 = output[keyLen:]
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 (h *HandshakeState) MessageNametag() MessageNametag {
output := getHKDF(h.handshakePattern.hashFn, h.ss.h, []byte{}, MessageNametagLength)
return BytesToMessageNametag(output)
}
// Handshake Processing
// Based on the message handshake direction and if the user is or not the initiator, returns a boolean tuple telling if the user
// has to read or write the next handshake message
func (h *HandshakeState) getReadingWritingState(direction MessageDirection) (reading bool, writing bool) {
if h.initiator && direction == Right {
// I'm Alice and direction is ->
writing = true
} else if h.initiator && direction == Left {
// I'm Alice and direction is <-
reading = true
} else if !h.initiator && direction == Right {
// I'm Bob and direction is ->
reading = true
} else if !h.initiator && direction == Left {
// I'm Bob and direction is <-
writing = true
}
return reading, writing
}
// Checks if a pre-message is valid according to Noise specifications
// http://www.noiseprotocol.org/noise.html#handshake-patterns
func (h *HandshakeState) isValid(msg []PreMessagePattern) bool {
// Non-empty pre-messages can only have patterns "e", "s", "e,s" in each direction
allowedPatterns := []PreMessagePattern{
NewPreMessagePattern(Right, []NoiseTokens{S}),
NewPreMessagePattern(Right, []NoiseTokens{E}),
NewPreMessagePattern(Right, []NoiseTokens{E, S}),
NewPreMessagePattern(Left, []NoiseTokens{S}),
NewPreMessagePattern(Left, []NoiseTokens{E}),
NewPreMessagePattern(Left, []NoiseTokens{E, S}),
}
// We check if pre message patterns are allowed
for _, p := range msg {
found := false
for _, allowed := range allowedPatterns {
if allowed.Equals(p) {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// Handshake messages processing procedures
// Processes pre-message patterns
func (h *HandshakeState) processPreMessagePatternTokens(inPreMessagePKs []*NoisePublicKey) error {
// I make a copy of the input pre-message public keys, so that I can easily delete processed ones without using iterators/counters
preMessagePKs := append([]*NoisePublicKey(nil), inPreMessagePKs...)
// Here we store currently processed pre message public key
var currPK *NoisePublicKey
// We retrieve the pre-message patterns to process, if any
// If none, there's nothing to do
if len(h.handshakePattern.premessagePatterns) == 0 {
return nil
}
// If not empty, we check that pre-message is valid according to Noise specifications
if !h.isValid(h.handshakePattern.premessagePatterns) {
return errors.New("invalid pre-message in handshake")
}
// We iterate over each pattern contained in the pre-message
for _, messagePattern := range h.handshakePattern.premessagePatterns {
direction := messagePattern.direction
tokens := messagePattern.tokens
// We get if the user is reading or writing the current pre-message pattern
reading, writing := h.getReadingWritingState(direction)
// We process each message pattern token
for _, token := range tokens {
// We process the pattern token
switch token {
case E:
// We expect an ephemeral key, so we attempt to read it (next PK to process will always be at index 0 of preMessagePKs)
if len(preMessagePKs) > 0 {
currPK = preMessagePKs[0]
} else {
return errors.New("noise pre-message read e, expected a public key")
}
// If user is reading the "e" token
if reading {
// We check if current key is encrypted or not. We assume pre-message public keys are all unencrypted on users' end
if currPK.Flag == 0 {
// Sets re and calls MixHash(re.public_key).
h.re = currPK.Public
h.ss.mixHash(h.re)
} else {
return errors.New("noise read e, incorrect encryption flag for pre-message public key")
}
// If user is writing the "e" token
} else if writing {
// When writing, the user is sending a public key,
// We check that the public part corresponds to the set local key and we call MixHash(e.public_key).
if bytes.Equal(h.e.Public, currPK.Public) {
h.ss.mixHash(h.e.Public)
} else {
return errors.New("noise pre-message e key doesn't correspond to locally set e key pair")
}
}
// Noise specification: section 9.2
// In non-PSK handshakes, the "e" token in a pre-message pattern or message pattern always results
// in a call to MixHash(e.public_key).
// In a PSK handshake, all of these calls are followed by MixKey(e.public_key).
if strings.Contains(h.handshakePattern.name, string(PSK)) {
h.ss.mixKey(currPK.Public)
}
// We delete processed public key
preMessagePKs = preMessagePKs[1:]
case S:
// We expect a static key, so we attempt to read it (next PK to process will always be at index of preMessagePKs)
if len(preMessagePKs) > 0 {
currPK = preMessagePKs[0]
} else {
return errors.New("noise pre-message read s, expected a public key")
}
// If user is reading the "s" token
if reading {
// We check if current key is encrypted or not. We assume pre-message public keys are all unencrypted on users' end
if currPK.Flag == 0 {
// Sets rs and calls MixHash(rs.public_key).
h.rs = currPK.Public
h.ss.mixHash(h.rs)
} else {
return errors.New("noise read s, incorrect encryption flag for pre-message public key")
}
// If user is writing the "s" token
} else if writing {
// If writing, it means that the user is sending a public key,
// We check that the public part corresponds to the set local key and we call MixHash(s.public_key).
if bytes.Equal(h.s.Public, currPK.Public) {
h.ss.mixHash(h.s.Public)
} else {
return errors.New("noise pre-message s key doesn't correspond to locally set s key pair")
}
}
// Noise specification: section 9.2
// In non-PSK handshakes, the "e" token in a pre-message pattern or message pattern always results
// in a call to MixHash(e.public_key).
// In a PSK handshake, all of these calls are followed by MixKey(e.public_key).
if strings.Contains(h.handshakePattern.name, string(PSK)) {
h.ss.mixKey(currPK.Public)
}
// We delete processed public key
preMessagePKs = preMessagePKs[1:]
default:
return errors.New("invalid Token for pre-message pattern")
}
}
}
return nil
}
// This procedure encrypts/decrypts the implicit payload attached at the end of every message pattern
// An optional extraAd to pass extra additional data in encryption/decryption can be set (useful to authenticate messageNametag)
func (h *HandshakeState) processMessagePatternPayload(transportMessage []byte, extraAd []byte) ([]byte, error) {
var payload []byte
var err error
// We retrieve current message pattern (direction + tokens) to process
direction := h.handshakePattern.messagePatterns[h.msgPatternIdx].direction
// We get if the user is reading or writing the input handshake message
reading, writing := h.getReadingWritingState(direction)
// We decrypt the transportMessage, if any
if reading {
payload, err = h.ss.decryptAndHash(transportMessage, extraAd)
if err != nil {
return nil, err
}
payload, err = PKCS7_Unpad(payload, NoisePaddingBlockSize)
if err != nil {
return nil, err
}
} else if writing {
payload, err = PKCS7_Pad(transportMessage, NoisePaddingBlockSize)
if err != nil {
return nil, err
}
payload, err = h.ss.encryptAndHash(payload, extraAd)
if err != nil {
return nil, err
}
} else {
return nil, errors.New("undefined state")
}
return payload, nil
}
// We process an input handshake message according to current handshake state and we return the next handshake step's handshake message
func (h *HandshakeState) processMessagePatternTokens(inputHandshakeMessage []*NoisePublicKey) ([]*NoisePublicKey, error) {
// We retrieve current message pattern (direction + tokens) to process
messagePattern := h.handshakePattern.messagePatterns[h.msgPatternIdx]
direction := messagePattern.direction
tokens := messagePattern.tokens
// We get if the user is reading or writing the input handshake message
reading, writing := h.getReadingWritingState(direction)
// I make a copy of the handshake message so that I can easily delete processed PKs without using iterators/counters
// (Possibly) non-empty if reading
inHandshakeMessage := append([]*NoisePublicKey(nil), inputHandshakeMessage...)
// The party's output public keys
// (Possibly) non-empty if writing
var outHandshakeMessage []*NoisePublicKey
// In currPK we store the currently processed public key from the handshake message
var currPK *NoisePublicKey
// We process each message pattern token
for _, token := range tokens {
switch token {
case E:
// If user is reading the "s" token
if reading {
// We expect an ephemeral key, so we attempt to read it (next PK to process will always be at index 0 of preMessagePKs)
if len(inHandshakeMessage) > 0 {
currPK = inHandshakeMessage[0]
} else {
return nil, errors.New("noise read e, expected a public key")
}
// We check if current key is encrypted or not
// Note: by specification, ephemeral keys should always be unencrypted. But we support encrypted ones.
if currPK.Flag == 0 {
// Unencrypted Public Key
// Sets re and calls MixHash(re.public_key).
h.re = currPK.Public
h.ss.mixHash(h.re)
// The following is out of specification: we call decryptAndHash for encrypted ephemeral keys, similarly as happens for (encrypted) static keys
} else if currPK.Flag == 1 {
// Encrypted public key
// Decrypts re, sets re and calls MixHash(re.public_key).
decRe, err := h.ss.decryptAndHash(currPK.Public, nil)
if err != nil {
return nil, err
}
h.re = decRe
} else {
return nil, errors.New("noise read e, incorrect encryption flag for public key")
}
// Noise specification: section 9.2
// In non-PSK handshakes, the "e" token in a pre-message pattern or message pattern always results
// in a call to MixHash(e.public_key).
// In a PSK handshake, all of these calls are followed by MixKey(e.public_key).
if strings.Contains(h.handshakePattern.name, string(PSK)) {
h.ss.mixKey(h.re)
}
// We delete processed public key
inHandshakeMessage = inHandshakeMessage[1:]
// If user is writing the "e" token
} else if writing {
// We generate a new ephemeral keypair
e, err := DH25519.GenerateKeypair()
if err != nil {
return nil, err
}
h.e = e
// We update the state
h.ss.mixHash(h.e.Public)
// Noise specification: section 9.2
// In non-PSK handshakes, the "e" token in a pre-message pattern or message pattern always results
// in a call to MixHash(e.public_key).
// In a PSK handshake, all of these calls are followed by MixKey(e.public_key).
if strings.Contains(h.handshakePattern.name, string(PSK)) {
h.ss.mixKey(h.e.Public)
}
// We add the ephemeral public key to the Waku payload
outHandshakeMessage = append(outHandshakeMessage, byteToNoisePublicKey(h.handshakePattern.dhKey, h.e.Public))
}
case S:
// If user is reading the "s" token
if reading {
// We expect a static key, so we attempt to read it (next PK to process will always be at index 0 of preMessagePKs)
if len(inHandshakeMessage) > 0 {
currPK = inHandshakeMessage[0]
} else {
return nil, errors.New("noise read s, expected a public key")
}
// We check if current key is encrypted or not
if currPK.Flag == 0 {
// Unencrypted Public Key
// Sets re and calls MixHash(re.public_key).
h.rs = currPK.Public
h.ss.mixHash(h.rs)
} else if currPK.Flag == 1 {
// Encrypted public key
// Decrypts rs, sets rs and calls MixHash(rs.public_key).
decRS, err := h.ss.decryptAndHash(currPK.Public, nil)
if err != nil {
return nil, err
}
h.rs = decRS
} else {
return nil, errors.New("noise read s, incorrect encryption flag for public key")
}
// We delete processed public key
inHandshakeMessage = inHandshakeMessage[1:]
// If user is writing the "s" token
} else if writing {
// If the local static key is not set (the handshake state was not properly initialized), we raise an error
if h.s.IsDefault() {
return nil, errors.New("static key not set")
}
// We encrypt the public part of the static key in case a key is set in the Cipher State
// That is, encS may either be an encrypted or unencrypted static key.
encS, err := h.ss.encryptAndHash(h.s.Public, nil)
if err != nil {
return nil, err
}
// We add the (encrypted) static public key to the Waku payload
// Note that encS = (Enc(s) || tag) if encryption key is set, otherwise encS = s.
// We distinguish these two cases by checking length of encryption and we set the proper encryption flag
if len(encS) > h.handshakePattern.dhKey.DHLen() {
outHandshakeMessage = append(outHandshakeMessage, byteToNoisePublicKey(h.handshakePattern.dhKey, encS))
} else {
outHandshakeMessage = append(outHandshakeMessage, byteToNoisePublicKey(h.handshakePattern.dhKey, encS))
}
}
case PSK:
// If user is reading the "psk" token
// Calls MixKeyAndHash(psk)
h.ss.mixKeyAndHash(h.psk)
case EE:
// If user is reading the "ee" token
// If local and/or remote ephemeral keys are not set, we raise an error
if h.e.IsDefault() || len(h.re) == 0 {
return nil, errors.New("local or remote ephemeral key not set")
}
// Calls MixKey(DH(e, re)).
k, err := h.handshakePattern.dhKey.DH(h.e.Private, h.re)
if err != nil {
return nil, err
}
h.ss.mixKey(k)
case ES:
// If user is reading the "es" token
// We check if keys are correctly set.
// If both present, we call MixKey(DH(e, rs)) if initiator, MixKey(DH(s, re)) if responder.
if h.initiator {
if h.e.IsDefault() || len(h.rs) == 0 {
return nil, errors.New("local or remote ephemeral/static key not set")
}
k, err := h.handshakePattern.dhKey.DH(h.e.Private, h.rs)
if err != nil {
return nil, err
}
h.ss.mixKey(k)
} else {
if len(h.re) == 0 || h.s.IsDefault() {
return nil, errors.New("local or remote ephemeral/static key not set")
}
k, err := h.handshakePattern.dhKey.DH(h.s.Private, h.re)
if err != nil {
return nil, err
}
h.ss.mixKey(k)
}
case SE:
// If user is reading the "se" token
// We check if keys are correctly set.
// If both present, call MixKey(DH(s, re)) if initiator, MixKey(DH(e, rs)) if responder.
if h.initiator {
if h.s.IsDefault() || len(h.re) == 0 {
return nil, errors.New("local or remote ephemeral/static key not set")
}
k, err := h.handshakePattern.dhKey.DH(h.s.Private, h.re)
if err != nil {
return nil, err
}
h.ss.mixKey(k)
} else {
if len(h.rs) == 0 || h.e.IsDefault() {
return nil, errors.New("local or remote ephemeral/static key not set")
}
k, err := h.handshakePattern.dhKey.DH(h.e.Private, h.rs)
if err != nil {
return nil, err
}
h.ss.mixKey(k)
}
case SS:
// If user is reading the "ss" token
// If local and/or remote static keys are not set, we raise an error
if h.s.IsDefault() || len(h.rs) == 0 {
return nil, errors.New("local or remote static key not set")
}
// Calls MixKey(DH(s, rs)).
k, err := h.handshakePattern.dhKey.DH(h.s.Private, h.rs)
if err != nil {
return nil, err
}
h.ss.mixKey(k)
}
}
return outHandshakeMessage, nil
}

137
messagenametag.go Normal file
View File

@ -0,0 +1,137 @@
package noise
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"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
}
func (t MessageNametag) String() string {
return hex.EncodeToString(t[:])
}
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++
}
}
}

279
noise.go Normal file
View File

@ -0,0 +1,279 @@
package noise
import (
"bytes"
"crypto/cipher"
"encoding/binary"
"errors"
"math"
)
// Waku Noise Protocols for Waku Payload Encryption
// Noise module implementing the Noise State Objects and ChaChaPoly encryption/decryption primitives
// See spec for more details:
// https://github.com/vacp2p/rfc/tree/master/content/docs/rfcs/35
//
// Implementation partially inspired by noise-libp2p and js-libp2p-noise
// https://github.com/status-im/nim-libp2p/blob/master/libp2p/protocols/secure/noise.nim
// https://github.com/ChainSafe/js-libp2p-noise
/*
# Noise state machine primitives
# Overview :
# - Alice and Bob process (i.e. read and write, based on their role) each token appearing in a handshake pattern, consisting of pre-message and message patterns;
# - Both users initialize and update according to processed tokens a Handshake State, a Symmetric State and a Cipher State;
# - A preshared key psk is processed by calling MixKeyAndHash(psk);
# - When an ephemeral public key e is read or written, the handshake hash value h is updated by calling mixHash(e); If the handshake expects a psk, MixKey(e) is further called
# - When an encrypted static public key s or a payload message m is read, it is decrypted with decryptAndHash;
# - When a static public key s or a payload message is written, it is encrypted with encryptAndHash;
# - When any Diffie-Hellman token ee, es, se, ss is read or written, the chaining key ck is updated by calling MixKey on the computed secret;
# - If all tokens are processed, users compute two new Cipher States by calling Split;
# - The two Cipher States obtained from Split are used to encrypt/decrypt outbound/inbound messages.
#################################
# Cipher State Primitives
#################################
*/
const nonceMax = math.MaxUint64 - 1 // max is reserved
func isEmptyKey(k []byte) bool {
return len(k) == 0
}
// The Cipher State as in https://noiseprotocol.org/noise.html#the-cipherstate-object
// Contains an encryption key k and a nonce n (used in Noise as a counter)
type CipherState struct {
k []byte
n uint64
cipherFn func([]byte) (cipher.AEAD, error)
}
func NewCipherState(k []byte, cipherFn func([]byte) (cipher.AEAD, error)) *CipherState {
return &CipherState{
k: k,
cipherFn: cipherFn,
}
}
func (c *CipherState) Equals(b *CipherState) bool {
return bytes.Equal(c.k[:], b.k[:]) && c.n == b.n
}
// Checks if a Cipher State has an encryption key set
func (c *CipherState) hasKey() bool {
return !isEmptyKey(c.k)
}
func (cs *CipherState) nonce() []byte {
var nonceBytes [12]byte // RFC7539 specifies 12 bytes for nonce.
binary.BigEndian.PutUint64(nonceBytes[4:], cs.n)
return nonceBytes[:]
}
// Encrypts a plaintext using key material in a Noise Cipher State
// The CipherState is updated increasing the nonce (used as a counter in Noise) by one
func (cs *CipherState) encryptWithAd(ad []byte, plaintext []byte) ([]byte, error) {
// We raise an error if encryption is called using a Cipher State with nonce greater than MaxNonce
if cs.n > nonceMax {
return nil, errors.New("noise max nonce value reached")
}
var ciphertext []byte
if cs.hasKey() {
c, err := cs.cipherFn(cs.k)
if err != nil {
panic(err)
}
// If an encryption key is set in the Cipher state, we proceed with encryption
ciphertext = c.Seal(nil, cs.nonce(), plaintext, ad)
// We increase the Cipher state nonce
cs.n++
// If the nonce is greater than the maximum allowed nonce, we raise an exception
if cs.n > nonceMax {
return nil, errors.New("noise max nonce value reached")
}
} else {
// Otherwise we return the input plaintext according to specification http://www.noiseprotocol.org/noise.html#the-cipherstate-object
ciphertext = plaintext
}
return ciphertext, nil
}
// Decrypts a ciphertext using key material in a Noise Cipher State
// The CipherState is updated increasing the nonce (used as a counter in Noise) by one
func (cs *CipherState) decryptWithAd(ad []byte, ciphertext []byte) ([]byte, error) {
// We raise an error if encryption is called using a Cipher State with nonce greater than MaxNonce
if cs.n > nonceMax {
return nil, errors.New("noise max nonce value reached")
}
if cs.hasKey() {
c, err := cs.cipherFn(cs.k)
if err != nil {
panic(err)
}
plaintext, err := c.Open(nil, cs.nonce(), ciphertext, ad)
if err != nil {
return nil, err
}
// We increase the Cipher state nonce
cs.n++
// If the nonce is greater than the maximum allowed nonce, we raise an exception
if cs.n > nonceMax {
return nil, errors.New("noise max nonce value reached")
}
return plaintext, nil
} else {
// Otherwise we return the input ciphertext according to specification
// http://www.noiseprotocol.org/noise.html#the-cipherstate-object
return ciphertext, nil
}
}
func hashProtocol(hsPattern HandshakePattern) []byte {
// If protocol_name is less than or equal to HASHLEN bytes in length,
// sets h equal to protocol_name with zero bytes appended to make HASHLEN bytes.
// Otherwise sets h = HASH(protocol_name).
protocolName := []byte(hsPattern.name)
if len(protocolName) <= hsPattern.hashFn().Size() {
result := make([]byte, hsPattern.hashFn().Size())
copy(result, protocolName)
return result
} else {
h := hsPattern.hashFn()
h.Write([]byte(hsPattern.name))
return h.Sum(nil)
}
}
// The Symmetric State as in https://noiseprotocol.org/noise.html#the-symmetricstate-object
// Contains a Cipher State cs, the chaining key ck and the handshake hash value h
type SymmetricState struct {
cs *CipherState
hsPattern HandshakePattern
h []byte // handshake hash
ck []byte // chaining key
}
func NewSymmetricState(hsPattern HandshakePattern) *SymmetricState {
h := hashProtocol(hsPattern)
s := &SymmetricState{
cs: NewCipherState([]byte{}, hsPattern.cipherFn),
hsPattern: hsPattern,
}
s.h = make([]byte, len(h))
copy(s.h, h)
s.ck = make([]byte, len(h))
copy(s.ck, h)
return s
}
func (s *SymmetricState) Equals(b *SymmetricState) bool {
return b.cs.Equals(s.cs) && bytes.Equal(s.ck, b.ck) && bytes.Equal(s.h, b.h) && s.hsPattern.Equals(b.hsPattern)
}
// MixKey as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Updates a Symmetric state chaining key and symmetric state
func (s *SymmetricState) mixKey(inputKeyMaterial []byte) {
// We derive two keys using HKDF
keyLen := s.hsPattern.hashFn().Size()
output := getHKDF(s.hsPattern.hashFn, s.ck, inputKeyMaterial, keyLen*2)
ck := output[:keyLen]
tempK := output[keyLen:]
// We update ck and the Cipher state's key k using the output of HDKF
s.cs = NewCipherState(tempK, s.hsPattern.cipherFn)
s.ck = ck
}
// MixHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Hashes data into a Symmetric State's handshake hash value h
func (s *SymmetricState) mixHash(data []byte) {
// We hash the previous handshake hash and input data and store the result in the Symmetric State's handshake hash value
h := s.hsPattern.hashFn()
h.Write(s.h[:])
h.Write(data)
s.h = h.Sum(nil)
}
// mixKeyAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Combines MixKey and MixHash
func (s *SymmetricState) mixKeyAndHash(inputKeyMaterial []byte) {
// Derives 3 keys using HKDF, the chaining key and the input key material
keyLen := s.hsPattern.hashFn().Size()
output := getHKDF(s.hsPattern.hashFn, s.ck, inputKeyMaterial, keyLen*3)
tmpKey0 := output[:keyLen]
tmpKey1 := output[keyLen : keyLen*2]
tmpKey2 := output[keyLen*2:]
// Sets the chaining key
s.ck = tmpKey0
// Updates the handshake hash value
s.mixHash(tmpKey1)
// Updates the Cipher state's key
// Note for later support of 512 bits hash functions: "If HASHLEN is 64, then truncates tempKeys[2] to 32 bytes."
s.cs = NewCipherState(tmpKey2, s.hsPattern.cipherFn)
}
// EncryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Combines encryptWithAd and mixHash
// Note that by setting extraAd, it is possible to pass extra additional data that will be concatenated to the ad specified by Noise (can be used to authenticate messageNametag)
func (s *SymmetricState) encryptAndHash(plaintext []byte, extraAd []byte) ([]byte, error) {
// The additional data
ad := append([]byte(nil), s.h[:]...)
ad = append(ad, extraAd...)
// Note that if an encryption key is not set yet in the Cipher state, ciphertext will be equal to plaintext
ciphertext, err := s.cs.encryptWithAd(ad, plaintext)
if err != nil {
return nil, err
}
// We call mixHash over the result
s.mixHash(ciphertext)
return ciphertext, nil
}
// DecryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Combines decryptWithAd and mixHash
func (s *SymmetricState) decryptAndHash(ciphertext []byte, extraAd []byte) ([]byte, error) {
// The additional data
ad := append([]byte(nil), s.h[:]...)
ad = append(ad, extraAd...)
// Note that if an encryption key is not set yet in the Cipher state, plaintext will be equal to ciphertext
plaintext, err := s.cs.decryptWithAd(ad, ciphertext)
if err != nil {
return nil, err
}
// According to specification, the ciphertext enters mixHash (and not the plaintext)
s.mixHash(ciphertext)
return plaintext, nil
}
// Split as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
// Once a handshake is complete, returns two Cipher States to encrypt/decrypt outbound/inbound messages
func (s *SymmetricState) split() (*CipherState, *CipherState) {
// Derives 2 keys using HKDF and the chaining key
keyLen := s.hsPattern.hashFn().Size()
output := getHKDF(s.hsPattern.hashFn, s.ck, []byte{}, keyLen*2)
tmpKey1 := output[:keyLen]
tmpKey2 := output[keyLen:]
// Returns a tuple of two Cipher States initialized with the derived keys
return NewCipherState(tmpKey1, s.hsPattern.cipherFn), NewCipherState(tmpKey2, s.hsPattern.cipherFn)
}

215
noise_test.go Normal file
View File

@ -0,0 +1,215 @@
package noise
import (
"crypto/rand"
"testing"
"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, _ := DH25519.GenerateKeypair()
pk2, _ := DH25519.GenerateKeypair()
handshakeMessages[0] = byteToNoisePublicKey(DH25519, pk1.Public)
handshakeMessages[1] = byteToNoisePublicKey(DH25519, pk2.Public)
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, MessageNametag{})
require.NoError(t, err)
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
bobStep, err := hsBob.Step(aliceStep.PayloadV2, nil, MessageNametag{})
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, MessageNametag{})
require.NoError(t, err)
// While Alice reads and returns the (decrypted) transport message
aliceStep, err = hsAlice.Step(bobStep.PayloadV2, nil, MessageNametag{})
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, MessageNametag{})
require.NoError(t, err)
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
bobStep, err = hsBob.Step(aliceStep.PayloadV2, nil, MessageNametag{})
require.NoError(t, err)
// check:
require.Equal(t, sentTransportMessage, bobStep.TransportMessage)
_, err = hsAlice.FinalizeHandshake()
require.NoError(t, err)
_, err = hsBob.FinalizeHandshake()
require.NoError(t, err)
// 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.IsComplete())
require.True(t, hsBob.IsComplete())
_, err = hsAlice.Step(nil, generateRandomBytes(t, 32), MessageNametag{})
require.ErrorIs(t, err, ErrHandshakeComplete)
_, err = hsBob.Step(nil, generateRandomBytes(t, 32), MessageNametag{})
require.ErrorIs(t, err, ErrHandshakeComplete)
// #########################
// After Handshake
// #########################
// We test read/write of random messages exchanged between Alice and Bob
defaultMessageNametagBuffer := NewMessageNametagBuffer(nil)
aliceHSResult, err := hsAlice.FinalizeHandshake()
require.NoError(t, err)
bobHSResult, err := hsBob.FinalizeHandshake()
require.NoError(t, err)
for i := 0; i < 10; i++ {
// Alice writes to Bob
message := generateRandomBytes(t, 32)
encryptedPayload, err := aliceHSResult.WriteMessage(message, defaultMessageNametagBuffer)
require.NoError(t, err)
plaintext, err := bobHSResult.ReadMessage(encryptedPayload, defaultMessageNametagBuffer)
require.NoError(t, err)
require.Equal(t, message, plaintext)
// Bob writes to Alice
message = generateRandomBytes(t, 32)
encryptedPayload, err = bobHSResult.WriteMessage(message, defaultMessageNametagBuffer)
require.NoError(t, err)
plaintext, err = aliceHSResult.ReadMessage(encryptedPayload, defaultMessageNametagBuffer)
require.NoError(t, err)
require.Equal(t, message, plaintext)
}
}
func TestNoiseXXHandshakeRoundtrip(t *testing.T) {
aliceKP, _ := DH25519.GenerateKeypair()
bobKP, _ := DH25519.GenerateKeypair()
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, _ := DH25519.GenerateKeypair()
bobKP, _ := DH25519.GenerateKeypair()
// 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, _ := DH25519.GenerateKeypair()
bobKP, _ := DH25519.GenerateKeypair()
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, _ := DH25519.GenerateKeypair()
bobKP, _ := DH25519.GenerateKeypair()
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, bobKP.Public, nil)
require.NoError(t, err)
handshakeTest(t, hsAlice, hsBob)
}
func TestPKCSPaddingUnpadding(t *testing.T) {
maxMessageLength := 3 * NoisePaddingBlockSize
for messageLen := 0; messageLen <= maxMessageLength; messageLen++ {
message := generateRandomBytes(t, messageLen)
padded, err := PKCS7_Pad(message, NoisePaddingBlockSize)
require.NoError(t, err)
unpadded, err := PKCS7_Unpad(padded, NoisePaddingBlockSize)
require.NoError(t, err)
require.Greater(t, len(padded), 0)
require.Equal(t, len(padded)%NoisePaddingBlockSize, 0)
require.Equal(t, message, unpadded)
}
}

48
padding.go Normal file
View File

@ -0,0 +1,48 @@
package noise
import (
"errors"
)
// PKCS7_Pad pads a payload according to PKCS#7 as per
// RFC 5652 https://datatracker.ietf.org/doc/html/rfc5652#section-6.3
func PKCS7_Pad(payload []byte, paddingSize int) ([]byte, error) {
if paddingSize >= 256 {
return nil, errors.New("invalid padding size")
}
k := paddingSize - (len(payload) % paddingSize)
var padVal int
if k != 0 {
padVal = k
} else {
padVal = paddingSize
}
padding := make([]byte, padVal)
for i := range padding {
padding[i] = byte(padVal)
}
return append(payload, padding...), nil
}
// PKCS7_Unpad unpads a payload according to PKCS#7 as per
// RFC 5652 https://datatracker.ietf.org/doc/html/rfc5652#section-6.3
func PKCS7_Unpad(payload []byte, paddingSize int) ([]byte, error) {
if paddingSize >= 256 {
return nil, errors.New("invalid padding size")
}
if len(payload) == 0 {
return nil, nil // empty payload
}
high := len(payload) - 1
k := payload[high]
unpadded := payload[0:(high + 1 - int(k))]
return unpadded, nil
}

312
patterns.go Normal file
View File

@ -0,0 +1,312 @@
package noise
import (
"crypto/cipher"
"crypto/sha256"
"errors"
"hash"
"golang.org/x/crypto/chacha20poly1305"
)
// The Noise tokens appearing in Noise (pre)message patterns
// as in http://www.noiseprotocol.org/noise.html#handshake-pattern-basics
type NoiseTokens string
const (
E NoiseTokens = "e"
S NoiseTokens = "s"
ES NoiseTokens = "es"
EE NoiseTokens = "ee"
SE NoiseTokens = "se"
SS NoiseTokens = "ss"
PSK NoiseTokens = "psk"
)
// The direction of a (pre)message pattern in canonical form (i.e. Alice-initiated form)
// as in http://www.noiseprotocol.org/noise.html#alice-and-bob
type MessageDirection string
const (
Right MessageDirection = "->"
Left MessageDirection = "<-"
)
// The pre message pattern consisting of a message direction and some Noise tokens, if any.
// (if non empty, only tokens e and s are allowed: http://www.noiseprotocol.org/noise.html#handshake-pattern-basics)
type PreMessagePattern struct {
direction MessageDirection
tokens []NoiseTokens
}
func NewPreMessagePattern(direction MessageDirection, tokens []NoiseTokens) PreMessagePattern {
return PreMessagePattern{
direction: direction,
tokens: tokens,
}
}
func (p PreMessagePattern) Equals(b PreMessagePattern) bool {
if p.direction != b.direction {
return false
}
if len(p.tokens) != len(b.tokens) {
return false
}
for i := range p.tokens {
if p.tokens[i] != b.tokens[i] {
return false
}
}
return true
}
// The message pattern consisting of a message direction and some Noise tokens
// All Noise tokens are allowed
type MessagePattern struct {
direction MessageDirection
tokens []NoiseTokens
}
func NewMessagePattern(direction MessageDirection, tokens []NoiseTokens) MessagePattern {
return MessagePattern{
direction: direction,
tokens: tokens,
}
}
func (p MessagePattern) Equals(b MessagePattern) bool {
if p.direction != b.direction {
return false
}
if len(p.tokens) != len(b.tokens) {
return false
}
for i := range p.tokens {
if p.tokens[i] != b.tokens[i] {
return false
}
}
return true
}
// The handshake pattern object. It stores the handshake protocol name, the handshake pre message patterns and the handshake message patterns
type HandshakePattern struct {
protocolID byte
name string
premessagePatterns []PreMessagePattern
messagePatterns []MessagePattern
hashFn func() hash.Hash
cipherFn func([]byte) (cipher.AEAD, error)
dhKey DHKey
}
func NewHandshakePattern(protocolID byte, name string, hashFn func() hash.Hash, cipherFn func([]byte) (cipher.AEAD, error), dhKey DHKey, preMessagePatterns []PreMessagePattern, messagePatterns []MessagePattern) HandshakePattern {
return HandshakePattern{
protocolID: protocolID,
name: name,
hashFn: hashFn,
cipherFn: cipherFn,
dhKey: dhKey,
premessagePatterns: preMessagePatterns,
messagePatterns: messagePatterns,
}
}
func (p HandshakePattern) Equals(b HandshakePattern) bool {
if len(p.premessagePatterns) != len(b.premessagePatterns) {
return false
}
for i := range p.premessagePatterns {
if !p.premessagePatterns[i].Equals(b.premessagePatterns[i]) {
return false
}
}
if len(p.messagePatterns) != len(b.messagePatterns) {
return false
}
for i := range p.messagePatterns {
if !p.messagePatterns[i].Equals(b.messagePatterns[i]) {
return false
}
}
return p.name == b.name
}
var EmptyPreMessage = []PreMessagePattern{}
// Supported Noise handshake patterns as defined in https://rfc.vac.dev/spec/35/#specification
var K1K1 = NewHandshakePattern(
Noise_K1K1_25519_ChaChaPoly_SHA256,
"Noise_K1K1_25519_ChaChaPoly_SHA256",
sha256.New,
chacha20poly1305.New,
DH25519,
[]PreMessagePattern{
NewPreMessagePattern(Right, []NoiseTokens{S}),
NewPreMessagePattern(Left, []NoiseTokens{S}),
},
[]MessagePattern{
NewMessagePattern(Right, []NoiseTokens{E}),
NewMessagePattern(Left, []NoiseTokens{E, EE, ES}),
NewMessagePattern(Right, []NoiseTokens{SE}),
},
)
var XK1 = NewHandshakePattern(
Noise_XK1_25519_ChaChaPoly_SHA256,
"Noise_XK1_25519_ChaChaPoly_SHA256",
sha256.New,
chacha20poly1305.New,
DH25519,
[]PreMessagePattern{
NewPreMessagePattern(Left, []NoiseTokens{S}),
},
[]MessagePattern{
NewMessagePattern(Right, []NoiseTokens{E}),
NewMessagePattern(Left, []NoiseTokens{E, EE, ES}),
NewMessagePattern(Right, []NoiseTokens{S, SE}),
},
)
var XX = NewHandshakePattern(
Noise_XX_25519_ChaChaPoly_SHA256,
"Noise_XX_25519_ChaChaPoly_SHA256",
sha256.New,
chacha20poly1305.New,
DH25519,
EmptyPreMessage,
[]MessagePattern{
NewMessagePattern(Right, []NoiseTokens{E}),
NewMessagePattern(Left, []NoiseTokens{E, EE, S, ES}),
NewMessagePattern(Right, []NoiseTokens{S, SE}),
},
)
var XXpsk0 = NewHandshakePattern(
Noise_XXpsk0_25519_ChaChaPoly_SHA256,
"Noise_XXpsk0_25519_ChaChaPoly_SHA256",
sha256.New,
chacha20poly1305.New,
DH25519,
EmptyPreMessage,
[]MessagePattern{
NewMessagePattern(Right, []NoiseTokens{PSK, E}),
NewMessagePattern(Left, []NoiseTokens{E, EE, S, ES}),
NewMessagePattern(Right, []NoiseTokens{S, SE}),
},
)
var WakuPairing = NewHandshakePattern(
Noise_WakuPairing_25519_ChaChaPoly_SHA256,
"Noise_WakuPairing_25519_ChaChaPoly_SHA256",
sha256.New,
chacha20poly1305.New,
DH25519,
[]PreMessagePattern{
NewPreMessagePattern(Left, []NoiseTokens{E}),
},
[]MessagePattern{
NewMessagePattern(Right, []NoiseTokens{E, EE}),
NewMessagePattern(Left, []NoiseTokens{S, ES}),
NewMessagePattern(Right, []NoiseTokens{S, SE, SS}),
},
)
// Supported Protocol ID for PayloadV2 objects
// Protocol IDs are defined according to https://rfc.vac.dev/spec/35/#specification
const Noise_K1K1_25519_ChaChaPoly_SHA256 = 10
const Noise_XK1_25519_ChaChaPoly_SHA256 = 11
const Noise_XX_25519_ChaChaPoly_SHA256 = 12
const Noise_XXpsk0_25519_ChaChaPoly_SHA256 = 13
const Noise_WakuPairing_25519_ChaChaPoly_SHA256 = 14
const ChaChaPoly = 30
const None = 0
func IsProtocolIDSupported(protocolID byte) 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 == Noise_WakuPairing_25519_ChaChaPoly_SHA256 ||
protocolID == None
}
func GetHandshakePattern(protocol byte) (HandshakePattern, error) {
switch protocol {
case Noise_K1K1_25519_ChaChaPoly_SHA256:
return K1K1, nil
case Noise_XK1_25519_ChaChaPoly_SHA256:
return XK1, nil
case Noise_XX_25519_ChaChaPoly_SHA256:
return XX, nil
case Noise_XXpsk0_25519_ChaChaPoly_SHA256:
return XXpsk0, nil
case Noise_WakuPairing_25519_ChaChaPoly_SHA256:
return WakuPairing, nil
default:
return HandshakePattern{}, errors.New("unsupported handshake pattern")
}
}
// NewHandshake_XX_25519_ChaChaPoly_SHA256 creates a handshake where the initiator and responder are not aware of each other static keys
func NewHandshake_XX_25519_ChaChaPoly_SHA256(staticKeypair Keypair, initiator bool, prologue []byte) (*Handshake, error) {
return NewHandshake(XX, staticKeypair, Keypair{}, prologue, nil, nil, initiator)
}
// NewHandshake_XXpsk0_25519_ChaChaPoly_SHA256 creates a handshake where the initiator and responder 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 Keypair, initiator bool, presharedKey []byte, prologue []byte) (*Handshake, error) {
return NewHandshake(XXpsk0, staticKeypair, Keypair{}, prologue, presharedKey, nil, initiator)
}
// 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 responder
func NewHandshake_K1K1_25519_ChaChaPoly_SHA256(myStaticKeypair Keypair, initiator bool, peerStaticKey []byte, prologue []byte) (*Handshake, error) {
var presharedKeys []*NoisePublicKey
if initiator {
presharedKeys = append(presharedKeys, byteToNoisePublicKey(K1K1.dhKey, myStaticKeypair.Public))
presharedKeys = append(presharedKeys, byteToNoisePublicKey(K1K1.dhKey, peerStaticKey))
} else {
presharedKeys = append(presharedKeys, byteToNoisePublicKey(K1K1.dhKey, peerStaticKey))
presharedKeys = append(presharedKeys, byteToNoisePublicKey(K1K1.dhKey, myStaticKeypair.Public))
}
return NewHandshake(K1K1, myStaticKeypair, Keypair{}, prologue, nil, presharedKeys, initiator)
}
// NewHandshake_XK1_25519_ChaChaPoly_SHA256 creates a handshake where the initiator knows the responder public static key. Within this handshake,
// the initiator and responder reciprocally authenticate their static keys using ephemeral keys. We note that while the responder's
// static key is assumed to be known to Alice (and hence is not transmitted), The initiator static key is sent to the
// responder encrypted with a key derived from both parties ephemeral keys and the responder's static key.
func NewHandshake_XK1_25519_ChaChaPoly_SHA256(myStaticKeypair Keypair, initiator bool, responderStaticKey []byte, prologue []byte) (*Handshake, error) {
if !initiator {
// Overwrite responderStaticKey with responder's static key in case they're different
responderStaticKey = myStaticKeypair.Public
}
pubK := byteToNoisePublicKey(XK1.dhKey, responderStaticKey)
return NewHandshake(XK1, myStaticKeypair, Keypair{}, prologue, nil, []*NoisePublicKey{pubK}, initiator)
}
// NewHandshake_WakuPairing_25519_ChaChaPoly_SHA256
func NewHandshake_WakuPairing_25519_ChaChaPoly_SHA256(myStaticKeypair Keypair, myEphemeralKeypair Keypair, initiator bool, prologue []byte, receiverEphemeralKey []byte) (*Handshake, error) {
if !initiator {
// Overwrite responderStaticKey with responder's static key in case they're different
receiverEphemeralKey = myEphemeralKeypair.Public
}
pubK := byteToNoisePublicKey(WakuPairing.dhKey, receiverEphemeralKey)
return NewHandshake(WakuPairing, myStaticKeypair, myEphemeralKeypair, prologue, nil, []*NoisePublicKey{pubK}, initiator)
}

210
payload.go Normal file
View File

@ -0,0 +1,210 @@
package noise
import (
"bytes"
"crypto/ed25519"
"encoding/binary"
"errors"
)
const MaxUint8 = 1<<8 - 1
// 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
MessageNametag MessageNametag
}
// 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, 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
len(p.TransportMessage), // transportMessageLen bytes for transportMessage
)
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
}
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
}
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 by reading the messageNametag
if err := binary.Read(payloadBuf, binary.BigEndian, &result.MessageNametag); err != nil {
return nil, err
}
// We read the Protocol ID
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(1 + pkLen)
} 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(1 + pkLen)
// 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
}

102
publickey.go Normal file
View File

@ -0,0 +1,102 @@
package noise
import (
"bytes"
"errors"
)
// A Noise public key is a public key exchanged during Noise handshakes (no private part)
// This follows https://rfc.vac.dev/spec/35/#public-keys-serialization
// pk contains the X coordinate of the public key, if unencrypted (this implies flag = 0)
// or the encryption of the X coordinate concatenated with the authorization tag, if encrypted (this implies flag = 1)
// Note: besides encryption, flag can be used to distinguish among multiple supported Elliptic Curves
type NoisePublicKey struct {
Flag byte
Public []byte
}
func NewNoisePublicKey(flag byte, public []byte) *NoisePublicKey {
return &NoisePublicKey{
Flag: flag,
Public: public,
}
}
func byteToNoisePublicKey(dhKey DHKey, input []byte) *NoisePublicKey {
flag := byte(0)
if len(input) > dhKey.DHLen() {
flag = 1
}
return &NoisePublicKey{
Flag: flag,
Public: input,
}
}
// Equals checks equality between two Noise public keys
func (pk *NoisePublicKey) Equals(pk2 *NoisePublicKey) bool {
return pk.Flag == pk2.Flag && bytes.Equal(pk.Public, pk2.Public)
}
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.Public)+1)
serializedPK[0] = pk.Flag
copy(serializedPK[1:], pk.Public)
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.Public = s[1:]
return pubk, nil
}
// Encrypt encrypts a Noise public key using a Cipher State
func (pk *NoisePublicKey) Encrypt(state *CipherState) error {
if pk.Flag == 0 {
// Authorization tag is appended to output
encPk, err := state.encryptWithAd(nil, pk.Public)
if err != nil {
return err
}
pk.Flag = 1
pk.Public = encPk
}
return nil
}
// Decrypts decrypts a Noise public key using a Cipher State
func (pk *NoisePublicKey) Decrypt(state *CipherState) error {
if pk.Flag == 1 {
decPk, err := state.decryptWithAd(nil, pk.Public) // encrypted pk should contain the auth tag
if err != nil {
return err
}
pk.Flag = 0
pk.Public = decPk
}
return nil
}

190
wakupairing_test.go Normal file
View File

@ -0,0 +1,190 @@
package noise
import (
"bytes"
"crypto/sha256"
"testing"
"github.com/stretchr/testify/require"
)
func TestWakuPairing(t *testing.T) {
// Pairing Phase
// ==========
// Alice static/ephemeral key initialization and commitment
aliceStaticKey, _ := DH25519.GenerateKeypair()
aliceEphemeralKey, _ := DH25519.GenerateKeypair()
s := generateRandomBytes(t, 32)
aliceCommittedStaticKey := CommitPublicKey(sha256.New, aliceStaticKey.Public, s)
// Bob static/ephemeral key initialization and commitment
bobStaticKey, _ := DH25519.GenerateKeypair()
bobEphemeralKey, _ := DH25519.GenerateKeypair()
r := generateRandomBytes(t, 32)
bobCommittedStaticKey := CommitPublicKey(sha256.New, bobStaticKey.Public, r)
prologue := generateRandomBytes(t, 100)
messageNametag := BytesToMessageNametag(generateRandomBytes(t, MessageNametagLength))
// 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, prologue, bobEphemeralKey.Public)
require.NoError(t, err)
bobHS, err := NewHandshake_WakuPairing_25519_ChaChaPoly_SHA256(bobStaticKey, bobEphemeralKey, false, prologue, bobEphemeralKey.Public)
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, messageNametag)
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.PayloadV2, nil, messageNametag)
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.PayloadV2, 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(WakuPairing.hashFn, aliceHS.hs.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.PayloadV2, 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(WakuPairing.hashFn, bobHS.hs.rs, bobStep.TransportMessage)
require.True(t, bytes.Equal(expectedAliceCommittedStaticKey, aliceCommittedStaticKey))
// Secure Transfer Phase
// ==========
aliceHSResult, err := aliceHS.FinalizeHandshake()
require.NoError(t, err)
bobHSResult, err := bobHS.FinalizeHandshake()
require.NoError(t, err)
// 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 < 1; i++ { //10*MessageNametagBufferSize; i++ {
// Alice writes to Bob
message := generateRandomBytes(t, 32)
payload, err := aliceHSResult.WriteMessage(message, nil)
require.NoError(t, err)
readMessage, err := bobHSResult.ReadMessage(payload, nil)
require.NoError(t, err)
require.True(t, bytes.Equal(message, readMessage))
// Bob writes to Alice
message = generateRandomBytes(t, 32)
payload, err = bobHSResult.WriteMessage(message, nil)
require.NoError(t, err)
readMessage, err = aliceHSResult.ReadMessage(payload, nil)
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 = aliceHSResult.WriteMessage(message, nil)
require.NoError(t, err)
message = generateRandomBytes(t, 32)
payload2, err := aliceHSResult.WriteMessage(message, nil)
require.NoError(t, err)
_, err = bobHSResult.ReadMessage(payload2, nil)
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.hsResult.nametagsInbound.Delete(2)
message = generateRandomBytes(t, 32)
payload2, err = bobHSResult.WriteMessage(message, nil)
require.NoError(t, err)
readMessage, err := aliceHSResult.ReadMessage(payload2, nil)
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 = aliceHSResult.WriteMessage(message, nil)
require.NoError(t, err)
bobHS.hsResult.nametagsInbound.Delete(1)
_, err = bobHSResult.ReadMessage(payload2, nil)
require.ErrorIs(t, err, ErrNametagNotFound)
}