lemon-tls 0.7.0 → 0.10.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 -20
- package/index.cjs +9 -7
- package/index.d.ts +323 -23
- package/index.js +126 -0
- package/package.json +39 -6
- package/src/compat.js +325 -46
- package/src/crypto.js +19 -0
- package/src/dtls_session.js +141 -17
- package/src/dtls_socket.js +32 -0
- package/src/node_options.js +138 -0
- package/src/record.js +192 -57
- package/src/secure_context.js +63 -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 +39 -4
- package/src/session/signing.js +56 -3
- package/src/session/ticket.js +38 -25
- package/src/tls_session.js +2835 -205
- package/src/tls_socket.js +839 -148
- package/src/utils.js +68 -1
- package/src/wire.js +148 -9
- package/types/crypto.d.ts +40 -0
- package/types/record.d.ts +40 -0
- package/types/session.d.ts +3 -0
- package/types/wire.d.ts +62 -0
package/package.json
CHANGED
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lemon-tls",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "TLS 1.3/1.2 DTLS 1.3/1.2 implementation for Node.js - full control over cryptographic keys, record layer, and handshake. Drop-in replacement for node:tls with advanced options impossible in OpenSSL.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"types": "index.d.ts",
|
|
8
|
+
"typesVersions": {
|
|
9
|
+
"*": {
|
|
10
|
+
"wire": [
|
|
11
|
+
"types/wire.d.ts"
|
|
12
|
+
],
|
|
13
|
+
"crypto": [
|
|
14
|
+
"types/crypto.d.ts"
|
|
15
|
+
],
|
|
16
|
+
"record": [
|
|
17
|
+
"types/record.d.ts"
|
|
18
|
+
],
|
|
19
|
+
"session": [
|
|
20
|
+
"types/session.d.ts"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
8
24
|
"exports": {
|
|
9
25
|
".": {
|
|
10
26
|
"types": "./index.d.ts",
|
|
@@ -12,22 +28,35 @@
|
|
|
12
28
|
"require": "./index.cjs",
|
|
13
29
|
"default": "./index.js"
|
|
14
30
|
},
|
|
15
|
-
"./wire":
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
31
|
+
"./wire": {
|
|
32
|
+
"types": "./types/wire.d.ts",
|
|
33
|
+
"default": "./src/wire.js"
|
|
34
|
+
},
|
|
35
|
+
"./crypto": {
|
|
36
|
+
"types": "./types/crypto.d.ts",
|
|
37
|
+
"default": "./src/crypto.js"
|
|
38
|
+
},
|
|
39
|
+
"./record": {
|
|
40
|
+
"types": "./types/record.d.ts",
|
|
41
|
+
"default": "./src/record.js"
|
|
42
|
+
},
|
|
43
|
+
"./session": {
|
|
44
|
+
"types": "./types/session.d.ts",
|
|
45
|
+
"default": "./src/tls_session.js"
|
|
46
|
+
}
|
|
19
47
|
},
|
|
20
48
|
"files": [
|
|
21
49
|
"index.js",
|
|
22
50
|
"index.cjs",
|
|
23
51
|
"index.d.ts",
|
|
52
|
+
"types/",
|
|
24
53
|
"src/",
|
|
25
54
|
"README.md",
|
|
26
55
|
"LICENSE",
|
|
27
56
|
"lemontls.svg"
|
|
28
57
|
],
|
|
29
58
|
"engines": {
|
|
30
|
-
"node": ">=
|
|
59
|
+
"node": ">=24.0.0"
|
|
31
60
|
},
|
|
32
61
|
"keywords": [
|
|
33
62
|
"tls",
|
|
@@ -70,6 +99,10 @@
|
|
|
70
99
|
"alpn",
|
|
71
100
|
"ja3",
|
|
72
101
|
"ja3-fingerprint",
|
|
102
|
+
"ja4",
|
|
103
|
+
"ja4-fingerprint",
|
|
104
|
+
"ja5",
|
|
105
|
+
"ja5-fingerprint",
|
|
73
106
|
"fingerprint",
|
|
74
107
|
"keylog",
|
|
75
108
|
"wireshark",
|
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);
|
|
@@ -387,6 +603,12 @@ function Server(options, connectionListener) {
|
|
|
387
603
|
return self._tcpServer.getConnections(cb);
|
|
388
604
|
};
|
|
389
605
|
|
|
606
|
+
// node:net's: whether it is listening, and whether the process may exit
|
|
607
|
+
// while it does.
|
|
608
|
+
Object.defineProperty(self, 'listening', { get: function() { return self._tcpServer.listening; }, enumerable: true });
|
|
609
|
+
self.ref = function() { self._tcpServer.ref(); return self; };
|
|
610
|
+
self.unref = function() { self._tcpServer.unref(); return self; };
|
|
611
|
+
|
|
390
612
|
// Ticket key management
|
|
391
613
|
self.getTicketKeys = function() {
|
|
392
614
|
return Buffer.from(self._ticketKeys);
|
|
@@ -402,13 +624,88 @@ function Server(options, connectionListener) {
|
|
|
402
624
|
self._ticketKeys = Buffer.from(keys);
|
|
403
625
|
};
|
|
404
626
|
|
|
405
|
-
//
|
|
627
|
+
// ECH keys. Rotation without a restart: publish the new config in DNS,
|
|
628
|
+
// then setEchKeys with the new key first (retry: true) and the old one
|
|
629
|
+
// kept (retry: false) until the old DNS record's TTL has run out — clients
|
|
630
|
+
// still holding it can then connect. New connections use the new set.
|
|
631
|
+
self.getEchKeys = function() {
|
|
632
|
+
return self._ech;
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
self.setEchKeys = function(ech) {
|
|
636
|
+
prepare_ech_server(ech); // throws on a bad key, keeping the old set
|
|
637
|
+
self._ech = ech || null;
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
// setSecureContext — replace cert/key without restart (Node.js compat).
|
|
641
|
+
// A bad one throws, as Node's does, and the certificate in use stays: it
|
|
642
|
+
// returned quietly when key or cert was missing, and a renewal that read
|
|
643
|
+
// an empty file went unnoticed until the old certificate expired.
|
|
406
644
|
self.setSecureContext = function(opts) {
|
|
407
|
-
if (opts
|
|
408
|
-
|
|
645
|
+
if (!opts || typeof opts !== 'object') throw new TypeError('setSecureContext requires { key, cert } or { secureContext }');
|
|
646
|
+
defaultCtx = opts.secureContext ? opts.secureContext : createSecureContext(opts);
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Node-compat: addContext(hostname, context)
|
|
651
|
+
*
|
|
652
|
+
* Register a certificate for an SNI host name or wildcard. `context` is
|
|
653
|
+
* either a compiled context from createSecureContext() or the plain
|
|
654
|
+
* { key, cert, ... } options to compile from — Node accepts both.
|
|
655
|
+
*
|
|
656
|
+
* The declarative alternative to writing an SNICallback by hand. It matters
|
|
657
|
+
* for more than tidiness: the common hand-rolled callback compiles a context
|
|
658
|
+
* per connection, and often reads the cert off disk inside it. Here every
|
|
659
|
+
* context is compiled once at registration and served by reference.
|
|
660
|
+
*/
|
|
661
|
+
self.addContext = function(hostname, context) {
|
|
662
|
+
if (!hostname) throw new TypeError('addContext requires a hostname');
|
|
663
|
+
let ctx = context;
|
|
664
|
+
if (!ctx || (!ctx.certificateChain && !ctx.privateKey)) {
|
|
665
|
+
if (!context || !context.key || !context.cert) {
|
|
666
|
+
throw new TypeError('addContext requires a SecureContext or { key, cert }');
|
|
667
|
+
}
|
|
668
|
+
ctx = createSecureContext(context);
|
|
409
669
|
}
|
|
670
|
+
let pattern = String(hostname).toLowerCase();
|
|
671
|
+
let entry = { pattern: pattern, re: sniPatternToRegExp(pattern), ctx: ctx };
|
|
672
|
+
|
|
673
|
+
// Replace in place rather than appending a duplicate, so re-registering a
|
|
674
|
+
// host swaps its certificate without leaving the old one shadowing it.
|
|
675
|
+
let i = sniContexts.findIndex(function(e){ return e.pattern === pattern; });
|
|
676
|
+
if (i >= 0) sniContexts[i] = entry;
|
|
677
|
+
else sniContexts.push(entry);
|
|
678
|
+
return self;
|
|
410
679
|
};
|
|
411
680
|
|
|
681
|
+
/**
|
|
682
|
+
* Wildcards match ONE label and only as the leftmost one, per RFC 6125:
|
|
683
|
+
* `*.example.com` covers `a.example.com` but neither `a.b.example.com` nor
|
|
684
|
+
* the bare `example.com`. A bare `*` matches any single-or-multi label name
|
|
685
|
+
* and is the documented catch-all.
|
|
686
|
+
*/
|
|
687
|
+
function sniPatternToRegExp(pattern) {
|
|
688
|
+
if (pattern === '*') return /^.+$/;
|
|
689
|
+
let esc = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
|
|
690
|
+
return new RegExp('^' + esc.replace(/\*/g, '[^.]+') + '$');
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Resolve a servername to a context. Exact matches beat wildcards; among
|
|
695
|
+
* equals the most recently added wins, which is what Node documents.
|
|
696
|
+
*/
|
|
697
|
+
function contextForServername(servername) {
|
|
698
|
+
if (!servername) return defaultCtx;
|
|
699
|
+
let name = String(servername).toLowerCase();
|
|
700
|
+
for (let i = sniContexts.length - 1; i >= 0; i--) {
|
|
701
|
+
if (sniContexts[i].pattern === name) return sniContexts[i].ctx;
|
|
702
|
+
}
|
|
703
|
+
for (let i = sniContexts.length - 1; i >= 0; i--) {
|
|
704
|
+
if (sniContexts[i].re.test(name)) return sniContexts[i].ctx;
|
|
705
|
+
}
|
|
706
|
+
return defaultCtx;
|
|
707
|
+
}
|
|
708
|
+
|
|
412
709
|
return self;
|
|
413
710
|
}
|
|
414
711
|
|
|
@@ -453,20 +750,14 @@ function addCompatMethods(socket) {
|
|
|
453
750
|
if (typeof socket[name] !== 'function') socket[name] = fn;
|
|
454
751
|
}
|
|
455
752
|
|
|
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
|
-
});
|
|
753
|
+
// isSessionReused, getFinished, getPeerFinished, getEphemeralKeyInfo and
|
|
754
|
+
// address now ship natively on TLSSocket, so their shims here were dead code
|
|
755
|
+
// that def() would never install — and they had drifted from Node in ways
|
|
756
|
+
// the native versions fix: getFinished returned null where Node documents
|
|
757
|
+
// undefined, and getEphemeralKeyInfo reported type 'X25519', which is not
|
|
758
|
+
// one of the three types Node defines ('DH', 'ECDH', 'TLSGroup'), and never
|
|
759
|
+
// returned null on a server socket. Two divergent implementations of one
|
|
760
|
+
// method is the duplication this layer exists to avoid, so they are gone.
|
|
470
761
|
|
|
471
762
|
/** Node.js compat: exportKeyingMaterial(length, label, context).
|
|
472
763
|
* TLSSocket now ships this natively (RFC 5705 / RFC 8446 §7.5 via the
|
|
@@ -476,15 +767,6 @@ function addCompatMethods(socket) {
|
|
|
476
767
|
return session.exportKeyingMaterial ? session.exportKeyingMaterial(length, label, context) : null;
|
|
477
768
|
});
|
|
478
769
|
|
|
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
770
|
/** Node.js compat: setServername(name) */
|
|
489
771
|
def('setServername', function(name) {
|
|
490
772
|
session.set_context({ local_sni: name });
|
|
@@ -493,11 +775,6 @@ function addCompatMethods(socket) {
|
|
|
493
775
|
/** Node.js compat: disableRenegotiation() — no-op (renegotiation not supported) */
|
|
494
776
|
def('disableRenegotiation', function() {});
|
|
495
777
|
|
|
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
778
|
}
|
|
502
779
|
|
|
503
780
|
// ===================== Exports =====================
|
|
@@ -510,6 +787,8 @@ export {
|
|
|
510
787
|
getCiphers,
|
|
511
788
|
checkServerIdentity,
|
|
512
789
|
addCompatMethods,
|
|
790
|
+
getCACertificates,
|
|
791
|
+
setDefaultCACertificates,
|
|
513
792
|
DEFAULT_MIN_VERSION,
|
|
514
793
|
DEFAULT_MAX_VERSION,
|
|
515
794
|
DEFAULT_CIPHERS,
|
package/src/crypto.js
CHANGED
|
@@ -129,6 +129,7 @@ const TLS_CIPHER_SUITES = {
|
|
|
129
129
|
hash: 'sha256'
|
|
130
130
|
},
|
|
131
131
|
0xCCAA: { // TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256
|
|
132
|
+
name: 'DHE-RSA-CHACHA20-POLY1305', standardName: 'TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256',
|
|
132
133
|
tls: 12,
|
|
133
134
|
kex: 'DHE_RSA',
|
|
134
135
|
sig: 'RSA',
|
|
@@ -171,6 +172,7 @@ const TLS_CIPHER_SUITES = {
|
|
|
171
172
|
// TLS 1.2 CBC (Legacy)
|
|
172
173
|
// ----------------------
|
|
173
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',
|
|
174
176
|
tls: 12,
|
|
175
177
|
kex: 'ECDHE_RSA',
|
|
176
178
|
sig: 'RSA',
|
|
@@ -183,6 +185,7 @@ const TLS_CIPHER_SUITES = {
|
|
|
183
185
|
hash: 'sha256'
|
|
184
186
|
},
|
|
185
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',
|
|
186
189
|
tls: 12,
|
|
187
190
|
kex: 'ECDHE_RSA',
|
|
188
191
|
sig: 'RSA',
|
|
@@ -195,6 +198,7 @@ const TLS_CIPHER_SUITES = {
|
|
|
195
198
|
hash: 'sha256'
|
|
196
199
|
},
|
|
197
200
|
0x003C: { // TLS_RSA_WITH_AES_128_CBC_SHA256
|
|
201
|
+
name: 'AES128-SHA256', standardName: 'TLS_RSA_WITH_AES_128_CBC_SHA256',
|
|
198
202
|
tls: 12,
|
|
199
203
|
kex: 'RSA',
|
|
200
204
|
sig: 'RSA',
|
|
@@ -207,6 +211,7 @@ const TLS_CIPHER_SUITES = {
|
|
|
207
211
|
hash: 'sha256'
|
|
208
212
|
},
|
|
209
213
|
0x003D: { // TLS_RSA_WITH_AES_256_CBC_SHA256
|
|
214
|
+
name: 'AES256-SHA256', standardName: 'TLS_RSA_WITH_AES_256_CBC_SHA256',
|
|
210
215
|
tls: 12,
|
|
211
216
|
kex: 'RSA',
|
|
212
217
|
sig: 'RSA',
|
|
@@ -236,10 +241,12 @@ function makeHashFn(algorithm, outputLen) {
|
|
|
236
241
|
|
|
237
242
|
let sha256 = makeHashFn('sha256', 32);
|
|
238
243
|
let sha384 = makeHashFn('sha384', 48);
|
|
244
|
+
let sha512 = makeHashFn('sha512', 64); // HPKE: HKDF-SHA512, DHKEM(P-521 / X448)
|
|
239
245
|
|
|
240
246
|
function getHashFn(hashName) {
|
|
241
247
|
if (hashName === 'sha256') return sha256;
|
|
242
248
|
if (hashName === 'sha384') return sha384;
|
|
249
|
+
if (hashName === 'sha512') return sha512;
|
|
243
250
|
throw new Error('Unsupported hash: ' + hashName);
|
|
244
251
|
}
|
|
245
252
|
|
|
@@ -534,6 +541,17 @@ function derive_handshake_traffic_secrets_psk(hashName, psk, shared_secret, tran
|
|
|
534
541
|
};
|
|
535
542
|
}
|
|
536
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
|
+
|
|
537
555
|
/**
|
|
538
556
|
* Like derive_handshake_traffic_secrets_psk but accepts a pre-computed transcript hash.
|
|
539
557
|
*/
|
|
@@ -899,6 +917,7 @@ export {
|
|
|
899
917
|
compute_psk_binder,
|
|
900
918
|
derive_handshake_traffic_secrets_psk,
|
|
901
919
|
derive_handshake_traffic_secrets_psk_with_hash,
|
|
920
|
+
derive_client_early_traffic_secret,
|
|
902
921
|
build_cert_verify_tbs,
|
|
903
922
|
build_cert_verify_tbs_with_hash,
|
|
904
923
|
get_handshake_finished,
|