├── LICENSE ├── README.md ├── cipher └── cipher.go ├── cipherstate.go ├── cipherstate_test.go ├── common.go ├── dh └── dh.go ├── example_test.go ├── go.mod ├── go.sum ├── handshakestate.go ├── handshakestate_test.go ├── hash └── hash.go ├── pattern ├── deferred.go ├── interactive.go ├── oneway.go ├── pattern.go ├── psk.go ├── validity.go └── validity_test.go ├── symmetricstate.go ├── testdata ├── cacophony.txt ├── noise-c-basic.txt └── snow.txt ├── vectors ├── hexbuffer.go └── vectors.go └── vectors_test.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019, 2021 Yawning Angel. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### nyquist - A Noise Protocol Framework implementation 2 | #### Yawning Angel (yawning at schwanenlied dot me) 3 | 4 | This package implements the [Noise Protocol Framework][1]. 5 | 6 | #### Why? 7 | 8 | > Yeah, well, I'm gonna go build my own theme park with blackjack and 9 | > hookers. In fact, forget the park. 10 | 11 | #### Notes 12 | 13 | It is assumed that developers using this package are familiar with the Noise 14 | Protocol Framework specification. 15 | 16 | As of revision 34 of the specification, the only standard functionality 17 | that is NOT implemented is "10.2. The `fallback` modifier". 18 | 19 | This package used to make a partial attempt to sanitize key material, but 20 | the author is now convinced that it is fundementally a lost cause due to 21 | several reasons including but not limited to copies on stack growth, the 22 | lack of a `memset_s` equivalent, and lack of support by most cryptographic 23 | primitives. And no, memguard is not a good solution either. 24 | 25 | This package will `panic` only if invariants are violated. Under normal 26 | use this situation should not occur ("normal" being defined as, "Yes, it 27 | will panic if an invalid configuration is provided when initializing a 28 | handshake"). 29 | 30 | Several non-standard protocol extensions are supported by this implementation: 31 | 32 | * The maximum message size can be set to an arbitrary value or entirely 33 | disabled, on a per-session basis. The implementation will default to 34 | the value in the specification. 35 | 36 | * AEAD algorithms with authentication tags that are not 128 bits (16 bytes) 37 | in size should be supported. While the package will not reject algorithms 38 | with tags sizes that are less than 128 bits, this is NOT RECOMMENED. 39 | 40 | * Non-standard DH, Cipher and Hash functions are trivial to support by 41 | implementing the appropriate interface, as long as the following 42 | constraints are met: 43 | 44 | * For any given DH scheme, all public keys must be `DHLEN` bytes in size. 45 | 46 | * For any given Hash function, `HASHLEN` must be at least 256 bits 47 | (32 bytes) in size. The specification requires exactly 256 or 512 48 | bits, however this package will tollerate any length, greater than 49 | or equal to 256 bits. 50 | 51 | * AEAD implementations must be able to tollerate always being passed 52 | a key that is 256 bits (32 bytes) in size. 53 | 54 | * Non-standard (or unimplemented) patterns are trivial to support by 55 | implementing the appropriate interface. The `pattern` sub-package 56 | includes a pattern validator that can verify a pattern against the 57 | specification's pattern validity rules. 58 | 59 | * A Cipher implementation backed by the Deoxys-II-256-128 MRAE primitive 60 | is provided. 61 | 62 | The test vectors under `testdata` were shamelessly stolen out of the [Snow][2] 63 | repository. 64 | 65 | [1]: https://noiseprotocol.org/ 66 | [2]: https://github.com/mcginty/snow/tree/master/tests/vectors 67 | -------------------------------------------------------------------------------- /cipher/cipher.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | // Package cipher implments the Noise Protocol Framework cipher function 31 | // abstract interface and standard cipher functions. 32 | package cipher // import "gitlab.com/yawning/nyquist.git/cipher" 33 | 34 | import ( 35 | "crypto/cipher" 36 | "encoding/binary" 37 | "fmt" 38 | 39 | "github.com/oasisprotocol/deoxysii" 40 | "gitlab.com/yawning/bsaes.git" 41 | "golang.org/x/crypto/chacha20poly1305" 42 | ) 43 | 44 | var supportedCiphers = map[string]Cipher{ 45 | "ChaChaPoly": ChaChaPoly, 46 | "AESGCM": AESGCM, 47 | "DeoxysII": DeoxysII, 48 | } 49 | 50 | // Cipher is an AEAD algorithm factory. 51 | type Cipher interface { 52 | fmt.Stringer 53 | 54 | // New constructs a new keyed `cipher.AEAD` instance, with the provided 55 | // key. 56 | New(key []byte) (cipher.AEAD, error) 57 | 58 | // EncodeNonce encodes a Noise nonce to a nonce suitable for use with 59 | // the `cipher.AEAD` instances created by `Cipher.New`. 60 | EncodeNonce(nonce uint64) []byte 61 | } 62 | 63 | // Rekeyable is the interface implemented by Cipher instances that have a 64 | // `REKEY(k)` function specifically defined. 65 | type Rekeyable interface { 66 | // Rekey returns a new 32-byte cipher key as a pseudorandom function of `k`. 67 | Rekey(k []byte) []byte 68 | } 69 | 70 | // FromString returns a Cipher by algorithm name, or nil. 71 | func FromString(s string) Cipher { 72 | return supportedCiphers[s] 73 | } 74 | 75 | // ChaChaPoly is the ChaChaPoly cipher functions. 76 | var ChaChaPoly Cipher = &cipherChaChaPoly{} 77 | 78 | type cipherChaChaPoly struct{} 79 | 80 | func (ci *cipherChaChaPoly) String() string { 81 | return "ChaChaPoly" 82 | } 83 | 84 | func (ci *cipherChaChaPoly) New(key []byte) (cipher.AEAD, error) { 85 | return chacha20poly1305.New(key) 86 | } 87 | 88 | func (ci *cipherChaChaPoly) EncodeNonce(nonce uint64) []byte { 89 | var encodedNonce [12]byte // 96 bits 90 | binary.LittleEndian.PutUint64(encodedNonce[4:], nonce) 91 | return encodedNonce[:] 92 | } 93 | 94 | // AESGCM is the AESGCM cipher functions. 95 | // 96 | // Note: This Cipher implementation is always constant time, even on systems 97 | // where the Go runtime library's is not. 98 | var AESGCM Cipher = &cipherAesGcm{} 99 | 100 | type cipherAesGcm struct{} 101 | 102 | func (ci *cipherAesGcm) String() string { 103 | return "AESGCM" 104 | } 105 | 106 | func (ci *cipherAesGcm) New(key []byte) (cipher.AEAD, error) { 107 | block, err := bsaes.NewCipher(key) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | return cipher.NewGCM(block) 113 | } 114 | 115 | func (ci *cipherAesGcm) EncodeNonce(nonce uint64) []byte { 116 | var encodedNonce [12]byte // 96 bits 117 | binary.BigEndian.PutUint64(encodedNonce[4:], nonce) 118 | return encodedNonce[:] 119 | } 120 | 121 | // DeoxysII is the DeoxysII cipher functions. 122 | // 123 | // Warning: This cipher is non-standard. 124 | var DeoxysII Cipher = &cipherDeoxysII{} 125 | 126 | type cipherDeoxysII struct{} 127 | 128 | func (ci *cipherDeoxysII) String() string { 129 | return "DeoxysII" 130 | } 131 | 132 | func (ci *cipherDeoxysII) New(key []byte) (cipher.AEAD, error) { 133 | return deoxysii.New(key) 134 | } 135 | 136 | func (ci *cipherDeoxysII) EncodeNonce(nonce uint64) []byte { 137 | // Using the full nonce-space is fine, and big endian follows how 138 | // Deoxys-II encodes things internally. 139 | var encodedNonce [deoxysii.NonceSize]byte // 120 bits 140 | binary.BigEndian.PutUint64(encodedNonce[7:], nonce) 141 | return encodedNonce[:] 142 | } 143 | 144 | // Register registers a new cipher for use with `FromString()`. 145 | func Register(cipher Cipher) { 146 | supportedCiphers[cipher.String()] = cipher 147 | } 148 | -------------------------------------------------------------------------------- /cipherstate.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | package nyquist 31 | 32 | import ( 33 | goCipher "crypto/cipher" 34 | "errors" 35 | "math" 36 | 37 | "gitlab.com/yawning/nyquist.git/cipher" 38 | ) 39 | 40 | const ( 41 | // SymmetricKeySize is the size a symmetric key in bytes. 42 | SymmetricKeySize = 32 43 | 44 | maxnonce = math.MaxUint64 45 | ) 46 | 47 | var ( 48 | errInvalidKeySize = errors.New("nyquist/CipherState: invalid key size") 49 | errNoExistingKey = errors.New("nyquist/CipherState: failed to rekey, no existing key") 50 | 51 | zeroes [32]byte 52 | ) 53 | 54 | // CipherState is a keyed AEAD algorithm instance. 55 | type CipherState struct { 56 | cipher cipher.Cipher 57 | 58 | aead goCipher.AEAD 59 | k []byte 60 | n uint64 61 | 62 | maxMessageSize int 63 | aeadOverhead int 64 | } 65 | 66 | // InitializeKey initializes sets the cipher key to `key`, and nonce to 0. 67 | func (cs *CipherState) InitializeKey(key []byte) { 68 | if err := cs.setKey(key); err != nil { 69 | panic("nyquist/CipherState: failed to initialize key: " + err.Error()) 70 | } 71 | cs.n = 0 72 | } 73 | 74 | func (cs *CipherState) setKey(key []byte) error { 75 | cs.Reset() 76 | 77 | switch len(key) { 78 | case 0: 79 | case SymmetricKeySize: 80 | var err error 81 | if cs.aead, err = cs.cipher.New(key); err != nil { 82 | return err 83 | } 84 | 85 | cs.aeadOverhead = cs.aead.Overhead() 86 | 87 | cs.k = make([]byte, SymmetricKeySize) 88 | copy(cs.k, key) 89 | default: 90 | return errInvalidKeySize 91 | } 92 | 93 | return nil 94 | } 95 | 96 | // HasKey returns true iff the CipherState is keyed. 97 | func (cs *CipherState) HasKey() bool { 98 | return cs.aead != nil 99 | } 100 | 101 | // SetNonce sets the CipherState's nonce to `nonce`. 102 | func (cs *CipherState) SetNonce(nonce uint64) { 103 | cs.n = nonce 104 | } 105 | 106 | // EncryptWithAd encrypts and authenticates the additional data and plaintext 107 | // and increments the nonce iff the CipherState is keyed, and otherwise returns 108 | // the plaintext. 109 | // 110 | // Note: The ciphertext is appended to `dst`, and the new slice is returned. 111 | func (cs *CipherState) EncryptWithAd(dst, ad, plaintext []byte) ([]byte, error) { 112 | aead := cs.aead 113 | if aead == nil { 114 | return append(dst, plaintext...), nil 115 | } 116 | 117 | if cs.n == maxnonce { 118 | return nil, ErrNonceExhausted 119 | } 120 | 121 | if cs.maxMessageSize > 0 && len(plaintext)+cs.aeadOverhead > cs.maxMessageSize { 122 | return nil, ErrMessageSize 123 | } 124 | 125 | nonce := cs.cipher.EncodeNonce(cs.n) 126 | ciphertext := aead.Seal(dst, nonce, plaintext, ad) 127 | cs.n++ 128 | 129 | return ciphertext, nil 130 | } 131 | 132 | // DecryptWihtAd authenticates and decrypts the additional data and ciphertext 133 | // and increments the nonce iff the CipherState is keyed, and otherwise returns 134 | // the plaintext. If an authentication failure occurs, the nonce is not 135 | // incremented. 136 | // 137 | // Note: The plaintext is appended to `dst`, and the new slice is returned. 138 | func (cs *CipherState) DecryptWithAd(dst, ad, ciphertext []byte) ([]byte, error) { 139 | aead := cs.aead 140 | if aead == nil { 141 | return append(dst, ciphertext...), nil 142 | } 143 | 144 | if cs.n == maxnonce { 145 | return nil, ErrNonceExhausted 146 | } 147 | 148 | if cs.maxMessageSize > 0 && len(ciphertext) > cs.maxMessageSize { 149 | return nil, ErrMessageSize 150 | } 151 | 152 | nonce := cs.cipher.EncodeNonce(cs.n) 153 | plaintext, err := aead.Open(dst, nonce, ciphertext, ad) 154 | if err != nil { 155 | return nil, ErrOpen 156 | } 157 | cs.n++ 158 | 159 | return plaintext, nil 160 | } 161 | 162 | // Rekey sets the CipherState's key to `REKEY(k)`. 163 | func (cs *CipherState) Rekey() error { 164 | if !cs.HasKey() { 165 | return errNoExistingKey 166 | } 167 | 168 | var newKey []byte 169 | if rekeyer, ok := (cs.cipher).(cipher.Rekeyable); ok { 170 | // The cipher function set has a specific `REKEY` function defined. 171 | newKey = rekeyer.Rekey(cs.k) 172 | } else { 173 | // The cipher function set has no `REKEY` function defined, use the 174 | // default generic implementation. 175 | nonce := cs.cipher.EncodeNonce(maxnonce) 176 | newKey = cs.aead.Seal(nil, nonce, zeroes[:], nil) 177 | 178 | // "defaults to returning the first 32 bytes" 179 | newKey = truncateTo32BytesMax(newKey) 180 | } 181 | 182 | err := cs.setKey(newKey) 183 | 184 | return err 185 | } 186 | 187 | // Reset sets the CipherState to a un-keyed state. 188 | func (cs *CipherState) Reset() { 189 | if cs.k != nil { 190 | cs.k = nil 191 | } 192 | if cs.aead != nil { 193 | cs.aead = nil 194 | cs.aeadOverhead = 0 195 | } 196 | } 197 | 198 | func newCipherState(cipher cipher.Cipher, maxMessageSize int) *CipherState { 199 | return &CipherState{ 200 | cipher: cipher, 201 | maxMessageSize: maxMessageSize, 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /cipherstate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | package nyquist 31 | 32 | import ( 33 | "testing" 34 | 35 | "github.com/stretchr/testify/require" 36 | 37 | "gitlab.com/yawning/nyquist.git/cipher" 38 | ) 39 | 40 | func TestCipherState(t *testing.T) { 41 | for _, v := range []struct { 42 | n string 43 | fn func(*testing.T) 44 | }{ 45 | {"MalformedKey", testCipherStateMalformedKey}, 46 | {"ExhaustedNonce", testCipherStateExhaustedNonce}, 47 | {"MaxMessageSize", testCipherStateMaxMessageSize}, 48 | {"Rekey", testCipherStateRekey}, 49 | {"Reset", testCipherStateReset}, 50 | {"Auth", testCipherStateAuth}, 51 | } { 52 | t.Run(v.n, v.fn) 53 | } 54 | } 55 | 56 | func testCipherStateMalformedKey(t *testing.T) { 57 | require := require.New(t) 58 | cs := newCipherState(cipher.ChaChaPoly, DefaultMaxMessageSize) 59 | 60 | var oversizedKey [33]byte 61 | err := cs.setKey(oversizedKey[:]) 62 | require.Error(err, "cs.setKey(oversized)") 63 | require.Panics(func() { 64 | cs.InitializeKey(oversizedKey[:]) 65 | }, "cs.InitializeKey(oversized)") 66 | 67 | var undersizedKey [16]byte 68 | err = cs.setKey(undersizedKey[:]) 69 | require.Error(err, "cs.setKey(undersized)") 70 | require.Panics(func() { 71 | cs.InitializeKey(undersizedKey[:]) 72 | }, "cs.InitializeKey(undersized)") 73 | } 74 | 75 | func testCipherStateExhaustedNonce(t *testing.T) { 76 | require := require.New(t) 77 | cs := newCipherState(cipher.ChaChaPoly, DefaultMaxMessageSize) 78 | 79 | var testKey [32]byte 80 | cs.InitializeKey(testKey[:]) 81 | cs.SetNonce(maxnonce) 82 | 83 | ciphertext, err := cs.EncryptWithAd(nil, nil, []byte("exhausted nonce plaintext")) 84 | require.Equal(ErrNonceExhausted, err, "cs.EncryptWithAd() - exhauted nonce") 85 | require.Nil(ciphertext, "cs.EncryptWithAd() - exhauted nonce") 86 | 87 | plaintext, err := cs.DecryptWithAd(nil, nil, []byte("exhausted nonce ciphertext")) 88 | require.Equal(ErrNonceExhausted, err, "cs.DecryptWithAd() - exhauted nonce") 89 | require.Nil(plaintext, "cs.DecryptWithAd() - exhausted nonce") 90 | } 91 | 92 | func testCipherStateMaxMessageSize(t *testing.T) { 93 | require := require.New(t) 94 | cs := newCipherState(cipher.DeoxysII, DefaultMaxMessageSize) 95 | 96 | var testKey [32]byte 97 | cs.InitializeKey(testKey[:]) 98 | 99 | // The max message size includes the tag. 100 | ciphertext, err := cs.EncryptWithAd(nil, nil, make([]byte, DefaultMaxMessageSize-15)) 101 | require.Equal(ErrMessageSize, err, "cs.EncryptWithAd(oversized)") 102 | require.Nil(ciphertext, "cs.EncryptWithAd(oversized)") 103 | 104 | maxPlaintext := make([]byte, DefaultMaxMessageSize-16) 105 | ciphertext, err = cs.EncryptWithAd(nil, nil, maxPlaintext) 106 | require.NoError(err, "cs.EncryptWithAd(maxMessageSize-tagLen)") 107 | require.NotNil(ciphertext, "cs.EncryptWithAd(maxMessageSize-tagLen)") 108 | 109 | plaintext, err := cs.DecryptWithAd(nil, nil, make([]byte, DefaultMaxMessageSize+1)) 110 | require.Equal(ErrMessageSize, err, "cs.DecryptWithAd(oversized)") 111 | require.Nil(plaintext, "cs.DecryptWithAd(oversized") 112 | 113 | cs.SetNonce(0) 114 | plaintext, err = cs.DecryptWithAd(nil, nil, ciphertext) 115 | require.NoError(err, "cs.DecryptWithAd(maxMessageSize)") 116 | require.Equal(maxPlaintext, plaintext, "cs.DecryptWithAd(maxMessageSize)") 117 | } 118 | 119 | func testCipherStateRekey(t *testing.T) { 120 | require := require.New(t) 121 | cs := newCipherState(cipher.ChaChaPoly, DefaultMaxMessageSize) 122 | 123 | err := cs.Rekey() 124 | require.Equal(errNoExistingKey, err, "cs.Rekey() - no key") 125 | 126 | testPlaintext := []byte("rekey test plaintext") 127 | 128 | var testKey [32]byte 129 | cs.InitializeKey(testKey[:]) 130 | ciphertext, err := cs.EncryptWithAd(nil, nil, testPlaintext) 131 | require.NoError(err, "cs.EncryptWithAd()") 132 | 133 | err = cs.Rekey() 134 | require.NoError(err, "cs.Rekey()") 135 | 136 | cs.SetNonce(0) 137 | newCiphertext, err := cs.EncryptWithAd(nil, nil, testPlaintext) 138 | require.NoError(err, "cs.EncryptWithAd() - rekeyed") 139 | require.NotEqual(ciphertext, newCiphertext, "rekey actually changed key") 140 | } 141 | 142 | func testCipherStateReset(t *testing.T) { 143 | require := require.New(t) 144 | cs := newCipherState(cipher.DeoxysII, DefaultMaxMessageSize) 145 | 146 | var testKey [32]byte 147 | cs.InitializeKey(testKey[:]) 148 | cs.Reset() 149 | 150 | require.Nil(cs.aead, "cs.Reset()") 151 | } 152 | 153 | func testCipherStateAuth(t *testing.T) { 154 | require := require.New(t) 155 | cs := newCipherState(cipher.DeoxysII, DefaultMaxMessageSize) 156 | 157 | testPlaintext := []byte("auth test plaintext") 158 | 159 | var testKey [32]byte 160 | cs.InitializeKey(testKey[:]) 161 | ciphertext, err := cs.EncryptWithAd(nil, nil, testPlaintext) 162 | require.NoError(err, "cs.EncryptWithAd()") 163 | 164 | cs.SetNonce(0) 165 | _, err = cs.DecryptWithAd(nil, []byte("bogus ad"), ciphertext) 166 | require.Equal(ErrOpen, err, "cs.DecryptWithAd(bogus ad)") 167 | 168 | ciphertext[0] ^= 0xa5 169 | _, err = cs.DecryptWithAd(nil, nil, ciphertext) 170 | require.Equal(ErrOpen, err, "cs.DecryptWithAd(tampered ciphertext)") 171 | 172 | ciphertext[0] ^= 0xa5 173 | plaintext, err := cs.DecryptWithAd(nil, nil, ciphertext) 174 | require.NoError(err, "cs.DecryptWithAd()") 175 | require.Equal(testPlaintext, plaintext, "cs.DecryptWithAd()") 176 | } 177 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | // Package nyquist implements the Noise Protocol Framework. 31 | package nyquist // import "gitlab.com/yawning/nyquist.git" 32 | 33 | import "errors" 34 | 35 | // Version is the revision of the Noise specification implemented. 36 | const Version = 34 37 | 38 | var ( 39 | // ErrNonceExhausted is the error returned when the CipherState's 40 | // nonce space is exhausted. 41 | ErrNonceExhausted = errors.New("nyquist: nonce exhausted") 42 | 43 | // ErrMessageSize is the error returned when an operation fails due 44 | // to the message size being exceeded. 45 | ErrMessageSize = errors.New("nyquist: oversized message") 46 | 47 | // ErrOpen is the error returned on a authenticated decryption failure. 48 | ErrOpen = errors.New("nyquist: decryption failure") 49 | 50 | // ErrInvalidConfig is the error returned when the configuration is invalid. 51 | ErrInvalidConfig = errors.New("nyquist: invalid configuration") 52 | 53 | // ErrOutOfOrder is the error returned when ReadMessage/WriteMessage 54 | // are called out of order, given the handshake's initiator status. 55 | ErrOutOfOrder = errors.New("nyquist: out of order handshake operation") 56 | 57 | // ErrDone is the error returned when the handshake is complete. 58 | ErrDone = errors.New("nyquist: handshake complete") 59 | 60 | // ErrProtocolNotSupported is the error returned when a requested protocol 61 | // is not supported. 62 | ErrProtocolNotSupported = errors.New("nyquist: protocol not supported") 63 | ) 64 | 65 | func truncateTo32BytesMax(b []byte) []byte { 66 | if len(b) <= 32 { 67 | return b 68 | } 69 | 70 | return b[:32] 71 | } 72 | -------------------------------------------------------------------------------- /dh/dh.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | // Package dh implments the Noise Protocol Framework Diffie-Hellman function 31 | // abstract interface and standard DH functions. 32 | package dh // import "gitlab.com/yawning/nyquist.git/dh" 33 | 34 | import ( 35 | "encoding" 36 | "errors" 37 | "fmt" 38 | "io" 39 | 40 | "gitlab.com/yawning/x448.git" 41 | "golang.org/x/crypto/curve25519" 42 | ) 43 | 44 | var ( 45 | // ErrMalformedPrivateKey is the error returned when a serialized 46 | // private key is malformed. 47 | ErrMalformedPrivateKey = errors.New("nyquist/dh: malformed private key") 48 | 49 | // ErrMalformedPublicKey is the error returned when a serialized public 50 | // key is malformed. 51 | ErrMalformedPublicKey = errors.New("nyquist/dh: malformed public key") 52 | 53 | // ErrMismatchedPublicKey is the error returned when a public key for an 54 | // unexpected algorithm is provided to a DH calculation. 55 | ErrMismatchedPublicKey = errors.New("nyquist/dh: mismatched public key") 56 | 57 | supportedDHs = map[string]DH{ 58 | "25519": X25519, 59 | "448": X448, 60 | } 61 | ) 62 | 63 | // DH is a Diffie-Hellman key exchange algorithm. 64 | type DH interface { 65 | fmt.Stringer 66 | 67 | // GenerateKeypair generates a new Diffie-Hellman keypair using the 68 | // provided entropy source. 69 | GenerateKeypair(rng io.Reader) (Keypair, error) 70 | 71 | // ParsePrivateKey parses a binary encoded private key. 72 | ParsePrivateKey(data []byte) (Keypair, error) 73 | 74 | // ParsePublicKey parses a binary encoded public key. 75 | ParsePublicKey(data []byte) (PublicKey, error) 76 | 77 | // Size returns the size of public keys and DH outputs in bytes (`DHLEN`). 78 | Size() int 79 | } 80 | 81 | // FromString returns a DH by algorithm name, or nil. 82 | func FromString(s string) DH { 83 | return supportedDHs[s] 84 | } 85 | 86 | // Keypair is a Diffie-Hellman keypair. 87 | type Keypair interface { 88 | encoding.BinaryMarshaler 89 | encoding.BinaryUnmarshaler 90 | 91 | // DropPrivate discards the private key. 92 | DropPrivate() 93 | 94 | // Public returns the public key of the keypair. 95 | Public() PublicKey 96 | 97 | // DH performs a Diffie-Hellman calculation between the private key 98 | // in the keypair and the provided public key. 99 | DH(publicKey PublicKey) ([]byte, error) 100 | } 101 | 102 | // PublicKey is a Diffie-Hellman public key. 103 | type PublicKey interface { 104 | encoding.BinaryMarshaler 105 | encoding.BinaryUnmarshaler 106 | 107 | // Bytes returns the binary serialized public key. 108 | // 109 | // Warning: Altering the returned slice is unsupported and will lead 110 | // to unexpected behavior. 111 | Bytes() []byte 112 | } 113 | 114 | // X25519 is the 25519 DH function. 115 | var X25519 DH = &dh25519{} 116 | 117 | type dh25519 struct{} 118 | 119 | func (dh *dh25519) String() string { 120 | return "25519" 121 | } 122 | 123 | func (dh *dh25519) GenerateKeypair(rng io.Reader) (Keypair, error) { 124 | var kp Keypair25519 125 | if _, err := io.ReadFull(rng, kp.rawPrivateKey[:]); err != nil { 126 | return nil, err 127 | } 128 | 129 | curve25519.ScalarBaseMult(&kp.publicKey.rawPublicKey, &kp.rawPrivateKey) 130 | 131 | return &kp, nil 132 | } 133 | 134 | func (dh *dh25519) ParsePrivateKey(data []byte) (Keypair, error) { 135 | var kp Keypair25519 136 | if err := kp.UnmarshalBinary(data); err != nil { 137 | return nil, err 138 | } 139 | 140 | return &kp, nil 141 | } 142 | 143 | func (dh *dh25519) ParsePublicKey(data []byte) (PublicKey, error) { 144 | var pk PublicKey25519 145 | if err := pk.UnmarshalBinary(data); err != nil { 146 | return nil, err 147 | } 148 | 149 | return &pk, nil 150 | } 151 | 152 | func (dh *dh25519) Size() int { 153 | return 32 154 | } 155 | 156 | // Keypair25519 is a X25519 keypair. 157 | type Keypair25519 struct { 158 | rawPrivateKey [32]byte 159 | publicKey PublicKey25519 160 | } 161 | 162 | // MarshalBinary marshals the keypair's private key to binary form. 163 | func (kp *Keypair25519) MarshalBinary() ([]byte, error) { 164 | out := make([]byte, 0, len(kp.rawPrivateKey)) 165 | return append(out, kp.rawPrivateKey[:]...), nil 166 | } 167 | 168 | // UnmarshalBinary unmarshals the keypair's private key from binary form, 169 | // and re-derives the corresponding public key. 170 | func (kp *Keypair25519) UnmarshalBinary(data []byte) error { 171 | if len(data) != 32 { 172 | return ErrMalformedPrivateKey 173 | } 174 | 175 | copy(kp.rawPrivateKey[:], data) 176 | curve25519.ScalarBaseMult(&kp.publicKey.rawPublicKey, &kp.rawPrivateKey) 177 | 178 | return nil 179 | } 180 | 181 | // Public returns the public key of the keypair. 182 | func (kp *Keypair25519) Public() PublicKey { 183 | return &kp.publicKey 184 | } 185 | 186 | // DH performs a Diffie-Hellman calculation between the private key in the 187 | // keypair and the provided public key. 188 | func (kp *Keypair25519) DH(publicKey PublicKey) ([]byte, error) { 189 | pubKey, ok := publicKey.(*PublicKey25519) 190 | if !ok { 191 | return nil, ErrMismatchedPublicKey 192 | } 193 | 194 | // Note: This intentionally used the deprecated API, as per-the 195 | // Noise specification (r34) 12.1: 196 | // 197 | // > Invalid public key values will produce an output of all zeros. 198 | // > 199 | // > Alternatively, implementations are allowed to detect inputs that produce an all-zeros 200 | // > output and signal an error instead. This behavior is discouraged because it adds 201 | // > complexity and implementation variance, and does not improve security. This behavior is 202 | // > allowed because it might match the behavior of some software. 203 | var sharedSecret [32]byte 204 | curve25519.ScalarMult(&sharedSecret, &kp.rawPrivateKey, &pubKey.rawPublicKey) //nolint:staticcheck 205 | 206 | return sharedSecret[:], nil 207 | } 208 | 209 | // DropPrivate discards the private key. 210 | func (kp *Keypair25519) DropPrivate() { 211 | for i := range kp.rawPrivateKey { 212 | kp.rawPrivateKey[i] = 0 213 | } 214 | } 215 | 216 | // PublicKey25519 is a X25519 public key. 217 | type PublicKey25519 struct { 218 | rawPublicKey [32]byte 219 | } 220 | 221 | // MarshalBinary marshals the public key to binary form. 222 | func (pk *PublicKey25519) MarshalBinary() ([]byte, error) { 223 | out := make([]byte, 0, len(pk.rawPublicKey)) 224 | return append(out, pk.rawPublicKey[:]...), nil 225 | } 226 | 227 | // UnmarshalBinary unmarshals the public key from binary form. 228 | func (pk *PublicKey25519) UnmarshalBinary(data []byte) error { 229 | if len(data) != 32 { 230 | return ErrMalformedPublicKey 231 | } 232 | 233 | copy(pk.rawPublicKey[:], data) 234 | 235 | return nil 236 | } 237 | 238 | // Bytes returns the binary serialized public key. 239 | // 240 | // Warning: Altering the returned slice is unsupported and will lead to 241 | // unexpected behavior. 242 | func (pk *PublicKey25519) Bytes() []byte { 243 | return pk.rawPublicKey[:] 244 | } 245 | 246 | // X448 is the X448 DH function. 247 | var X448 DH = &dh448{} 248 | 249 | type dh448 struct{} 250 | 251 | func (dh *dh448) String() string { 252 | return "448" 253 | } 254 | 255 | func (dh *dh448) GenerateKeypair(rng io.Reader) (Keypair, error) { 256 | var kp Keypair448 257 | if _, err := io.ReadFull(rng, kp.rawPrivateKey[:]); err != nil { 258 | return nil, err 259 | } 260 | 261 | x448.ScalarBaseMult(&kp.publicKey.rawPublicKey, &kp.rawPrivateKey) 262 | 263 | return &kp, nil 264 | } 265 | 266 | func (dh *dh448) ParsePrivateKey(data []byte) (Keypair, error) { 267 | var kp Keypair448 268 | if err := kp.UnmarshalBinary(data); err != nil { 269 | return nil, err 270 | } 271 | 272 | return &kp, nil 273 | } 274 | 275 | func (dh *dh448) ParsePublicKey(data []byte) (PublicKey, error) { 276 | var pk PublicKey448 277 | if err := pk.UnmarshalBinary(data); err != nil { 278 | return nil, err 279 | } 280 | 281 | return &pk, nil 282 | } 283 | 284 | func (dh *dh448) Size() int { 285 | return 56 286 | } 287 | 288 | // Keypair448 is a X448 keypair. 289 | type Keypair448 struct { 290 | rawPrivateKey [56]byte 291 | publicKey PublicKey448 292 | } 293 | 294 | // MarshalBinary marshals the keypair's private key to binary form. 295 | func (kp *Keypair448) MarshalBinary() ([]byte, error) { 296 | out := make([]byte, 0, len(kp.rawPrivateKey)) 297 | return append(out, kp.rawPrivateKey[:]...), nil 298 | } 299 | 300 | // UnmarshalBinary unmarshals the keypair's private key from binary form, 301 | // and re-derives the corresponding public key. 302 | func (kp *Keypair448) UnmarshalBinary(data []byte) error { 303 | if len(data) != 56 { 304 | return ErrMalformedPrivateKey 305 | } 306 | 307 | copy(kp.rawPrivateKey[:], data) 308 | x448.ScalarBaseMult(&kp.publicKey.rawPublicKey, &kp.rawPrivateKey) 309 | 310 | return nil 311 | } 312 | 313 | // Public returns the public key of the keypair. 314 | func (kp *Keypair448) Public() PublicKey { 315 | return &kp.publicKey 316 | } 317 | 318 | // DH performs a Diffie-Hellman calculation between the private key in the 319 | // keypair and the provided public key. 320 | func (kp *Keypair448) DH(publicKey PublicKey) ([]byte, error) { 321 | pubKey, ok := publicKey.(*PublicKey448) 322 | if !ok { 323 | return nil, ErrMismatchedPublicKey 324 | } 325 | 326 | var sharedSecret [56]byte 327 | x448.ScalarMult(&sharedSecret, &kp.rawPrivateKey, &pubKey.rawPublicKey) 328 | 329 | return sharedSecret[:], nil 330 | } 331 | 332 | // DropPrivate discards the private key. 333 | func (kp *Keypair448) DropPrivate() { 334 | for i := range kp.rawPrivateKey { 335 | kp.rawPrivateKey[i] = 0 336 | } 337 | } 338 | 339 | // PublicKey448 is a X448 public key. 340 | type PublicKey448 struct { 341 | rawPublicKey [56]byte 342 | } 343 | 344 | // MarshalBinary marshals the public key to binary form. 345 | func (pk *PublicKey448) MarshalBinary() ([]byte, error) { 346 | out := make([]byte, 0, len(pk.rawPublicKey)) 347 | return append(out, pk.rawPublicKey[:]...), nil 348 | } 349 | 350 | // UnmarshalBinary unmarshals the public key from binary form. 351 | func (pk *PublicKey448) UnmarshalBinary(data []byte) error { 352 | if len(data) != 56 { 353 | return ErrMalformedPublicKey 354 | } 355 | 356 | copy(pk.rawPublicKey[:], data) 357 | 358 | return nil 359 | } 360 | 361 | // Bytes returns the binary serialized public key. 362 | // 363 | // Warning: Altering the returned slice is unsupported and will lead to 364 | // unexpected behavior. 365 | func (pk *PublicKey448) Bytes() []byte { 366 | return pk.rawPublicKey[:] 367 | } 368 | 369 | // Register registers a new Diffie-Hellman algorithm for use with `FromString()`. 370 | func Register(dh DH) { 371 | supportedDHs[dh.String()] = dh 372 | } 373 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | package nyquist 31 | 32 | import ( 33 | "crypto/rand" 34 | "testing" 35 | 36 | "github.com/stretchr/testify/require" 37 | 38 | "gitlab.com/yawning/nyquist.git/cipher" 39 | "gitlab.com/yawning/nyquist.git/dh" 40 | "gitlab.com/yawning/nyquist.git/hash" 41 | "gitlab.com/yawning/nyquist.git/pattern" 42 | ) 43 | 44 | func TestExample(t *testing.T) { 45 | require := require.New(t) 46 | 47 | // Protocols can be constructed by parsing a protocol name. 48 | protocol, err := NewProtocol("Noise_XX_25519_ChaChaPoly_BLAKE2s") 49 | require.NoError(err, "NewProtocol") 50 | 51 | // Protocols can also be constructed manually. 52 | protocol2 := &Protocol{ 53 | Pattern: pattern.XX, 54 | DH: dh.X25519, 55 | Cipher: cipher.ChaChaPoly, 56 | Hash: hash.BLAKE2s, 57 | } 58 | require.Equal(protocol, protocol2) 59 | 60 | // Each side needs a HandshakeConfig, properly filled out. 61 | aliceStatic, err := protocol.DH.GenerateKeypair(rand.Reader) 62 | require.NoError(err, "Generate Alice's static keypair") 63 | aliceCfg := &HandshakeConfig{ 64 | Protocol: protocol, 65 | LocalStatic: aliceStatic, 66 | IsInitiator: true, 67 | } 68 | 69 | bobStatic, err := protocol.DH.GenerateKeypair(rand.Reader) 70 | require.NoError(err, "Generate Bob's static keypair") 71 | bobCfg := &HandshakeConfig{ 72 | Protocol: protocol, 73 | LocalStatic: bobStatic, 74 | IsInitiator: false, 75 | } 76 | 77 | // Each side then constructs a HandshakeState. 78 | aliceHs, err := NewHandshake(aliceCfg) 79 | require.NoError(err, "NewHandshake(aliceCfg)") 80 | 81 | bobHs, err := NewHandshake(bobCfg) 82 | require.NoError(err, "NewHandshake(bobCfg") 83 | 84 | // Ensuring that HandshakeState.Reset() is called, will make sure that 85 | // the HandshakeState isn't inadvertently reused. 86 | defer aliceHs.Reset() 87 | defer bobHs.Reset() 88 | 89 | // The SymmetricState and CipherState objects embedded in the 90 | // HandshakeState can be accessed while the handshake is in progress, 91 | // though most users likely will not need to do this. 92 | aliceSs := aliceHs.SymmetricState() 93 | require.NotNil(aliceSs, "aliceHs.SymmetricState()") 94 | aliceCs := aliceSs.CipherState() 95 | require.NotNil(aliceCs, "aliceSS.CipherState()") 96 | 97 | // Then, each side calls hs.ReadMessage/hs.WriteMessage as appropriate. 98 | alicePlaintextE := []byte("alice e plaintext") // Handshake message payloads are optional. 99 | aliceMsg1, err := aliceHs.WriteMessage(nil, alicePlaintextE) 100 | require.NoError(err, "aliceHs.WriteMessage(1)") // (alice) -> e 101 | 102 | bobRecv, err := bobHs.ReadMessage(nil, aliceMsg1) 103 | require.NoError(err, "bobHs.ReadMessage(alice1)") 104 | require.Equal(bobRecv, alicePlaintextE) 105 | 106 | bobMsg1, err := bobHs.WriteMessage(nil, nil) // (bob) -> e, ee, s, es 107 | require.NoError(err, "bobHS.WriteMessage(bob1)") 108 | 109 | // When the handshake is complete, ErrDone is returned as the error. 110 | _, err = aliceHs.ReadMessage(nil, bobMsg1) 111 | require.NoError(err, "aliceHS.ReadMessage(bob1)") 112 | 113 | aliceMsg2, err := aliceHs.WriteMessage(nil, nil) // (alice) -> s, se 114 | require.Equal(ErrDone, err, "aliceHs.WriteMessage(alice2)") 115 | 116 | _, err = bobHs.ReadMessage(nil, aliceMsg2) 117 | require.Equal(ErrDone, err, "bobHs.ReadMessage(alice2)") 118 | 119 | // Once a handshake is completed, the CipherState objects, handshake hash 120 | // and various public keys can be pulled out of the HandshakeStatus object. 121 | aliceStatus := aliceHs.GetStatus() 122 | bobStatus := bobHs.GetStatus() 123 | 124 | require.Equal(aliceStatus.HandshakeHash, bobStatus.HandshakeHash, "Handshake hashes match") 125 | require.Equal(aliceStatus.LocalEphemeral.Bytes(), bobStatus.RemoteEphemeral.Bytes()) 126 | require.Equal(bobStatus.LocalEphemeral.Bytes(), aliceStatus.RemoteEphemeral.Bytes()) 127 | require.Equal(aliceStatus.RemoteStatic.Bytes(), bobStatic.Public().Bytes()) 128 | require.Equal(bobStatus.RemoteStatic.Bytes(), aliceStatic.Public().Bytes()) 129 | 130 | // Then the CipherState objects can be used to exchange messages. 131 | aliceTx, aliceRx := aliceStatus.CipherStates[0], aliceStatus.CipherStates[1] 132 | bobRx, bobTx := bobStatus.CipherStates[0], bobStatus.CipherStates[1] // Reversed from alice! 133 | 134 | // Naturally CipherState.Reset() also exists. 135 | defer func() { 136 | aliceTx.Reset() 137 | aliceRx.Reset() 138 | }() 139 | defer func() { 140 | bobTx.Reset() 141 | bobRx.Reset() 142 | }() 143 | 144 | // Alice -> Bob, post-handshake. 145 | alicePlaintext := []byte("alice transport plaintext") 146 | aliceMsg3, err := aliceTx.EncryptWithAd(nil, nil, alicePlaintext) 147 | require.NoError(err, "aliceTx.EncryptWithAd()") 148 | 149 | bobRecv, err = bobRx.DecryptWithAd(nil, nil, aliceMsg3) 150 | require.NoError(err, "bobRx.DecryptWithAd()") 151 | require.Equal(alicePlaintext, bobRecv) 152 | 153 | // Bob -> Alice, post-handshake. 154 | bobPlaintext := []byte("bob transport plaintext") 155 | bobMsg2, err := bobTx.EncryptWithAd(nil, nil, bobPlaintext) 156 | require.NoError(err, "bobTx.EncryptWithAd()") 157 | 158 | aliceRecv, err := aliceRx.DecryptWithAd(nil, nil, bobMsg2) 159 | require.NoError(err, "aliceRx.DecryptWithAd") 160 | require.Equal(bobPlaintext, aliceRecv) 161 | } 162 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gitlab.com/yawning/nyquist.git 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.3 6 | 7 | require ( 8 | github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 9 | github.com/stretchr/testify v1.8.0 10 | gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec 11 | gitlab.com/yawning/x448.git v0.0.0-20221003101044-617eb9b7d9b7 12 | golang.org/x/crypto v0.38.0 13 | ) 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | golang.org/x/sys v0.33.0 // indirect 19 | gopkg.in/yaml.v3 v3.0.1 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4= 5 | github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 10 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 11 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 12 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 13 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 14 | gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= 15 | gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= 16 | gitlab.com/yawning/x448.git v0.0.0-20221003101044-617eb9b7d9b7 h1:ITrNVw6uSwSdEap0RR4us4RV1CHPBHvBZApENRcDk3c= 17 | gitlab.com/yawning/x448.git v0.0.0-20221003101044-617eb9b7d9b7/go.mod h1:BC2R0OW0tAYTMNLB4UMXwkk7WKokoDZP5n73hyLPyCo= 18 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 19 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 20 | golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 21 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 22 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 24 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 25 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 26 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 27 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 28 | -------------------------------------------------------------------------------- /handshakestate.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | package nyquist 31 | 32 | import ( 33 | "crypto/rand" 34 | "errors" 35 | "fmt" 36 | "io" 37 | "strings" 38 | 39 | "gitlab.com/yawning/nyquist.git/cipher" 40 | "gitlab.com/yawning/nyquist.git/dh" 41 | "gitlab.com/yawning/nyquist.git/hash" 42 | "gitlab.com/yawning/nyquist.git/pattern" 43 | ) 44 | 45 | const ( 46 | // DefaultMaxMessageSize is the default maximum message size. 47 | DefaultMaxMessageSize = 65535 48 | 49 | // PreSharedKeySize is the size of the pre-shared symmetric key. 50 | PreSharedKeySize = 32 51 | 52 | protocolPrefix = "Noise" 53 | invalidProtocol = "[invalid protocol]" 54 | ) 55 | 56 | var ( 57 | errTruncatedE = errors.New("nyquist/HandshakeState/ReadMessage/e: truncated message") 58 | errTruncatedS = errors.New("nyquist/HandshakeState/ReadMessage/s: truncated message") 59 | errMissingS = errors.New("nyquist/HandshakeState/WriteMessage/s: s not set") 60 | 61 | errMissingPSK = errors.New("nyquist/New: missing or excessive PreSharedKey(s)") 62 | errBadPSK = errors.New("nyquist/New: malformed PreSharedKey(s)") 63 | ) 64 | 65 | // Protocol is a the protocol to be used with a handshake. 66 | type Protocol struct { 67 | Pattern pattern.Pattern 68 | 69 | DH dh.DH 70 | Cipher cipher.Cipher 71 | Hash hash.Hash 72 | } 73 | 74 | // String returns the string representation of the protocol name. 75 | func (pr *Protocol) String() string { 76 | if pr.Pattern == nil || pr.DH == nil || pr.Cipher == nil || pr.Hash == nil { 77 | return invalidProtocol 78 | } 79 | 80 | parts := []string{ 81 | protocolPrefix, 82 | pr.Pattern.String(), 83 | pr.DH.String(), 84 | pr.Cipher.String(), 85 | pr.Hash.String(), 86 | } 87 | return strings.Join(parts, "_") 88 | } 89 | 90 | // NewProtocol returns a Protocol from the provided (case-sensitive) protocol 91 | // name. Returned protocol objects may be reused across multiple 92 | // HandshakeConfigs. 93 | // 94 | // Note: Only protocols that can be built with the built-in crypto and patterns 95 | // are supported. Using custom crypto/patterns will require manually building 96 | // a Protocol object. 97 | func NewProtocol(s string) (*Protocol, error) { 98 | parts := strings.Split(s, "_") 99 | if len(parts) != 5 || parts[0] != protocolPrefix { 100 | return nil, ErrProtocolNotSupported 101 | } 102 | 103 | var pr Protocol 104 | pr.Pattern = pattern.FromString(parts[1]) 105 | pr.DH = dh.FromString(parts[2]) 106 | pr.Cipher = cipher.FromString(parts[3]) 107 | pr.Hash = hash.FromString(parts[4]) 108 | 109 | if pr.Pattern == nil || pr.DH == nil || pr.Cipher == nil || pr.Hash == nil { 110 | return nil, ErrProtocolNotSupported 111 | } 112 | 113 | return &pr, nil 114 | } 115 | 116 | // HandshakeConfig is a handshake configuration. 117 | // 118 | // Warning: While the config may contain sensitive material like DH private 119 | // keys or a pre-shared key, sanitizing such things are the responsibility of 120 | // the caller, after the handshake completes (or aborts due to an error). 121 | // 122 | // Altering any of the members of this structure while a handshake is in 123 | // progress will result in undefined behavior. 124 | type HandshakeConfig struct { 125 | // Protocol is the noise protocol to use for this handshake. 126 | Protocol *Protocol 127 | 128 | // Prologue is the optional pre-handshake prologue input to be included 129 | // in the handshake hash. 130 | Prologue []byte 131 | 132 | // LocalStatic is the local static keypair, if any (`s`). 133 | LocalStatic dh.Keypair 134 | 135 | // LocalEphemeral is the local ephemeral keypair, if any (`e`). 136 | LocalEphemeral dh.Keypair 137 | 138 | // RemoteStatic is the remote static public key, if any (`rs`). 139 | RemoteStatic dh.PublicKey 140 | 141 | // RemoteEphemeral is the remote ephemeral public key, if any (`re`). 142 | RemoteEphemeral dh.PublicKey 143 | 144 | // PreSharedKeys is the vector of pre-shared symmetric key for PSK mode 145 | // handshakes. 146 | PreSharedKeys [][]byte 147 | 148 | // Observer is the optional handshake observer. 149 | Observer HandshakeObserver 150 | 151 | // Rng is the entropy source to be used when generating new DH key pairs. 152 | // If the value is `nil`, `crypto/rand.Reader` will be used. 153 | Rng io.Reader 154 | 155 | // MaxMessageSize specifies the maximum Noise message size the handshake 156 | // and session will process or generate. If the value is `0`, 157 | // `DefaultMaxMessageSize` will be used. A negative value will disable 158 | // the maximum message size enforcement entirely. 159 | // 160 | // Warning: Values other than the default is a non-standard extension 161 | // to the protocol. 162 | MaxMessageSize int 163 | 164 | // IsInitiator should be set to true if this handshake is in the 165 | // initiator role. 166 | IsInitiator bool 167 | } 168 | 169 | // HandshakeStatus is the status of a handshake. 170 | // 171 | // Warning: It is the caller's responsibility to sanitize the CipherStates 172 | // if desired. Altering any of the members of this structure while a handshake 173 | // is in progress will result in undefined behavior. 174 | type HandshakeStatus struct { 175 | // Err is the error representing the status of the handshake. 176 | // 177 | // It will be `nil` if the handshake is in progess, `ErrDone` if the 178 | // handshake is complete, and any other error if the handshake has failed. 179 | Err error 180 | 181 | // LocalEphemeral is the local ephemeral public key, if any (`e`). 182 | LocalEphemeral dh.PublicKey 183 | 184 | // RemoteStatic is the remote static public key, if any (`rs`). 185 | RemoteStatic dh.PublicKey 186 | 187 | // RemoteEphemeral is the remote ephemeral public key, if any (`re`). 188 | RemoteEphemeral dh.PublicKey 189 | 190 | // CipherStates is the resulting CipherState pair (`(cs1, cs2)`). 191 | // 192 | // Note: To prevent misuse, for one-way patterns `cs2` will be nil. 193 | CipherStates []*CipherState 194 | 195 | // HandshakeHash is the handshake hash (`h`). This field is only set 196 | // once the handshake is completed. 197 | HandshakeHash []byte 198 | } 199 | 200 | // HandshakeObserver is a handshake observer for monitoring handshake status. 201 | type HandshakeObserver interface { 202 | // OnPeerPublicKey will be called when a public key is received from 203 | // the peer, with the handshake pattern token (`pattern.Token_e`, 204 | // `pattern.Token_s`) and public key. 205 | // 206 | // Returning a non-nil error will abort the handshake immediately. 207 | OnPeerPublicKey(pattern.Token, dh.PublicKey) error 208 | } 209 | 210 | func (cfg *HandshakeConfig) getRng() io.Reader { 211 | if cfg.Rng == nil { 212 | return rand.Reader 213 | } 214 | return cfg.Rng 215 | } 216 | 217 | func (cfg *HandshakeConfig) getMaxMessageSize() int { 218 | if cfg.MaxMessageSize > 0 { 219 | return cfg.MaxMessageSize 220 | } 221 | if cfg.MaxMessageSize == 0 { 222 | return DefaultMaxMessageSize 223 | } 224 | return 0 225 | } 226 | 227 | // HandshakeState is the per-handshake state. 228 | type HandshakeState struct { 229 | cfg *HandshakeConfig 230 | 231 | dh dh.DH 232 | patterns []pattern.Message 233 | 234 | ss *SymmetricState 235 | 236 | s dh.Keypair 237 | e dh.Keypair 238 | rs dh.PublicKey 239 | re dh.PublicKey 240 | 241 | status *HandshakeStatus 242 | 243 | patternIndex int 244 | pskIndex int 245 | maxMessageSize int 246 | dhLen int 247 | isInitiator bool 248 | } 249 | 250 | // SymmetricState returns the HandshakeState's encapsulated SymmetricState. 251 | // 252 | // Warning: There should be no reason to call this, ever. 253 | func (hs *HandshakeState) SymmetricState() *SymmetricState { 254 | return hs.ss 255 | } 256 | 257 | // GetStatus returns the HandshakeState's status. 258 | func (hs *HandshakeState) GetStatus() *HandshakeStatus { 259 | return hs.status 260 | } 261 | 262 | // Reset clears the HandshakeState, to prevent future calls. 263 | // 264 | // Warning: If either of the local keypairs were provided by the 265 | // HandshakeConfig, they will be left intact. 266 | func (hs *HandshakeState) Reset() { 267 | if hs.ss != nil { 268 | hs.ss.Reset() 269 | hs.ss = nil 270 | } 271 | if hs.s != nil && hs.s != hs.cfg.LocalStatic { 272 | // Having a local static key, that isn't from the config currently can't 273 | // happen, but this is harmless. 274 | hs.s.DropPrivate() 275 | } 276 | if hs.e != nil && hs.e != hs.cfg.LocalEphemeral { 277 | hs.e.DropPrivate() 278 | } 279 | // TODO: Should this set hs.status.Err? 280 | } 281 | 282 | func (hs *HandshakeState) onWriteTokenE(dst []byte) []byte { 283 | // hs.cfg.LocalEphemeral can be used to pre-generate the ephemeral key, 284 | // so only generate when required. 285 | if hs.e == nil { 286 | if hs.e, hs.status.Err = hs.dh.GenerateKeypair(hs.cfg.getRng()); hs.status.Err != nil { 287 | return nil 288 | } 289 | } 290 | eBytes := hs.e.Public().Bytes() 291 | hs.ss.MixHash(eBytes) 292 | if hs.cfg.Protocol.Pattern.NumPSKs() > 0 { 293 | hs.ss.MixKey(eBytes) 294 | } 295 | hs.status.LocalEphemeral = hs.e.Public() 296 | return append(dst, eBytes...) 297 | } 298 | 299 | func (hs *HandshakeState) onReadTokenE(payload []byte) []byte { 300 | if len(payload) < hs.dhLen { 301 | hs.status.Err = errTruncatedE 302 | return nil 303 | } 304 | eBytes, tail := payload[:hs.dhLen], payload[hs.dhLen:] 305 | if hs.re, hs.status.Err = hs.dh.ParsePublicKey(eBytes); hs.status.Err != nil { 306 | return nil 307 | } 308 | hs.status.RemoteEphemeral = hs.re 309 | if hs.cfg.Observer != nil { 310 | if hs.status.Err = hs.cfg.Observer.OnPeerPublicKey(pattern.Token_e, hs.re); hs.status.Err != nil { 311 | return nil 312 | } 313 | } 314 | hs.ss.MixHash(eBytes) 315 | if hs.cfg.Protocol.Pattern.NumPSKs() > 0 { 316 | hs.ss.MixKey(eBytes) 317 | } 318 | return tail 319 | } 320 | 321 | func (hs *HandshakeState) onWriteTokenS(dst []byte) []byte { 322 | if hs.s == nil { 323 | hs.status.Err = errMissingS 324 | return nil 325 | } 326 | sBytes := hs.s.Public().Bytes() 327 | return hs.ss.EncryptAndHash(dst, sBytes) 328 | } 329 | 330 | func (hs *HandshakeState) onReadTokenS(payload []byte) []byte { 331 | tempLen := hs.dhLen 332 | if hs.ss.cs.HasKey() { 333 | // The spec says `DHLEN + 16`, but doing it this way allows this 334 | // implementation to support any AEAD implementation, regardless of 335 | // tag size. 336 | tempLen += hs.ss.cs.aead.Overhead() 337 | } 338 | if len(payload) < tempLen { 339 | hs.status.Err = errTruncatedS 340 | return nil 341 | } 342 | temp, tail := payload[:tempLen], payload[tempLen:] 343 | 344 | var sBytes []byte 345 | if sBytes, hs.status.Err = hs.ss.DecryptAndHash(nil, temp); hs.status.Err != nil { 346 | return nil 347 | } 348 | if hs.rs, hs.status.Err = hs.dh.ParsePublicKey(sBytes); hs.status.Err != nil { 349 | return nil 350 | } 351 | hs.status.RemoteStatic = hs.rs 352 | if hs.cfg.Observer != nil { 353 | if hs.status.Err = hs.cfg.Observer.OnPeerPublicKey(pattern.Token_s, hs.rs); hs.status.Err != nil { 354 | return nil 355 | } 356 | } 357 | return tail 358 | } 359 | 360 | func (hs *HandshakeState) onTokenEE() { 361 | var eeBytes []byte 362 | if eeBytes, hs.status.Err = hs.e.DH(hs.re); hs.status.Err != nil { 363 | return 364 | } 365 | hs.ss.MixKey(eeBytes) 366 | } 367 | 368 | func (hs *HandshakeState) onTokenES() { 369 | var esBytes []byte 370 | if hs.isInitiator { 371 | esBytes, hs.status.Err = hs.e.DH(hs.rs) 372 | } else { 373 | esBytes, hs.status.Err = hs.s.DH(hs.re) 374 | } 375 | if hs.status.Err != nil { 376 | return 377 | } 378 | hs.ss.MixKey(esBytes) 379 | } 380 | 381 | func (hs *HandshakeState) onTokenSE() { 382 | var seBytes []byte 383 | if hs.isInitiator { 384 | seBytes, hs.status.Err = hs.s.DH(hs.re) 385 | } else { 386 | seBytes, hs.status.Err = hs.e.DH(hs.rs) 387 | } 388 | if hs.status.Err != nil { 389 | return 390 | } 391 | hs.ss.MixKey(seBytes) 392 | } 393 | 394 | func (hs *HandshakeState) onTokenSS() { 395 | var ssBytes []byte 396 | if ssBytes, hs.status.Err = hs.s.DH(hs.rs); hs.status.Err != nil { 397 | return 398 | } 399 | hs.ss.MixKey(ssBytes) 400 | } 401 | 402 | func (hs *HandshakeState) onTokenPsk() { 403 | // PSK is validated at handshake creation. 404 | hs.ss.MixKeyAndHash(hs.cfg.PreSharedKeys[hs.pskIndex]) 405 | hs.pskIndex++ 406 | } 407 | 408 | func (hs *HandshakeState) onDone(dst []byte) ([]byte, error) { 409 | hs.patternIndex++ 410 | if hs.patternIndex < len(hs.patterns) { 411 | return dst, nil 412 | } 413 | 414 | hs.status.Err = ErrDone 415 | cs1, cs2 := hs.ss.Split() 416 | if hs.cfg.Protocol.Pattern.IsOneWay() { 417 | cs2.Reset() 418 | cs2 = nil 419 | } 420 | hs.status.CipherStates = []*CipherState{cs1, cs2} 421 | hs.status.HandshakeHash = hs.ss.GetHandshakeHash() 422 | 423 | // This will end up being called redundantly if the developer has any 424 | // sense at al, but it's cheap foot+gun avoidance. 425 | hs.Reset() 426 | 427 | return dst, hs.status.Err 428 | } 429 | 430 | // WriteMessage processes a write step of the handshake protocol, appending the 431 | // handshake protocol message to dst, and returning the potentially new slice. 432 | // 433 | // Iff the handshake is complete, the error returned will be `ErrDone`. 434 | func (hs *HandshakeState) WriteMessage(dst, payload []byte) ([]byte, error) { 435 | if hs.status.Err != nil { 436 | return nil, hs.status.Err 437 | } 438 | 439 | if hs.isInitiator != (hs.patternIndex&1 == 0) { 440 | hs.status.Err = ErrOutOfOrder 441 | return nil, hs.status.Err 442 | } 443 | 444 | baseLen := len(dst) 445 | for _, v := range hs.patterns[hs.patternIndex] { 446 | switch v { 447 | case pattern.Token_e: 448 | dst = hs.onWriteTokenE(dst) 449 | case pattern.Token_s: 450 | dst = hs.onWriteTokenS(dst) 451 | case pattern.Token_ee: 452 | hs.onTokenEE() 453 | case pattern.Token_es: 454 | hs.onTokenES() 455 | case pattern.Token_se: 456 | hs.onTokenSE() 457 | case pattern.Token_ss: 458 | hs.onTokenSS() 459 | case pattern.Token_psk: 460 | hs.onTokenPsk() 461 | default: 462 | hs.status.Err = errors.New("nyquist/HandshakeState/WriteMessage: invalid token: " + v.String()) 463 | } 464 | 465 | if hs.status.Err != nil { 466 | return nil, hs.status.Err 467 | } 468 | } 469 | 470 | dst = hs.ss.EncryptAndHash(dst, payload) 471 | if hs.maxMessageSize > 0 && len(dst)-baseLen > hs.maxMessageSize { 472 | hs.status.Err = ErrMessageSize 473 | return nil, hs.status.Err 474 | } 475 | 476 | return hs.onDone(dst) 477 | } 478 | 479 | // ReadMessage processes a read step of the handshake protocol, appending the 480 | // authentiated/decrypted message payload to dst, and returning the potentially 481 | // new slice. 482 | // 483 | // Iff the handshake is complete, the error returned will be `ErrDone`. 484 | func (hs *HandshakeState) ReadMessage(dst, payload []byte) ([]byte, error) { 485 | if hs.status.Err != nil { 486 | return nil, hs.status.Err 487 | } 488 | 489 | if hs.maxMessageSize > 0 && len(payload) > hs.maxMessageSize { 490 | hs.status.Err = ErrMessageSize 491 | return nil, hs.status.Err 492 | } 493 | 494 | if hs.isInitiator != (hs.patternIndex&1 != 0) { 495 | hs.status.Err = ErrOutOfOrder 496 | return nil, hs.status.Err 497 | } 498 | 499 | for _, v := range hs.patterns[hs.patternIndex] { 500 | switch v { 501 | case pattern.Token_e: 502 | payload = hs.onReadTokenE(payload) 503 | case pattern.Token_s: 504 | payload = hs.onReadTokenS(payload) 505 | case pattern.Token_ee: 506 | hs.onTokenEE() 507 | case pattern.Token_es: 508 | hs.onTokenES() 509 | case pattern.Token_se: 510 | hs.onTokenSE() 511 | case pattern.Token_ss: 512 | hs.onTokenSS() 513 | case pattern.Token_psk: 514 | hs.onTokenPsk() 515 | default: 516 | hs.status.Err = errors.New("nyquist/HandshakeState/ReadMessage: invalid token: " + v.String()) 517 | } 518 | 519 | if hs.status.Err != nil { 520 | return nil, hs.status.Err 521 | } 522 | } 523 | 524 | dst, hs.status.Err = hs.ss.DecryptAndHash(dst, payload) 525 | if hs.status.Err != nil { 526 | return nil, hs.status.Err 527 | } 528 | 529 | return hs.onDone(dst) 530 | } 531 | 532 | func (hs *HandshakeState) handlePreMessages() error { 533 | preMessages := hs.cfg.Protocol.Pattern.PreMessages() 534 | if len(preMessages) == 0 { 535 | return nil 536 | } 537 | 538 | // Gather all the public keys from the config, from the initiator's 539 | // point of view. 540 | var s, e, rs, re dh.PublicKey 541 | rs, re = hs.rs, hs.re 542 | if hs.s != nil { 543 | s = hs.s.Public() 544 | } 545 | if hs.e != nil { 546 | e = hs.e.Public() 547 | } 548 | if !hs.isInitiator { 549 | s, e, rs, re = rs, re, s, e 550 | } 551 | 552 | for i, keys := range []struct { 553 | s, e dh.PublicKey 554 | side string 555 | }{ 556 | {s, e, "initiator"}, 557 | {rs, re, "responder"}, 558 | } { 559 | if i+1 > len(preMessages) { 560 | break 561 | } 562 | 563 | for _, v := range preMessages[i] { 564 | switch v { 565 | case pattern.Token_e: 566 | // While the specification allows for `e` tokens in the 567 | // pre-messages, there are currently no patterns that use 568 | // such a construct. 569 | // 570 | // While it is possible to generate `e` if it is the local 571 | // one that is missing, that would be stretching a use-case 572 | // that is already somewhat nonsensical. 573 | if keys.e == nil { 574 | return fmt.Errorf("nyquist/New: %s e not set", keys.side) 575 | } 576 | pkBytes := keys.e.Bytes() 577 | hs.ss.MixHash(pkBytes) 578 | if hs.cfg.Protocol.Pattern.NumPSKs() > 0 { 579 | hs.ss.MixKey(pkBytes) 580 | } 581 | case pattern.Token_s: 582 | if keys.s == nil { 583 | return fmt.Errorf("nyquist/New: %s s not set", keys.side) 584 | } 585 | hs.ss.MixHash(keys.s.Bytes()) 586 | default: 587 | return errors.New("nyquist/New: invalid pre-message token: " + v.String()) 588 | } 589 | } 590 | } 591 | 592 | return nil 593 | } 594 | 595 | // NewHandshake constructs a new HandshakeState with the provided configuration. 596 | // This call is equivalent to the `Initialize` HandshakeState call in the 597 | // Noise Protocol Framework specification. 598 | func NewHandshake(cfg *HandshakeConfig) (*HandshakeState, error) { 599 | // TODO: Validate the config further? 600 | 601 | if cfg.Protocol.Pattern.NumPSKs() != len(cfg.PreSharedKeys) { 602 | return nil, errMissingPSK 603 | } 604 | for _, v := range cfg.PreSharedKeys { 605 | if len(v) != PreSharedKeySize { 606 | return nil, errBadPSK 607 | } 608 | } 609 | 610 | maxMessageSize := cfg.getMaxMessageSize() 611 | hs := &HandshakeState{ 612 | cfg: cfg, 613 | dh: cfg.Protocol.DH, 614 | patterns: cfg.Protocol.Pattern.Messages(), 615 | ss: newSymmetricState(cfg.Protocol.Cipher, cfg.Protocol.Hash, maxMessageSize), 616 | s: cfg.LocalStatic, 617 | e: cfg.LocalEphemeral, 618 | rs: cfg.RemoteStatic, 619 | re: cfg.RemoteEphemeral, 620 | status: &HandshakeStatus{ 621 | RemoteStatic: cfg.RemoteStatic, 622 | RemoteEphemeral: cfg.RemoteEphemeral, 623 | }, 624 | maxMessageSize: maxMessageSize, 625 | dhLen: cfg.Protocol.DH.Size(), 626 | isInitiator: cfg.IsInitiator, 627 | } 628 | if cfg.LocalEphemeral != nil { 629 | hs.status.LocalEphemeral = cfg.LocalEphemeral.Public() 630 | } 631 | 632 | hs.ss.InitializeSymmetric([]byte(cfg.Protocol.String())) 633 | hs.ss.MixHash(cfg.Prologue) 634 | if err := hs.handlePreMessages(); err != nil { 635 | return nil, err 636 | } 637 | 638 | return hs, nil 639 | } 640 | -------------------------------------------------------------------------------- /handshakestate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | package nyquist 31 | 32 | import ( 33 | "crypto/rand" 34 | "errors" 35 | "testing" 36 | 37 | "github.com/stretchr/testify/require" 38 | 39 | "gitlab.com/yawning/nyquist.git/dh" 40 | "gitlab.com/yawning/nyquist.git/pattern" 41 | ) 42 | 43 | const xFixedSize = 32 + 32 + 16 + 16 44 | 45 | var errFailReader = errors.New("nyquist/test: failReader.Read") 46 | 47 | type failReader struct{} 48 | 49 | func (r *failReader) Read(p []byte) (int, error) { 50 | return 0, errFailReader 51 | } 52 | 53 | func mustMakeX(t *testing.T, mms int) (*HandshakeState, *HandshakeState) { 54 | require := require.New(t) 55 | 56 | protocol, err := NewProtocol("Noise_X_25519_ChaChaPoly_BLAKE2s") 57 | require.NotNil(protocol, "NewProtocol") 58 | require.NoError(err, "NewProtocol") 59 | 60 | aliceStatic, err := protocol.DH.GenerateKeypair(rand.Reader) 61 | require.NoError(err, "Generate Alice's static keypair") 62 | 63 | bobStatic, err := protocol.DH.GenerateKeypair(rand.Reader) 64 | require.NoError(err, "Generate Bob's static keypair") 65 | 66 | aliceCfg := &HandshakeConfig{ 67 | Protocol: protocol, 68 | LocalStatic: aliceStatic, 69 | RemoteStatic: bobStatic.Public(), 70 | MaxMessageSize: mms, 71 | IsInitiator: true, 72 | } 73 | aliceHs, err := NewHandshake(aliceCfg) 74 | require.NoError(err, "NewHandshake(aliceCfg)") 75 | 76 | bobCfg := &HandshakeConfig{ 77 | Protocol: protocol, 78 | LocalStatic: bobStatic, 79 | MaxMessageSize: mms, 80 | } 81 | bobHs, err := NewHandshake(bobCfg) 82 | require.NoError(err, "NewHandshake(bobCfg)") 83 | 84 | return aliceHs, bobHs 85 | } 86 | 87 | func TestHandshakeState(t *testing.T) { 88 | for _, v := range []struct { 89 | n string 90 | fn func(*testing.T) 91 | }{ 92 | {"BadProtocol", testHandshakeStateBadProtocol}, 93 | {"KeygenFailure", testHandshakeStateKeygenFailure}, 94 | {"TruncatedE", testHandshakeStateTruncatedE}, 95 | {"TruncatedS", testHandshakeStateTruncatedS}, 96 | {"OutOfOrder", testHandshakeStateOutOfOrder}, 97 | {"MaxMessageSize", testHandshakeStateMaxMessageSize}, 98 | {"Observer", testHandshakeStateObserver}, 99 | {"BadPSK", testHandshakeStateBadPSK}, 100 | {"MissingS", testHandshakeStateMissingS}, 101 | } { 102 | t.Run(v.n, v.fn) 103 | } 104 | } 105 | 106 | func testHandshakeStateBadProtocol(t *testing.T) { 107 | require := require.New(t) 108 | 109 | var nilProtocol Protocol 110 | s := nilProtocol.String() 111 | require.Equal(invalidProtocol, s, "Bad protocol object String()s") 112 | 113 | protocol, err := NewProtocol("Signal_XX_25519_ChaChaPoly_BLAKE2s") 114 | require.Nil(protocol, "NewProtocol(invalid)") 115 | require.Equal(ErrProtocolNotSupported, err, "NewProtocol(invalid)") 116 | } 117 | 118 | func testHandshakeStateKeygenFailure(t *testing.T) { 119 | require := require.New(t) 120 | 121 | protocol, err := NewProtocol("Noise_N_25519_ChaChaPoly_BLAKE2s") 122 | require.NotNil(protocol, "NewProtocol") 123 | require.NoError(err, "NewProtocol") 124 | 125 | bobStatic, err := protocol.DH.GenerateKeypair(rand.Reader) 126 | require.NoError(err, "Generate Bob's static keypair") 127 | 128 | aliceCfg := &HandshakeConfig{ 129 | Protocol: protocol, 130 | RemoteStatic: bobStatic.Public(), 131 | Rng: &failReader{}, 132 | IsInitiator: true, 133 | } 134 | aliceHs, err := NewHandshake(aliceCfg) 135 | require.NoError(err, "NewHandshake(aliceCfg)") 136 | 137 | dst, err := aliceHs.WriteMessage(nil, nil) 138 | require.Nil(dst, "aliceHs.WriteMessage - e generation will fail") 139 | require.Equal(errFailReader, err, "aliceHs.WriteMessage - e generation will fail") 140 | } 141 | 142 | func testHandshakeStateTruncatedE(t *testing.T) { 143 | require := require.New(t) 144 | 145 | _, bobHs := mustMakeX(t, 0) 146 | dst, err := bobHs.ReadMessage(nil, make([]byte, 31)) 147 | require.Nil(dst, "bobHs.ReadMessage - truncated E") 148 | require.Equal(errTruncatedE, err) 149 | } 150 | 151 | func testHandshakeStateTruncatedS(t *testing.T) { 152 | require := require.New(t) 153 | 154 | aliceHs, bobHs := mustMakeX(t, 0) 155 | dst, err := aliceHs.WriteMessage(nil, nil) 156 | require.Equal(ErrDone, err, "aliceHs.WriteMessage") 157 | require.Len(dst, xFixedSize, "aliceHs.WriteMessage") // e, es, s, ss 158 | 159 | dst, err = bobHs.ReadMessage(nil, dst[:32+32]) // Clip off both tags. 160 | require.Nil(dst, "bobHs.ReadMessage - truncated s") 161 | require.Equal(errTruncatedS, err) 162 | } 163 | 164 | func testHandshakeStateOutOfOrder(t *testing.T) { 165 | require := require.New(t) 166 | 167 | aliceHs, bobHs := mustMakeX(t, 0) 168 | 169 | dst, err := aliceHs.ReadMessage(nil, []byte("never read, whatever")) 170 | require.Nil(dst, "aliceHs.ReadMessage - out of order") 171 | require.Equal(ErrOutOfOrder, err, "aliceHs.ReadMessage - out of order") 172 | 173 | dst, err = bobHs.WriteMessage(nil, []byte("placeholder plaintext pls ignore")) 174 | require.Nil(dst, "bobHs.WriteMessage - after critical failure") 175 | require.Equal(ErrOutOfOrder, err, "bobHs.WriteMessage - after critical failure") 176 | 177 | // While we are here and have two busted HandshakeState objects, make 178 | // sure that the errors are sticky. 179 | dst, err = aliceHs.WriteMessage(nil, []byte("placeholder plaintext pls ignore")) 180 | require.Nil(dst, "aliceHs.WriteMessage - after critical failure") 181 | require.Equal(ErrOutOfOrder, err, "aliceHs.WriteMessage - after critical failure") 182 | require.Equal(err, aliceHs.GetStatus().Err) 183 | 184 | dst, err = bobHs.ReadMessage(nil, []byte("never read, whatever")) 185 | require.Nil(dst, "bobHs.ReadMessage - after critical failure") 186 | require.Equal(ErrOutOfOrder, err, "bobHs.WriteMessage - after critical failure") 187 | require.Equal(err, bobHs.GetStatus().Err) 188 | } 189 | 190 | func testHandshakeStateMaxMessageSize(t *testing.T) { 191 | const testMMS = 127 192 | 193 | require := require.New(t) 194 | 195 | // Ensure that exactly the maximum message size passes. 196 | aliceHs, bobHs := mustMakeX(t, testMMS) 197 | maxSizedPayload := make([]byte, testMMS-xFixedSize) 198 | _, _ = rand.Read(maxSizedPayload) 199 | dst, err := aliceHs.WriteMessage(nil, maxSizedPayload) 200 | require.Equal(ErrDone, err, "aliceHs.WriteMessage(maxSize)") 201 | require.Len(dst, testMMS, "aliceHs.WriteMessage(maxSize)") 202 | 203 | dst, err = bobHs.ReadMessage(nil, dst) 204 | require.Equal(ErrDone, err, "bobHs.ReadMessage(maxSize)") 205 | require.Equal(maxSizedPayload, dst, "bobHs.ReadMessage(maxSize)") 206 | 207 | // Ensure that the payloads at 1 past the limit fail. 208 | aliceHs, bobHs = mustMakeX(t, testMMS) 209 | oversizedPayload := append(maxSizedPayload, 23) 210 | dst, err = aliceHs.WriteMessage(nil, oversizedPayload) 211 | require.Equal(ErrMessageSize, err, "aliceHs.WriteMessage(overSize)") 212 | require.Nil(dst, "aliceHs.WriteMessage(overSize)") 213 | 214 | dst, err = bobHs.ReadMessage(nil, make([]byte, testMMS+1)) 215 | require.Equal(ErrMessageSize, err, "bobHs.ReadMessage(overSize)") 216 | require.Nil(dst, "bobHs.ReadMessage(overSize)") 217 | 218 | // Ensure that a negative mms disables limit enforcement. 219 | aliceHs, bobHs = mustMakeX(t, -1) 220 | giantPayload := make([]byte, DefaultMaxMessageSize*10) 221 | _, _ = rand.Read(giantPayload) 222 | dst, err = aliceHs.WriteMessage(nil, giantPayload) 223 | require.Equal(ErrDone, err, "aliceHs.WriteMessage(giantSize)") 224 | require.Len(dst, len(giantPayload)+xFixedSize, "aliceHs.WriteMessage(giantSize)") 225 | 226 | dst, err = bobHs.ReadMessage(nil, dst) 227 | require.Equal(ErrDone, err, "bobHs.ReadMessage(giantSize)") 228 | require.Equal(giantPayload, dst, "bobHs.ReadMessage(giantSize)") 229 | } 230 | 231 | type proxyObserver struct { 232 | callbackFn func(pattern.Token, dh.PublicKey) error 233 | } 234 | 235 | func (proxy *proxyObserver) OnPeerPublicKey(token pattern.Token, pk dh.PublicKey) error { 236 | return proxy.callbackFn(token, pk) 237 | } 238 | 239 | func testHandshakeStateObserver(t *testing.T) { 240 | require := require.New(t) 241 | 242 | aliceHs, bobHs := mustMakeX(t, 0) 243 | 244 | var seenE, seenS bool 245 | proxy := &proxyObserver{ 246 | callbackFn: func(token pattern.Token, pk dh.PublicKey) error { 247 | switch token { 248 | case pattern.Token_e: 249 | require.False(seenE) 250 | require.Equal(pk.Bytes(), aliceHs.GetStatus().LocalEphemeral.Bytes()) 251 | seenE = true 252 | case pattern.Token_s: 253 | require.False(seenS) 254 | require.Equal(pk.Bytes(), aliceHs.cfg.LocalStatic.Public().Bytes()) 255 | seenS = true 256 | default: 257 | panic("unknown token: " + token.String()) 258 | } 259 | return nil 260 | }, 261 | } 262 | bobHs.cfg.Observer = proxy // Yeah this is ugly, but it works. 263 | 264 | dst, err := aliceHs.WriteMessage(nil, nil) 265 | require.Equal(ErrDone, err, "aliceHs.WriteMessage()") 266 | require.Len(dst, xFixedSize, "aliceHs.WriteMessage()") 267 | 268 | dst, err = bobHs.ReadMessage(nil, dst) 269 | require.Equal(ErrDone, err, "bobHs.ReadMessage()") 270 | require.Len(dst, 0, "bobHs.ReadMessage()") 271 | require.True(seenE, "bobHs observer saw alice e") 272 | require.True(seenS, "bobHs observer saw alice s") 273 | } 274 | 275 | func testHandshakeStateBadPSK(t *testing.T) { 276 | require := require.New(t) 277 | 278 | protocol, err := NewProtocol("Noise_Xpsk1_25519_ChaChaPoly_BLAKE2s") 279 | require.NotNil(protocol, "NewProtocol") 280 | require.NoError(err, "NewProtocol") 281 | 282 | aliceCfg := &HandshakeConfig{ 283 | Protocol: protocol, 284 | IsInitiator: true, 285 | } 286 | _, err = NewHandshake(aliceCfg) 287 | require.Equal(errMissingPSK, err, "NewHandshake() - missing PSK") 288 | 289 | aliceCfg.PreSharedKeys = [][]byte{ 290 | make([]byte, PreSharedKeySize+1), 291 | } 292 | _, err = NewHandshake(aliceCfg) 293 | require.Equal(errBadPSK, err, "NewHandshake() - malformed PSK") 294 | } 295 | 296 | func testHandshakeStateMissingS(t *testing.T) { 297 | require := require.New(t) 298 | 299 | protocol, err := NewProtocol("Noise_X_25519_ChaChaPoly_BLAKE2s") 300 | require.NotNil(protocol, "NewProtocol") 301 | require.NoError(err, "NewProtocol") 302 | 303 | aliceCfg := &HandshakeConfig{ 304 | Protocol: protocol, 305 | IsInitiator: true, 306 | } 307 | _, err = NewHandshake(aliceCfg) 308 | require.EqualError(err, "nyquist/New: responder s not set", "NewHandshake() - missing s") 309 | 310 | aliceHs, _ := mustMakeX(t, 0) 311 | aliceHs.s = nil // Not the best way to do this, but this also works. 312 | dst, err := aliceHs.WriteMessage(nil, nil) 313 | require.Equal(errMissingS, err, "aliceHs.WriteMessage()") 314 | require.Nil(dst, "aliceHs.WriteMessage()") 315 | } 316 | -------------------------------------------------------------------------------- /hash/hash.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | // Package hash implments the Noise Protocol Framework hash function abstract 31 | // interface and standard hash functions. 32 | package hash // import "gitlab.com/yawning/nyquist.git/hash" 33 | 34 | import ( 35 | "crypto/sha256" 36 | "crypto/sha512" 37 | "fmt" 38 | "hash" 39 | 40 | "golang.org/x/crypto/blake2b" 41 | "golang.org/x/crypto/blake2s" 42 | ) 43 | 44 | var ( 45 | // SHA256 is the SHA256 hash function. 46 | SHA256 Hash = &hashSha256{} 47 | 48 | // SHA512 is the SHA512 hash function. 49 | SHA512 Hash = &hashSha512{} 50 | 51 | // BLAKE2s is the BLAKE2s hash function. 52 | BLAKE2s Hash = &hashBlake2s{} 53 | 54 | // BLAKE2b is the BLAKE2b hash function. 55 | BLAKE2b Hash = &hashBlake2b{} 56 | 57 | supportedHashes = map[string]Hash{ 58 | "SHA256": SHA256, 59 | "SHA512": SHA512, 60 | "BLAKE2s": BLAKE2s, 61 | "BLAKE2b": BLAKE2b, 62 | } 63 | ) 64 | 65 | // Hash is a collision-resistant cryptographic hash function factory. 66 | type Hash interface { 67 | fmt.Stringer 68 | 69 | // New constructs a new `hash.Hash` instance. 70 | New() hash.Hash 71 | 72 | // Size returns the hash function's digest size in bytes (`HASHLEN`). 73 | Size() int 74 | } 75 | 76 | // FromString returns a Hash by algorithm name, or nil. 77 | func FromString(s string) Hash { 78 | return supportedHashes[s] 79 | } 80 | 81 | type hashSha256 struct{} 82 | 83 | func (h *hashSha256) String() string { 84 | return "SHA256" 85 | } 86 | 87 | func (h *hashSha256) New() hash.Hash { 88 | return sha256.New() 89 | } 90 | 91 | func (h *hashSha256) Size() int { 92 | return sha256.Size 93 | } 94 | 95 | type hashSha512 struct{} 96 | 97 | func (h *hashSha512) String() string { 98 | return "SHA512" 99 | } 100 | 101 | func (h *hashSha512) New() hash.Hash { 102 | return sha512.New() 103 | } 104 | 105 | func (h *hashSha512) Size() int { 106 | return sha512.Size 107 | } 108 | 109 | type hashBlake2s struct{} 110 | 111 | func (h *hashBlake2s) String() string { 112 | return "BLAKE2s" 113 | } 114 | 115 | func (h *hashBlake2s) New() hash.Hash { 116 | ret, _ := blake2s.New256(nil) 117 | return ret 118 | } 119 | 120 | func (h *hashBlake2s) Size() int { 121 | return blake2s.Size 122 | } 123 | 124 | type hashBlake2b struct{} 125 | 126 | func (h *hashBlake2b) String() string { 127 | return "BLAKE2b" 128 | } 129 | 130 | func (h *hashBlake2b) New() hash.Hash { 131 | ret, _ := blake2b.New512(nil) 132 | return ret 133 | } 134 | 135 | func (h *hashBlake2b) Size() int { 136 | return blake2b.Size 137 | } 138 | 139 | // Register registers a new hash algorithm for use with `FromString()`. 140 | func Register(hash Hash) { 141 | supportedHashes[hash.String()] = hash 142 | } 143 | -------------------------------------------------------------------------------- /pattern/deferred.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | package pattern 31 | 32 | var ( 33 | // NK1 is the NK1 deferred pattern. 34 | NK1 Pattern = &builtIn{ 35 | name: "NK1", 36 | preMessages: []Message{ 37 | nil, 38 | {Token_s}, 39 | }, 40 | messages: []Message{ 41 | {Token_e}, 42 | {Token_e, Token_ee, Token_es}, 43 | }, 44 | } 45 | 46 | // NX1 is the NX1 deferred pattern. 47 | NX1 Pattern = &builtIn{ 48 | name: "NX1", 49 | messages: []Message{ 50 | {Token_e}, 51 | {Token_e, Token_ee, Token_s}, 52 | {Token_es}, 53 | }, 54 | } 55 | 56 | // X1N is the X1N deferred pattern. 57 | X1N Pattern = &builtIn{ 58 | name: "X1N", 59 | messages: []Message{ 60 | {Token_e}, 61 | {Token_e, Token_ee}, 62 | {Token_s}, 63 | {Token_se}, 64 | }, 65 | } 66 | 67 | // X1K is the X1K deferred pattern. 68 | X1K Pattern = &builtIn{ 69 | name: "X1K", 70 | preMessages: []Message{ 71 | nil, 72 | {Token_s}, 73 | }, 74 | messages: []Message{ 75 | {Token_e, Token_es}, 76 | {Token_e, Token_ee}, 77 | {Token_s}, 78 | {Token_se}, 79 | }, 80 | } 81 | 82 | // XK1 is the XK1 deferred pattern. 83 | XK1 Pattern = &builtIn{ 84 | name: "XK1", 85 | preMessages: []Message{ 86 | nil, 87 | {Token_s}, 88 | }, 89 | messages: []Message{ 90 | {Token_e}, 91 | {Token_e, Token_ee, Token_es}, 92 | {Token_s, Token_se}, 93 | }, 94 | } 95 | 96 | // X1K1 is the X1K1 deferred pattern. 97 | X1K1 Pattern = &builtIn{ 98 | name: "X1K1", 99 | preMessages: []Message{ 100 | nil, 101 | {Token_s}, 102 | }, 103 | messages: []Message{ 104 | {Token_e}, 105 | {Token_e, Token_ee, Token_es}, 106 | {Token_s}, 107 | {Token_se}, 108 | }, 109 | } 110 | 111 | // X1X is the X1X deferred pattern. 112 | X1X Pattern = &builtIn{ 113 | name: "X1X", 114 | messages: []Message{ 115 | {Token_e}, 116 | {Token_e, Token_ee, Token_s, Token_es}, 117 | {Token_s}, 118 | {Token_se}, 119 | }, 120 | } 121 | 122 | // XX1 is the XX1 deferred pattern. 123 | XX1 Pattern = &builtIn{ 124 | name: "XX1", 125 | messages: []Message{ 126 | {Token_e}, 127 | {Token_e, Token_ee, Token_s}, 128 | {Token_es, Token_s, Token_se}, 129 | }, 130 | } 131 | 132 | // X1X1 is the X1X1 deferred pattern. 133 | X1X1 Pattern = &builtIn{ 134 | name: "X1X1", 135 | messages: []Message{ 136 | {Token_e}, 137 | {Token_e, Token_ee, Token_s}, 138 | {Token_es, Token_s}, 139 | {Token_se}, 140 | }, 141 | } 142 | 143 | // K1N is the K1N deferred pattern. 144 | K1N Pattern = &builtIn{ 145 | name: "K1N", 146 | preMessages: []Message{ 147 | {Token_s}, 148 | }, 149 | messages: []Message{ 150 | {Token_e}, 151 | {Token_e, Token_ee}, 152 | {Token_se}, 153 | }, 154 | } 155 | 156 | // K1K is the K1K deferred pattern. 157 | K1K Pattern = &builtIn{ 158 | name: "K1K", 159 | preMessages: []Message{ 160 | {Token_s}, 161 | {Token_s}, 162 | }, 163 | messages: []Message{ 164 | {Token_e, Token_es}, 165 | {Token_e, Token_ee}, 166 | {Token_se}, 167 | }, 168 | } 169 | 170 | // KK1 is the KK1 deferred pattern. 171 | KK1 Pattern = &builtIn{ 172 | name: "KK1", 173 | preMessages: []Message{ 174 | {Token_s}, 175 | {Token_s}, 176 | }, 177 | messages: []Message{ 178 | {Token_e}, 179 | {Token_e, Token_ee, Token_se, Token_es}, 180 | }, 181 | } 182 | 183 | // K1K1 is the K1K1 deferred pattern. 184 | K1K1 Pattern = &builtIn{ 185 | name: "K1K1", 186 | preMessages: []Message{ 187 | {Token_s}, 188 | {Token_s}, 189 | }, 190 | messages: []Message{ 191 | {Token_e}, 192 | {Token_e, Token_ee, Token_es}, 193 | {Token_se}, 194 | }, 195 | } 196 | 197 | // K1X is the K1X deferred pattern. 198 | K1X Pattern = &builtIn{ 199 | name: "K1X", 200 | preMessages: []Message{ 201 | {Token_s}, 202 | }, 203 | messages: []Message{ 204 | {Token_e}, 205 | {Token_e, Token_ee, Token_s, Token_es}, 206 | {Token_se}, 207 | }, 208 | } 209 | 210 | // KX1 is the KX1 deferred pattern. 211 | KX1 Pattern = &builtIn{ 212 | name: "KX1", 213 | preMessages: []Message{ 214 | {Token_s}, 215 | }, 216 | messages: []Message{ 217 | {Token_e}, 218 | {Token_e, Token_ee, Token_se, Token_s}, 219 | {Token_es}, 220 | }, 221 | } 222 | 223 | // K1X1 is the K1X1 deferred pattern. 224 | K1X1 Pattern = &builtIn{ 225 | name: "K1X1", 226 | preMessages: []Message{ 227 | {Token_s}, 228 | }, 229 | messages: []Message{ 230 | {Token_e}, 231 | {Token_e, Token_ee, Token_s}, 232 | {Token_se, Token_es}, 233 | }, 234 | } 235 | 236 | // I1N is the I1N deferred pattern. 237 | I1N Pattern = &builtIn{ 238 | name: "I1N", 239 | messages: []Message{ 240 | {Token_e, Token_s}, 241 | {Token_e, Token_ee}, 242 | {Token_se}, 243 | }, 244 | } 245 | 246 | // I1K is the I1K deferred pattern. 247 | I1K Pattern = &builtIn{ 248 | name: "I1K", 249 | preMessages: []Message{ 250 | nil, 251 | {Token_s}, 252 | }, 253 | messages: []Message{ 254 | {Token_e, Token_es, Token_s}, 255 | {Token_e, Token_ee}, 256 | {Token_se}, 257 | }, 258 | } 259 | 260 | // IK1 is the IK1 deferred pattern. 261 | IK1 Pattern = &builtIn{ 262 | name: "IK1", 263 | preMessages: []Message{ 264 | nil, 265 | {Token_s}, 266 | }, 267 | messages: []Message{ 268 | {Token_e, Token_s}, 269 | {Token_e, Token_ee, Token_se, Token_es}, 270 | }, 271 | } 272 | 273 | // I1K1 is the I1K1 deferred pattern. 274 | I1K1 Pattern = &builtIn{ 275 | name: "I1K1", 276 | preMessages: []Message{ 277 | nil, 278 | {Token_s}, 279 | }, 280 | messages: []Message{ 281 | {Token_e, Token_s}, 282 | {Token_e, Token_ee, Token_es}, 283 | {Token_se}, 284 | }, 285 | } 286 | 287 | // I1X is the I1X deferred pattern. 288 | I1X Pattern = &builtIn{ 289 | name: "I1X", 290 | messages: []Message{ 291 | {Token_e, Token_s}, 292 | {Token_e, Token_ee, Token_s, Token_es}, 293 | {Token_se}, 294 | }, 295 | } 296 | 297 | // IX1 is the IX1 deferred pattern. 298 | IX1 Pattern = &builtIn{ 299 | name: "IX1", 300 | messages: []Message{ 301 | {Token_e, Token_s}, 302 | {Token_e, Token_ee, Token_se, Token_s}, 303 | {Token_es}, 304 | }, 305 | } 306 | 307 | // I1X1 is the I1X1 deferred pattern. 308 | I1X1 Pattern = &builtIn{ 309 | name: "I1X1", 310 | messages: []Message{ 311 | {Token_e, Token_s}, 312 | {Token_e, Token_ee, Token_s}, 313 | {Token_se, Token_es}, 314 | }, 315 | } 316 | ) 317 | -------------------------------------------------------------------------------- /pattern/interactive.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | package pattern 31 | 32 | var ( 33 | // NN is the NN interactive (fundemental) pattern. 34 | NN Pattern = &builtIn{ 35 | name: "NN", 36 | messages: []Message{ 37 | {Token_e}, 38 | {Token_e, Token_ee}, 39 | }, 40 | } 41 | 42 | // NK is the NK interactive (fundemental) pattern. 43 | NK Pattern = &builtIn{ 44 | name: "NK", 45 | preMessages: []Message{ 46 | nil, 47 | {Token_s}, 48 | }, 49 | messages: []Message{ 50 | {Token_e, Token_es}, 51 | {Token_e, Token_ee}, 52 | }, 53 | } 54 | 55 | // NX is the NX interactive (fundemental) pattern. 56 | NX Pattern = &builtIn{ 57 | name: "NX", 58 | messages: []Message{ 59 | {Token_e}, 60 | {Token_e, Token_ee, Token_s, Token_es}, 61 | }, 62 | } 63 | 64 | // XN is the XN interactive (fundemental) pattern. 65 | XN Pattern = &builtIn{ 66 | name: "XN", 67 | messages: []Message{ 68 | {Token_e}, 69 | {Token_e, Token_ee}, 70 | {Token_s, Token_se}, 71 | }, 72 | } 73 | 74 | // XK is the XK interactive (fundemental) pattern. 75 | XK Pattern = &builtIn{ 76 | name: "XK", 77 | preMessages: []Message{ 78 | nil, 79 | {Token_s}, 80 | }, 81 | messages: []Message{ 82 | {Token_e, Token_es}, 83 | {Token_e, Token_ee}, 84 | {Token_s, Token_se}, 85 | }, 86 | } 87 | 88 | // XX is the XX interactive (fundemental) pattern. 89 | XX Pattern = &builtIn{ 90 | name: "XX", 91 | messages: []Message{ 92 | {Token_e}, 93 | {Token_e, Token_ee, Token_s, Token_es}, 94 | {Token_s, Token_se}, 95 | }, 96 | } 97 | 98 | // KN is the KN interactive (fundemental) pattern. 99 | KN Pattern = &builtIn{ 100 | name: "KN", 101 | preMessages: []Message{ 102 | {Token_s}, 103 | }, 104 | messages: []Message{ 105 | {Token_e}, 106 | {Token_e, Token_ee, Token_se}, 107 | }, 108 | } 109 | 110 | // KK is the KK interactive (fundemental) pattern. 111 | KK Pattern = &builtIn{ 112 | name: "KK", 113 | preMessages: []Message{ 114 | {Token_s}, 115 | {Token_s}, 116 | }, 117 | messages: []Message{ 118 | {Token_e, Token_es, Token_ss}, 119 | {Token_e, Token_ee, Token_se}, 120 | }, 121 | } 122 | 123 | // KX is the KX interactive (fundemental) pattern. 124 | KX Pattern = &builtIn{ 125 | name: "KX", 126 | preMessages: []Message{ 127 | {Token_s}, 128 | }, 129 | messages: []Message{ 130 | {Token_e}, 131 | {Token_e, Token_ee, Token_se, Token_s, Token_es}, 132 | }, 133 | } 134 | 135 | // IN is the IN interactive (fundemental) pattern. 136 | IN Pattern = &builtIn{ 137 | name: "IN", 138 | messages: []Message{ 139 | {Token_e, Token_s}, 140 | {Token_e, Token_ee, Token_se}, 141 | }, 142 | } 143 | 144 | // IK is the IK interactive (fundemental) pattern. 145 | IK Pattern = &builtIn{ 146 | name: "IK", 147 | preMessages: []Message{ 148 | nil, 149 | {Token_s}, 150 | }, 151 | messages: []Message{ 152 | {Token_e, Token_es, Token_s, Token_ss}, 153 | {Token_e, Token_ee, Token_se}, 154 | }, 155 | } 156 | 157 | // IX is the IX interactive (fundemental) pattern. 158 | IX Pattern = &builtIn{ 159 | name: "IX", 160 | messages: []Message{ 161 | {Token_e, Token_s}, 162 | {Token_e, Token_ee, Token_se, Token_s, Token_es}, 163 | }, 164 | } 165 | 166 | // NNpsk0 is the NNpsk0 interactive (fundemental) pattern. 167 | NNpsk0 = mustMakePSK(NN, "psk0") 168 | 169 | // NNpsk2 is the NNpsk2 interactive (fundemental) pattern. 170 | NNpsk2 = mustMakePSK(NN, "psk2") 171 | 172 | // NKpsk0 is the NKpsk0 interactive (fundemental) pattern. 173 | NKpsk0 = mustMakePSK(NK, "psk0") 174 | 175 | // NKpsk2 is the NKpsk2 interactive (fundemental) pattern. 176 | NKpsk2 = mustMakePSK(NK, "psk2") 177 | 178 | // NXpsk2 is the NXpsk2 interactive (fundemental) pattern. 179 | NXpsk2 = mustMakePSK(NX, "psk2") 180 | 181 | // XNpsk3 is the XNpsk3 interactive (fundemental) pattern. 182 | XNpsk3 = mustMakePSK(XN, "psk3") 183 | 184 | // XKpsk3 is the XKpsk3 interactive (fundemental) pattern. 185 | XKpsk3 = mustMakePSK(XK, "psk3") 186 | 187 | // XXpsk3 is the XXpsk3 interactive (fundemental) pattern. 188 | XXpsk3 = mustMakePSK(XX, "psk3") 189 | 190 | // KNpsk0 is the KNpsk0 interactive (fundemental) pattern. 191 | KNpsk0 = mustMakePSK(KN, "psk0") 192 | 193 | // KNpsk2 is the KNpsk2 interactive (fundemental) pattern. 194 | KNpsk2 = mustMakePSK(KN, "psk2") 195 | 196 | // KKpsk0 is the KKpsk0 interactive (fundemental) pattern. 197 | KKpsk0 = mustMakePSK(KK, "psk0") 198 | 199 | // KKpsk2 is the KKpsk2 interactive (fundemental) pattern. 200 | KKpsk2 = mustMakePSK(KK, "psk2") 201 | 202 | // KXpsk2 is the KXpsk2 interactive (fundemental) pattern. 203 | KXpsk2 = mustMakePSK(KX, "psk2") 204 | 205 | // INpsk1 is the INpsk1 interactive (fundemental) pattern. 206 | INpsk1 = mustMakePSK(IN, "psk1") 207 | 208 | // INpsk2 is the INpsk2 interactive (fundemental) pattern. 209 | INpsk2 = mustMakePSK(IN, "psk2") 210 | 211 | // IKpsk1 is the IKpsk1 interactive (fundemental) pattern. 212 | IKpsk1 = mustMakePSK(IK, "psk1") 213 | 214 | // IKpsk2 is the IKpsk2 interactive (fundemental) pattern. 215 | IKpsk2 = mustMakePSK(IK, "psk2") 216 | 217 | // IXpsk2 is the IXpsk2 interactive (fundemental) pattern. 218 | IXpsk2 = mustMakePSK(IX, "psk2") 219 | ) 220 | -------------------------------------------------------------------------------- /pattern/oneway.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | package pattern 31 | 32 | var ( 33 | // N is the N one-way handshake pattern. 34 | N Pattern = &builtIn{ 35 | name: "N", 36 | preMessages: []Message{ 37 | nil, 38 | {Token_s}, 39 | }, 40 | messages: []Message{ 41 | {Token_e, Token_es}, 42 | }, 43 | isOneWay: true, 44 | } 45 | 46 | // K is the K one-way handshake pattern. 47 | K Pattern = &builtIn{ 48 | name: "K", 49 | preMessages: []Message{ 50 | {Token_s}, 51 | {Token_s}, 52 | }, 53 | messages: []Message{ 54 | {Token_e, Token_es, Token_ss}, 55 | }, 56 | isOneWay: true, 57 | } 58 | 59 | // X is the X one-way handshake pattern. 60 | X Pattern = &builtIn{ 61 | name: "X", 62 | preMessages: []Message{ 63 | nil, 64 | {Token_s}, 65 | }, 66 | messages: []Message{ 67 | {Token_e, Token_es, Token_s, Token_ss}, 68 | }, 69 | isOneWay: true, 70 | } 71 | 72 | // Npsk0 is the Npsk0 one-way handshake pattern. 73 | Npsk0 = mustMakePSK(N, "psk0") 74 | 75 | // Kpsk0 is the Kpsk0 one-way handshake pattern. 76 | Kpsk0 = mustMakePSK(K, "psk0") 77 | 78 | // Xpsk1 is the Xpsk1 one-way handshake pattern. 79 | Xpsk1 = mustMakePSK(X, "psk1") 80 | ) 81 | -------------------------------------------------------------------------------- /pattern/pattern.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | // Package pattern implements the Noise Protocol Framework handshake pattern 31 | // abstract interface and standard patterns. 32 | package pattern // import "gitlab.com/yawning/nyquist.git/pattern" 33 | 34 | import "fmt" 35 | 36 | var supportedPatterns = make(map[string]Pattern) 37 | 38 | // Token is a Noise handshake pattern token. 39 | type Token uint8 40 | 41 | const ( 42 | Token_invalid Token = iota 43 | Token_e 44 | Token_s 45 | Token_ee 46 | Token_es 47 | Token_se 48 | Token_ss 49 | Token_psk 50 | ) 51 | 52 | // String returns the string representation of a Token. 53 | func (t Token) String() string { 54 | switch t { 55 | case Token_e: 56 | return "e" 57 | case Token_s: 58 | return "s" 59 | case Token_ee: 60 | return "ee" 61 | case Token_es: 62 | return "es" 63 | case Token_se: 64 | return "se" 65 | case Token_ss: 66 | return "ss" 67 | case Token_psk: 68 | return "psk" 69 | default: 70 | return fmt.Sprintf("[invalid token: %d]", int(t)) 71 | } 72 | } 73 | 74 | // Message is a sequence of pattern tokens. 75 | type Message []Token 76 | 77 | // Pattern is a handshake pattern. 78 | type Pattern interface { 79 | fmt.Stringer 80 | 81 | // PreMessages returns the pre-message message patterns. 82 | PreMessages() []Message 83 | 84 | // Mesages returns the message patterns. 85 | Messages() []Message 86 | 87 | // NumPSKs returns the number of `psk` modifiers in the pattern. 88 | NumPSKs() int 89 | 90 | // IsOneWay returns true iff the pattern is one-way. 91 | IsOneWay() bool 92 | } 93 | 94 | // FromString returns a Pattern by pattern name, or nil. 95 | func FromString(s string) Pattern { 96 | return supportedPatterns[s] 97 | } 98 | 99 | type builtIn struct { 100 | name string 101 | preMessages []Message 102 | messages []Message 103 | numPSKs int 104 | isOneWay bool 105 | } 106 | 107 | func (pa *builtIn) String() string { 108 | return pa.name 109 | } 110 | 111 | func (pa *builtIn) PreMessages() []Message { 112 | return pa.preMessages 113 | } 114 | 115 | func (pa *builtIn) Messages() []Message { 116 | return pa.messages 117 | } 118 | 119 | func (pa *builtIn) NumPSKs() int { 120 | return pa.numPSKs 121 | } 122 | 123 | func (pa *builtIn) IsOneWay() bool { 124 | return pa.isOneWay 125 | } 126 | 127 | // Register registers a new pattern for use with `FromString()`. 128 | func Register(pa Pattern) error { 129 | if err := IsValid(pa); err != nil { 130 | return err 131 | } 132 | supportedPatterns[pa.String()] = pa 133 | 134 | return nil 135 | } 136 | 137 | func init() { 138 | for _, v := range []Pattern{ 139 | // One-way patterns. 140 | N, 141 | K, 142 | X, 143 | Npsk0, 144 | Kpsk0, 145 | Xpsk1, 146 | 147 | // Interactive (fundemental) patterns. 148 | NN, 149 | NK, 150 | NX, 151 | XN, 152 | XK, 153 | XX, 154 | KN, 155 | KK, 156 | KX, 157 | IN, 158 | IK, 159 | IX, 160 | NNpsk0, 161 | NNpsk2, 162 | NKpsk0, 163 | NKpsk2, 164 | NXpsk2, 165 | XNpsk3, 166 | XKpsk3, 167 | XXpsk3, 168 | KNpsk0, 169 | KNpsk2, 170 | KKpsk0, 171 | KKpsk2, 172 | KXpsk2, 173 | INpsk1, 174 | INpsk2, 175 | IKpsk1, 176 | IKpsk2, 177 | IXpsk2, 178 | 179 | // Deferred patterns. 180 | NK1, 181 | NX1, 182 | X1N, 183 | X1K, 184 | XK1, 185 | X1K1, 186 | X1X, 187 | XX1, 188 | X1X1, 189 | K1N, 190 | K1K, 191 | KK1, 192 | K1K1, 193 | K1X, 194 | KX1, 195 | K1X1, 196 | I1N, 197 | I1K, 198 | IK1, 199 | I1K1, 200 | I1X, 201 | IX1, 202 | I1X1, 203 | } { 204 | if err := Register(v); err != nil { 205 | panic("nyquist/pattern: failed to register built-in pattern: " + err.Error()) 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /pattern/psk.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | package pattern 31 | 32 | import ( 33 | "errors" 34 | "strconv" 35 | "strings" 36 | ) 37 | 38 | const prefixPSK = "psk" 39 | 40 | // MakePSK applies `psk` modifiers to an existing pattern, returning the new 41 | // pattern. 42 | func MakePSK(template Pattern, modifier string) (Pattern, error) { 43 | if template.NumPSKs() > 0 { 44 | return nil, errors.New("nyquist/pattern: PSK template pattern already has PSKs") 45 | } 46 | 47 | pa := &builtIn{ 48 | name: template.String() + modifier, 49 | preMessages: template.PreMessages(), 50 | isOneWay: template.IsOneWay(), 51 | } 52 | 53 | // Deep-copy the messages. 54 | templateMessages := template.Messages() 55 | pa.messages = make([]Message, 0, len(templateMessages)) 56 | for _, v := range templateMessages { 57 | pa.messages = append(pa.messages, append(Message{}, v...)) 58 | } 59 | 60 | // Apply the psk modifiers to all of the patterns. 61 | indexes := make(map[int]bool) 62 | splitModifier := strings.Split(modifier, "+") 63 | for _, v := range splitModifier { 64 | if !strings.HasPrefix(v, prefixPSK) { 65 | return nil, errors.New("nyquist/pattern: non-PSK modifier: " + v) 66 | } 67 | v = strings.TrimPrefix(v, prefixPSK) 68 | pskIndex, err := strconv.Atoi(v) 69 | if err != nil { 70 | return nil, errors.New("nyquist/pattern: failed to parse PSK index: " + err.Error()) 71 | } 72 | 73 | if indexes[pskIndex] { 74 | return nil, errors.New("nyquist/pattern: redundant PSK modifier: " + prefixPSK + v) 75 | } 76 | if pskIndex < 0 || pskIndex > len(templateMessages) { 77 | return nil, errors.New("nyquist/pattern: invalid PSK modifier: " + prefixPSK + v) 78 | } 79 | switch pskIndex { 80 | case 0: 81 | pa.messages[0] = append(Message{Token_psk}, pa.messages[0]...) 82 | default: 83 | idx := pskIndex - 1 84 | pa.messages[idx] = append(pa.messages[idx], Token_psk) 85 | } 86 | indexes[pskIndex] = true 87 | } 88 | pa.numPSKs = len(indexes) 89 | 90 | return pa, nil 91 | } 92 | 93 | func mustMakePSK(template Pattern, modifier string) Pattern { 94 | pa, err := MakePSK(template, modifier) 95 | if err != nil { 96 | panic(err) 97 | } 98 | return pa 99 | } 100 | -------------------------------------------------------------------------------- /pattern/validity.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | package pattern 31 | 32 | import ( 33 | "errors" 34 | "fmt" 35 | ) 36 | 37 | // IsValid checks a pattern for validity according to the handshake pattern 38 | // validity rules, and implementation limitations. 39 | // 40 | // Warning: This is not particularly fast, and should only be called when 41 | // validating custom patterns, or testing. 42 | func IsValid(pa Pattern) error { 43 | initTokens := make(map[Token]bool) 44 | respTokens := make(map[Token]bool) 45 | 46 | getSide := func(idx int) (map[Token]bool, bool, string) { 47 | isInitiator := idx&1 == 0 48 | if isInitiator { 49 | return initTokens, true, "initiator" 50 | } 51 | return respTokens, false, "responder" 52 | } 53 | 54 | inEither := func(t Token) bool { 55 | return initTokens[t] || respTokens[t] 56 | } 57 | 58 | inBoth := func(t Token) bool { 59 | return initTokens[t] && respTokens[t] 60 | } 61 | 62 | // Sanity-check the pre-messages. 63 | preMessages := pa.PreMessages() 64 | if len(preMessages) > 2 { 65 | return errors.New("nyquist/pattern: excessive pre-messages") 66 | } 67 | for i, msg := range preMessages { 68 | m, _, side := getSide(i) 69 | for _, v := range msg { 70 | switch v { 71 | case Token_e, Token_s: 72 | // 2. Parties must not send their static public key or ephemeral 73 | // public key more than once per handshake. 74 | if m[v] { 75 | return fmt.Errorf("nyquist/pattern: redundant pre-message token (%s): %s", side, v) 76 | } 77 | m[v] = true 78 | default: 79 | return fmt.Errorf("nyquist/pattern: invalid pre-message token: %s", v) 80 | } 81 | } 82 | } 83 | 84 | // Validate the messages. 85 | messages := pa.Messages() 86 | if len(messages) == 0 { 87 | return errors.New("nyquist/pattern: no handshake messages") 88 | } 89 | if pa.IsOneWay() && len(messages) != 1 { 90 | return errors.New("nyquist/pattern: excessive messages for one-way pattern") 91 | } 92 | var numDHs, numPSKs int 93 | for i, msg := range messages { 94 | m, isInitiator, side := getSide(i) 95 | for _, v := range msg { 96 | switch v { 97 | case Token_e, Token_s: 98 | // 2. Parties must not send their static public key or ephemeral 99 | // public key more than once per handshake. 100 | if m[v] { 101 | return fmt.Errorf("nyquist/pattern: redundant public key (%s): %s", side, v) 102 | } 103 | case Token_ee, Token_es, Token_se, Token_ss: 104 | // 3. Parties must not perform a DH calculation more than once 105 | // per handshake. 106 | if inEither(v) { 107 | return fmt.Errorf("nyquist/pattern: redundant DH calcuation: %s", v) 108 | } 109 | numDHs++ 110 | case Token_psk: 111 | numPSKs++ 112 | default: 113 | return fmt.Errorf("nyquist/pattern: invalid message token: %s", v) 114 | } 115 | 116 | // 1. Parties can only perform DH between private keys and public 117 | // keys they posess. 118 | var impossibleDH Token 119 | switch v { 120 | case Token_ee: 121 | if !inBoth(Token_e) { 122 | impossibleDH = v 123 | } 124 | case Token_ss: 125 | if !inBoth(Token_s) { 126 | impossibleDH = v 127 | } 128 | case Token_es: 129 | if !initTokens[Token_e] || !respTokens[Token_s] { 130 | impossibleDH = v 131 | } 132 | case Token_se: 133 | if !initTokens[Token_s] || !respTokens[Token_e] { 134 | impossibleDH = v 135 | } 136 | default: 137 | } 138 | if impossibleDH != Token_invalid { 139 | return fmt.Errorf("nyquist/pattern: impossible DH: %s", v) 140 | } 141 | 142 | m[v] = true 143 | } 144 | 145 | // 4. After performing a DH between a remote public key (either static 146 | // or ephemeral) and the local static key, the local party must not 147 | // call ENCRYPT() unless it has also performed a DH between its local 148 | // ephemeral key and the remote public key. 149 | var missingDH Token 150 | if isInitiator { 151 | if inEither(Token_se) && !inEither(Token_ee) { 152 | missingDH = Token_ee 153 | } 154 | if inEither(Token_ss) && !inEither(Token_es) { 155 | missingDH = Token_es 156 | } 157 | } else { 158 | if inEither(Token_es) && !inEither(Token_ee) { 159 | missingDH = Token_ee 160 | } 161 | if inEither(Token_ss) && !inEither(Token_se) { 162 | missingDH = Token_se 163 | } 164 | } 165 | if missingDH != Token_invalid { 166 | return fmt.Errorf("nyquist/pattern: missing DH calculation (%s): %s", side, missingDH) 167 | } 168 | 169 | if inEither(Token_psk) { 170 | // A party may not send any encrypted data after it processes a 171 | // "psk" token unless it has previously sent an epmeheral public 172 | // key (an "e" token), either before or after the "psk" token. 173 | if !m[Token_e] { 174 | return fmt.Errorf("nyquist/pattern: payload after pre-shared key without ephemeral (%s)", side) 175 | } 176 | } 177 | } 178 | 179 | // Patterns without any DH calculations may be "valid", but are 180 | // nonsensical. 181 | if numDHs == 0 { 182 | return errors.New("nyquist/pattern: no DH calculations at all") 183 | } 184 | 185 | // Make sure the PSK hint interface function is implemented correctly. 186 | if numPSKs != pa.NumPSKs() { 187 | return errors.New("nyquist/pattern: NumPSKs() mismatch with (pre-)messages") 188 | } 189 | 190 | return nil 191 | } 192 | -------------------------------------------------------------------------------- /pattern/validity_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | package pattern 31 | 32 | import ( 33 | "testing" 34 | 35 | "github.com/stretchr/testify/require" 36 | ) 37 | 38 | func TestBuiltInPatternValidity(t *testing.T) { 39 | for _, v := range supportedPatterns { 40 | t.Run(v.String(), func(t *testing.T) { 41 | require := require.New(t) 42 | 43 | err := IsValid(v) 44 | require.NoError(err, "IsValid(pattern)") 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /symmetricstate.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | package nyquist 31 | 32 | import ( 33 | "io" 34 | 35 | "golang.org/x/crypto/hkdf" 36 | 37 | "gitlab.com/yawning/nyquist.git/cipher" 38 | "gitlab.com/yawning/nyquist.git/hash" 39 | ) 40 | 41 | // SymmetricState encapsulates all symmetric cryptography used by the Noise 42 | // protocol during a handshake. 43 | // 44 | // Warning: There should be no reason to interact directly with this ever. 45 | type SymmetricState struct { 46 | cipher cipher.Cipher 47 | hash hash.Hash 48 | 49 | cs *CipherState 50 | 51 | ck []byte 52 | h []byte 53 | 54 | hashLen int 55 | } 56 | 57 | // InitializeSymmetric initializes the SymmetricState with the initial 58 | // chaining key and handshake hash, based on the protocol name. 59 | func (ss *SymmetricState) InitializeSymmetric(protocolName []byte) { 60 | if len(protocolName) <= ss.hashLen { 61 | ss.h = make([]byte, ss.hashLen) 62 | copy(ss.h, protocolName) 63 | } else { 64 | h := ss.hash.New() 65 | _, _ = h.Write(protocolName) 66 | ss.h = h.Sum(nil) 67 | } 68 | 69 | ss.ck = make([]byte, 0, ss.hashLen) 70 | ss.ck = append(ss.ck, ss.h...) 71 | 72 | ss.cs.InitializeKey(nil) 73 | } 74 | 75 | // MixKey mixes the provided material with the chaining key, and initializes 76 | // the encapsulated CipherState's key with the output. 77 | func (ss *SymmetricState) MixKey(inputKeyMaterial []byte) { 78 | tempK := make([]byte, ss.hashLen) 79 | 80 | ss.hkdfHash(inputKeyMaterial, ss.ck, tempK) 81 | tempK = truncateTo32BytesMax(tempK) 82 | ss.cs.InitializeKey(tempK) 83 | } 84 | 85 | // MixHash mixes the provided data with the handshake hash. 86 | func (ss *SymmetricState) MixHash(data []byte) { 87 | h := ss.hash.New() 88 | _, _ = h.Write(ss.h) 89 | _, _ = h.Write(data) 90 | ss.h = h.Sum(ss.h[:0]) 91 | } 92 | 93 | // MixKeyAndHash mises the provided material with the chaining key, and mixes 94 | // the handshake and initializes the encapsulated CipherState with the output. 95 | func (ss *SymmetricState) MixKeyAndHash(inputKeyMaterial []byte) { 96 | tempH, tempK := make([]byte, ss.hashLen), make([]byte, ss.hashLen) 97 | 98 | ss.hkdfHash(inputKeyMaterial, ss.ck, tempH, tempK) 99 | ss.MixHash(tempH) 100 | tempK = truncateTo32BytesMax(tempK) 101 | ss.cs.InitializeKey(tempK) 102 | } 103 | 104 | // GetHandshakeHash returns the handshake hash `h`. 105 | func (ss *SymmetricState) GetHandshakeHash() []byte { 106 | return ss.h 107 | } 108 | 109 | // EncryptAndHash encrypts and authenticates the plaintext, mixes the 110 | // ciphertext with the handshake hash, appends the ciphertext to dst, 111 | // and returns the potentially new slice. 112 | func (ss *SymmetricState) EncryptAndHash(dst, plaintext []byte) []byte { 113 | var err error 114 | ciphertextOff := len(dst) 115 | if dst, err = ss.cs.EncryptWithAd(dst, ss.h, plaintext); err != nil { 116 | panic("nyquist/SymmetricState: encryptAndHash() failed: " + err.Error()) 117 | } 118 | ss.MixHash(dst[ciphertextOff:]) 119 | return dst 120 | } 121 | 122 | // DecryptAndHash authenticates and decrypts the ciphertext, mixes the 123 | // ciphertext with the handshake hash, appends the plaintext to dst, 124 | // and returns the potentially new slice. 125 | func (ss *SymmetricState) DecryptAndHash(dst, ciphertext []byte) ([]byte, error) { 126 | // `dst` and `ciphertext` could alias, so save a copy of `h` so that the 127 | // `MixHash()` call can be called prior to `DecryptWithAd`. 128 | hPrev := make([]byte, 0, len(ss.h)) 129 | hPrev = append(hPrev, ss.h...) 130 | 131 | ss.MixHash(ciphertext) 132 | 133 | return ss.cs.DecryptWithAd(dst, hPrev, ciphertext) 134 | } 135 | 136 | // Split returns a pair of CipherState objects for encrypted transport messages. 137 | func (ss *SymmetricState) Split() (*CipherState, *CipherState) { 138 | tempK1, tempK2 := make([]byte, ss.hashLen), make([]byte, ss.hashLen) 139 | 140 | ss.hkdfHash(nil, tempK1, tempK2) 141 | tempK1 = truncateTo32BytesMax(tempK1) 142 | tempK2 = truncateTo32BytesMax(tempK2) 143 | 144 | c1, c2 := newCipherState(ss.cipher, ss.cs.maxMessageSize), newCipherState(ss.cipher, ss.cs.maxMessageSize) 145 | c1.InitializeKey(tempK1) 146 | c2.InitializeKey(tempK2) 147 | 148 | return c1, c2 149 | } 150 | 151 | // CipherState returns the SymmetricState's encapsualted CipherState. 152 | // 153 | // Warning: There should be no reason to call this, ever. 154 | func (ss *SymmetricState) CipherState() *CipherState { 155 | return ss.cs 156 | } 157 | 158 | func (ss *SymmetricState) hkdfHash(inputKeyMaterial []byte, outputs ...[]byte) { 159 | // There is no way to sanitize the HKDF reader state. While it is tempting 160 | // to just write a HKDF implementation that supports sanitization, neither 161 | // `crypto/hmac` nor the actual hash function implementations support 162 | // sanitization correctly either due to: 163 | // 164 | // * `Reset()`ing a HMAC instance resets it to the keyed (initialized) 165 | // state. 166 | // * All of the concrete hash function implementations do not `Reset()` 167 | // the cloned instance when `Sum([]byte)` is called. 168 | 169 | r := hkdf.New(ss.hash.New, inputKeyMaterial, ss.ck, nil) 170 | for _, output := range outputs { 171 | if len(output) != ss.hashLen { 172 | panic("nyquist/SymmetricState: non-HASHLEN sized output to HKDF-HASH") 173 | } 174 | _, _ = io.ReadFull(r, output) 175 | } 176 | } 177 | 178 | // Reset clears the SymmetricState, to prevent future calls. 179 | // 180 | // Warning: The transcript hash (`h`) is left intact to allow for clearing 181 | // this state as early as possible, while preserving the ability to call 182 | // `GetHandshakeHash`. 183 | func (ss *SymmetricState) Reset() { 184 | if ss.ck != nil { 185 | ss.ck = nil 186 | } 187 | if ss.cs != nil { 188 | ss.cs.Reset() 189 | ss.cs = nil 190 | } 191 | } 192 | 193 | func newSymmetricState(cipher cipher.Cipher, hash hash.Hash, maxMessageSize int) *SymmetricState { 194 | return &SymmetricState{ 195 | cipher: cipher, 196 | hash: hash, 197 | cs: newCipherState(cipher, maxMessageSize), 198 | hashLen: hash.Size(), 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /vectors/hexbuffer.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | package vectors 31 | 32 | import "encoding/hex" 33 | 34 | // HexBuffer is a byte slice that will marshal to/unmarshal from a hex encoded 35 | // string. 36 | type HexBuffer []byte 37 | 38 | // MarshalText implements the TextMarshaler interface. 39 | func (x *HexBuffer) MarshalText() ([]byte, error) { 40 | return []byte(hex.EncodeToString(*x)), nil 41 | } 42 | 43 | // UnmarshalText implements the TextUnmarshaler interface. 44 | func (x *HexBuffer) UnmarshalText(data []byte) error { 45 | b, err := hex.DecodeString(string(data)) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | if len(b) == 0 { 51 | *x = nil 52 | } else { 53 | *x = b 54 | } 55 | 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /vectors/vectors.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | // Package vectors provides types for the JSON formatted test vectors. 31 | package vectors // import "gitlab.com/yawning/nyquist.git/vectors" 32 | 33 | // Message is a test vector handshake message. 34 | type Message struct { 35 | Payload HexBuffer `json:"payload"` 36 | Ciphertext HexBuffer `json:"ciphertext"` 37 | } 38 | 39 | // Vector is a single test vector case. 40 | type Vector struct { 41 | Name string `json:"name"` 42 | 43 | ProtocolName string `json:"protocol_name"` 44 | Fail bool `json:"fail"` 45 | Fallback bool `json:"fallback"` 46 | FallbackPattern string `json:"fallback_pattern"` 47 | 48 | InitPrologue HexBuffer `json:"init_prologue"` 49 | InitPsks []HexBuffer `json:"init_psks"` 50 | InitStatic HexBuffer `json:"init_static"` 51 | InitEphemeral HexBuffer `json:"init_ephemeral"` 52 | InitRemoteStatic HexBuffer `json:"init_remote_static"` 53 | 54 | RespPrologue HexBuffer `json:"resp_prologue"` 55 | RespPsks []HexBuffer `json:"resp_psks"` 56 | RespStatic HexBuffer `json:"resp_static"` 57 | RespEphemeral HexBuffer `json:"resp_ephemeral"` 58 | RespRemoteStatic HexBuffer `json:"resp_remote_static"` 59 | 60 | HandshakeHash HexBuffer `json:"handshake_hash"` 61 | 62 | Messages []Message `json:"messages"` 63 | } 64 | 65 | // File is a collection of test vectors. 66 | type File struct { 67 | Vectors []Vector `json:"vectors"` 68 | } 69 | -------------------------------------------------------------------------------- /vectors_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019, 2021 Yawning Angel. All rights reserved. 2 | // 3 | // Redistribution and use in source and binary forms, with or without 4 | // modification, are permitted provided that the following conditions are 5 | // met: 6 | // 7 | // 1. Redistributions of source code must retain the above copyright 8 | // notice, this list of conditions and the following disclaimer. 9 | // 10 | // 2. Redistributions in binary form must reproduce the above copyright 11 | // notice, this list of conditions and the following disclaimer in the 12 | // documentation and/or other materials provided with the distribution. 13 | // 14 | // 3. Neither the name of the copyright holder nor the names of its 15 | // contributors may be used to endorse or promote products derived from 16 | // this software without specific prior written permission. 17 | // 18 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 19 | // IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 20 | // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 21 | // PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 | // HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED 24 | // TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 25 | // PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 26 | // LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 27 | // NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | package nyquist 31 | 32 | import ( 33 | "encoding/json" 34 | "io/ioutil" 35 | "path/filepath" 36 | "testing" 37 | 38 | "github.com/stretchr/testify/require" 39 | 40 | "gitlab.com/yawning/nyquist.git/dh" 41 | "gitlab.com/yawning/nyquist.git/pattern" 42 | "gitlab.com/yawning/nyquist.git/vectors" 43 | ) 44 | 45 | func TestVectors(t *testing.T) { 46 | // Register the multi-PSK suites covered by the snow test vectors. 47 | t.Run("RegisterMultiPSK", doRegisterMultiPSK) 48 | 49 | srcImpls := []struct { 50 | name string 51 | skipOk bool 52 | }{ 53 | {"cacophony", false}, 54 | {"snow", false}, 55 | {"noise-c-basic", true}, // PSK patterns use a non-current name. 56 | } 57 | 58 | for _, v := range srcImpls { 59 | t.Run(v.name, func(t *testing.T) { 60 | doTestVectorsFile(t, v.name, v.skipOk) 61 | }) 62 | } 63 | } 64 | 65 | func doRegisterMultiPSK(t *testing.T) { 66 | require := require.New(t) 67 | 68 | for _, v := range []struct { 69 | base pattern.Pattern 70 | modifier string 71 | }{ 72 | {pattern.NN, "psk0+psk2"}, 73 | {pattern.NX, "psk0+psk1+psk2"}, 74 | {pattern.XN, "psk1+psk3"}, 75 | {pattern.XK, "psk0+psk3"}, 76 | {pattern.KN, "psk1+psk2"}, 77 | {pattern.KK, "psk0+psk2"}, 78 | {pattern.IN, "psk1+psk2"}, 79 | {pattern.IK, "psk0+psk2"}, 80 | {pattern.IX, "psk0+psk2"}, 81 | {pattern.XX, "psk0+psk1"}, 82 | {pattern.XX, "psk0+psk2"}, 83 | {pattern.XX, "psk0+psk3"}, 84 | {pattern.XX, "psk0+psk1+psk2+psk3"}, 85 | } { 86 | pa, err := pattern.MakePSK(v.base, v.modifier) 87 | require.NoError(err, "MakePSK(%s, %s)", v.base.String(), v.modifier) 88 | 89 | err = pattern.Register(pa) 90 | require.NoError(err, "Register(%s)", pa) 91 | } 92 | } 93 | 94 | func doTestVectorsFile(t *testing.T, impl string, skipOk bool) { 95 | require := require.New(t) 96 | fn := filepath.Join("./testdata/", impl+".txt") 97 | b, err := ioutil.ReadFile(fn) 98 | require.NoError(err, "ReadFile(%v)", fn) 99 | 100 | var vectorsFile vectors.File 101 | err = json.Unmarshal(b, &vectorsFile) 102 | require.NoError(err, "json.Unmarshal") 103 | 104 | for _, v := range vectorsFile.Vectors { 105 | if v.Name == "" { 106 | v.Name = v.ProtocolName 107 | } 108 | if v.ProtocolName == "" { 109 | // The noise-c test vectors have `name` but not `protocol_name`. 110 | v.ProtocolName = v.Name 111 | } 112 | if v.ProtocolName == "" { 113 | continue 114 | } 115 | t.Run(v.Name, func(t *testing.T) { 116 | doTestVector(t, &v, skipOk) 117 | }) 118 | } 119 | } 120 | 121 | func doTestVector(t *testing.T, v *vectors.Vector, skipOk bool) { 122 | if v.Fail { 123 | t.Skip("fail tests not supported") 124 | } 125 | if v.Fallback || v.FallbackPattern != "" { 126 | t.Skip("fallback patterns not supported") 127 | } 128 | 129 | require := require.New(t) 130 | initCfg, respCfg := configsFromVector(t, v, skipOk) 131 | 132 | initHs, err := NewHandshake(initCfg) 133 | require.NoError(err, "NewHandshake(initCfg)") 134 | defer initHs.Reset() 135 | 136 | respHs, err := NewHandshake(respCfg) 137 | require.NoError(err, "NewHandshake(respCfg)") 138 | defer respHs.Reset() 139 | 140 | t.Run("Initiator", func(t *testing.T) { 141 | doTestVectorMessages(t, initHs, v) 142 | }) 143 | t.Run("Responder", func(t *testing.T) { 144 | doTestVectorMessages(t, respHs, v) 145 | }) 146 | } 147 | 148 | func doTestVectorMessages(t *testing.T, hs *HandshakeState, v *vectors.Vector) { 149 | require := require.New(t) 150 | 151 | writeOnEven := hs.isInitiator 152 | 153 | var ( 154 | status *HandshakeStatus 155 | txCs, rxCs *CipherState 156 | ) 157 | for idx, msg := range v.Messages { 158 | var ( 159 | dst, expectedDst []byte 160 | err error 161 | ) 162 | 163 | if status == nil { 164 | // Handshake message(s). 165 | if (idx&1 == 0) == writeOnEven { 166 | dst, err = hs.WriteMessage(nil, msg.Payload) 167 | expectedDst = msg.Ciphertext 168 | } else { 169 | dst, err = hs.ReadMessage(nil, msg.Ciphertext) 170 | expectedDst = msg.Payload 171 | } 172 | 173 | switch err { 174 | case ErrDone: 175 | status = hs.GetStatus() 176 | require.Equal(status.Err, ErrDone, "Status.Err indicates normal completion") 177 | if len(v.HandshakeHash) > 0 { 178 | // The handshake hash is an optional field in the test vectors, 179 | // and the ones generated by snow, don't include it. 180 | require.EqualValues(v.HandshakeHash, status.HandshakeHash, "HandshakeHash matches") 181 | } 182 | require.Len(status.CipherStates, 2, "Status has 2 CipherState objects") 183 | if hs.cfg.Protocol.Pattern.IsOneWay() { 184 | require.Nil(status.CipherStates[1], "Status CipherStates[1] is nil") 185 | } 186 | 187 | txCs, rxCs = status.CipherStates[0], status.CipherStates[1] 188 | if !hs.isInitiator { 189 | txCs, rxCs = rxCs, txCs 190 | } 191 | case nil: 192 | default: 193 | require.NoError(err, "Handshake Message - %d", idx) 194 | } 195 | } else { 196 | // The messages that use the derived cipherstates just follow the 197 | // handshake message(s), and the flow continues. 198 | if hs.cfg.Protocol.Pattern.IsOneWay() { 199 | // Except one-way patterns which go from initiator to responder. 200 | if hs.isInitiator { 201 | dst, err = txCs.EncryptWithAd(nil, nil, msg.Payload) 202 | expectedDst = msg.Ciphertext 203 | } else { 204 | dst, err = rxCs.DecryptWithAd(nil, nil, msg.Ciphertext) 205 | expectedDst = msg.Payload 206 | } 207 | } else { 208 | if (idx&1 == 0) == writeOnEven { 209 | dst, err = txCs.EncryptWithAd(nil, nil, msg.Payload) 210 | expectedDst = msg.Ciphertext 211 | } else { 212 | dst, err = rxCs.DecryptWithAd(nil, nil, msg.Ciphertext) 213 | expectedDst = msg.Payload 214 | } 215 | } 216 | require.NoError(err, "Transport Message - %d", idx) 217 | } 218 | require.EqualValues(expectedDst, dst, "Message - #%d, output matches", idx) 219 | } 220 | 221 | // Sanity-check the test vectors for stupidity. 222 | require.NotNil(status, "Status != nil (test vector sanity check)") 223 | 224 | // These usually would be done by defer, but invoke them manually to make 225 | // sure nothing panics. 226 | if txCs != nil { 227 | txCs.Reset() 228 | } 229 | if rxCs != nil { 230 | rxCs.Reset() 231 | } 232 | } 233 | 234 | func configsFromVector(t *testing.T, v *vectors.Vector, skipOk bool) (*HandshakeConfig, *HandshakeConfig) { 235 | require := require.New(t) 236 | 237 | protoName := v.ProtocolName 238 | protocol, err := NewProtocol(protoName) 239 | if err == ErrProtocolNotSupported && skipOk { 240 | t.Skipf("protocol not supported") 241 | } 242 | require.NoError(err, "NewProtocol(%v)", protoName) 243 | require.Equal(protoName, protocol.String(), "derived protocol name matches test case") 244 | err = pattern.IsValid(protocol.Pattern) 245 | require.NoError(err, "IsValid(protocol.Pattern)") 246 | 247 | // Initiator side. 248 | var initStatic dh.Keypair 249 | if len(v.InitStatic) != 0 { 250 | initStatic, err = protocol.DH.ParsePrivateKey(v.InitStatic) 251 | require.NoError(err, "parse InitStatic") 252 | } 253 | 254 | var initEphemeral dh.Keypair 255 | if len(v.InitEphemeral) != 0 { 256 | initEphemeral, err = protocol.DH.ParsePrivateKey(v.InitEphemeral) 257 | require.NoError(err, "parse InitEphemeral") 258 | } 259 | 260 | var initRemoteStatic dh.PublicKey 261 | if len(v.InitRemoteStatic) != 0 { 262 | initRemoteStatic, err = protocol.DH.ParsePublicKey(v.InitRemoteStatic) 263 | require.NoError(err, "parse InitRemoteStatic") 264 | } 265 | 266 | initCfg := &HandshakeConfig{ 267 | Protocol: protocol, 268 | Prologue: v.InitPrologue, 269 | LocalStatic: initStatic, 270 | LocalEphemeral: initEphemeral, 271 | RemoteStatic: initRemoteStatic, 272 | Rng: &failReader{}, 273 | IsInitiator: true, 274 | } 275 | 276 | require.Len(v.InitPsks, protocol.Pattern.NumPSKs(), "test vector has the expected number of InitPsks") 277 | for _, psk := range v.InitPsks { 278 | initCfg.PreSharedKeys = append(initCfg.PreSharedKeys, []byte(psk)) 279 | } 280 | 281 | // Responder side. 282 | var respStatic dh.Keypair 283 | if len(v.RespStatic) != 0 { 284 | respStatic, err = protocol.DH.ParsePrivateKey(v.RespStatic) 285 | require.NoError(err, "parse RespStatic") 286 | } 287 | 288 | var respEphemeral dh.Keypair 289 | if len(v.RespEphemeral) != 0 { 290 | respEphemeral, err = protocol.DH.ParsePrivateKey(v.RespEphemeral) 291 | require.NoError(err, "parse RespEphemeral") 292 | } 293 | 294 | var respRemoteStatic dh.PublicKey 295 | if len(v.RespRemoteStatic) != 0 { 296 | respRemoteStatic, err = protocol.DH.ParsePublicKey(v.RespRemoteStatic) 297 | require.NoError(err, "parse RespRemoteStatic") 298 | } 299 | 300 | respCfg := &HandshakeConfig{ 301 | Protocol: protocol, 302 | Prologue: v.RespPrologue, 303 | LocalStatic: respStatic, 304 | LocalEphemeral: respEphemeral, 305 | RemoteStatic: respRemoteStatic, 306 | Rng: &failReader{}, 307 | IsInitiator: false, 308 | } 309 | 310 | require.Len(v.RespPsks, protocol.Pattern.NumPSKs(), "test vector has the expected number of RespPsks") 311 | for _, psk := range v.RespPsks { 312 | respCfg.PreSharedKeys = append(respCfg.PreSharedKeys, []byte(psk)) 313 | } 314 | 315 | return initCfg, respCfg 316 | } 317 | --------------------------------------------------------------------------------