lemon-tls 0.6.0 → 0.9.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 +253 -20
- package/index.d.ts +94 -6
- package/index.js +126 -0
- package/package.json +2 -7
- package/src/compat.js +316 -45
- package/src/crypto.js +33 -3
- package/src/dtls_session.js +477 -41
- package/src/dtls_socket.js +32 -0
- package/src/node_options.js +138 -0
- package/src/record.js +210 -27
- package/src/secure_context.js +49 -4
- package/src/session/cert_compression.js +157 -0
- package/src/session/ecdh.js +237 -10
- package/src/session/ech.js +715 -0
- package/src/session/hpke.js +414 -0
- package/src/session/message.js +44 -5
- package/src/session/signing.js +56 -3
- package/src/session/ticket.js +44 -28
- package/src/tls_session.js +3003 -217
- package/src/tls_socket.js +839 -149
- package/src/utils.js +75 -2
- package/src/wire.js +220 -13
package/src/compat.js
CHANGED
|
@@ -6,12 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import net from 'node:net';
|
|
9
|
+
import nodeTls from 'node:tls'; // for the shipped CA bundle only
|
|
9
10
|
import crypto from 'node:crypto';
|
|
10
11
|
import { EventEmitter } from 'node:events';
|
|
11
12
|
import { TLS_CIPHER_SUITES } from './crypto.js';
|
|
12
13
|
import TLSSession from './tls_session.js';
|
|
13
14
|
import TLSSocket from './tls_socket.js';
|
|
14
15
|
import createSecureContext from './secure_context.js';
|
|
16
|
+
import { parseCiphers, parseEcdhCurve, parseSigalgs } from './node_options.js';
|
|
17
|
+
import { prepare_ech_server } from './session/ech.js';
|
|
15
18
|
|
|
16
19
|
// ===================== Constants =====================
|
|
17
20
|
|
|
@@ -44,6 +47,46 @@ function getCiphers() {
|
|
|
44
47
|
return out;
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
/* ===================== Default CA store ===================== */
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The trust anchors Node ships (Mozilla's CA bundle). Exposed under the same
|
|
54
|
+
* name Node uses so `tls.rootCertificates` works unchanged, and so this
|
|
55
|
+
* library has a real default to chain against.
|
|
56
|
+
*
|
|
57
|
+
* That default matters more than parity. Until now, "no `ca` configured" meant
|
|
58
|
+
* "authorize every certificate", while `rejectUnauthorized` defaulted to true
|
|
59
|
+
* — the connection reported itself verified without ever verifying anything.
|
|
60
|
+
* Node never behaved that way: it falls back to this bundle.
|
|
61
|
+
*
|
|
62
|
+
* Parsing all ~144 certificates costs a few tens of milliseconds, so it is
|
|
63
|
+
* done once, lazily, and only if something actually needs it.
|
|
64
|
+
*/
|
|
65
|
+
let _rootPems = null;
|
|
66
|
+
function getRootCertificates() {
|
|
67
|
+
if (_rootPems === null) {
|
|
68
|
+
try { _rootPems = nodeTls.rootCertificates.slice(); }
|
|
69
|
+
catch (e) { _rootPems = []; } // a build without the bundle
|
|
70
|
+
}
|
|
71
|
+
return _rootPems;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Node-compat: tls.getCACertificates([type]). 'default' and 'bundled' both
|
|
75
|
+
* resolve to the shipped bundle; 'extra' and 'system' are not sourced here
|
|
76
|
+
* and return empty rather than pretending. */
|
|
77
|
+
function getCACertificates(type) {
|
|
78
|
+
let t = type || 'default';
|
|
79
|
+
if (t === 'default' || t === 'bundled') return getRootCertificates().slice();
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Node-compat: tls.setDefaultCACertificates(certs). Replaces the bundle for
|
|
84
|
+
* every subsequent connection that does not pass its own `ca`. */
|
|
85
|
+
function setDefaultCACertificates(certs) {
|
|
86
|
+
if (!Array.isArray(certs)) throw new TypeError('certs must be an array');
|
|
87
|
+
_rootPems = certs.slice();
|
|
88
|
+
}
|
|
89
|
+
|
|
47
90
|
// ===================== tls.checkServerIdentity() =====================
|
|
48
91
|
|
|
49
92
|
/**
|
|
@@ -177,20 +220,49 @@ function connect(/* ...args */) {
|
|
|
177
220
|
isServer: false,
|
|
178
221
|
servername: options.servername || host,
|
|
179
222
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
223
|
+
checkServerIdentity: options.checkServerIdentity,
|
|
224
|
+
// Stream-shape options Node forwards to the socket. Both were dropped
|
|
225
|
+
// here, so allowHalfOpen could not be set through connect() at all.
|
|
226
|
+
allowHalfOpen: options.allowHalfOpen,
|
|
227
|
+
highWaterMark: options.highWaterMark,
|
|
228
|
+
secureContext: options.secureContext,
|
|
229
|
+
passphrase: options.passphrase,
|
|
180
230
|
ca: options.ca,
|
|
231
|
+
useDefaultCA: options.useDefaultCA,
|
|
232
|
+
defaultCA: getRootCertificates,
|
|
181
233
|
session: options.session,
|
|
182
234
|
ALPNProtocols: options.ALPNProtocols,
|
|
183
235
|
minVersion: options.minVersion,
|
|
184
236
|
maxVersion: options.maxVersion,
|
|
185
|
-
|
|
186
|
-
|
|
237
|
+
// node:tls's own strings (ciphers, ecdhCurve, sigalgs) — taken by
|
|
238
|
+
// nothing until now — or lemon-tls's code lists, which win.
|
|
239
|
+
signatureAlgorithms: options.signatureAlgorithms || parseSigalgs(options.sigalgs),
|
|
240
|
+
groups: options.groups || parseEcdhCurve(options.ecdhCurve),
|
|
187
241
|
prioritizeChaCha: options.prioritizeChaCha,
|
|
242
|
+
// Forwarded on the server side but dropped here, so a client could not
|
|
243
|
+
// narrow the suites it offers.
|
|
244
|
+
allowedCipherSuites: options.allowedCipherSuites || parseCiphers(options.ciphers),
|
|
188
245
|
maxRecordSize: options.maxRecordSize,
|
|
189
246
|
sessionTickets: options.sessionTickets,
|
|
190
247
|
ticketLifetime: options.ticketLifetime,
|
|
248
|
+
// RFC 8879: algorithms to offer for the server's certificate chain.
|
|
249
|
+
certificateCompression: options.certificateCompression,
|
|
250
|
+
// node:tls: ask the server for a stapled OCSP response ('OCSPResponse').
|
|
251
|
+
requestOCSP: options.requestOCSP === true,
|
|
191
252
|
cert: options.cert,
|
|
192
253
|
key: options.key,
|
|
193
254
|
maxHandshakeSize: options.maxHandshakeSize,
|
|
255
|
+
// Encrypted Client Hello (RFC 9849): the server's ECHConfigList (bytes or
|
|
256
|
+
// base64 — the `ech=` of its HTTPS record), or GREASE ECH without one.
|
|
257
|
+
ech: options.ech,
|
|
258
|
+
echGrease: options.echGrease === true,
|
|
259
|
+
// ClientHello shaping, documented as connect() options and dropped
|
|
260
|
+
// here: neither reached the socket.
|
|
261
|
+
grease: options.grease,
|
|
262
|
+
permuteExtensions: options.permuteExtensions,
|
|
263
|
+
// 0-RTT: with `session` whose ticket allows it, what is written before
|
|
264
|
+
// 'secureConnect' goes in the first flight ('earlyData' says if taken).
|
|
265
|
+
earlyData: options.earlyData === true,
|
|
194
266
|
});
|
|
195
267
|
|
|
196
268
|
addCompatMethods(socket);
|
|
@@ -199,9 +271,32 @@ function connect(/* ...args */) {
|
|
|
199
271
|
socket.on('secureConnect', connectListener);
|
|
200
272
|
}
|
|
201
273
|
|
|
202
|
-
let tcp =
|
|
274
|
+
let tcp = null;
|
|
275
|
+
|
|
276
|
+
if (options.socket) {
|
|
277
|
+
// Node: "Establish secure connection on a given socket rather than
|
|
278
|
+
// creating a new socket... If this option is specified, path, host, and
|
|
279
|
+
// port are ignored, except for certificate validation." Crucially it also
|
|
280
|
+
// says connection and teardown of that socket remain the caller's job and
|
|
281
|
+
// that tls.connect() will not call net.connect() at all — so we attach and
|
|
282
|
+
// nothing else. This is what STARTTLS needs: the caller speaks cleartext
|
|
283
|
+
// first (SMTP, IMAP, PostgreSQL), then upgrades the same connection.
|
|
284
|
+
//
|
|
285
|
+
// The socket is usually already connected, but it need not be; the
|
|
286
|
+
// handshake simply starts once it is writable.
|
|
287
|
+
tcp = options.socket;
|
|
203
288
|
socket.setSocket(tcp);
|
|
204
|
-
}
|
|
289
|
+
} else {
|
|
290
|
+
// The rest of the options go to net.connect, as node:tls does ("any
|
|
291
|
+
// socket.connect() option not already listed"): noDelay, keepAlive,
|
|
292
|
+
// keepAliveInitialDelay, localAddress, localPort, family, lookup, timeout.
|
|
293
|
+
// Dropped here, none of them could be set through connect() at all.
|
|
294
|
+
let netOptions = Object.assign({}, options, { port: port, host: host });
|
|
295
|
+
delete netOptions.socket;
|
|
296
|
+
tcp = net.connect(netOptions, function() {
|
|
297
|
+
socket.setSocket(tcp);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
205
300
|
|
|
206
301
|
tcp.on('error', function(e) {
|
|
207
302
|
socket.emit('error', e);
|
|
@@ -224,6 +319,7 @@ function connect(/* ...args */) {
|
|
|
224
319
|
* Provides:
|
|
225
320
|
* - listen(), close(), address(), getConnections()
|
|
226
321
|
* - getTicketKeys(), setTicketKeys(), setSecureContext()
|
|
322
|
+
* - getEchKeys(), setEchKeys() — Encrypted Client Hello keys (RFC 9849)
|
|
227
323
|
*/
|
|
228
324
|
function Server(options, connectionListener) {
|
|
229
325
|
if (!(this instanceof Server)) return new Server(options, connectionListener);
|
|
@@ -232,9 +328,28 @@ function Server(options, connectionListener) {
|
|
|
232
328
|
let self = this;
|
|
233
329
|
options = options || {};
|
|
234
330
|
|
|
331
|
+
// Node throws on this combination rather than picking one, and so do we: the
|
|
332
|
+
// two answer the same question by different means, and silently preferring
|
|
333
|
+
// either would leave a server running a policy its author did not write.
|
|
334
|
+
if (options.ALPNProtocols && options.ALPNCallback) {
|
|
335
|
+
throw new TypeError(
|
|
336
|
+
'The ALPNProtocols and ALPNCallback options are mutually exclusive');
|
|
337
|
+
}
|
|
338
|
+
|
|
235
339
|
// Shared ticketKeys across all connections
|
|
236
340
|
self._ticketKeys = options.ticketKeys ? Buffer.from(options.ticketKeys) : crypto.randomBytes(48);
|
|
237
341
|
|
|
342
|
+
// Encrypted Client Hello keys, checked here — a key that does not match
|
|
343
|
+
// its config throws now, not as every client's handshake failing.
|
|
344
|
+
prepare_ech_server(options.ech);
|
|
345
|
+
self._ech = options.ech || null;
|
|
346
|
+
|
|
347
|
+
// node:tls's strings, read once (node_options.js) — a bad one throws here,
|
|
348
|
+
// as Node's createServer does, not on the first connection.
|
|
349
|
+
let nodeSuites = options.allowedCipherSuites || parseCiphers(options.ciphers);
|
|
350
|
+
let nodeGroups = options.groups || parseEcdhCurve(options.ecdhCurve);
|
|
351
|
+
let nodeSigalgs = options.signatureAlgorithms || parseSigalgs(options.sigalgs);
|
|
352
|
+
|
|
238
353
|
// sessionTimeout: seconds to cache a TLS 1.2 Session ID (Node default: 300)
|
|
239
354
|
let sessionTimeoutSec = (typeof options.sessionTimeout === 'number' && options.sessionTimeout > 0)
|
|
240
355
|
? (options.sessionTimeout >>> 0)
|
|
@@ -251,10 +366,28 @@ function Server(options, connectionListener) {
|
|
|
251
366
|
sessionIdContextHex = sidCtxBuf.toString('hex');
|
|
252
367
|
}
|
|
253
368
|
|
|
254
|
-
// Pre-compiled default SecureContext
|
|
369
|
+
// Pre-compiled default SecureContext.
|
|
370
|
+
//
|
|
371
|
+
// Node parity: a caller may hand us a context they built once with
|
|
372
|
+
// tls.createSecureContext() and reuse across servers, instead of paying to
|
|
373
|
+
// re-parse the same PEM on every construction. An explicit `secureContext`
|
|
374
|
+
// wins over key/cert — same precedence Node uses.
|
|
375
|
+
// SNI contexts registered through addContext(). Each is compiled ONCE here
|
|
376
|
+
// and handed out by reference, which is the whole point: a hand-written
|
|
377
|
+
// SNICallback that calls createSecureContext() per connection re-parses the
|
|
378
|
+
// same PEM every time, and if it reads the files inside the callback it also
|
|
379
|
+
// does disk I/O on the handshake path.
|
|
380
|
+
//
|
|
381
|
+
// Node's matching rule: last registration wins on a tie, and an exact
|
|
382
|
+
// hostname beats a wildcard. Insertion order is preserved, so a later
|
|
383
|
+
// addContext() for the same pattern replaces the earlier one in place.
|
|
384
|
+
let sniContexts = []; // [{ pattern, re, ctx }]
|
|
385
|
+
|
|
255
386
|
let defaultCtx = null;
|
|
256
|
-
if (options.
|
|
257
|
-
defaultCtx =
|
|
387
|
+
if (options.secureContext) {
|
|
388
|
+
defaultCtx = options.secureContext;
|
|
389
|
+
} else if (options.key && options.cert) {
|
|
390
|
+
defaultCtx = createSecureContext(options);
|
|
258
391
|
}
|
|
259
392
|
|
|
260
393
|
// In-memory fallback session store for TLS 1.2 Session IDs.
|
|
@@ -271,12 +404,40 @@ function Server(options, connectionListener) {
|
|
|
271
404
|
let socketOpts = {
|
|
272
405
|
isServer: true,
|
|
273
406
|
ticketKeys: self._ticketKeys,
|
|
274
|
-
|
|
407
|
+
// node:tls's sessionTimeout is how long a session stays good — a
|
|
408
|
+
// ticket's lifetime as much as a cached Session ID's. It set the cache
|
|
409
|
+
// alone, and tickets went out with the default 7200 s whatever it said.
|
|
410
|
+
// ticketLifetime, lemon-tls's own name, where given.
|
|
411
|
+
ticketLifetime: options.ticketLifetime != null ? options.ticketLifetime
|
|
412
|
+
: (typeof options.sessionTimeout === 'number' ? options.sessionTimeout : undefined),
|
|
413
|
+
ticketData: options.ticketData,
|
|
414
|
+
// RFC 8879: compress the chain for a client that offers it (all
|
|
415
|
+
// algorithms unless set; false: never).
|
|
416
|
+
certificateCompression: options.certificateCompression,
|
|
417
|
+
// node:tls: the cipher suite by the server's order (default: the client's).
|
|
418
|
+
honorCipherOrder: options.honorCipherOrder === true,
|
|
419
|
+
// Encrypted Client Hello (RFC 9849): { keys: [{ config, privateKey, retry }] }.
|
|
420
|
+
ech: self._ech,
|
|
421
|
+
// node:tls's 'OCSPRequest': (certificate, issuer, callback) when a
|
|
422
|
+
// client asks for a stapled OCSP response. Without a listener nothing
|
|
423
|
+
// is stapled — and nothing waits.
|
|
424
|
+
ocspRequest: function (certificate, issuer, callback) {
|
|
425
|
+
if (self.listenerCount('OCSPRequest') === 0) return callback(null, null);
|
|
426
|
+
self.emit('OCSPRequest', Buffer.from(certificate), issuer ? Buffer.from(issuer) : null, callback);
|
|
427
|
+
},
|
|
275
428
|
ALPNProtocols: options.ALPNProtocols,
|
|
429
|
+
ALPNCallback: options.ALPNCallback,
|
|
430
|
+
// 0-RTT (off unless asked for): tickets that allow it, and taking it
|
|
431
|
+
// under the session's checks — read on each socket as it comes
|
|
432
|
+
// (socket.isEarlyData), before 'secureConnection'.
|
|
433
|
+
earlyData: options.earlyData === true,
|
|
434
|
+
earlyDataMaxAge: options.earlyDataMaxAge,
|
|
435
|
+
earlyDataReplay: options.earlyDataReplay,
|
|
436
|
+
maxEarlyData: options.maxEarlyData,
|
|
276
437
|
minVersion: options.minVersion || DEFAULT_MIN_VERSION,
|
|
277
438
|
maxVersion: options.maxVersion || DEFAULT_MAX_VERSION,
|
|
278
|
-
signatureAlgorithms:
|
|
279
|
-
groups:
|
|
439
|
+
signatureAlgorithms: nodeSigalgs,
|
|
440
|
+
groups: nodeGroups,
|
|
280
441
|
prioritizeChaCha: options.prioritizeChaCha,
|
|
281
442
|
maxRecordSize: options.maxRecordSize,
|
|
282
443
|
sessionTickets: options.sessionTickets,
|
|
@@ -292,22 +453,37 @@ function Server(options, connectionListener) {
|
|
|
292
453
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
293
454
|
ca: options.ca,
|
|
294
455
|
maxHandshakeSize: options.maxHandshakeSize,
|
|
295
|
-
allowedCipherSuites:
|
|
296
|
-
|
|
456
|
+
allowedCipherSuites: nodeSuites,
|
|
457
|
+
// node:tls's default for a server, 120 s. TLSSocket's own is none
|
|
458
|
+
// (it is also the client's, where Node has none either), and a server
|
|
459
|
+
// left at it let a client that sent half a ClientHello hold its socket
|
|
460
|
+
// for as long as it liked.
|
|
461
|
+
handshakeTimeout: options.handshakeTimeout != null ? options.handshakeTimeout : 120000,
|
|
462
|
+
// The server's default certificate. With an explicit SNICallback it was
|
|
463
|
+
// never handed to the socket, so a client sending no server_name got
|
|
464
|
+
// the callback asked with null instead of this certificate, as in Node.
|
|
465
|
+
secureContext: defaultCtx || undefined,
|
|
297
466
|
};
|
|
298
467
|
|
|
299
468
|
if (options.SNICallback) {
|
|
469
|
+
// An explicit callback wins outright — it may do things the registry
|
|
470
|
+
// cannot, such as fetching a certificate on demand.
|
|
300
471
|
socketOpts.SNICallback = options.SNICallback;
|
|
301
|
-
} else if (defaultCtx) {
|
|
472
|
+
} else if (defaultCtx || sniContexts.length > 0) {
|
|
302
473
|
socketOpts.SNICallback = function(servername, cb) {
|
|
303
|
-
cb(null,
|
|
474
|
+
cb(null, contextForServername(servername));
|
|
304
475
|
};
|
|
305
476
|
}
|
|
306
477
|
|
|
307
478
|
return socketOpts;
|
|
308
479
|
}
|
|
309
480
|
|
|
310
|
-
|
|
481
|
+
function acceptConnection(tcp) {
|
|
482
|
+
// Node: "emitted when a new TCP stream is established, before the TLS
|
|
483
|
+
// handshake begins... can also be explicitly emitted by users to inject
|
|
484
|
+
// connections into the TLS server. In that case, any Duplex stream can be
|
|
485
|
+
// passed." Emitting first is what makes that injection path work.
|
|
486
|
+
self.emit('connection', tcp);
|
|
311
487
|
let socket;
|
|
312
488
|
try {
|
|
313
489
|
socket = new TLSSocket(tcp, buildSocketOpts());
|
|
@@ -368,8 +544,48 @@ function Server(options, connectionListener) {
|
|
|
368
544
|
self.emit('tlsClientError', err, socket);
|
|
369
545
|
}
|
|
370
546
|
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// node:tls's Server IS a net.Server, built from the same options object, so
|
|
550
|
+
// the TCP ones apply to every connection it accepts. Only those: the stream
|
|
551
|
+
// shape of the raw socket (allowHalfOpen, pauseOnConnect) is this layer's
|
|
552
|
+
// to manage, and the TLS socket takes allowHalfOpen itself.
|
|
553
|
+
self._tcpServer = net.createServer({
|
|
554
|
+
noDelay: options.noDelay === true,
|
|
555
|
+
keepAlive: options.keepAlive === true,
|
|
556
|
+
keepAliveInitialDelay: options.keepAliveInitialDelay || 0,
|
|
557
|
+
}, acceptConnection);
|
|
558
|
+
|
|
559
|
+
// The net.Server's own events, re-emitted: node:tls's Server IS a net.Server
|
|
560
|
+
// and emits these itself, and code written for it listens for them here.
|
|
561
|
+
// Without this, 'listening' never fired, and a failed bind (EADDRINUSE,
|
|
562
|
+
// EACCES) was emitted on an inner object nobody could reach — an uncaught
|
|
563
|
+
// exception that took the whole process down.
|
|
564
|
+
['listening', 'error', 'close', 'drop'].forEach(function (name) {
|
|
565
|
+
self._tcpServer.on(name, function () {
|
|
566
|
+
if (name === 'error' && self.listenerCount('error') === 0) {
|
|
567
|
+
// Same as node:tls when nobody listens: it throws. Preserved, rather
|
|
568
|
+
// than swallowed — but from the Server a caller can now listen on.
|
|
569
|
+
throw arguments[0];
|
|
570
|
+
}
|
|
571
|
+
self.emit.apply(self, [name].concat(Array.prototype.slice.call(arguments)));
|
|
572
|
+
});
|
|
371
573
|
});
|
|
372
574
|
|
|
575
|
+
/**
|
|
576
|
+
* Node documents 'connection' as injectable: emitting it by hand feeds an
|
|
577
|
+
* arbitrary Duplex into the TLS server. That cannot be done by listening for
|
|
578
|
+
* our own event — acceptConnection emits it, so the listener would re-enter
|
|
579
|
+
* on every real connection and loop. A method is the honest shape.
|
|
580
|
+
*/
|
|
581
|
+
self.injectConnection = function(stream) {
|
|
582
|
+
if (!stream || typeof stream.write !== 'function') {
|
|
583
|
+
throw new TypeError('injectConnection requires a Duplex stream');
|
|
584
|
+
}
|
|
585
|
+
acceptConnection(stream);
|
|
586
|
+
return self;
|
|
587
|
+
};
|
|
588
|
+
|
|
373
589
|
// Delegate net.Server methods
|
|
374
590
|
self.listen = function() {
|
|
375
591
|
return self._tcpServer.listen.apply(self._tcpServer, arguments);
|
|
@@ -402,13 +618,86 @@ function Server(options, connectionListener) {
|
|
|
402
618
|
self._ticketKeys = Buffer.from(keys);
|
|
403
619
|
};
|
|
404
620
|
|
|
621
|
+
// ECH keys. Rotation without a restart: publish the new config in DNS,
|
|
622
|
+
// then setEchKeys with the new key first (retry: true) and the old one
|
|
623
|
+
// kept (retry: false) until the old DNS record's TTL has run out — clients
|
|
624
|
+
// still holding it can then connect. New connections use the new set.
|
|
625
|
+
self.getEchKeys = function() {
|
|
626
|
+
return self._ech;
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
self.setEchKeys = function(ech) {
|
|
630
|
+
prepare_ech_server(ech); // throws on a bad key, keeping the old set
|
|
631
|
+
self._ech = ech || null;
|
|
632
|
+
};
|
|
633
|
+
|
|
405
634
|
// setSecureContext — replace cert/key without restart (Node.js compat)
|
|
406
635
|
self.setSecureContext = function(opts) {
|
|
407
|
-
if (opts
|
|
408
|
-
|
|
636
|
+
if (!opts) return;
|
|
637
|
+
if (opts.secureContext) { defaultCtx = opts.secureContext; return; }
|
|
638
|
+
if (opts.key && opts.cert) defaultCtx = createSecureContext(opts);
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Node-compat: addContext(hostname, context)
|
|
643
|
+
*
|
|
644
|
+
* Register a certificate for an SNI host name or wildcard. `context` is
|
|
645
|
+
* either a compiled context from createSecureContext() or the plain
|
|
646
|
+
* { key, cert, ... } options to compile from — Node accepts both.
|
|
647
|
+
*
|
|
648
|
+
* The declarative alternative to writing an SNICallback by hand. It matters
|
|
649
|
+
* for more than tidiness: the common hand-rolled callback compiles a context
|
|
650
|
+
* per connection, and often reads the cert off disk inside it. Here every
|
|
651
|
+
* context is compiled once at registration and served by reference.
|
|
652
|
+
*/
|
|
653
|
+
self.addContext = function(hostname, context) {
|
|
654
|
+
if (!hostname) throw new TypeError('addContext requires a hostname');
|
|
655
|
+
let ctx = context;
|
|
656
|
+
if (!ctx || (!ctx.certificateChain && !ctx.privateKey)) {
|
|
657
|
+
if (!context || !context.key || !context.cert) {
|
|
658
|
+
throw new TypeError('addContext requires a SecureContext or { key, cert }');
|
|
659
|
+
}
|
|
660
|
+
ctx = createSecureContext(context);
|
|
409
661
|
}
|
|
662
|
+
let pattern = String(hostname).toLowerCase();
|
|
663
|
+
let entry = { pattern: pattern, re: sniPatternToRegExp(pattern), ctx: ctx };
|
|
664
|
+
|
|
665
|
+
// Replace in place rather than appending a duplicate, so re-registering a
|
|
666
|
+
// host swaps its certificate without leaving the old one shadowing it.
|
|
667
|
+
let i = sniContexts.findIndex(function(e){ return e.pattern === pattern; });
|
|
668
|
+
if (i >= 0) sniContexts[i] = entry;
|
|
669
|
+
else sniContexts.push(entry);
|
|
670
|
+
return self;
|
|
410
671
|
};
|
|
411
672
|
|
|
673
|
+
/**
|
|
674
|
+
* Wildcards match ONE label and only as the leftmost one, per RFC 6125:
|
|
675
|
+
* `*.example.com` covers `a.example.com` but neither `a.b.example.com` nor
|
|
676
|
+
* the bare `example.com`. A bare `*` matches any single-or-multi label name
|
|
677
|
+
* and is the documented catch-all.
|
|
678
|
+
*/
|
|
679
|
+
function sniPatternToRegExp(pattern) {
|
|
680
|
+
if (pattern === '*') return /^.+$/;
|
|
681
|
+
let esc = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
682
|
+
return new RegExp('^' + esc.replace(/\*/g, '[^.]+') + '$');
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Resolve a servername to a context. Exact matches beat wildcards; among
|
|
687
|
+
* equals the most recently added wins, which is what Node documents.
|
|
688
|
+
*/
|
|
689
|
+
function contextForServername(servername) {
|
|
690
|
+
if (!servername) return defaultCtx;
|
|
691
|
+
let name = String(servername).toLowerCase();
|
|
692
|
+
for (let i = sniContexts.length - 1; i >= 0; i--) {
|
|
693
|
+
if (sniContexts[i].pattern === name) return sniContexts[i].ctx;
|
|
694
|
+
}
|
|
695
|
+
for (let i = sniContexts.length - 1; i >= 0; i--) {
|
|
696
|
+
if (sniContexts[i].re.test(name)) return sniContexts[i].ctx;
|
|
697
|
+
}
|
|
698
|
+
return defaultCtx;
|
|
699
|
+
}
|
|
700
|
+
|
|
412
701
|
return self;
|
|
413
702
|
}
|
|
414
703
|
|
|
@@ -453,20 +742,14 @@ function addCompatMethods(socket) {
|
|
|
453
742
|
if (typeof socket[name] !== 'function') socket[name] = fn;
|
|
454
743
|
}
|
|
455
744
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
/** Node.js compat: getPeerFinished() */
|
|
467
|
-
def('getPeerFinished', function() {
|
|
468
|
-
return session.getPeerFinished ? session.getPeerFinished() : null;
|
|
469
|
-
});
|
|
745
|
+
// isSessionReused, getFinished, getPeerFinished, getEphemeralKeyInfo and
|
|
746
|
+
// address now ship natively on TLSSocket, so their shims here were dead code
|
|
747
|
+
// that def() would never install — and they had drifted from Node in ways
|
|
748
|
+
// the native versions fix: getFinished returned null where Node documents
|
|
749
|
+
// undefined, and getEphemeralKeyInfo reported type 'X25519', which is not
|
|
750
|
+
// one of the three types Node defines ('DH', 'ECDH', 'TLSGroup'), and never
|
|
751
|
+
// returned null on a server socket. Two divergent implementations of one
|
|
752
|
+
// method is the duplication this layer exists to avoid, so they are gone.
|
|
470
753
|
|
|
471
754
|
/** Node.js compat: exportKeyingMaterial(length, label, context).
|
|
472
755
|
* TLSSocket now ships this natively (RFC 5705 / RFC 8446 §7.5 via the
|
|
@@ -476,15 +759,6 @@ function addCompatMethods(socket) {
|
|
|
476
759
|
return session.exportKeyingMaterial ? session.exportKeyingMaterial(length, label, context) : null;
|
|
477
760
|
});
|
|
478
761
|
|
|
479
|
-
/** Node.js compat: getEphemeralKeyInfo() */
|
|
480
|
-
def('getEphemeralKeyInfo', function() {
|
|
481
|
-
let group = session.context.selected_group;
|
|
482
|
-
if (group === 0x001d) return { type: 'X25519', size: 253 };
|
|
483
|
-
if (group === 0x0017) return { type: 'ECDH', name: 'prime256v1', size: 256 };
|
|
484
|
-
if (group === 0x0018) return { type: 'ECDH', name: 'secp384r1', size: 384 };
|
|
485
|
-
return {};
|
|
486
|
-
});
|
|
487
|
-
|
|
488
762
|
/** Node.js compat: setServername(name) */
|
|
489
763
|
def('setServername', function(name) {
|
|
490
764
|
session.set_context({ local_sni: name });
|
|
@@ -493,11 +767,6 @@ function addCompatMethods(socket) {
|
|
|
493
767
|
/** Node.js compat: disableRenegotiation() — no-op (renegotiation not supported) */
|
|
494
768
|
def('disableRenegotiation', function() {});
|
|
495
769
|
|
|
496
|
-
/** Node.js compat: address() — delegates to underlying transport */
|
|
497
|
-
def('address', function() {
|
|
498
|
-
try { return session.context && session.context.transport ? session.context.transport.address() : {}; }
|
|
499
|
-
catch(e) { return {}; }
|
|
500
|
-
});
|
|
501
770
|
}
|
|
502
771
|
|
|
503
772
|
// ===================== Exports =====================
|
|
@@ -510,6 +779,8 @@ export {
|
|
|
510
779
|
getCiphers,
|
|
511
780
|
checkServerIdentity,
|
|
512
781
|
addCompatMethods,
|
|
782
|
+
getCACertificates,
|
|
783
|
+
setDefaultCACertificates,
|
|
513
784
|
DEFAULT_MIN_VERSION,
|
|
514
785
|
DEFAULT_MAX_VERSION,
|
|
515
786
|
DEFAULT_CIPHERS,
|
package/src/crypto.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
import {
|
|
3
2
|
concatUint8Arrays
|
|
4
3
|
} from './utils.js';
|
|
@@ -130,6 +129,7 @@ const TLS_CIPHER_SUITES = {
|
|
|
130
129
|
hash: 'sha256'
|
|
131
130
|
},
|
|
132
131
|
0xCCAA: { // TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256
|
|
132
|
+
name: 'DHE-RSA-CHACHA20-POLY1305', standardName: 'TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256',
|
|
133
133
|
tls: 12,
|
|
134
134
|
kex: 'DHE_RSA',
|
|
135
135
|
sig: 'RSA',
|
|
@@ -172,6 +172,7 @@ const TLS_CIPHER_SUITES = {
|
|
|
172
172
|
// TLS 1.2 CBC (Legacy)
|
|
173
173
|
// ----------------------
|
|
174
174
|
0xC013: { // TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
|
|
175
|
+
name: 'ECDHE-RSA-AES128-SHA', standardName: 'TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA',
|
|
175
176
|
tls: 12,
|
|
176
177
|
kex: 'ECDHE_RSA',
|
|
177
178
|
sig: 'RSA',
|
|
@@ -184,6 +185,7 @@ const TLS_CIPHER_SUITES = {
|
|
|
184
185
|
hash: 'sha256'
|
|
185
186
|
},
|
|
186
187
|
0xC014: { // TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
|
|
188
|
+
name: 'ECDHE-RSA-AES256-SHA', standardName: 'TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA',
|
|
187
189
|
tls: 12,
|
|
188
190
|
kex: 'ECDHE_RSA',
|
|
189
191
|
sig: 'RSA',
|
|
@@ -196,6 +198,7 @@ const TLS_CIPHER_SUITES = {
|
|
|
196
198
|
hash: 'sha256'
|
|
197
199
|
},
|
|
198
200
|
0x003C: { // TLS_RSA_WITH_AES_128_CBC_SHA256
|
|
201
|
+
name: 'AES128-SHA256', standardName: 'TLS_RSA_WITH_AES_128_CBC_SHA256',
|
|
199
202
|
tls: 12,
|
|
200
203
|
kex: 'RSA',
|
|
201
204
|
sig: 'RSA',
|
|
@@ -208,6 +211,7 @@ const TLS_CIPHER_SUITES = {
|
|
|
208
211
|
hash: 'sha256'
|
|
209
212
|
},
|
|
210
213
|
0x003D: { // TLS_RSA_WITH_AES_256_CBC_SHA256
|
|
214
|
+
name: 'AES256-SHA256', standardName: 'TLS_RSA_WITH_AES_256_CBC_SHA256',
|
|
211
215
|
tls: 12,
|
|
212
216
|
kex: 'RSA',
|
|
213
217
|
sig: 'RSA',
|
|
@@ -237,10 +241,12 @@ function makeHashFn(algorithm, outputLen) {
|
|
|
237
241
|
|
|
238
242
|
let sha256 = makeHashFn('sha256', 32);
|
|
239
243
|
let sha384 = makeHashFn('sha384', 48);
|
|
244
|
+
let sha512 = makeHashFn('sha512', 64); // HPKE: HKDF-SHA512, DHKEM(P-521 / X448)
|
|
240
245
|
|
|
241
246
|
function getHashFn(hashName) {
|
|
242
247
|
if (hashName === 'sha256') return sha256;
|
|
243
248
|
if (hashName === 'sha384') return sha384;
|
|
249
|
+
if (hashName === 'sha512') return sha512;
|
|
244
250
|
throw new Error('Unsupported hash: ' + hashName);
|
|
245
251
|
}
|
|
246
252
|
|
|
@@ -535,6 +541,17 @@ function derive_handshake_traffic_secrets_psk(hashName, psk, shared_secret, tran
|
|
|
535
541
|
};
|
|
536
542
|
}
|
|
537
543
|
|
|
544
|
+
/**
|
|
545
|
+
* client_early_traffic_secret (RFC 8446 §7.1): Derive-Secret(Early Secret,
|
|
546
|
+
* "c e traffic", ClientHello) — the 0-RTT key's secret, from the PSK and the
|
|
547
|
+
* hash of the ClientHello alone.
|
|
548
|
+
*/
|
|
549
|
+
function derive_client_early_traffic_secret(hashName, psk, clienthello_hash, labelPrefix) {
|
|
550
|
+
let hashLen = getHashFn(hashName).outputLen | 0;
|
|
551
|
+
let early_secret = hkdf_extract(hashName, new Uint8Array(0), psk);
|
|
552
|
+
return hkdf_expand_label(hashName, early_secret, 'c e traffic', clienthello_hash, hashLen, labelPrefix);
|
|
553
|
+
}
|
|
554
|
+
|
|
538
555
|
/**
|
|
539
556
|
* Like derive_handshake_traffic_secrets_psk but accepts a pre-computed transcript hash.
|
|
540
557
|
*/
|
|
@@ -735,7 +752,19 @@ function tls13_exporter(hashName, exporter_master_secret, label, context_value,
|
|
|
735
752
|
let h_empty = hashFn(new Uint8Array(0));
|
|
736
753
|
let derived = hkdf_expand_label(hashName, exporter_master_secret, label, h_empty, hashLen, labelPrefix);
|
|
737
754
|
let h_ctx = hashFn(context_value || new Uint8Array(0));
|
|
738
|
-
|
|
755
|
+
// BOTH expansions carry the prefix. RFC 8446 §7.5 defines the exporter
|
|
756
|
+
// as two HKDF-Expand-Label steps, and in DTLS 1.3 both use "dtls13"
|
|
757
|
+
// (RFC 9147 §5.9) — this second one was omitting it and falling back
|
|
758
|
+
// to "tls13 ", so the exported key differed from a conforming peer's.
|
|
759
|
+
// For WebRTC that key IS the DTLS-SRTP keying material: the handshake
|
|
760
|
+
// completes, SRTP reports ready, packets flow in both directions, and
|
|
761
|
+
// NOTHING decrypts. Two peers running this library derive the same
|
|
762
|
+
// wrong key and interoperate perfectly with each other, which is why
|
|
763
|
+
// it only appears against Chrome — and appears as "media does not
|
|
764
|
+
// flow" rather than as a crypto error, since a failed unprotect is
|
|
765
|
+
// just a dropped packet.
|
|
766
|
+
return hkdf_expand(hashName, derived,
|
|
767
|
+
build_hkdf_label('exporter', h_ctx, length, labelPrefix), length);
|
|
739
768
|
}
|
|
740
769
|
|
|
741
770
|
/**
|
|
@@ -888,6 +917,7 @@ export {
|
|
|
888
917
|
compute_psk_binder,
|
|
889
918
|
derive_handshake_traffic_secrets_psk,
|
|
890
919
|
derive_handshake_traffic_secrets_psk_with_hash,
|
|
920
|
+
derive_client_early_traffic_secret,
|
|
891
921
|
build_cert_verify_tbs,
|
|
892
922
|
build_cert_verify_tbs_with_hash,
|
|
893
923
|
get_handshake_finished,
|
|
@@ -896,4 +926,4 @@ export {
|
|
|
896
926
|
derive_exporter_master_secret_with_hash,
|
|
897
927
|
tls13_exporter,
|
|
898
928
|
tls12_exporter,
|
|
899
|
-
};
|
|
929
|
+
};
|