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/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
- noTickets: options.noTickets,
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.createServer() =====================
213
+ // ===================== tls.Server =====================
115
214
 
116
215
  /**
117
- * Node.js compatible tls.createServer().
216
+ * Node.js compatible tls.Server class.
118
217
  *
119
- * Usage:
120
- * tls.createServer(options, connectionListener)
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 createServer(options, connectionListener) {
123
- if (typeof options === 'function') {
124
- connectionListener = options;
125
- options = {};
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
- let ctx = null;
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
- ctx = createSecureContext({ key: options.key, cert: options.cert });
257
+ defaultCtx = createSecureContext({ key: options.key, cert: options.cert });
132
258
  }
133
259
 
134
- // Shared ticketKeys for all connections (enables PSK across connections)
135
- let sharedTicketKeys = options.ticketKeys || crypto.randomBytes(48);
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
- let server = net.createServer(function(tcp) {
266
+ function storeKey(id) {
267
+ return sessionIdContextHex + ':' + toHex(id);
268
+ }
269
+
270
+ function buildSocketOpts() {
138
271
  let socketOpts = {
139
272
  isServer: true,
140
- ticketKeys: sharedTicketKeys,
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
- noTickets: options.noTickets,
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 (ctx) {
291
+ } else if (defaultCtx) {
157
292
  socketOpts.SNICallback = function(servername, cb) {
158
- cb(null, ctx);
293
+ cb(null, defaultCtx);
159
294
  };
160
295
  }
161
296
 
162
- let socket = new TLSSocket(tcp, socketOpts);
163
- addCompatMethods(socket);
297
+ return socketOpts;
298
+ }
164
299
 
165
- if (connectionListener) {
166
- socket.on('secureConnect', function() {
167
- connectionListener(socket);
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
- server.emit('secureConnection', socket);
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
- return server;
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.getSession();
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
- let output = Buffer.alloc(N * hashLen);
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
- h.update(Buffer.from([i]));
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.subarray(0, length));
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
- let prefix = 'tls13 ';
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, binder_key, transcript_hash);
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
  };
@@ -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,