lemon-tls 0.2.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +258 -203
- package/index.d.ts +145 -14
- package/index.js +12 -0
- package/package.json +2 -2
- package/src/compat.js +290 -31
- package/src/crypto.js +127 -7
- package/src/dtls_socket.js +3 -0
- package/src/record.js +408 -61
- package/src/session/message.js +28 -3
- package/src/session/ticket.js +185 -0
- package/src/tls_session.js +820 -99
- package/src/tls_socket.js +815 -249
- package/src/wire.js +25 -0
package/src/compat.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* compat.js — Node.js tls module API compatibility layer.
|
|
3
3
|
*
|
|
4
|
-
* Provides tls.connect(), tls.createServer(), and additional
|
|
4
|
+
* Provides tls.connect(), tls.createServer(), tls.Server, and additional
|
|
5
5
|
* TLSSocket methods to match Node.js tls API conventions.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import net from 'node:net';
|
|
9
9
|
import crypto from 'node:crypto';
|
|
10
|
+
import { EventEmitter } from 'node:events';
|
|
10
11
|
import { TLS_CIPHER_SUITES } from './crypto.js';
|
|
11
12
|
import TLSSession from './tls_session.js';
|
|
12
13
|
import TLSSocket from './tls_socket.js';
|
|
@@ -17,6 +18,20 @@ import createSecureContext from './secure_context.js';
|
|
|
17
18
|
const DEFAULT_MIN_VERSION = 'TLSv1.2';
|
|
18
19
|
const DEFAULT_MAX_VERSION = 'TLSv1.3';
|
|
19
20
|
|
|
21
|
+
// Matches Node's default cipher list for TLS 1.3 and the modern-TLS 1.2 subset
|
|
22
|
+
// we actually support. Apps can pass a `ciphers` option to override per-connection.
|
|
23
|
+
const DEFAULT_CIPHERS = [
|
|
24
|
+
'TLS_AES_256_GCM_SHA384',
|
|
25
|
+
'TLS_CHACHA20_POLY1305_SHA256',
|
|
26
|
+
'TLS_AES_128_GCM_SHA256',
|
|
27
|
+
'ECDHE-RSA-AES256-GCM-SHA384',
|
|
28
|
+
'ECDHE-ECDSA-AES256-GCM-SHA384',
|
|
29
|
+
'ECDHE-RSA-AES128-GCM-SHA256',
|
|
30
|
+
'ECDHE-ECDSA-AES128-GCM-SHA256',
|
|
31
|
+
].join(':');
|
|
32
|
+
|
|
33
|
+
const DEFAULT_ECDH_CURVE = 'auto'; // Node default since v13
|
|
34
|
+
|
|
20
35
|
// ===================== tls.getCiphers() =====================
|
|
21
36
|
|
|
22
37
|
/** Returns array of supported cipher names (lowercase, OpenSSL-style). */
|
|
@@ -29,6 +44,90 @@ function getCiphers() {
|
|
|
29
44
|
return out;
|
|
30
45
|
}
|
|
31
46
|
|
|
47
|
+
// ===================== tls.checkServerIdentity() =====================
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Verifies that the peer certificate matches the hostname per RFC 6125.
|
|
51
|
+
* Returns undefined on success, or an Error on mismatch.
|
|
52
|
+
*
|
|
53
|
+
* This is the default identity check Node uses when `rejectUnauthorized: true`
|
|
54
|
+
* (the default for tls.connect). Apps can override via the `checkServerIdentity`
|
|
55
|
+
* option in tls.connect to supply their own check.
|
|
56
|
+
*
|
|
57
|
+
* Matching rules:
|
|
58
|
+
* 1. If the cert has Subject Alternative Name (SAN) entries:
|
|
59
|
+
* - For DNS SANs: compare against hostname (supports leftmost-label wildcards
|
|
60
|
+
* like *.example.com). IP address in hostname only matches IP SAN, not DNS SAN.
|
|
61
|
+
* - For IP SANs: compare against hostname as IP.
|
|
62
|
+
* - CN is IGNORED (per RFC 6125 and modern browsers).
|
|
63
|
+
* 2. If no SAN entries exist, fall back to the cert's CN (common name)
|
|
64
|
+
* with the same matching rules — legacy path, deprecated by RFC 6125
|
|
65
|
+
* but still accepted by Node and common CAs.
|
|
66
|
+
*
|
|
67
|
+
* cert: the object returned by tlsSocket.getPeerCertificate() — must have
|
|
68
|
+
* `subject` (with `CN`) and `subjectaltname` (OpenSSL-formatted string).
|
|
69
|
+
*/
|
|
70
|
+
function checkServerIdentity(hostname, cert) {
|
|
71
|
+
if (!cert) return new Error('checkServerIdentity: no certificate');
|
|
72
|
+
const host = String(hostname || '').toLowerCase();
|
|
73
|
+
if (!host) return new Error('checkServerIdentity: hostname required');
|
|
74
|
+
|
|
75
|
+
const isIp = /^(\d{1,3}\.){3}\d{1,3}$|^\[?[0-9a-fA-F:]+\]?$/.test(host);
|
|
76
|
+
|
|
77
|
+
// Parse subjectaltname (OpenSSL format: "DNS:a.com, DNS:*.b.com, IP Address:1.2.3.4")
|
|
78
|
+
const altnames = [];
|
|
79
|
+
if (cert.subjectaltname && typeof cert.subjectaltname === 'string') {
|
|
80
|
+
const parts = cert.subjectaltname.split(',');
|
|
81
|
+
for (let p of parts) {
|
|
82
|
+
p = p.trim();
|
|
83
|
+
if (p.startsWith('DNS:')) altnames.push({ type: 'DNS', value: p.slice(4).toLowerCase() });
|
|
84
|
+
else if (p.startsWith('IP Address:')) altnames.push({ type: 'IP', value: p.slice(11).trim() });
|
|
85
|
+
else if (p.startsWith('IP:')) altnames.push({ type: 'IP', value: p.slice(3).trim() });
|
|
86
|
+
// Other SAN types (URI, email, etc.) ignored
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// DNS wildcard matcher: the wildcard must be the leftmost label only.
|
|
91
|
+
// *.example.com matches foo.example.com, NOT foo.bar.example.com, NOT example.com.
|
|
92
|
+
function dnsMatches(pattern, name) {
|
|
93
|
+
if (pattern === name) return true;
|
|
94
|
+
if (!pattern.startsWith('*.')) return false;
|
|
95
|
+
const dot = name.indexOf('.');
|
|
96
|
+
if (dot < 0) return false;
|
|
97
|
+
return pattern.slice(2) === name.slice(dot + 1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// IP match: string-equal for IPv4; normalize brackets/case for IPv6.
|
|
101
|
+
function ipMatches(pattern, name) {
|
|
102
|
+
return pattern.replace(/^\[|\]$/g, '').toLowerCase()
|
|
103
|
+
=== name.replace(/^\[|\]$/g, '').toLowerCase();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// RFC 6125: if SANs are present, do NOT fall back to CN.
|
|
107
|
+
if (altnames.length > 0) {
|
|
108
|
+
for (const s of altnames) {
|
|
109
|
+
if (isIp && s.type === 'IP' && ipMatches(s.value, host)) return undefined;
|
|
110
|
+
if (!isIp && s.type === 'DNS' && dnsMatches(s.value, host)) return undefined;
|
|
111
|
+
}
|
|
112
|
+
const details = altnames.map(s => `${s.type}:${s.value}`).join(', ');
|
|
113
|
+
return new Error(`Hostname/IP does not match certificate's altnames: Host: ${hostname}. is not in the cert's altnames: ${details}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Legacy CN fallback (only when no SAN present)
|
|
117
|
+
const cn = cert.subject && cert.subject.CN;
|
|
118
|
+
if (cn) {
|
|
119
|
+
const cnLower = String(cn).toLowerCase();
|
|
120
|
+
if (isIp) {
|
|
121
|
+
if (ipMatches(cnLower, host)) return undefined;
|
|
122
|
+
} else {
|
|
123
|
+
if (dnsMatches(cnLower, host)) return undefined;
|
|
124
|
+
}
|
|
125
|
+
return new Error(`Hostname/IP does not match certificate's CN: Host: ${hostname}. is not cert's CN: ${cn}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return new Error(`Hostname/IP does not match any certificate identity`);
|
|
129
|
+
}
|
|
130
|
+
|
|
32
131
|
// ===================== tls.connect() =====================
|
|
33
132
|
|
|
34
133
|
/**
|
|
@@ -43,7 +142,6 @@ function getCiphers() {
|
|
|
43
142
|
function connect(/* ...args */) {
|
|
44
143
|
let port, host, options, connectListener;
|
|
45
144
|
|
|
46
|
-
// Parse overloaded arguments
|
|
47
145
|
let args = Array.from(arguments);
|
|
48
146
|
|
|
49
147
|
if (typeof args[0] === 'object' && !Array.isArray(args[0])) {
|
|
@@ -88,7 +186,8 @@ function connect(/* ...args */) {
|
|
|
88
186
|
groups: options.groups,
|
|
89
187
|
prioritizeChaCha: options.prioritizeChaCha,
|
|
90
188
|
maxRecordSize: options.maxRecordSize,
|
|
91
|
-
|
|
189
|
+
sessionTickets: options.sessionTickets,
|
|
190
|
+
ticketLifetime: options.ticketLifetime,
|
|
92
191
|
cert: options.cert,
|
|
93
192
|
key: options.key,
|
|
94
193
|
maxHandshakeSize: options.maxHandshakeSize,
|
|
@@ -111,33 +210,68 @@ function connect(/* ...args */) {
|
|
|
111
210
|
return socket;
|
|
112
211
|
}
|
|
113
212
|
|
|
114
|
-
// ===================== tls.
|
|
213
|
+
// ===================== tls.Server =====================
|
|
115
214
|
|
|
116
215
|
/**
|
|
117
|
-
* Node.js compatible tls.
|
|
216
|
+
* Node.js compatible tls.Server class.
|
|
118
217
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
218
|
+
* Wraps a net.Server and emits:
|
|
219
|
+
* - 'secureConnection' (socket) — after TLS handshake completes
|
|
220
|
+
* - 'newSession' (id, data, callback) — for TLS 1.2 Session ID storage
|
|
221
|
+
* - 'resumeSession' (id, callback(err,data)) — for TLS 1.2 Session ID retrieval
|
|
222
|
+
* - 'tlsClientError' (err, socket) — handshake errors
|
|
223
|
+
*
|
|
224
|
+
* Provides:
|
|
225
|
+
* - listen(), close(), address(), getConnections()
|
|
226
|
+
* - getTicketKeys(), setTicketKeys(), setSecureContext()
|
|
121
227
|
*/
|
|
122
|
-
function
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
228
|
+
function Server(options, connectionListener) {
|
|
229
|
+
if (!(this instanceof Server)) return new Server(options, connectionListener);
|
|
230
|
+
EventEmitter.call(this);
|
|
231
|
+
|
|
232
|
+
let self = this;
|
|
127
233
|
options = options || {};
|
|
128
234
|
|
|
129
|
-
|
|
235
|
+
// Shared ticketKeys across all connections
|
|
236
|
+
self._ticketKeys = options.ticketKeys ? Buffer.from(options.ticketKeys) : crypto.randomBytes(48);
|
|
237
|
+
|
|
238
|
+
// sessionTimeout: seconds to cache a TLS 1.2 Session ID (Node default: 300)
|
|
239
|
+
let sessionTimeoutSec = (typeof options.sessionTimeout === 'number' && options.sessionTimeout > 0)
|
|
240
|
+
? (options.sessionTimeout >>> 0)
|
|
241
|
+
: 300;
|
|
242
|
+
|
|
243
|
+
// sessionIdContext: opaque tag. Sessions stored under one context cannot be resumed
|
|
244
|
+
// under another (matches Node/OpenSSL behavior — prevents cross-server leakage).
|
|
245
|
+
// Stored as a hex prefix on cache keys.
|
|
246
|
+
let sessionIdContextHex = '';
|
|
247
|
+
if (options.sessionIdContext != null) {
|
|
248
|
+
let sidCtxBuf = Buffer.isBuffer(options.sessionIdContext)
|
|
249
|
+
? options.sessionIdContext
|
|
250
|
+
: Buffer.from(String(options.sessionIdContext));
|
|
251
|
+
sessionIdContextHex = sidCtxBuf.toString('hex');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Pre-compiled default SecureContext if key+cert provided directly (no SNICallback)
|
|
255
|
+
let defaultCtx = null;
|
|
130
256
|
if (options.key && options.cert) {
|
|
131
|
-
|
|
257
|
+
defaultCtx = createSecureContext({ key: options.key, cert: options.cert });
|
|
132
258
|
}
|
|
133
259
|
|
|
134
|
-
//
|
|
135
|
-
|
|
260
|
+
// In-memory fallback session store for TLS 1.2 Session IDs.
|
|
261
|
+
// Entry shape: { data: Buffer, expiresAt: ms }
|
|
262
|
+
// Only used when the user hasn't registered their own 'newSession'/'resumeSession' handlers.
|
|
263
|
+
// Keys are namespaced by sessionIdContext so multiple servers don't cross-pollinate.
|
|
264
|
+
let inMemoryStore = {};
|
|
136
265
|
|
|
137
|
-
|
|
266
|
+
function storeKey(id) {
|
|
267
|
+
return sessionIdContextHex + ':' + toHex(id);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function buildSocketOpts() {
|
|
138
271
|
let socketOpts = {
|
|
139
272
|
isServer: true,
|
|
140
|
-
ticketKeys:
|
|
273
|
+
ticketKeys: self._ticketKeys,
|
|
274
|
+
ticketLifetime: options.ticketLifetime,
|
|
141
275
|
ALPNProtocols: options.ALPNProtocols,
|
|
142
276
|
minVersion: options.minVersion || DEFAULT_MIN_VERSION,
|
|
143
277
|
maxVersion: options.maxVersion || DEFAULT_MAX_VERSION,
|
|
@@ -145,41 +279,161 @@ function createServer(options, connectionListener) {
|
|
|
145
279
|
groups: options.groups,
|
|
146
280
|
prioritizeChaCha: options.prioritizeChaCha,
|
|
147
281
|
maxRecordSize: options.maxRecordSize,
|
|
148
|
-
|
|
282
|
+
sessionTickets: options.sessionTickets,
|
|
149
283
|
requestCert: options.requestCert,
|
|
150
284
|
maxHandshakeSize: options.maxHandshakeSize,
|
|
151
285
|
allowedCipherSuites: options.allowedCipherSuites,
|
|
286
|
+
handshakeTimeout: options.handshakeTimeout,
|
|
152
287
|
};
|
|
153
288
|
|
|
154
289
|
if (options.SNICallback) {
|
|
155
290
|
socketOpts.SNICallback = options.SNICallback;
|
|
156
|
-
} else if (
|
|
291
|
+
} else if (defaultCtx) {
|
|
157
292
|
socketOpts.SNICallback = function(servername, cb) {
|
|
158
|
-
cb(null,
|
|
293
|
+
cb(null, defaultCtx);
|
|
159
294
|
};
|
|
160
295
|
}
|
|
161
296
|
|
|
162
|
-
|
|
163
|
-
|
|
297
|
+
return socketOpts;
|
|
298
|
+
}
|
|
164
299
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
300
|
+
self._tcpServer = net.createServer(function(tcp) {
|
|
301
|
+
let socket;
|
|
302
|
+
try {
|
|
303
|
+
socket = new TLSSocket(tcp, buildSocketOpts());
|
|
304
|
+
} catch (err) {
|
|
305
|
+
self.emit('tlsClientError', err, null);
|
|
306
|
+
try { tcp.destroy(); } catch(e){}
|
|
307
|
+
return;
|
|
169
308
|
}
|
|
170
309
|
|
|
171
|
-
|
|
310
|
+
addCompatMethods(socket);
|
|
311
|
+
|
|
312
|
+
// Bridge TLS 1.2 Session ID events from socket → server.
|
|
313
|
+
// If the user has registered their own handlers, delegate to them. Otherwise,
|
|
314
|
+
// use the built-in in-memory cache (with sessionTimeout expiry and sessionIdContext
|
|
315
|
+
// isolation).
|
|
316
|
+
socket.on('newSession', function(id, data, cb) {
|
|
317
|
+
if (self.listenerCount('newSession') > 0) {
|
|
318
|
+
self.emit('newSession', Buffer.from(id), Buffer.from(data), cb);
|
|
319
|
+
} else {
|
|
320
|
+
inMemoryStore[storeKey(id)] = {
|
|
321
|
+
data: Buffer.from(data),
|
|
322
|
+
expiresAt: Date.now() + sessionTimeoutSec * 1000,
|
|
323
|
+
};
|
|
324
|
+
cb();
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
socket.on('resumeSession', function(id, cb) {
|
|
329
|
+
if (self.listenerCount('resumeSession') > 0) {
|
|
330
|
+
self.emit('resumeSession', Buffer.from(id), cb);
|
|
331
|
+
} else {
|
|
332
|
+
let key = storeKey(id);
|
|
333
|
+
let entry = inMemoryStore[key];
|
|
334
|
+
if (!entry) return cb(null, null);
|
|
335
|
+
if (entry.expiresAt < Date.now()) {
|
|
336
|
+
// Expired → evict and treat as cache miss
|
|
337
|
+
delete inMemoryStore[key];
|
|
338
|
+
return cb(null, null);
|
|
339
|
+
}
|
|
340
|
+
cb(null, entry.data);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// 'secureConnection' fires AFTER handshake completes (Node.js semantics)
|
|
345
|
+
socket.on('secureConnect', function() {
|
|
346
|
+
if (connectionListener) connectionListener(socket);
|
|
347
|
+
self.emit('secureConnection', socket);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Forward keylog to server level with Node.js signature (line, tlsSocket)
|
|
351
|
+
socket.on('keylog', function(line) {
|
|
352
|
+
self.emit('keylog', line, socket);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Surface pre-handshake errors as 'tlsClientError' (Node.js semantics)
|
|
356
|
+
socket.on('error', function(err) {
|
|
357
|
+
if (!socket.secureEstablished) {
|
|
358
|
+
self.emit('tlsClientError', err, socket);
|
|
359
|
+
}
|
|
360
|
+
});
|
|
172
361
|
});
|
|
173
362
|
|
|
174
|
-
|
|
363
|
+
// Delegate net.Server methods
|
|
364
|
+
self.listen = function() {
|
|
365
|
+
return self._tcpServer.listen.apply(self._tcpServer, arguments);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
self.close = function(cb) {
|
|
369
|
+
return self._tcpServer.close(cb);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
self.address = function() {
|
|
373
|
+
return self._tcpServer.address();
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
self.getConnections = function(cb) {
|
|
377
|
+
return self._tcpServer.getConnections(cb);
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// Ticket key management
|
|
381
|
+
self.getTicketKeys = function() {
|
|
382
|
+
return Buffer.from(self._ticketKeys);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
self.setTicketKeys = function(keys) {
|
|
386
|
+
if (!Buffer.isBuffer(keys) && !(keys instanceof Uint8Array)) {
|
|
387
|
+
throw new TypeError('setTicketKeys requires a Buffer/Uint8Array');
|
|
388
|
+
}
|
|
389
|
+
if (keys.length !== 48) {
|
|
390
|
+
throw new RangeError('ticketKeys must be exactly 48 bytes');
|
|
391
|
+
}
|
|
392
|
+
self._ticketKeys = Buffer.from(keys);
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
// setSecureContext — replace cert/key without restart (Node.js compat)
|
|
396
|
+
self.setSecureContext = function(opts) {
|
|
397
|
+
if (opts && opts.key && opts.cert) {
|
|
398
|
+
defaultCtx = createSecureContext({ key: opts.key, cert: opts.cert });
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
return self;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Inherit from EventEmitter
|
|
406
|
+
Object.setPrototypeOf(Server.prototype, EventEmitter.prototype);
|
|
407
|
+
Object.setPrototypeOf(Server, EventEmitter);
|
|
408
|
+
|
|
409
|
+
// ===================== tls.createServer() =====================
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Node.js compatible tls.createServer().
|
|
413
|
+
* tls.createServer([options][, connectionListener])
|
|
414
|
+
*/
|
|
415
|
+
function createServer(options, connectionListener) {
|
|
416
|
+
if (typeof options === 'function') {
|
|
417
|
+
connectionListener = options;
|
|
418
|
+
options = {};
|
|
419
|
+
}
|
|
420
|
+
return new Server(options, connectionListener);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ===================== Helpers =====================
|
|
424
|
+
|
|
425
|
+
function toHex(buf) {
|
|
426
|
+
if (!buf) return '';
|
|
427
|
+
let b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
|
|
428
|
+
return b.toString('hex');
|
|
175
429
|
}
|
|
176
430
|
|
|
177
431
|
// ===================== Compat methods for TLSSocket =====================
|
|
178
432
|
|
|
179
433
|
function addCompatMethods(socket) {
|
|
180
|
-
let session = socket.
|
|
434
|
+
let session = socket._getTLSSession();
|
|
181
435
|
|
|
182
|
-
/** Node.js compat: isSessionReused() */
|
|
436
|
+
/** Node.js compat: isSessionReused() — method form (same as isResumed getter) */
|
|
183
437
|
socket.isSessionReused = function() {
|
|
184
438
|
return socket.isResumed;
|
|
185
439
|
};
|
|
@@ -204,6 +458,7 @@ function addCompatMethods(socket) {
|
|
|
204
458
|
let group = session.context.selected_group;
|
|
205
459
|
if (group === 0x001d) return { type: 'X25519', size: 253 };
|
|
206
460
|
if (group === 0x0017) return { type: 'ECDH', name: 'prime256v1', size: 256 };
|
|
461
|
+
if (group === 0x0018) return { type: 'ECDH', name: 'secp384r1', size: 384 };
|
|
207
462
|
return {};
|
|
208
463
|
};
|
|
209
464
|
|
|
@@ -227,9 +482,13 @@ function addCompatMethods(socket) {
|
|
|
227
482
|
export {
|
|
228
483
|
connect,
|
|
229
484
|
createServer,
|
|
485
|
+
Server,
|
|
230
486
|
createSecureContext,
|
|
231
487
|
getCiphers,
|
|
488
|
+
checkServerIdentity,
|
|
232
489
|
addCompatMethods,
|
|
233
490
|
DEFAULT_MIN_VERSION,
|
|
234
491
|
DEFAULT_MAX_VERSION,
|
|
492
|
+
DEFAULT_CIPHERS,
|
|
493
|
+
DEFAULT_ECDH_CURVE,
|
|
235
494
|
};
|
package/src/crypto.js
CHANGED
|
@@ -276,19 +276,22 @@ function hkdf_extract(hashName, saltU8, ikmU8) {
|
|
|
276
276
|
function hkdf_expand(hashName, prkU8, infoU8, length) {
|
|
277
277
|
let hashLen = getHashLen(hashName);
|
|
278
278
|
let N = Math.ceil(length / hashLen);
|
|
279
|
-
|
|
279
|
+
// allocUnsafe is safe here: every byte is overwritten by prev.copy() below.
|
|
280
|
+
let output = Buffer.allocUnsafe(N * hashLen);
|
|
280
281
|
let prev = Buffer.alloc(0);
|
|
282
|
+
let counter = Buffer.allocUnsafe(1);
|
|
281
283
|
|
|
282
284
|
for (let i = 1; i <= N; i++) {
|
|
283
285
|
let h = crypto.createHmac(hashName, prkU8);
|
|
284
286
|
h.update(prev);
|
|
285
287
|
h.update(infoU8);
|
|
286
|
-
|
|
288
|
+
counter[0] = i;
|
|
289
|
+
h.update(counter);
|
|
287
290
|
prev = h.digest();
|
|
288
291
|
prev.copy(output, (i - 1) * hashLen);
|
|
289
292
|
}
|
|
290
293
|
|
|
291
|
-
return new Uint8Array(output.
|
|
294
|
+
return new Uint8Array(output.buffer, output.byteOffset, length);
|
|
292
295
|
}
|
|
293
296
|
|
|
294
297
|
|
|
@@ -296,10 +299,12 @@ function hkdf_expand(hashName, prkU8, infoU8, length) {
|
|
|
296
299
|
// TLS 1.3 HKDF-Expand-Label (RFC 8446 section 7.1)
|
|
297
300
|
// ============================================================
|
|
298
301
|
|
|
302
|
+
// Module-level TextEncoder — creating one per hkdf call added significant overhead
|
|
303
|
+
// to handshakes (TextEncoder construction is not free in V8).
|
|
304
|
+
const _TEXT_ENCODER = new TextEncoder();
|
|
305
|
+
|
|
299
306
|
function build_hkdf_label(label, context, length) {
|
|
300
|
-
|
|
301
|
-
let enc = new TextEncoder();
|
|
302
|
-
let full = enc.encode(prefix + label);
|
|
307
|
+
const full = _TEXT_ENCODER.encode('tls13 ' + label);
|
|
303
308
|
const info = new Uint8Array(2 + 1 + full.length + 1 + context.length);
|
|
304
309
|
|
|
305
310
|
info[0] = (length >>> 8) & 0xff;
|
|
@@ -345,6 +350,31 @@ function derive_handshake_traffic_secrets(hashName, shared_secret, transcript) {
|
|
|
345
350
|
};
|
|
346
351
|
}
|
|
347
352
|
|
|
353
|
+
/**
|
|
354
|
+
* Like derive_handshake_traffic_secrets but accepts a pre-computed transcript
|
|
355
|
+
* hash — skips the hashFn(transcript) step and its allocation. Use this when
|
|
356
|
+
* the caller has already computed the hash via an incremental running hash.
|
|
357
|
+
*/
|
|
358
|
+
function derive_handshake_traffic_secrets_with_hash(hashName, shared_secret, transcript_hash) {
|
|
359
|
+
let hashFn = getHashFn(hashName);
|
|
360
|
+
let hashLen = hashFn.outputLen | 0;
|
|
361
|
+
const empty = new Uint8Array(0);
|
|
362
|
+
const zeros = new Uint8Array(hashLen);
|
|
363
|
+
|
|
364
|
+
let early_secret = hkdf_extract(hashName, empty, zeros);
|
|
365
|
+
let h_empty = hashFn(empty);
|
|
366
|
+
let derived_secret = hkdf_expand_label(hashName, early_secret, 'derived', h_empty, hashLen);
|
|
367
|
+
let handshake_secret = hkdf_extract(hashName, derived_secret, shared_secret);
|
|
368
|
+
let client_handshake_traffic_secret = hkdf_expand_label(hashName, handshake_secret, 'c hs traffic', transcript_hash, hashLen);
|
|
369
|
+
let server_handshake_traffic_secret = hkdf_expand_label(hashName, handshake_secret, 's hs traffic', transcript_hash, hashLen);
|
|
370
|
+
|
|
371
|
+
return {
|
|
372
|
+
handshake_secret: handshake_secret,
|
|
373
|
+
client_handshake_traffic_secret: client_handshake_traffic_secret,
|
|
374
|
+
server_handshake_traffic_secret: server_handshake_traffic_secret,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
348
378
|
|
|
349
379
|
// ============================================================
|
|
350
380
|
// TLS 1.3: derive application traffic secrets
|
|
@@ -370,6 +400,28 @@ function derive_app_traffic_secrets(hashName, handshake_secret, transcript) {
|
|
|
370
400
|
};
|
|
371
401
|
}
|
|
372
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Like derive_app_traffic_secrets but accepts a pre-computed transcript hash.
|
|
405
|
+
*/
|
|
406
|
+
function derive_app_traffic_secrets_with_hash(hashName, handshake_secret, transcript_hash) {
|
|
407
|
+
let hashFn = getHashFn(hashName);
|
|
408
|
+
let hashLen = hashFn.outputLen | 0;
|
|
409
|
+
const empty = new Uint8Array(0);
|
|
410
|
+
const zeros = new Uint8Array(hashLen);
|
|
411
|
+
|
|
412
|
+
let h_empty = hashFn(empty);
|
|
413
|
+
let derived_secret = hkdf_expand_label(hashName, handshake_secret, 'derived', h_empty, hashLen);
|
|
414
|
+
let master_secret = hkdf_extract(hashName, derived_secret, zeros);
|
|
415
|
+
let client_app_traffic_secret = hkdf_expand_label(hashName, master_secret, 'c ap traffic', transcript_hash, hashLen);
|
|
416
|
+
let server_app_traffic_secret = hkdf_expand_label(hashName, master_secret, 's ap traffic', transcript_hash, hashLen);
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
client_app_traffic_secret: client_app_traffic_secret,
|
|
420
|
+
server_app_traffic_secret: server_app_traffic_secret,
|
|
421
|
+
master_secret: master_secret
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
373
425
|
|
|
374
426
|
// ============================================================
|
|
375
427
|
// TLS 1.3: resumption master secret (RFC 8446 §7.1)
|
|
@@ -386,6 +438,14 @@ function derive_resumption_master_secret(hashName, master_secret, transcript) {
|
|
|
386
438
|
return hkdf_expand_label(hashName, master_secret, 'res master', transcript_hash, hashLen);
|
|
387
439
|
}
|
|
388
440
|
|
|
441
|
+
/**
|
|
442
|
+
* Like derive_resumption_master_secret but accepts a pre-computed transcript hash.
|
|
443
|
+
*/
|
|
444
|
+
function derive_resumption_master_secret_with_hash(hashName, master_secret, transcript_hash) {
|
|
445
|
+
let hashLen = getHashLen(hashName);
|
|
446
|
+
return hkdf_expand_label(hashName, master_secret, 'res master', transcript_hash, hashLen);
|
|
447
|
+
}
|
|
448
|
+
|
|
389
449
|
/**
|
|
390
450
|
* Derive PSK from a resumption_master_secret + ticket_nonce.
|
|
391
451
|
* Used by the server when creating a ticket, and by the client when resuming.
|
|
@@ -416,11 +476,21 @@ function derive_binder_key(hashName, psk, isExternal) {
|
|
|
416
476
|
* Compute a PSK binder value.
|
|
417
477
|
* binder_key = derive_binder_key(...)
|
|
418
478
|
* transcript = ClientHello up to (but not including) the binders list.
|
|
479
|
+
*
|
|
480
|
+
* Per RFC 8446 §4.1.4: "PskBinderEntry is computed in the same way as the Finished
|
|
481
|
+
* message (Section 4.4.4) but with the BaseKey being the binder_key derived via
|
|
482
|
+
* the key schedule from the corresponding PSK."
|
|
483
|
+
*
|
|
484
|
+
* So we must derive a finished_key from binder_key (as in get_handshake_finished)
|
|
485
|
+
* before the HMAC, NOT use binder_key directly.
|
|
419
486
|
*/
|
|
420
487
|
function compute_psk_binder(hashName, binder_key, truncated_transcript) {
|
|
421
488
|
let hashFn = getHashFn(hashName);
|
|
489
|
+
let hashLen = hashFn.outputLen | 0;
|
|
490
|
+
const empty = new Uint8Array(0);
|
|
491
|
+
let finished_key = hkdf_expand_label(hashName, binder_key, 'finished', empty, hashLen);
|
|
422
492
|
let transcript_hash = hashFn(truncated_transcript);
|
|
423
|
-
return hmac(hashName,
|
|
493
|
+
return hmac(hashName, finished_key, transcript_hash);
|
|
424
494
|
}
|
|
425
495
|
|
|
426
496
|
/**
|
|
@@ -447,6 +517,28 @@ function derive_handshake_traffic_secrets_psk(hashName, psk, shared_secret, tran
|
|
|
447
517
|
};
|
|
448
518
|
}
|
|
449
519
|
|
|
520
|
+
/**
|
|
521
|
+
* Like derive_handshake_traffic_secrets_psk but accepts a pre-computed transcript hash.
|
|
522
|
+
*/
|
|
523
|
+
function derive_handshake_traffic_secrets_psk_with_hash(hashName, psk, shared_secret, transcript_hash) {
|
|
524
|
+
let hashFn = getHashFn(hashName);
|
|
525
|
+
let hashLen = hashFn.outputLen | 0;
|
|
526
|
+
const empty = new Uint8Array(0);
|
|
527
|
+
|
|
528
|
+
let early_secret = hkdf_extract(hashName, empty, psk);
|
|
529
|
+
let h_empty = hashFn(empty);
|
|
530
|
+
let derived_secret = hkdf_expand_label(hashName, early_secret, 'derived', h_empty, hashLen);
|
|
531
|
+
let handshake_secret = hkdf_extract(hashName, derived_secret, shared_secret);
|
|
532
|
+
let client_handshake_traffic_secret = hkdf_expand_label(hashName, handshake_secret, 'c hs traffic', transcript_hash, hashLen);
|
|
533
|
+
let server_handshake_traffic_secret = hkdf_expand_label(hashName, handshake_secret, 's hs traffic', transcript_hash, hashLen);
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
handshake_secret: handshake_secret,
|
|
537
|
+
client_handshake_traffic_secret: client_handshake_traffic_secret,
|
|
538
|
+
server_handshake_traffic_secret: server_handshake_traffic_secret,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
450
542
|
|
|
451
543
|
// ============================================================
|
|
452
544
|
// TLS 1.2 PRF (RFC 5246 section 5)
|
|
@@ -539,6 +631,18 @@ function build_cert_verify_tbs(hashName, isServer, transcript) {
|
|
|
539
631
|
return concatUint8Arrays([padding, label, separator, transcript_hash]);
|
|
540
632
|
}
|
|
541
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Like build_cert_verify_tbs but accepts a pre-computed transcript hash.
|
|
636
|
+
*/
|
|
637
|
+
function build_cert_verify_tbs_with_hash(hashName, isServer, transcript_hash) {
|
|
638
|
+
let label = new TextEncoder().encode(
|
|
639
|
+
isServer ? "TLS 1.3, server CertificateVerify" : "TLS 1.3, client CertificateVerify"
|
|
640
|
+
);
|
|
641
|
+
const separator = new Uint8Array([0x00]);
|
|
642
|
+
const padding = new Uint8Array(64).fill(0x20);
|
|
643
|
+
return concatUint8Arrays([padding, label, separator, transcript_hash]);
|
|
644
|
+
}
|
|
645
|
+
|
|
542
646
|
|
|
543
647
|
// ============================================================
|
|
544
648
|
// TLS 1.3 Finished verify_data
|
|
@@ -552,6 +656,16 @@ function get_handshake_finished(hashName, traffic_secret, transcript) {
|
|
|
552
656
|
return hmac(hashName, finished_key, transcript_hash);
|
|
553
657
|
}
|
|
554
658
|
|
|
659
|
+
/**
|
|
660
|
+
* Like get_handshake_finished but accepts a pre-computed transcript hash.
|
|
661
|
+
*/
|
|
662
|
+
function get_handshake_finished_with_hash(hashName, traffic_secret, transcript_hash) {
|
|
663
|
+
let hashLen = getHashLen(hashName);
|
|
664
|
+
const empty = new Uint8Array(0);
|
|
665
|
+
let finished_key = hkdf_expand_label(hashName, traffic_secret, 'finished', empty, hashLen);
|
|
666
|
+
return hmac(hashName, finished_key, transcript_hash);
|
|
667
|
+
}
|
|
668
|
+
|
|
555
669
|
|
|
556
670
|
// ============================================================
|
|
557
671
|
// DTLS 1.3: record number encryption key (RFC 9147 §5.9)
|
|
@@ -579,13 +693,19 @@ export {
|
|
|
579
693
|
tls_derive_from_master_secret_tls12,
|
|
580
694
|
tls12_prf,
|
|
581
695
|
derive_handshake_traffic_secrets,
|
|
696
|
+
derive_handshake_traffic_secrets_with_hash,
|
|
582
697
|
derive_app_traffic_secrets,
|
|
698
|
+
derive_app_traffic_secrets_with_hash,
|
|
583
699
|
derive_resumption_master_secret,
|
|
700
|
+
derive_resumption_master_secret_with_hash,
|
|
584
701
|
derive_psk,
|
|
585
702
|
derive_binder_key,
|
|
586
703
|
compute_psk_binder,
|
|
587
704
|
derive_handshake_traffic_secrets_psk,
|
|
705
|
+
derive_handshake_traffic_secrets_psk_with_hash,
|
|
588
706
|
build_cert_verify_tbs,
|
|
707
|
+
build_cert_verify_tbs_with_hash,
|
|
589
708
|
get_handshake_finished,
|
|
709
|
+
get_handshake_finished_with_hash,
|
|
590
710
|
derive_sn_key,
|
|
591
711
|
};
|
package/src/dtls_socket.js
CHANGED
|
@@ -44,6 +44,7 @@ function DTLSSocket(udpSocket, options) {
|
|
|
44
44
|
cert: options.cert,
|
|
45
45
|
key: options.key,
|
|
46
46
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
47
|
+
requestCert: options.requestCert,
|
|
47
48
|
ca: options.ca,
|
|
48
49
|
alpnProtocols: options.alpnProtocols,
|
|
49
50
|
minVersion: options.minVersion,
|
|
@@ -159,6 +160,7 @@ function createDTLSServer(options, connectionListener) {
|
|
|
159
160
|
remotePort: rinfo.port,
|
|
160
161
|
cert: options.cert,
|
|
161
162
|
key: options.key,
|
|
163
|
+
requestCert: options.requestCert,
|
|
162
164
|
SNICallback: options.SNICallback,
|
|
163
165
|
alpnProtocols: options.alpnProtocols,
|
|
164
166
|
minVersion: options.minVersion,
|
|
@@ -239,6 +241,7 @@ function connectDTLS(options, callback) {
|
|
|
239
241
|
remotePort: port,
|
|
240
242
|
servername: options.servername || host,
|
|
241
243
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
244
|
+
requestCert: options.requestCert,
|
|
242
245
|
ca: options.ca,
|
|
243
246
|
alpnProtocols: options.alpnProtocols,
|
|
244
247
|
minVersion: options.minVersion,
|