-
Notifications
You must be signed in to change notification settings - Fork 15
/
ClientHandshakeHandler.js
341 lines (270 loc) · 11.8 KB
/
ClientHandshakeHandler.js
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
"use strict";
var log = require( 'logg' ).getLogger( 'dtls.ClientHandshakeHandler' );
var crypto = require( 'crypto' );
var constants = require( 'constants' );
var dtls = require( './dtls' );
var HandshakeBuilder = require( './HandshakeBuilder' );
var CipherInfo = require( './CipherInfo' );
var prf = require( './prf' );
var Certificate = require( './Certificate' );
var DtlsHandshake = require( './packets/DtlsHandshake' );
var DtlsClientHello = require( './packets/DtlsClientHello' );
var DtlsHelloVerifyRequest = require( './packets/DtlsHelloVerifyRequest' );
var DtlsServerHello = require( './packets/DtlsServerHello' );
var DtlsCertificate = require( './packets/DtlsCertificate' );
var DtlsServerHelloDone = require( './packets/DtlsServerHelloDone' );
var DtlsClientKeyExchange_rsa = require( './packets/DtlsClientKeyExchange_rsa' );
var DtlsPreMasterSecret = require( './packets/DtlsPreMasterSecret' );
var DtlsChangeCipherSpec = require( './packets/DtlsChangeCipherSpec' );
var DtlsFinished = require( './packets/DtlsFinished' );
var DtlsProtocolVersion = require( './packets/DtlsProtocolVersion' );
var DtlsRandom = require( './packets/DtlsRandom' );
var DtlsExtension = require( './packets/DtlsExtension' );
/* Note the methods in this class aren't grouped with similar methods. Instead
* the handle_ and send_ methods follow the logical order as defined in the
* DTLS/TLS specs.
*/
/**
* Implements the DTLS client handshake.
*/
var ClientHandshakeHandler = function( parameters, keyContext ) {
this.parameters = parameters;
this.handshakeBuilder = new HandshakeBuilder();
this.cookie = new Buffer(0);
this.certificate = null;
// Handshake builder makes sure that the normal handling methods never
// receive duplicate packets. Duplicate packets may mean that the last
// flight of packets we sent got lost though so we need to handle these.
this.handshakeBuilder.onRetransmission = this.retransmitLast.bind( this );
this.version = new DtlsProtocolVersion( ~1, ~2 );
};
/**
* Processes an incoming handshake message from the server.
*
* @param {DtlsPlaintext} message
* The TLS envelope containing the handshake message.
*/
ClientHandshakeHandler.prototype.process = function( message ) {
// Enqueue the current handshake.
var newHandshake = new DtlsHandshake( message.fragment );
var newHandshakeName = dtls.HandshakeTypeName[ newHandshake.msgType ];
log.info( 'Received handshake fragment; sequence:',
newHandshake.messageSeq + ':' + newHandshakeName );
this.handshakeBuilder.add( newHandshake );
// Process available defragmented handshakes.
var handshake = this.handshakeBuilder.next();
while( handshake ) {
var handshakeName = dtls.HandshakeTypeName[ handshake.msgType ];
var handler = this[ 'handle_' + handshakeName ];
if( !handler ) {
log.error( 'Handshake handler not found for ', handshakeName );
continue;
}
log.info( 'Processing handshake:',
handshake.messageSeq + ':' + handshakeName );
var action = this[ 'handle_' + handshakeName ]( handshake, message );
// Digest this message after handling it.
// This way the ClientHello can create the new SecurityParamters before
// we digest this so it'll get digested in the correct context AND the
// Finished message can verify its digest without counting itself in
// it.
//
// TODO: Make sure 'message' contains the defragmented buffer.
// We read the buffer in HandshakeBuilder anyway so there's no real
// reason to call getBuffer() here.
if( this.newParameters ) {
this.newParameters.digestHandshake( handshake.getBuffer() );
}
// However to get the digests in correct order, the handle_ method
// above couldn't have invoked the send_ methods as those take care of
// digesting their own messages. So instead they returned the action
// and we'll invoke them after the digest.
if( action )
action.call( this );
handshake = this.handshakeBuilder.next();
}
};
ClientHandshakeHandler.prototype.renegotiate = function() {
this.send_clientHello();
};
/**
* Sends the ServerHello message
*/
ClientHandshakeHandler.prototype.send_clientHello = function() {
// TLS spec require all implementations MUST implement the
// TLS_RSA_WITH_AES_128_CBC_SHA cipher.
var cipher = CipherInfo.TLS_RSA_WITH_AES_128_CBC_SHA;
var clientHello = new DtlsClientHello({
clientVersion: this.version,
random: new DtlsRandom(),
sessionId: new Buffer(0),
cookie: this.cookie || new Buffer(0),
cipherSuites: [ cipher.id ],
compressionMethods: [ 0 ],
// TODO: Remove the requirement for extensions. Currently packets with
// 0 extensions will serialize wrong. I don't even remember which
// extension this is. Maybe heartbeat? Whatever it is, we definitely do
// not implement it. :)
extensions: [
new DtlsExtension({
extensionType: 0x000f,
extensionData: new Buffer([ 1 ])
})
]
});
// Store more parameters.
// We'll assume DTLS 1.0 so the plaintext layer is understood by everyone.
// The clientVersion contains the real supported DTLS version for the
// server. In serverHello handler we'll read the DTLS version the server
// decided on and use that for the future record layer packets.
this.newParameters = this.parameters.initNew(
new DtlsProtocolVersion( ~1, ~0 ) );
this.newParameters.isServer = false;
this.newParameters.clientRandom = clientHello.random.getBuffer();
log.info( 'Sending ClientHello' );
var handshakes = this.handshakeBuilder.createHandshakes([ clientHello ]);
handshakes = handshakes.map( function(h) { return h.getBuffer(); });
this.newParameters.digestHandshake( handshakes );
var packets = this.handshakeBuilder.fragmentHandshakes( handshakes );
this.setResponse( packets );
};
ClientHandshakeHandler.prototype.handle_helloVerifyRequest = function( handshake ) {
var verifyRequest = new DtlsHelloVerifyRequest( handshake.body );
this.cookie = verifyRequest.cookie;
return this.send_clientHello;
};
/**
* Handles the ClientHello message.
*
* The message is accepted only if it contains the correct cookie. If the
* cookie is wrong, we'll send a HelloVerifyRequest packet instead of
* proceeding with the handshake.
*/
ClientHandshakeHandler.prototype.handle_serverHello = function( handshake, message ) {
var serverHello = new DtlsServerHello( handshake.body );
log.fine( 'ServerHello received. Server version:',
~serverHello.serverVersion.major + '.' +
~serverHello.serverVersion.minor );
// TODO: Validate server version
this.newParameters.version = serverHello.serverVersion;
this.newParameters.serverRandom = serverHello.random.getBuffer();
var cipher = CipherInfo.get( serverHello.cipherSuite );
this.newParameters.setFrom( cipher );
this.newParameters.compressionMethod = serverHello.compressionMethod;
if( !this.parameters.first.version )
this.parameters.first.version = serverHello.serverVersion;
this.setResponse( null );
};
ClientHandshakeHandler.prototype.handle_certificate = function( handshake, message ) {
var certificate = new DtlsCertificate( handshake.body );
// Just grab the first ceritificate ":D"
this.certificate = certificate.certificateList[ 0 ];
this.setResponse( null );
};
ClientHandshakeHandler.prototype.handle_serverHelloDone = function( handshake, message ) {
log.info( 'Server hello done' );
// We need to use the real client version here, not the negotiated version.
var preMasterKey = Buffer.concat([
this.version.getBuffer(),
crypto.randomBytes( 46 ) ]);
this.newParameters.calculateMasterKey( preMasterKey );
this.newParameters.preMasterKey = preMasterKey;
this.newParameters.init();
return this.send_keyExchange;
};
ClientHandshakeHandler.prototype.send_keyExchange = function() {
log.info( 'Constructing key exchange' );
var publicKey = Certificate.getPublicKeyPem( this.certificate );
var exchangeKeys = crypto.publicEncrypt({
key: publicKey,
padding: constants.RSA_PKCS1_PADDING
}, this.newParameters.preMasterKey );
var keyExchange = new DtlsClientKeyExchange_rsa({
exchangeKeys: exchangeKeys
});
var keyExchangeHandshake = this.handshakeBuilder.createHandshakes(
keyExchange ).getBuffer();
this.newParameters.digestHandshake( keyExchangeHandshake );
this.newParameters.preMasterKey = null;
var changeCipherSpec = new DtlsChangeCipherSpec({ value: 1 });
var prf_func = prf( this.newParameters.version );
var verifyData = prf_func(
this.newParameters.masterKey,
"client finished",
this.newParameters.getHandshakeDigest(),
12
);
var finished = new DtlsFinished({ verifyData: verifyData });
var finishedHandshake = this.handshakeBuilder.createHandshakes(
finished ).getBuffer();
this.newParameters.digestHandshake( finishedHandshake );
var keyExchangeFragments = this.handshakeBuilder.fragmentHandshakes( keyExchangeHandshake );
var finishedFragments = this.handshakeBuilder.fragmentHandshakes( finishedHandshake );
var outgoingFragments = keyExchangeFragments;
outgoingFragments.push( changeCipherSpec );
outgoingFragments = outgoingFragments.concat( finishedFragments );
this.setResponse( outgoingFragments );
};
/**
* Handles the client Finished message.
*
* Technically there is a ChangeCipherSpec message between ClientKeyExchange
* and Finished messages. ChangeCipherSpec isn't a handshake message though so
* it never makes it here. That message is handled in the RecordLayer.
*/
ClientHandshakeHandler.prototype.handle_finished = function( handshake, message ) {
var finished = new DtlsFinished( handshake.body );
var prf_func = prf( this.newParameters.version );
var expected = prf_func(
this.newParameters.masterKey,
"server finished",
this.newParameters.getHandshakeDigest(),
finished.verifyData.length
);
if( !finished.verifyData.equals( expected ) ) {
log.warn( 'Finished digest does not match. Expected:',
expected,
'Actual:',
finished.verifyData );
return;
}
this.setResponse( null );
// The handle_ methods should RETURN the response action.
// See the handle() method for explanation.
return this.onHandshake();
};
/**
* Sets the response for the last client message.
*
* The last flight of packets is stored so we can somewhat automatically handle
* retransmission when we see the client doing it.
*/
ClientHandshakeHandler.prototype.setResponse = function( packets, done ) {
this.lastFlight = packets;
// Clear the retansmit timer for the last packets.
if( this.retransmitTimer )
clearTimeout( this.retransmitTimer );
// If there are no new packets to send, we don't need to send anything
// or even set the retransmit timer again.
if( !packets )
return;
// Send the packets and set up the timer for retransmit.
this.onSend( packets, done );
this.retransmitTimer = setTimeout( function() {
this.retransmitLast();
}.bind( this ), 1000 );
};
/**
* Retransmits the last response in case it got lost on the way last time.
*
* @param {DtlsPlaintext} message
* The received packet that triggered this retransmit.
*/
ClientHandshakeHandler.prototype.retransmitLast = function( message ) {
if( this.lastFlight )
this.onSend( this.lastFlight );
this.retransmitTimer = setTimeout( function() {
this.retransmitLast();
}.bind( this ), 1000 );
};
module.exports = ClientHandshakeHandler;