protocol.go 10.2 KB
Newer Older
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
1
2
3
4
5
6
7
8
package secio

import (
	"bytes"
	"crypto/rand"
	"errors"
	"fmt"
	"io"
9
10
	"sync"
	"time"
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
11

Jeromy's avatar
Jeromy committed
12
13
	msgio "github.com/jbenet/go-msgio"
	context "golang.org/x/net/context"
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
14
15
16
	ci "github.com/ipfs/go-libp2p/p2p/crypto"
	pb "github.com/ipfs/go-libp2p/p2p/crypto/secio/pb"
	peer "github.com/ipfs/go-libp2p/p2p/peer"
Jeromy's avatar
Jeromy committed
17
18
	u "util"
	logging "QmWRypnfEwrgH4k93KEHN5hng7VjKYkWmzDYRuTZeh2Mgh/go-log"
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
19
20
)

Jeromy's avatar
Jeromy committed
21
var log = logging.Logger("secio")
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
22
23
24
25
26
27
28

// ErrUnsupportedKeyType is returned when a private key cast/type switch fails.
var ErrUnsupportedKeyType = errors.New("unsupported key type")

// ErrClosed signals the closing of a connection.
var ErrClosed = errors.New("connection closed")

29
30
31
// ErrEcho is returned when we're attempting to handshake with the same keys and nonces.
var ErrEcho = errors.New("same keys and nonces. one side talking to self.")

32
33
34
35
36
37
// HandshakeTimeout governs how long the handshake will be allowed to take place for.
// Making this number large means there could be many bogus connections waiting to
// timeout in flight. Typical handshakes take ~3RTTs, so it should be completed within
// seconds across a typical planet in the solar system.
var HandshakeTimeout = time.Second * 30

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
38
39
40
41
42
43
// nonceSize is the size of our nonces (in bytes)
const nonceSize = 16

// secureSession encapsulates all the parameters needed for encrypting
// and decrypting traffic from an insecure channel.
type secureSession struct {
44
45
	ctx    context.Context
	cancel context.CancelFunc
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
46

47
48
	secure    msgio.ReadWriteCloser
	insecure  io.ReadWriteCloser
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
49
50
51
52
53
54
55
56
57
58
	insecureM msgio.ReadWriter

	localKey   ci.PrivKey
	localPeer  peer.ID
	remotePeer peer.ID

	local  encParams
	remote encParams

	sharedSecret []byte
59
60
61
62

	handshakeMu   sync.Mutex // guards handshakeDone + handshakeErr
	handshakeDone bool
	handshakeErr  error
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
63
64
}

65
66
67
68
69
70
71
72
func (s *secureSession) Loggable() map[string]interface{} {
	m := make(map[string]interface{})
	m["localPeer"] = s.localPeer.Pretty()
	m["remotePeer"] = s.remotePeer.Pretty()
	m["established"] = (s.secure != nil)
	return m
}

73
func newSecureSession(ctx context.Context, local peer.ID, key ci.PrivKey, insecure io.ReadWriteCloser) (*secureSession, error) {
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
74
	s := &secureSession{localPeer: local, localKey: key}
75
	s.ctx, s.cancel = context.WithCancel(ctx)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
76
77
78
79
80
81
82
83

	switch {
	case s.localPeer == "":
		return nil, errors.New("no local id provided")
	case s.localKey == nil:
		return nil, errors.New("no local private key provided")
	case !s.localPeer.MatchesPrivateKey(s.localKey):
		return nil, fmt.Errorf("peer.ID does not match PrivateKey")
84
85
	case insecure == nil:
		return nil, fmt.Errorf("insecure ReadWriter is nil")
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
86
87
	}

88
89
90
	s.ctx = ctx
	s.insecure = insecure
	s.insecureM = msgio.NewReadWriter(insecure)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
91
92
93
	return s, nil
}

94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
func (s *secureSession) Handshake() error {
	s.handshakeMu.Lock()
	defer s.handshakeMu.Unlock()

	if s.handshakeErr != nil {
		return s.handshakeErr
	}

	if !s.handshakeDone {
		s.handshakeErr = s.runHandshake()
		s.handshakeDone = true
	}
	return s.handshakeErr
}

// runHandshake performs initial communication over insecure channel to share
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
110
111
// keys, IDs, and initiate communication, assigning all necessary params.
// requires the duplex channel to be a msgio.ReadWriter (for framed messaging)
112
113
114
func (s *secureSession) runHandshake() error {
	ctx, cancel := context.WithTimeout(s.ctx, HandshakeTimeout) // remove
	defer cancel()
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
115
116
117
118
119
120
121
122
123
124
125
126

	// =============================================================================
	// step 1. Propose -- propose cipher suite + send pubkeys + nonce

	// Generate and send Hello packet.
	// Hello = (rand, PublicKey, Supported)
	nonceOut := make([]byte, nonceSize)
	_, err := rand.Read(nonceOut)
	if err != nil {
		return err
	}

127
	defer log.EventBegin(ctx, "secureHandshake", s).Done()
128

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
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
	s.local.permanentPubKey = s.localKey.GetPublic()
	myPubKeyBytes, err := s.local.permanentPubKey.Bytes()
	if err != nil {
		return err
	}

	proposeOut := new(pb.Propose)
	proposeOut.Rand = nonceOut
	proposeOut.Pubkey = myPubKeyBytes
	proposeOut.Exchanges = &SupportedExchanges
	proposeOut.Ciphers = &SupportedCiphers
	proposeOut.Hashes = &SupportedHashes

	// log.Debugf("1.0 Propose: nonce:%s exchanges:%s ciphers:%s hashes:%s",
	// 	nonceOut, SupportedExchanges, SupportedCiphers, SupportedHashes)

	// Send Propose packet (respects ctx)
	proposeOutBytes, err := writeMsgCtx(ctx, s.insecureM, proposeOut)
	if err != nil {
		return err
	}

	// Receive + Parse their Propose packet and generate an Exchange packet.
	proposeIn := new(pb.Propose)
	proposeInBytes, err := readMsgCtx(ctx, s.insecureM, proposeIn)
	if err != nil {
		return err
	}

	// log.Debugf("1.0.1 Propose recv: nonce:%s exchanges:%s ciphers:%s hashes:%s",
	// 	proposeIn.GetRand(), proposeIn.GetExchanges(), proposeIn.GetCiphers(), proposeIn.GetHashes())

	// =============================================================================
	// step 1.1 Identify -- get identity from their key

	// get remote identity
	s.remote.permanentPubKey, err = ci.UnmarshalPublicKey(proposeIn.GetPubkey())
	if err != nil {
		return err
	}

	// get peer id
	s.remotePeer, err = peer.IDFromPublicKey(s.remote.permanentPubKey)
	if err != nil {
		return err
	}

	log.Debugf("1.1 Identify: %s Remote Peer Identified as %s", s.localPeer, s.remotePeer)

	// =============================================================================
	// step 1.2 Selection -- select/agree on best encryption parameters

181
	// to determine order, use cmp(H(remote_pubkey||local_rand), H(local_pubkey||remote_rand)).
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
182
183
184
	oh1 := u.Hash(append(proposeIn.GetPubkey(), nonceOut...))
	oh2 := u.Hash(append(myPubKeyBytes, proposeIn.GetRand()...))
	order := bytes.Compare(oh1, oh2)
185
186
187
188
	if order == 0 {
		return ErrEcho // talking to self (same socket. must be reuseport + dialing self)
	}

Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
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
	s.local.curveT, err = selectBest(order, SupportedExchanges, proposeIn.GetExchanges())
	if err != nil {
		return err
	}

	s.local.cipherT, err = selectBest(order, SupportedCiphers, proposeIn.GetCiphers())
	if err != nil {
		return err
	}

	s.local.hashT, err = selectBest(order, SupportedHashes, proposeIn.GetHashes())
	if err != nil {
		return err
	}

	// we use the same params for both directions (must choose same curve)
	// WARNING: if they dont SelectBest the same way, this won't work...
	s.remote.curveT = s.local.curveT
	s.remote.cipherT = s.local.cipherT
	s.remote.hashT = s.local.hashT

	// log.Debugf("1.2 selection: exchange:%s cipher:%s hash:%s",
	// 	s.local.curveT, s.local.cipherT, s.local.hashT)

	// =============================================================================
	// step 2. Exchange -- exchange (signed) ephemeral keys. verify signatures.

	// Generate EphemeralPubKey
	var genSharedKey ci.GenSharedKey
	s.local.ephemeralPubKey, genSharedKey, err = ci.GenerateEKeyPair(s.local.curveT)

	// Gather corpus to sign.
221
	selectionOut := new(bytes.Buffer)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
	selectionOut.Write(proposeOutBytes)
	selectionOut.Write(proposeInBytes)
	selectionOut.Write(s.local.ephemeralPubKey)
	selectionOutBytes := selectionOut.Bytes()

	// log.Debugf("2.0 exchange: %v", selectionOutBytes)
	exchangeOut := new(pb.Exchange)
	exchangeOut.Epubkey = s.local.ephemeralPubKey
	exchangeOut.Signature, err = s.localKey.Sign(selectionOutBytes)
	if err != nil {
		return err
	}

	// Send Propose packet (respects ctx)
	if _, err := writeMsgCtx(ctx, s.insecureM, exchangeOut); err != nil {
		return err
	}

240
	// Receive + Parse their Exchange packet.
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
241
242
243
244
245
246
247
248
249
250
251
	exchangeIn := new(pb.Exchange)
	if _, err := readMsgCtx(ctx, s.insecureM, exchangeIn); err != nil {
		return err
	}

	// =============================================================================
	// step 2.1. Verify -- verify their exchange packet is good.

	// get their ephemeral pub key
	s.remote.ephemeralPubKey = exchangeIn.GetEpubkey()

252
	selectionIn := new(bytes.Buffer)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
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
	selectionIn.Write(proposeInBytes)
	selectionIn.Write(proposeOutBytes)
	selectionIn.Write(s.remote.ephemeralPubKey)
	selectionInBytes := selectionIn.Bytes()
	// log.Debugf("2.0.1 exchange recv: %v", selectionInBytes)

	// u.POut("Remote Peer Identified as %s\n", s.remote)
	sigOK, err := s.remote.permanentPubKey.Verify(selectionInBytes, exchangeIn.GetSignature())
	if err != nil {
		// log.Error("2.1 Verify: failed: %s", err)
		return err
	}

	if !sigOK {
		err := errors.New("Bad signature!")
		// log.Error("2.1 Verify: failed: %s", err)
		return err
	}
	// log.Debugf("2.1 Verify: signature verified.")

	// =============================================================================
	// step 2.2. Keys -- generate keys for mac + encryption

	// OK! seems like we're good to go.
	s.sharedSecret, err = genSharedKey(exchangeIn.GetEpubkey())
	if err != nil {
		return err
	}

	// generate two sets of keys (stretching)
	k1, k2 := ci.KeyStretcher(s.local.cipherT, s.local.hashT, s.sharedSecret)

	// use random nonces to decide order.
286
287
288
289
	switch {
	case order > 0:
		// just break
	case order < 0:
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
290
291
		k1, k2 = k2, k1 // swap
	default:
292
293
		// we should've bailed before this. but if not, bail here.
		return ErrEcho
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
	}
	s.local.keys = k1
	s.remote.keys = k2

	// log.Debug("2.2 keys:\n\tshared: %v\n\tk1: %v\n\tk2: %v",
	// 	s.sharedSecret, s.local.keys, s.remote.keys)

	// =============================================================================
	// step 2.3. MAC + Cipher -- prepare MAC + cipher

	if err := s.local.makeMacAndCipher(); err != nil {
		return err
	}

	if err := s.remote.makeMacAndCipher(); err != nil {
		return err
	}

	// log.Debug("2.3 mac + cipher.")

	// =============================================================================
315
	// step 3. Finish -- send expected message to verify encryption works (send local nonce)
Juan Batiz-Benet's avatar
Juan Batiz-Benet committed
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

	// setup ETM ReadWriter
	w := NewETMWriter(s.insecure, s.local.cipher, s.local.mac)
	r := NewETMReader(s.insecure, s.remote.cipher, s.remote.mac)
	s.secure = msgio.Combine(w, r).(msgio.ReadWriteCloser)

	// log.Debug("3.0 finish. sending: %v", proposeIn.GetRand())
	// send their Nonce.
	if _, err := s.secure.Write(proposeIn.GetRand()); err != nil {
		return fmt.Errorf("Failed to write Finish nonce: %s", err)
	}

	// read our Nonce
	nonceOut2 := make([]byte, len(nonceOut))
	if _, err := io.ReadFull(s.secure, nonceOut2); err != nil {
		return fmt.Errorf("Failed to read Finish nonce: %s", err)
	}

	// log.Debug("3.0 finish.\n\texpect: %v\n\tactual: %v", nonceOut, nonceOut2)
	if !bytes.Equal(nonceOut, nonceOut2) {
		return fmt.Errorf("Failed to read our encrypted nonce: %s != %s", nonceOut2, nonceOut)
	}

	// Whew! ok, that's all folks.
	return nil
}