-
Notifications
You must be signed in to change notification settings - Fork 1
/
handshake.go
392 lines (336 loc) · 12.4 KB
/
handshake.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
package noise
import (
"crypto/ed25519"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"log"
"net"
"github.com/flynn/noise"
"github.com/oxtoacart/bpool"
"golang.org/x/crypto/chacha20poly1305"
)
// [DHKey] is a keypair used for Diffie-Hellman key agreement.
// Please see [docs] for more details.
//
// [docs]: http://www.noiseprotocol.org/noise.html#dh-functions
type DHKey = noise.DHKey
type PublicKey = ed25519.PublicKey
type PrivateKey = ed25519.PrivateKey
// [EDKeyPair] hold public/private using entropy from rand.
// Every new handshake generate a new key pair.
type EDKeyPair struct {
Private PrivateKey
Public PublicKey
}
// [KeyRing] hold the set of local keys to use during handshake and session.
type KeyRing struct {
kp DHKey // encrypt-decrypt key pair exchange
sv EDKeyPair // ED25519 local sign-verify keys
}
// [CipherState] provides symmetric encryption and decryption after a successful handshake.
// Please see [docs] for more information.
//
// [docs]: http://www.noiseprotocol.org/noise.html#the-cipherstate-object
type CipherState = *noise.CipherState
// [BytePool] implements a leaky pool of []byte in the form of a bounded channel.
type BytePool = *bpool.BytePool
// [HandshakeState] tracks the state of a Noise handshake.
// It may be discarded after the handshake is complete.
// Please see [docs] for more information.
//
// [docs]: http://www.noiseprotocol.org/noise.html#the-handshakestate-object
type HandshakeState interface {
// WriteMessage appends a handshake message to out. The message will include the
// optional payload if provided. If the handshake is completed by the call, two
// CipherStates will be returned, one is used for encryption of messages to the
// remote peer, the other is used for decryption of messages from the remote
// peer. It is an error to call this method out of sync with the handshake
// pattern.
WriteMessage(out, payload []byte) ([]byte, CipherState, CipherState, error)
// ReadMessage processes a received handshake message and appends the payload,
// if any to out. If the handshake is completed by the call, two CipherStates
// will be returned, one is used for encryption of messages to the remote peer,
// the other is used for decryption of messages from the remote peer. It is an
// error to call this method out of sync with the handshake pattern.
ReadMessage(out, message []byte) ([]byte, CipherState, CipherState, error)
// PeerStatic returns the static key provided by the remote peer during
// a handshake. It is an error to call this method if a handshake message
// containing a static key has not been read.
PeerStatic() []byte
// MessageIndex returns the current handshake message id
MessageIndex() int
}
// Buffer pools
// If bPools >= 1 a new buffered pool is created.
// If bPools == 0 a new no-buffered pool is created
const bPools = 1
const headerSize = 2
// [CipherSuite] is a set of cryptographic primitives used in a Noise protocol.
// Based on: Diffie-Hellman X25519, [Blake2] and [ChaCha20-Poly1305]
// Please see [NoisePatternExplorer] for more details.
//
// [ChaCha20-Poly1305]: https://en.wikipedia.org/wiki/ChaCha20-Poly1305
// [Diffie-Hellman X25519]: https://en.wikipedia.org/wiki/Curve25519
// [Blake2]: https://www.blake2.net/
var CipherSuite = noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, noise.HashBLAKE2b)
// Default Handshake "XX" noise pattern.
// Our approach its use a balanced "time/security" pattern.
// Please see [NoisePatternExplorer] for more details.
//
// [NoisePatternExplorer]: https://noiseexplorer.com/patterns/XX/
var HandshakePattern = noise.HandshakeXX
// GenerateKeypair generates a new keypair using random as a source of entropy.
// Please see [Docs] for more details.
//
// [Docs]: http://www.noiseprotocol.org/noise.html#dh-functions
func newDHKeyPair() (DHKey, error) {
// Diffie-Hellman key pair
var err error
var kp DHKey
kp, err = noise.DH25519.GenerateKeypair(rand.Reader)
if err != nil {
err := fmt.Errorf("error trying to generate `s` keypair: %w", err)
return kp, errDuringHandshake(err)
}
log.Printf("generated X25519 public key")
return kp, nil
}
// newED25519KeyPair generate a new kp for sign-verify using P256 128 bit
func newED25519KeyPair() (EDKeyPair, error) {
// ref: https://github.com/openssl/openssl/issues/18448
// ref: https://csrc.nist.gov/csrc/media/events/workshop-on-elliptic-curve-cryptography-standards/documents/papers/session6-adalier-mehmet.pdf
// TODO should i persist seed to keep the same public key for peer identity?
// TODO rand.Reader store it and retrieve it to avoid change the pub key in every new handshake?
pb, pv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
return EDKeyPair{}, err
}
log.Print("generated ECDSA25519 public key")
return EDKeyPair{pv, pb}, nil
}
// NewHandshakeState starts a new handshake using the provided configuration.
// A HandshakeState tracks the state of a Noise handshake
// Please see [Handshake State] for more details.
//
// [Handshake State]: https://pkg.go.dev/github.com/flynn/noise#HandshakeState
func newHandshakeState(conf noise.Config) (*noise.HandshakeState, error) {
// handshake state
hs, err := noise.NewHandshakeState(conf)
if err != nil {
log.Print(err)
err = fmt.Errorf("error creating handshake state: %v", err)
return nil, errDuringHandshake(err)
}
return hs, nil
}
// newHandshakeConfig create a noise config.
// A Config provides the details necessary to process a Noise handshake.
// It is never modified by this package, and can be reused.
func newHandshakeConfig(initiator bool, kp noise.DHKey) noise.Config {
return noise.Config{
CipherSuite: CipherSuite,
Pattern: HandshakePattern,
Initiator: initiator,
StaticKeypair: kp,
}
}
// handshake execute the steps needed for the noise handshake XX pattern.
// Please see [XX Pattern] for more details. [XX Explorer] pattern.
//
// [XX Pattern]: http://www.noiseprotocol.org/noise.html#handshake-patterns
// [XX Explorer]: https://noiseexplorer.com/patterns/XX/
type handshake struct {
s *session // ref: https://go.dev/doc/effective_go#embedding
kr KeyRing
hs HandshakeState
p BytePool
i bool
}
// newKeyRing create a bundle of local keys needed during session + handshake.
func newKeyRing() (KeyRing, error) {
sv, err := newED25519KeyPair()
if err != nil {
return KeyRing{}, err
}
kp, err := newDHKeyPair()
if err != nil {
return KeyRing{}, err
}
return KeyRing{kp, sv}, nil
}
// newHandshake create a new handshake handler using provided connection and role.
func newHandshake(conn net.Conn, initiator bool) (*handshake, error) {
kr, err := newKeyRing()
if err != nil {
return nil, err
}
// set handshake state as initiator?
conf := newHandshakeConfig(initiator, kr.kp)
// A HandshakeState tracks the state of a Noise handshake
state, err := newHandshakeState(conf)
if err != nil {
return nil, errDuringHandshake(err)
}
// Setup the max of size possible for tokens exchanged between peers.
edKeyLen := ed25519.PublicKeySize // 32 bytes
dhKeyLen := 2 * noise.DH25519.DHLen() // 64 bytes
cipherLen := 2 * chacha20poly1305.Overhead // 32 bytes
// Sum the needed memory size for pool
size := dhKeyLen + edKeyLen + cipherLen + headerSize
pool := bpool.NewBytePool(bPools, size) // N bytes pool
// Create a new session handler
session, err := newSession(conn, kr)
if err != nil {
// Fail creating new session
return nil, errDuringHandshake(err)
}
return &handshake{session, kr, state, pool, initiator}, nil
}
// Session return secured session after handshake.
// This session is invalid if handshake isn't finished.
func (h *handshake) Session() *session {
if !h.Finish() {
return nil
}
return h.s
}
// Finish return the handshake state.
// Return true if handshake is finished otherwise false.
func (h *handshake) Finish() bool {
return h.hs.MessageIndex() >= len(HandshakePattern.Messages)
}
// Valid check if handshake sync is valid.
// If the process finished but the keys are not exchange as expected return error.
func (h *handshake) Valid(enc, dec CipherState) error {
// This cyphers will be `not nil` until exchange patterns finish
if h.Finish() && (enc == nil || dec == nil) {
err := errors.New("invalid enc/dec cipher after handshake")
return errDuringHandshake(err)
}
return nil
}
// Start initialize handshake based on peer rol
// If peer is initiator then Initiate method run else Answer
func (h *handshake) Start() error {
if h.i {
// Run as initiator role
return h.Initiate()
}
// Run as remote "peer" role
return h.Answer()
}
// Initiate start a new handshake with peer as a "dialer".
func (h *handshake) Initiate() error {
// Send initial #1 message
// bytes size = DHLen for e = ephemeral key
log.Print("sending e to remote")
enc, dec, err := h.Send()
if err != nil {
err = fmt.Errorf("error sending `e` state: %v", err)
return errDuringHandshake(err)
}
// Receive message #2 stage
log.Print("waiting for e, ee, s, es from remote")
enc, dec, err = h.Receive()
if err != nil {
err = fmt.Errorf("error receiving `e, ee, s, es` state: %v", err)
return errDuringHandshake(err)
}
// Send last handshake message #3 stage
log.Print("sending s, se to remote")
enc, dec, err = h.Send()
if err != nil {
err = fmt.Errorf("error sending `s, se` state: %v", err)
return errDuringHandshake(err)
}
// Check if synchronization is valid
if err := h.Valid(enc, dec); err != nil {
return err
}
// Add keys for encrypt/decrypt operations in session.
h.s.SetCyphers(enc, dec)
return nil
}
// Answer start an answer for remote peer handshake request.
func (h *handshake) Answer() error {
// Receive message #1 stage
log.Print("waiting for e from remote")
enc, dec, err := h.Receive()
if err != nil {
err = fmt.Errorf("error receiving `e` state: %v", err)
return errDuringHandshake(err)
}
// Send answer message #2 stage
log.Print("sending e, ee, s, es to remote")
enc, dec, err = h.Send()
if err != nil {
err = fmt.Errorf("error sending `e, ee, s, es` state: %v", err)
return errDuringHandshake(err)
}
// Receive message #2 stage
log.Print("waiting for s, se from remote")
enc, dec, err = h.Receive()
if err != nil {
err = fmt.Errorf("error receiving `s, se` state: %v", err)
return errDuringHandshake(err)
}
// Check if synchronization is valid
if err := h.Valid(enc, dec); err != nil {
return err
}
// Add keys for encrypt/decrypt operations in session.
h.s.SetCyphers(dec, enc)
return nil
}
// Send create a new token based on message pattern synchronization and send it to remote peer.
func (h *handshake) Send() (e, d CipherState, err error) {
var msg []byte
// Get a chunk of bytes from pool
// We need an empty buffer slice here
buffer := h.p.Get()[:0]
defer h.p.Put(buffer)
// WriteMessage appends a handshake message to out. The message will include the
// optional payload if provided. If the handshake is completed by the call, two
// CipherStates will be returned, one is used for encryption of messages to the
// remote peer, the other is used for decryption of messages from the remote
// peer. Append public signature key in payload to share with remote.
msg, e, d, err = h.hs.WriteMessage(buffer, h.kr.sv.Public)
if err != nil {
return
}
// 2 bytes of header size
binary.Write(h.s, binary.BigEndian, uint16(len(msg)))
if _, err = h.s.Write(msg); err != nil {
return
}
return
}
// Receive get a token from remote peer and synchronize it with local peer handshake state.
func (h *handshake) Receive() (e, d CipherState, err error) {
var payload []byte // sent payload
var size uint16 // read bytes size from header
// Read incoming message size
err = binary.Read(h.s, binary.BigEndian, &size)
if err != nil {
return
}
// With size sent get a chunk from pool
// Maybe here we don't need a new pool, just getting a chunk of current could help?
buffer := h.p.Get()[:size]
defer h.p.Put(buffer)
// Wait for incoming message from remote
if _, err = h.s.Read(buffer); err != nil {
return
}
// ReadMessage processes a received handshake message and appends the payload,
// if any to out. If the handshake is completed by the call, two CipherStates
// will be returned, one is used for encryption of messages to the remote peer,
// the other is used for decryption of messages from the remote peer. It is an
// error to call this method out of sync with the handshake pattern.
payload, e, d, err = h.hs.ReadMessage(nil, buffer)
// Set remote signature validation public key
h.s.SetRemotePublicKey(payload)
return
}