lemon-tls 0.9.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 CHANGED
@@ -530,6 +530,11 @@ Everything above has a DTLS twin. Same TLS state machine underneath, same
530
530
  methods, plus datagram-specific plumbing (retransmission flights, epoch
531
531
  handling, cookie exchange):
532
532
 
533
+ Retransmission under loss: in DTLS 1.3 an ACK that covers only part of a
534
+ flight retires only those records — the rest are resent when the timer
535
+ fires (RFC 9147 §7.1) — and a client keeps resending its final flight until
536
+ the peer ACKs it or sends protected data (§5.8.3).
537
+
533
538
  ```js
534
539
  import { DTLSSession, DTLSSocket, createDTLSServer, connectDTLS } from 'lemon-tls';
535
540
  ```
package/index.cjs CHANGED
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * CJS wrapper for lemon-tls.
3
- *
4
- * On Node 22+, require('lemon-tls') works natively with the ESM module.
5
- * On Node 16-21, this wrapper provides require() support via dynamic import.
6
3
  *
7
- * Usage:
8
- * const tls = require('lemon-tls'); // sync on Node 22+
9
- * const tls = await require('lemon-tls'); // async on Node 16-21
4
+ * lemon-tls needs Node 24+ (package.json "engines"), where require() of an
5
+ * ES module is built in: require('lemon-tls') returns the module itself,
6
+ * synchronously.
7
+ *
8
+ * const tls = require('lemon-tls');
9
+ *
10
+ * The fallback below — a Promise of the module — is for a runtime or bundler
11
+ * whose require() cannot load ES modules; there, `await require('lemon-tls')`.
10
12
  */
11
13
 
12
14
  let cached = null;
@@ -21,7 +23,7 @@ try {
21
23
  const m = require('./index.js');
22
24
  module.exports = m.default || m;
23
25
  } catch (e) {
24
- // Node 16-21: return a promise that resolves to the module
26
+ // require() of ES modules not available: a promise of the module.
25
27
  // Users need: const tls = await require('lemon-tls');
26
28
  module.exports = load();
27
29
  module.exports.__esModule = true;
package/index.d.ts CHANGED
@@ -7,9 +7,58 @@ import type { X509Certificate } from 'node:crypto';
7
7
 
8
8
  // ======================== Types ========================
9
9
 
10
+ /** A key or certificate: PEM or DER bytes, or a file path. */
11
+ export type PemInput = Buffer | Uint8Array | string;
12
+
13
+ /**
14
+ * createSecureContext()'s options. `key` and `cert` as arrays — node:tls's
15
+ * form — for several certificates of different kinds (an ML-DSA one for
16
+ * clients that ask for post-quantum authentication, an ECDSA or RSA one for
17
+ * the rest): each key is paired with the chain whose leaf carries its public
18
+ * key, the first being the default.
19
+ */
20
+ export interface SecureContextOptions {
21
+ key?: PemInput | Array<PemInput | { pem: PemInput; passphrase?: string }>;
22
+ cert?: PemInput | PemInput[];
23
+ ca?: PemInput | PemInput[];
24
+ passphrase?: string;
25
+ pfx?: PemInput;
26
+ /** A stapled OCSP response (DER) for the leaf. */
27
+ ocsp?: PemInput;
28
+ ticketKeys?: Buffer | Uint8Array;
29
+ [option: string]: any;
30
+ }
31
+
10
32
  export interface SecureContext {
11
33
  certificateChain: Array<{ cert: Buffer }>;
12
34
  privateKey: any;
35
+ ca?: any[];
36
+ /** The other key/certificate pairs (key/cert arrays). */
37
+ alternates?: Array<{ certificateChain: Array<{ cert: Buffer }>; privateKey: any }>;
38
+ }
39
+
40
+ /** An Encrypted Client Hello key (RFC 9849), from generateEchKey(). */
41
+ export interface EchKey {
42
+ config: Uint8Array;
43
+ privateKey: Uint8Array;
44
+ configList: Uint8Array;
45
+ /** The value of `ech=` in the domain's HTTPS record. */
46
+ configListBase64: string;
47
+ }
48
+
49
+ /** A server's `ech` option. `retry: false`: still accepted, no longer offered to clients. */
50
+ export interface EchServerOption {
51
+ keys: Array<{ config: Uint8Array | Buffer | string; privateKey: Uint8Array | Buffer | string; retry?: boolean }>;
52
+ }
53
+
54
+ /** A saved session ('session' event's Buffer) described without its secrets: sessionInfo(). */
55
+ export interface SessionInfo {
56
+ version: 'TLSv1.3' | 'TLSv1.2' | null;
57
+ servername: string | null;
58
+ alpn: string | null;
59
+ created: number | null;
60
+ expiresAt: number | null;
61
+ earlyData: boolean;
13
62
  }
14
63
 
15
64
  export interface CipherInfo {
@@ -19,6 +68,7 @@ export interface CipherInfo {
19
68
  }
20
69
 
21
70
  export interface PeerCertificate {
71
+ /** RDN fields as node:tls has them: { C, ST, L, O, OU, CN }. */
22
72
  subject: any;
23
73
  issuer: any;
24
74
  subjectaltname?: string;
@@ -107,12 +157,19 @@ export interface CertificateCallbackInfo {
107
157
  export interface TLSSocketOptions {
108
158
  isServer?: boolean;
109
159
  servername?: string;
110
- SNICallback?: (servername: string, cb: (err: Error | null, ctx: SecureContext | null) => void) => void;
160
+ SNICallback?: (servername: string, cb: (err: Error | null, ctx?: SecureContext | null) => void) => void;
161
+ /** Server, node:tls's: pick the protocol from what the client offers (exclusive with ALPNProtocols on a Server). */
162
+ ALPNCallback?: (info: { servername: string | null; protocols: string[] }) => string | undefined;
111
163
  minVersion?: 'TLSv1.2' | 'TLSv1.3';
112
164
  maxVersion?: 'TLSv1.2' | 'TLSv1.3';
113
165
  ALPNProtocols?: string[];
114
166
  rejectUnauthorized?: boolean;
115
- ca?: Buffer | string;
167
+ /** Client: the identity check (node:tls's); undefined for a match, an Error otherwise. */
168
+ checkServerIdentity?: (hostname: string, cert: PeerCertificate) => Error | undefined;
169
+ ca?: PemInput | PemInput[];
170
+ /** A context from createSecureContext(), in place of key/cert/ca. */
171
+ secureContext?: SecureContext;
172
+ passphrase?: string;
116
173
  ticketKeys?: Buffer;
117
174
  /**
118
175
  * Server: what to carry inside each session ticket issued. Called once per
@@ -128,8 +185,53 @@ export interface TLSSocketOptions {
128
185
  */
129
186
  session?: Buffer | Uint8Array;
130
187
  requestCert?: boolean;
131
- cert?: Buffer | string;
132
- key?: Buffer | string;
188
+ cert?: PemInput;
189
+ key?: PemInput;
190
+
191
+ // node:tls's option strings — taken as Node takes them. lemon-tls's code
192
+ // lists (allowedCipherSuites, groups, signatureAlgorithms) win when given.
193
+ /** 'TLS_AES_128_GCM_SHA256:ECDHE-RSA-AES128-GCM-SHA256…' */
194
+ ciphers?: string;
195
+ /** 'X25519:P-256', or 'auto'. */
196
+ ecdhCurve?: string;
197
+ /** 'ecdsa_secp256r1_sha256:rsa_pss_rsae_sha256…' */
198
+ sigalgs?: string;
199
+ /** Server: the cipher suite by the server's order (default: the client's). */
200
+ honorCipherOrder?: boolean;
201
+
202
+ // ---- Encrypted Client Hello (RFC 9849) ----
203
+ /** Server: { keys }. Client: the server's ECHConfigList (bytes, or base64 — `ech=` of its HTTPS record). */
204
+ ech?: EchServerOption | Uint8Array | Buffer | string;
205
+ /** Client: GREASE ECH, without a config. */
206
+ echGrease?: boolean;
207
+
208
+ // ---- OCSP stapling ----
209
+ /** Server: asked when a client requests status; callback(null, response DER) or (null, null). */
210
+ ocspRequest?: (certificate: Buffer, issuer: Buffer | null, callback: (err: Error | null, response: Buffer | Uint8Array | null) => void) => void;
211
+ /** Client: ask the server for a stapled response ('OCSPResponse'). */
212
+ requestOCSP?: boolean;
213
+
214
+ /** RFC 8879: ['brotli', 'zlib', 'zstd'] in order, true (all this build has) or false (none). */
215
+ certificateCompression?: Array<'brotli' | 'zlib' | 'zstd'> | boolean;
216
+
217
+ // ---- 0-RTT (RFC 8446 §4.2.10) ----
218
+ /** Server: issue tickets that allow it, and take it. Client: with `session`, what is written before 'secureConnect' goes early. */
219
+ earlyData?: boolean;
220
+ /** Server: ms the client's ticket age may differ from ours. */
221
+ earlyDataMaxAge?: number;
222
+ /** Server: the anti-replay store — cb(null, true) when `id` is new (keep it `ttl` ms). */
223
+ earlyDataReplay?: (id: string, ttl: number, cb: (err: Error | null, fresh: boolean) => void) => void;
224
+ /** Server: bytes of early data a ticket allows (16 KB). */
225
+ maxEarlyData?: number;
226
+
227
+ // ---- ClientHello shaping (client) ----
228
+ grease?: boolean;
229
+ permuteExtensions?: boolean;
230
+
231
+ /** Client: trust the bundled CAs when no `ca` is given (default: true). */
232
+ useDefaultCA?: boolean;
233
+ allowHalfOpen?: boolean;
234
+ highWaterMark?: number;
133
235
 
134
236
  // TCP options, as in node:net. connect() passes them (and the rest of
135
237
  // net.connect's: localAddress, localPort, family, lookup, timeout) to
@@ -162,6 +264,27 @@ export interface TLSSocketOptions {
162
264
  customExtensions?: Array<{ type: number; data: Uint8Array }>;
163
265
  }
164
266
 
267
+ /** connect()'s: the socket's options, where, and node:net's. */
268
+ export interface ConnectOptions extends TLSSocketOptions {
269
+ port?: number;
270
+ host?: string;
271
+ /** STARTTLS: an existing socket to run TLS on (no net.connect). */
272
+ socket?: Duplex;
273
+ lookup?: (...args: any[]) => void;
274
+ [option: string]: any;
275
+ }
276
+
277
+ /** createServer()'s: the server side of TLSSocketOptions, and node:tls's server ones. */
278
+ export interface ServerOptions extends Omit<TLSSocketOptions, 'key' | 'cert'> {
279
+ key?: SecureContextOptions['key'];
280
+ cert?: SecureContextOptions['cert'];
281
+ /** Seconds a session stays good — tickets and TLS 1.2 Session IDs (Node's default, 300). */
282
+ sessionTimeout?: number;
283
+ /** Sessions under one context do not resume under another. */
284
+ sessionIdContext?: string | Buffer;
285
+ pauseOnConnect?: boolean;
286
+ }
287
+
165
288
  // ======================== TLSSocket ========================
166
289
 
167
290
  export class TLSSocket extends Duplex {
@@ -170,7 +293,8 @@ export class TLSSocket extends Duplex {
170
293
  // ------- Node.js tls.TLSSocket compatible API -------
171
294
  getProtocol(): string | null;
172
295
  getCipher(): CipherInfo | null;
173
- getPeerCertificate(): PeerCertificate | null;
296
+ /** `detailed` is accepted for node:tls's signature; the leaf is described either way. */
297
+ getPeerCertificate(detailed?: boolean): PeerCertificate | null;
174
298
  /** Returns our LOCAL cert (what we presented). Empty object if none. */
175
299
  getCertificate(): PeerCertificate | {};
176
300
  /** Returns the peer cert as a native crypto.X509Certificate object (Node 15.9+). */
@@ -180,10 +304,16 @@ export class TLSSocket extends Duplex {
180
304
  isSessionReused(): boolean;
181
305
  getFinished(): Buffer | null;
182
306
  getPeerFinished(): Buffer | null;
183
- exportKeyingMaterial(length: number, label: string, context: Buffer): Buffer;
184
- getEphemeralKeyInfo(): EphemeralKeyInfo;
307
+ /** null before the handshake completes (Node throws). */
308
+ exportKeyingMaterial(length: number, label: string, context?: Buffer | Uint8Array): Buffer | null;
309
+ /** null on a server socket, as in Node. */
310
+ getEphemeralKeyInfo(): EphemeralKeyInfo | {} | null;
311
+ /** On sockets from connect() and createServer() (compat layer). */
185
312
  disableRenegotiation(): void;
313
+ /** On sockets from connect() and createServer() (compat layer). */
186
314
  setServername(name: string): void;
315
+ /** The transport's bound address, as node:tls's. */
316
+ address(): { port?: number; family?: string; address?: string };
187
317
  /** Returns the opaque serialized session Buffer, or undefined. */
188
318
  getSession(): Buffer | undefined;
189
319
  /** Returns the TLS 1.2 session ticket as a Buffer, or undefined (incl. for TLS 1.3). */
@@ -225,6 +355,18 @@ export class TLSSocket extends Duplex {
225
355
  /** Returns the underlying TLSSession for low-level access. */
226
356
  session: TLSSession;
227
357
  readonly handshakeDuration: number | null;
358
+ /** 0-RTT. Server: what is read now came early (it may be a replay). Client: it is sending early. */
359
+ readonly isEarlyData: boolean;
360
+ readonly earlyDataBytes: number;
361
+ readonly earlyDataStatus: 'offered' | 'accepted' | 'rejected' | null;
362
+ /** Encrypted Client Hello: what happened to it on this connection. */
363
+ readonly echStatus: string | null;
364
+ readonly echAccepted: boolean;
365
+ /** Server: the certificate to present, from inside ALPNCallback (a context or { key, cert }). */
366
+ setKeyCert(context: SecureContext | SecureContextOptions): void;
367
+ /** The peer's hello extensions: [{ type, name, data, value }]. */
368
+ getRemoteExtensions(): Array<{ type: number; name: string | null; data: Uint8Array; value: any }>;
369
+ getRemoteExtension(type: number): { type: number; name: string | null; data: Uint8Array; value: any } | null;
228
370
  /** JA4 of the ClientHello — the published format, what fingerprint
229
371
  * databases match. Changes when a client resumes (pre_shared_key). */
230
372
  getJA4(opts?: FingerprintOptions): FingerprintResult | null;
@@ -256,6 +398,13 @@ export class TLSSocket extends Duplex {
256
398
  on(event: 'certificateRequest', listener: (msg: any) => void): this;
257
399
  on(event: 'newSession', listener: (id: Buffer, data: Buffer, cb: () => void) => void): this;
258
400
  on(event: 'resumeSession', listener: (id: Buffer, cb: (err: Error | null, data: Buffer | null) => void) => void): this;
401
+ /** Client: the server's stapled OCSP response (requestOCSP). */
402
+ on(event: 'OCSPResponse', listener: (response: Buffer) => void): this;
403
+ /** 0-RTT decided: whether the server took it. */
404
+ on(event: 'earlyData', listener: (accepted: boolean) => void): this;
405
+ on(event: 'alert', listener: (alert: { level: number; description: number }) => void): this;
406
+ on(event: 'secure', listener: () => void): this;
407
+ on(event: 'psk', listener: (identity: any, callback: (result: { psk: Buffer; cipher: number } | null) => void) => void): this;
259
408
  on(event: 'error', listener: (err: Error) => void): this;
260
409
  on(event: 'close', listener: () => void): this;
261
410
  on(event: string, listener: (...args: any[]) => void): this;
@@ -274,8 +423,19 @@ export class Server extends EventEmitter {
274
423
  close(callback?: (err?: Error) => void): this;
275
424
  address(): { port: number; family: string; address: string } | string | null;
276
425
 
277
- /** Replace the server's key/cert at runtime (Let's Encrypt renewals, etc.). */
278
- setSecureContext(options: { key: Buffer | string; cert: Buffer | string }): void;
426
+ getConnections(callback: (err: Error | null, count: number) => void): void;
427
+ /** Node's documented injection of a Duplex into the server ('connection'), as a method. */
428
+ injectConnection(stream: Duplex): this;
429
+ ref(): this;
430
+ unref(): this;
431
+
432
+ /** Replace the server's key/cert at runtime (Let's Encrypt renewals, etc.). Throws on a bad one; the old stays. */
433
+ setSecureContext(options: SecureContextOptions | { secureContext: SecureContext }): void;
434
+ /** A certificate for an SNI name or wildcard ('*.example.com': one label). The latest wins; an exact name beats a wildcard. */
435
+ addContext(hostname: string, context: SecureContext | SecureContextOptions): this;
436
+ /** Encrypted Client Hello keys, rotated without a restart. */
437
+ getEchKeys(): EchServerOption | null;
438
+ setEchKeys(ech: EchServerOption | null): void;
279
439
  /** Returns the 48-byte TLS session ticket encryption keys. */
280
440
  getTicketKeys(): Buffer;
281
441
  /** Sets the 48-byte TLS session ticket encryption keys (for clustered deployments). */
@@ -287,6 +447,11 @@ export class Server extends EventEmitter {
287
447
  on(event: 'secureConnection', listener: (socket: TLSSocket) => void): this;
288
448
  on(event: 'tlsClientError', listener: (err: Error, socket: TLSSocket | null) => void): this;
289
449
  on(event: 'keylog', listener: (line: Buffer, socket: TLSSocket) => void): this;
450
+ /** Before the TLS handshake: the raw socket. */
451
+ on(event: 'connection', listener: (socket: Duplex) => void): this;
452
+ on(event: 'OCSPRequest', listener: (certificate: Buffer, issuer: Buffer | null, callback: (err: Error | null, response: Buffer | Uint8Array | null) => void) => void): this;
453
+ on(event: 'listening', listener: () => void): this;
454
+ on(event: 'drop', listener: (data: any) => void): this;
290
455
  on(event: 'newSession', listener: (id: Buffer, data: Buffer, cb: () => void) => void): this;
291
456
  on(event: 'resumeSession', listener: (id: Buffer, cb: (err: Error | null, data: Buffer | null) => void) => void): this;
292
457
  on(event: 'error', listener: (err: Error) => void): this;
@@ -389,16 +554,41 @@ export class TLSSession extends EventEmitter {
389
554
 
390
555
  // ======================== Module Functions ========================
391
556
 
392
- export function createSecureContext(options: { key: Buffer | string; cert: Buffer | string }): SecureContext;
557
+ export function createSecureContext(options: SecureContextOptions): SecureContext;
393
558
 
394
- export function connect(port: number, host: string, options?: TLSSocketOptions, callback?: () => void): TLSSocket;
559
+ export function connect(port: number, host: string, options?: ConnectOptions, callback?: () => void): TLSSocket;
395
560
  export function connect(port: number, host: string, callback?: () => void): TLSSocket;
396
- export function connect(options: TLSSocketOptions & { port: number; host?: string }, callback?: () => void): TLSSocket;
397
-
398
- export function createServer(
399
- options: TLSSocketOptions & { key?: Buffer | string; cert?: Buffer | string },
400
- connectionListener?: (socket: TLSSocket) => void
401
- ): Server;
561
+ export function connect(options: ConnectOptions, callback?: () => void): TLSSocket;
562
+
563
+ export function createServer(options: ServerOptions, connectionListener?: (socket: TLSSocket) => void): Server;
564
+ export function createServer(connectionListener?: (socket: TLSSocket) => void): Server;
565
+
566
+ // ---- node:tls's option strings → code lists (null when unset). They throw
567
+ // on a name they do not know, as Node's createServer does.
568
+ export function parseCiphers(ciphers: string | null | undefined): number[] | null;
569
+ export function parseEcdhCurve(ecdhCurve: string | null | undefined): number[] | null;
570
+ export function parseSigalgs(sigalgs: string | null | undefined): number[] | null;
571
+ export function parseTlsVersion(version: string | null | undefined, optionName?: string): number | null;
572
+ export function parseCertificateCompression(value: Array<'brotli' | 'zlib' | 'zstd'> | boolean | null | undefined): number[] | null;
573
+
574
+ /** A server's certificate chain compressed now, in the background (RFC 8879), so the first handshake does not wait for it. */
575
+ export function precompressCertificate(secureContext: SecureContext, algorithms?: Array<'brotli' | 'zlib' | 'zstd'> | boolean, callback?: () => void): void;
576
+ export function precompressCertificate(secureContext: SecureContext, callback?: () => void): void;
577
+
578
+ /** The key-exchange groups a server offers by default, in order (code points). */
579
+ export const DEFAULT_SERVER_GROUPS: readonly number[];
580
+ /** The signature schemes offered by default, in order (code points). */
581
+ export const DEFAULT_SIGNATURE_SCHEMES: readonly number[];
582
+
583
+ /** A saved session described without its secrets; null if it is not one. */
584
+ export function sessionInfo(session: Buffer | Uint8Array): SessionInfo | null;
585
+ /** A certificate (DER) as getPeerCertificate() describes it. */
586
+ export function certificateObject(der: Buffer | Uint8Array): PeerCertificate | { raw: Buffer | Uint8Array } | null;
587
+
588
+ /** A new ECH key for a server. publicName: the name the outer hello carries (the server holds a certificate for it). */
589
+ export function generateEchKey(options: { publicName: string; configId?: number; maxNameLength?: number; kem?: string | number }): EchKey;
590
+ /** Throws, saying what is wrong, if a server's `ech` option would not work. */
591
+ export function checkEchKeys(ech: EchServerOption | null | undefined): void;
402
592
 
403
593
  export function getCiphers(): string[];
404
594
 
@@ -448,6 +638,16 @@ export declare const crypto: {
448
638
  /** RFC 8446 §7.5 exporter; labelPrefix defaults to TLS 1.3's ('tls13 '). */
449
639
  tls13_exporter: (hash: string, exporterMasterSecret: Uint8Array, label: string,
450
640
  context: Uint8Array | null, length: number, labelPrefix?: string) => Uint8Array;
641
+ /** HPKE (RFC 9180) — what ECH encrypts with. */
642
+ hpke: {
643
+ suite: (...args: any[]) => any;
644
+ setupSender: (...args: any[]) => any;
645
+ setupRecipient: (...args: any[]) => any;
646
+ seal: (...args: any[]) => any;
647
+ open: (...args: any[]) => any;
648
+ generateKeyPair: (...args: any[]) => any;
649
+ deriveKeyPair: (...args: any[]) => any;
650
+ };
451
651
  };
452
652
 
453
653
  export declare const wire: any;
@@ -493,6 +693,18 @@ declare const _default: {
493
693
  crypto: typeof crypto;
494
694
  wire: typeof wire;
495
695
  record: typeof record;
696
+ parseCiphers: typeof parseCiphers;
697
+ parseEcdhCurve: typeof parseEcdhCurve;
698
+ parseSigalgs: typeof parseSigalgs;
699
+ parseTlsVersion: typeof parseTlsVersion;
700
+ parseCertificateCompression: typeof parseCertificateCompression;
701
+ precompressCertificate: typeof precompressCertificate;
702
+ DEFAULT_SERVER_GROUPS: typeof DEFAULT_SERVER_GROUPS;
703
+ DEFAULT_SIGNATURE_SCHEMES: typeof DEFAULT_SIGNATURE_SCHEMES;
704
+ sessionInfo: typeof sessionInfo;
705
+ certificateObject: typeof certificateObject;
706
+ generateEchKey: typeof generateEchKey;
707
+ checkEchKeys: typeof checkEchKeys;
496
708
  DTLSSession: typeof DTLSSession;
497
709
  DTLSSocket: typeof DTLSSocket;
498
710
  createDTLSServer: typeof createDTLSServer;
package/package.json CHANGED
@@ -1,10 +1,26 @@
1
1
  {
2
2
  "name": "lemon-tls",
3
- "version": "0.9.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": "./src/wire.js",
16
- "./crypto": "./src/crypto.js",
17
- "./record": "./src/record.js",
18
- "./session": "./src/tls_session.js"
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": ">=16.0.0"
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
@@ -603,6 +603,12 @@ function Server(options, connectionListener) {
603
603
  return self._tcpServer.getConnections(cb);
604
604
  };
605
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
+
606
612
  // Ticket key management
607
613
  self.getTicketKeys = function() {
608
614
  return Buffer.from(self._ticketKeys);
@@ -631,11 +637,13 @@ function Server(options, connectionListener) {
631
637
  self._ech = ech || null;
632
638
  };
633
639
 
634
- // setSecureContext — replace cert/key without restart (Node.js compat)
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.
635
644
  self.setSecureContext = function(opts) {
636
- if (!opts) return;
637
- if (opts.secureContext) { defaultCtx = opts.secureContext; return; }
638
- if (opts.key && opts.cert) defaultCtx = createSecureContext(opts);
645
+ if (!opts || typeof opts !== 'object') throw new TypeError('setSecureContext requires { key, cert } or { secureContext }');
646
+ defaultCtx = opts.secureContext ? opts.secureContext : createSecureContext(opts);
639
647
  };
640
648
 
641
649
  /**
@@ -291,6 +291,7 @@ function DTLSSession(options) {
291
291
  // the ACK arrives.
292
292
  pendingWriteEpoch: null,
293
293
  keyUpdateFlight: null, // { payload, epoch, timer, attempts, timeout }
294
+ finalFlightPending: false, // DTLS 1.3 client: final flight not yet confirmed (see secureConnect)
294
295
 
295
296
  retransmitTimer: null,
296
297
  retransmitCount: 0,
@@ -750,9 +751,10 @@ function DTLSSession(options) {
750
751
  }
751
752
 
752
753
  for (let i = 0; i < frags.length; i++) {
753
- let record = buildRecord(dtlsEpoch, CT.HANDSHAKE, frags[i]);
754
754
  // The flight captures the fragment, not the record — see currentFlight.
755
- ctx.currentFlight.push({ epoch: dtlsEpoch, contentType: CT.HANDSHAKE, payload: frags[i] });
755
+ let entry = { epoch: dtlsEpoch, contentType: CT.HANDSHAKE, payload: frags[i] };
756
+ let record = buildFlightRecord(entry);
757
+ ctx.currentFlight.push(entry);
756
758
  if (record) {
757
759
  _ldbgTx(record, 'L543'); ev.emit('packet', record);
758
760
  } else {
@@ -1074,6 +1076,9 @@ function DTLSSession(options) {
1074
1076
  processHandshakeRecord(record.content, record.epoch, record.encrypted);
1075
1077
  } else if (record.type === CT.APPLICATION_DATA) {
1076
1078
  if (ctx.state === 'connected') {
1079
+ // Protected data from the peer means it completed the handshake,
1080
+ // so it has our Finished.
1081
+ if (ctx.finalFlightPending) finalFlightDone();
1077
1082
  ev.emit('data', record.content);
1078
1083
  }
1079
1084
  } else if (record.type === CT.CHANGE_CIPHER_SPEC) {
@@ -1574,17 +1579,31 @@ function DTLSSession(options) {
1574
1579
  // payload never came from the application at all.
1575
1580
  // RFC 9147 §5.8: retransmission is bounded by the handshake; after
1576
1581
  // it, ACKs are the mechanism, not a timer.
1577
- if (ctx.state === 'connected' || ctx.state === 'closed') {
1582
+ if (ctx.state === 'closed' || (ctx.state === 'connected' && !ctx.finalFlightPending)) {
1578
1583
  cancelRetransmit();
1579
1584
  return;
1580
1585
  }
1581
1586
  for (let i = 0; i < ctx.currentFlight.length; i++) {
1582
1587
  let f = ctx.currentFlight[i];
1583
- let record = buildRecord(f.epoch, f.contentType, f.payload);
1588
+ // DTLS 1.3 (RFC 9147 §7.1): records the peer has ACKed are not resent.
1589
+ if (f.acked) continue;
1590
+ let record = buildFlightRecord(f);
1584
1591
  if (record) { _ldbgTx(record, 'L1280'); ev.emit('packet', record); }
1585
1592
  }
1586
1593
  }
1587
1594
 
1595
+ /**
1596
+ * Build the record for a flight entry and remember its record number
1597
+ * (epoch, sequence) — every transmission gets a fresh one, and a DTLS 1.3
1598
+ * ACK names the records it covers by those numbers.
1599
+ */
1600
+ function buildFlightRecord(f) {
1601
+ let seq = (f.epoch in ctx.writeSeq) ? ctx.writeSeq[f.epoch] : 0;
1602
+ let record = buildRecord(f.epoch, f.contentType, f.payload);
1603
+ if (record) (f.sent || (f.sent = [])).push(f.epoch * 0x1000000000000 + seq);
1604
+ return record;
1605
+ }
1606
+
1588
1607
  function startRetransmitTimer() {
1589
1608
  cancelRetransmit();
1590
1609
  // ONE GATE, AT THE SOURCE. Four call sites arm this timer and only
@@ -1593,8 +1612,13 @@ function DTLSSession(options) {
1593
1612
  // flight into the application-data epoch. Refusing to arm once
1594
1613
  // connected (or closed) covers every caller, including any added
1595
1614
  // later, without each having to remember the rule.
1596
- if (ctx.state === 'connected' || ctx.state === 'closed') return;
1615
+ if (ctx.state === 'closed' || (ctx.state === 'connected' && !ctx.finalFlightPending)) return;
1597
1616
  ctx.retransmitTimer = setTimeout(function() {
1617
+ if (ctx.finalFlightPending && ctx.retransmitCount >= ctx.maxRetransmits) {
1618
+ // The connection is up on our side; stop resending without failing it.
1619
+ finalFlightDone();
1620
+ return;
1621
+ }
1598
1622
  if (ctx.retransmitCount >= ctx.maxRetransmits) {
1599
1623
  ev.emit('error', new Error('DTLS handshake timeout — max retransmits exceeded'));
1600
1624
  ctx.state = 'closed';
@@ -1613,6 +1637,13 @@ function DTLSSession(options) {
1613
1637
  if (ctx.retransmitTimer.unref) ctx.retransmitTimer.unref();
1614
1638
  }
1615
1639
 
1640
+ // The client's final flight is known to have arrived: stop resending it.
1641
+ function finalFlightDone() {
1642
+ ctx.finalFlightPending = false;
1643
+ cancelRetransmit();
1644
+ startNewFlight();
1645
+ }
1646
+
1616
1647
  function cancelRetransmit() {
1617
1648
  if (ctx.retransmitTimer !== null) {
1618
1649
  clearTimeout(ctx.retransmitTimer);
@@ -1646,10 +1677,37 @@ function DTLSSession(options) {
1646
1677
  return;
1647
1678
  }
1648
1679
 
1649
- // Everything else keeps the original behaviour verbatim. In particular the
1650
- // ACK for the client's final flight (which RFC 9147 §7.1 requires the peer
1651
- // to send explicitly, and which arrives at the application epoch) must
1652
- // still cancel that flight's retransmit timer.
1680
+ // During the handshake an ACK may cover only PART of our flight (RFC 9147
1681
+ // §7.1): the peer lists the records it received. Those are done; the
1682
+ // rest stay under the retransmit timer and are what the next
1683
+ // retransmission resends. Treating any ACK as acknowledging the whole
1684
+ // flight cancelled the timer while a record was still missing — the
1685
+ // peer, waiting for it, ACKed again and again, and the handshake hung
1686
+ // (≈ half of all handshakes at 5% loss, when the lost record was inside
1687
+ // the server's flight).
1688
+ if ((ctx.state !== 'connected' || ctx.finalFlightPending) && ctx.currentFlight.length > 0) {
1689
+ let got = new Set();
1690
+ for (let i = 0; i < acks.length; i++) got.add(acks[i].epoch * 0x1000000000000 + acks[i].seq);
1691
+ let remaining = 0;
1692
+ for (let i = 0; i < ctx.currentFlight.length; i++) {
1693
+ let f = ctx.currentFlight[i];
1694
+ if (!f.acked && f.sent) {
1695
+ for (let k = 0; k < f.sent.length; k++) {
1696
+ if (got.has(f.sent[k])) { f.acked = true; break; }
1697
+ }
1698
+ }
1699
+ // Plaintext epoch-0 records (ServerHello, CCS) are never ACKed — the
1700
+ // peer acknowledges them implicitly by answering — so they do not
1701
+ // keep the timer alive on their own.
1702
+ if (!f.acked && f.epoch !== 0) remaining++;
1703
+ }
1704
+ if (remaining > 0) return;
1705
+ }
1706
+
1707
+ // The whole flight is acknowledged — or, the ACK for the client's final
1708
+ // flight (which RFC 9147 §7.1 requires the peer to send explicitly, and
1709
+ // which arrives at the application epoch): cancel the retransmit timer.
1710
+ ctx.finalFlightPending = false;
1653
1711
  cancelRetransmit();
1654
1712
  startNewFlight();
1655
1713
  }
@@ -1700,8 +1758,18 @@ function DTLSSession(options) {
1700
1758
  _ldbg('handshake complete — secureConnect (version=0x' +
1701
1759
  ((ctx.selectedVersion || tls.context.selected_version || 0).toString(16)) + ')');
1702
1760
  ctx.state = 'connected';
1703
- cancelRetransmit();
1704
- startNewFlight();
1761
+ // DTLS 1.3 client (RFC 9147 §5.8.3, §7.1): our final flight (…Finished)
1762
+ // is answered by nothing but an ACK, so it must stay under the
1763
+ // retransmit timer until that ACK — or anything else from the peer at
1764
+ // the application epoch — shows it arrived. Dropping it here meant one
1765
+ // lost Finished left us "connected" and the peer waiting for good.
1766
+ if (!isServer && ctx.selectedVersion === DTLS_VERSION.DTLS1_3 && ctx.currentFlight.length > 0) {
1767
+ ctx.finalFlightPending = true;
1768
+ startRetransmitTimer();
1769
+ } else {
1770
+ cancelRetransmit();
1771
+ startNewFlight();
1772
+ }
1705
1773
  ev.emit('connect');
1706
1774
  });
1707
1775
 
package/src/record.js CHANGED
@@ -665,6 +665,21 @@ function readU48(buf, off) {
665
665
  }
666
666
 
667
667
  /** AES-ECB encrypt a single 16-byte block (for DTLS 1.3 record number encryption). */
668
+ // One AES-ECB cipher per sn_key, reused for every record's number mask
669
+ // (RFC 9147 §4.2.3) instead of creating one per record. ECB keeps no state
670
+ // between blocks, so update() on 16 bytes always returns exactly that block's
671
+ // encryption. Keyed by the key object, which lives as long as its epoch's keys.
672
+ var _snEcbCache = new WeakMap();
673
+ function _snKeyEcb(key) {
674
+ var c = _snEcbCache.get(key);
675
+ if (!c) {
676
+ c = crypto.createCipheriv(key.length === 32 ? 'aes-256-ecb' : 'aes-128-ecb', key, null);
677
+ c.setAutoPadding(false);
678
+ _snEcbCache.set(key, c);
679
+ }
680
+ return c;
681
+ }
682
+
668
683
  function aesEcbEncrypt(key, block) {
669
684
  let algo = key.length === 32 ? 'aes-256-ecb' : 'aes-128-ecb';
670
685
  let cipher = crypto.createCipheriv(algo, key, null);
@@ -785,8 +800,8 @@ function decryptDtls12(fragment, key, ivSalt, epoch, seq, type) {
785
800
  // ChaCha20-Poly1305: no explicit nonce; nonce = iv(12) XOR (epoch||seq).
786
801
  if (ivSalt.length === 12) {
787
802
  if (fragment.length < 16) throw new Error('DTLS 1.2 ChaCha fragment too short');
788
- let ct = fragment.slice(0, fragment.length - 16);
789
- let tag = fragment.slice(fragment.length - 16);
803
+ let ct = fragment.subarray(0, fragment.length - 16); // views: only read
804
+ let tag = fragment.subarray(fragment.length - 16);
790
805
  let seq64 = epoch * 0x1000000000000 + seq;
791
806
  let nonce = getChachaNonce12(ivSalt, seq64);
792
807
  let aad = buildDtlsAad12(epoch, seq, type, ct.length);
@@ -802,9 +817,9 @@ function decryptDtls12(fragment, key, ivSalt, epoch, seq, type) {
802
817
 
803
818
  // AES-GCM: strip the 8-byte explicit nonce.
804
819
  if (fragment.length < 24) throw new Error('DTLS 1.2 fragment too short');
805
- let explicit = fragment.slice(0, 8);
806
- let tag = fragment.slice(fragment.length - 16);
807
- let ct = fragment.slice(8, fragment.length - 16);
820
+ let explicit = fragment.subarray(0, 8); // views: only read
821
+ let tag = fragment.subarray(fragment.length - 16);
822
+ let ct = fragment.subarray(8, fragment.length - 16);
808
823
  let nonce = getNonce12(ivSalt, explicit);
809
824
  let aad = buildDtlsAad12(epoch, seq, type, ct.length);
810
825
  let algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
@@ -819,8 +834,38 @@ function decryptDtls12(fragment, key, ivSalt, epoch, seq, type) {
819
834
 
820
835
  /** Build complete encrypted DTLS 1.2 record (header + encrypted payload). */
821
836
  function buildEncryptedDtls12(type, epoch, seq, plaintext, keys) {
822
- let encrypted = encryptDtls12(plaintext, keys.key, keys.iv, epoch, seq, type);
823
- return buildDtlsPlaintext(type, epoch, seq, encrypted);
837
+ // Same bytes as buildDtlsPlaintext(type, epoch, seq, encryptDtls12(...)),
838
+ // written straight into the record instead of through two more buffers.
839
+ let key = keys.key, ivSalt = keys.iv;
840
+ let chacha = ivSalt.length === 12;
841
+ let explicitLen = chacha ? 0 : 8;
842
+ let fragLen = explicitLen + plaintext.length + 16;
843
+ let out = new Uint8Array(13 + fragLen);
844
+ let off = 0;
845
+ off = w_u8(out, off, type);
846
+ off = w_u16(out, off, 0xFEFD);
847
+ off = w_u16(out, off, epoch);
848
+ off = w_u48(out, off, seq);
849
+ off = w_u16(out, off, fragLen);
850
+ let aad = buildDtlsAad12(epoch, seq, type, plaintext.length);
851
+ let cipher;
852
+ if (chacha) {
853
+ let nonce = getChachaNonce12(ivSalt, epoch * 0x1000000000000 + seq);
854
+ cipher = crypto.createCipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
855
+ cipher.setAAD(aad, { plaintextLength: plaintext.length });
856
+ } else {
857
+ // AES-GCM: explicit nonce (= record seq) prepended.
858
+ let explicit = seqToBytes(seq);
859
+ out.set(explicit, off); off += 8;
860
+ cipher = crypto.createCipheriv(key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm', key, getNonce12(ivSalt, explicit));
861
+ cipher.setAAD(aad);
862
+ }
863
+ let ct = cipher.update(plaintext);
864
+ out.set(ct, off); off += ct.length;
865
+ let fin = cipher.final();
866
+ if (fin.length) { out.set(fin, off); off += fin.length; }
867
+ out.set(cipher.getAuthTag(), off);
868
+ return out;
824
869
  }
825
870
 
826
871
 
@@ -898,7 +943,7 @@ function maskRecordNumber(snKey, rnBytes, ciphertext, algo) {
898
943
  let c = crypto.createCipheriv('chacha20', snKey, iv);
899
944
  mask = new Uint8Array(c.update(Buffer.alloc(16)));
900
945
  } else {
901
- mask = aesEcbEncrypt(snKey, ciphertext.subarray(0, 16));
946
+ mask = _snKeyEcb(snKey).update(ciphertext.subarray(0, 16));
902
947
  }
903
948
 
904
949
  let out = new Uint8Array(rnBytes.length);
@@ -926,42 +971,43 @@ function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys) {
926
971
  let info = buildUnifiedHdr(epoch, true, true, false);
927
972
 
928
973
  // Plaintext record number
929
- let rn = new Uint8Array(2);
930
- rn[0] = (seq >>> 8) & 0xFF;
931
- rn[1] = seq & 0xFF;
932
-
933
- // Inner plaintext: content + content_type
934
- let inner = new Uint8Array(plaintext.length + 1);
935
- inner.set(plaintext, 0);
936
- inner[plaintext.length] = innerType;
974
+ let rn0 = (seq >>> 8) & 0xFF, rn1 = seq & 0xFF;
937
975
 
938
- let encLen = inner.length + 16;
976
+ // Inner plaintext = content || content_type. Fed to the cipher in two
977
+ // pieces rather than first copied into one buffer.
978
+ let innerLen = plaintext.length + 1;
979
+ let encLen = innerLen + 16;
939
980
 
940
981
  // AAD = header with PLAINTEXT record number (before RN encryption)
941
982
  let aad = new Uint8Array(5);
942
983
  aad[0] = info;
943
- aad[1] = rn[0];
944
- aad[2] = rn[1];
984
+ aad[1] = rn0;
985
+ aad[2] = rn1;
945
986
  aad[3] = (encLen >>> 8) & 0xFF;
946
987
  aad[4] = encLen & 0xFF;
947
988
 
948
989
  // Nonce (reuse TLS 1.3 nonce construction)
949
990
  let nonce = getNonce(keys.iv, seq);
950
991
 
951
- // AEAD encrypt
992
+ // AEAD encrypt, written straight into the record: info(1) + encrypted
993
+ // rn(2) + length(2) + ciphertext. (It used to pass through three buffers:
994
+ // inner plaintext, ciphertext+tag, then the record.)
952
995
  let cipher = crypto.createCipheriv(algo, keys.key, nonce,
953
996
  isChaCha ? { authTagLength: 16 } : undefined);
954
- cipher.setAAD(aad, isChaCha ? { plaintextLength: inner.length } : undefined);
955
- let ct = cipher.update(inner);
956
- cipher.final();
957
- let tag = cipher.getAuthTag();
958
-
959
- let ciphertext = new Uint8Array(ct.length + tag.length);
960
- ciphertext.set(new Uint8Array(ct), 0);
961
- ciphertext.set(new Uint8Array(tag), ct.length);
997
+ cipher.setAAD(aad, isChaCha ? { plaintextLength: innerLen } : undefined);
998
+ let record = new Uint8Array(5 + encLen);
999
+ let off = 5;
1000
+ let ct1 = cipher.update(plaintext);
1001
+ record.set(ct1, off); off += ct1.length;
1002
+ let ct2 = cipher.update(_contentTypeByte(innerType));
1003
+ record.set(ct2, off); off += ct2.length;
1004
+ let ct3 = cipher.final();
1005
+ if (ct3.length) { record.set(ct3, off); off += ct3.length; }
1006
+ record.set(cipher.getAuthTag(), off);
1007
+ let ciphertext = record.subarray(5);
962
1008
 
963
1009
  // Encrypt record number
964
- let encRn = maskRecordNumber(keys.snKey, rn, ciphertext, algo);
1010
+ let encRn = maskRecordNumber(keys.snKey, [rn0, rn1], ciphertext, algo);
965
1011
 
966
1012
  // ── DIAGNOSTIC (LEMON_DEBUG=1 / WEBRTC_DEBUG=1) ──────────────────────
967
1013
  // Everything a peer needs to PARSE this record, printed as we build it.
@@ -971,34 +1017,39 @@ function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys) {
971
1017
  // present), the sequence number before and after masking, and the
972
1018
  // declared length against the actual ciphertext size. If two records
973
1019
  // are accepted and the third is not, the difference between them shows
974
- // up here.
975
- try {
976
- if (typeof process !== 'undefined' && process.env &&
977
- (process.env.LEMON_DEBUG === '1' || process.env.WEBRTC_DEBUG === '1')) {
1020
+ // up here. (Flag read once at load: process.env is slow to read per record.)
1021
+ if (_REC_DEBUG) {
1022
+ try {
978
1023
  var _h = function (b) { return ('0' + (b & 0xFF).toString(16)).slice(-2); };
979
1024
  console.log('[lemon-rec] TX info=0x' + _h(info) +
980
1025
  ' [C=' + ((info >> 4) & 1) + ' S=' + ((info >> 3) & 1) +
981
1026
  ' L=' + ((info >> 2) & 1) + ' epoch=' + (info & 0x03) + ']' +
982
- ' seqPlain=' + _h(rn[0]) + _h(rn[1]) +
1027
+ ' seqPlain=' + _h(rn0) + _h(rn1) +
983
1028
  ' seqMasked=' + _h(encRn[0]) + _h(encRn[1]) +
984
1029
  ' declaredLen=' + ciphertext.length +
985
- ' aadLen=' + (aad ? aad.length : 'null') +
1030
+ ' aadLen=' + aad.length +
986
1031
  ' snKey=' + (keys.snKey ? 'set' : 'MISSING'));
987
- }
988
- } catch (_e) {}
1032
+ } catch (_e) {}
1033
+ }
989
1034
  // ─────────────────────────────────────────────────────────────────────
990
1035
 
991
- // Assemble: info(1) + encrypted_rn(2) + length(2) + ciphertext
992
- let record = new Uint8Array(5 + ciphertext.length);
993
1036
  record[0] = info;
994
1037
  record[1] = encRn[0];
995
1038
  record[2] = encRn[1];
996
1039
  record[3] = (ciphertext.length >>> 8) & 0xFF;
997
1040
  record[4] = ciphertext.length & 0xFF;
998
- record.set(ciphertext, 5);
999
1041
  return record;
1000
1042
  }
1001
1043
 
1044
+ var _REC_DEBUG = typeof process !== 'undefined' && process.env &&
1045
+ (process.env.LEMON_DEBUG === '1' || process.env.WEBRTC_DEBUG === '1');
1046
+
1047
+ // The one-byte content type appended to a DTLS 1.3 inner plaintext.
1048
+ var _CT_BYTES = [];
1049
+ function _contentTypeByte(t) {
1050
+ return _CT_BYTES[t] || (_CT_BYTES[t] = new Uint8Array([t]));
1051
+ }
1052
+
1002
1053
  /**
1003
1054
  * Reconstruct a full DTLS 1.3 record sequence number from its truncated wire
1004
1055
  * encoding (RFC 9147 §4.2.2).
@@ -1093,7 +1144,9 @@ function decryptEncryptedDtls13(data, keys, fullEpoch) {
1093
1144
  }
1094
1145
 
1095
1146
  if (off + ctLen > data.length) return null;
1096
- let ciphertext = data.slice(off, off + ctLen);
1147
+ // A view: only read here (record-number mask, AEAD input); the plaintext
1148
+ // comes out of the decipher as a fresh buffer.
1149
+ let ciphertext = data.subarray(off, off + ctLen);
1097
1150
 
1098
1151
  // Resolve the AEAD before unmasking: the record-number mask is built with
1099
1152
  // the same primitive as the AEAD (AES-ECB vs ChaCha20), so `algo` is needed
@@ -1178,23 +1231,34 @@ function decryptEncryptedDtls13(data, keys, fullEpoch) {
1178
1231
  // next to maxReadSeq, because both are per-epoch receive state.
1179
1232
  // replayWindow: bitmask of the 64 seqs at or below maxReadSeq that have
1180
1233
  // been accepted. Bit 0 = maxReadSeq, bit k = maxReadSeq - k.
1234
+ // The 64-bit window is kept as two 32-bit numbers (replayLo = bits
1235
+ // 0..31, replayHi = 32..63): same window, no BigInt allocations per
1236
+ // record.
1181
1237
  if (keys.maxReadSeq === undefined || keys.maxReadSeq < 0) {
1182
1238
  // First accepted record in this epoch: seed the window with this seq as
1183
1239
  // the high-water mark (bit 0 set), so an immediate replay of it is
1184
1240
  // caught below rather than sailing through an unset window.
1185
1241
  keys.maxReadSeq = seq;
1186
- keys.replayWindow = 1n;
1242
+ keys.replayLo = 1; keys.replayHi = 0;
1187
1243
  } else if (seq > keys.maxReadSeq) {
1188
1244
  // Advance: shift the window up by the gap, then set bit 0.
1189
- let shift = BigInt(seq - keys.maxReadSeq);
1190
- keys.replayWindow = shift >= 64n ? 1n : ((keys.replayWindow << shift) | 1n) & ((1n << 64n) - 1n);
1245
+ let shift = seq - keys.maxReadSeq;
1246
+ let lo = keys.replayLo, hi = keys.replayHi;
1247
+ if (shift >= 64) { lo = 0; hi = 0; }
1248
+ else if (shift >= 32) { hi = (lo << (shift - 32)) >>> 0; lo = 0; }
1249
+ else { hi = ((hi << shift) | (lo >>> (32 - shift))) >>> 0; lo = (lo << shift) >>> 0; }
1250
+ keys.replayLo = (lo | 1) >>> 0; keys.replayHi = hi;
1191
1251
  keys.maxReadSeq = seq;
1192
1252
  } else {
1193
- let back = BigInt(keys.maxReadSeq - seq);
1194
- if (back >= 64n) return null; // too old — outside window
1195
- let bit = 1n << back;
1196
- if (keys.replayWindow & bit) return null; // already seen — replay
1197
- keys.replayWindow |= bit;
1253
+ let back = keys.maxReadSeq - seq;
1254
+ if (back >= 64) return null; // too old — outside window
1255
+ if (back < 32) {
1256
+ if ((keys.replayLo >>> back) & 1) return null; // already seen — replay
1257
+ keys.replayLo = (keys.replayLo | (1 << back)) >>> 0;
1258
+ } else {
1259
+ if ((keys.replayHi >>> (back - 32)) & 1) return null;
1260
+ keys.replayHi = (keys.replayHi | (1 << (back - 32))) >>> 0;
1261
+ }
1198
1262
  }
1199
1263
 
1200
1264
  return {
@@ -1292,19 +1356,23 @@ function parseDtlsDatagram(data, keysByEpoch) {
1292
1356
  let length = readU16(data, off + 11);
1293
1357
  if (off + 13 + length > data.length) break;
1294
1358
 
1295
- let payload = data.slice(off + 13, off + 13 + length);
1359
+ let payload = null;
1296
1360
  let encrypted = false;
1297
1361
 
1298
- // DTLS 1.2: decrypt if epoch > 0 and keys available
1362
+ // DTLS 1.2: decrypt if epoch > 0 and keys available. The ciphertext is
1363
+ // only read (the plaintext comes out of the decipher as a fresh
1364
+ // buffer), so it is passed as a view; a record kept as it is — not
1365
+ // decrypted — gets its own copy, as before.
1299
1366
  if (epoch > 0 && keysByEpoch && keysByEpoch[epoch]) {
1300
1367
  let keys = keysByEpoch[epoch];
1301
1368
  try {
1302
- payload = decryptDtls12(payload, keys.key, keys.iv, epoch, seq, type);
1369
+ payload = decryptDtls12(data.subarray(off + 13, off + 13 + length), keys.key, keys.iv, epoch, seq, type);
1303
1370
  encrypted = true;
1304
1371
  } catch(e) {
1305
1372
  // Decryption failed — return raw
1306
1373
  }
1307
1374
  }
1375
+ if (payload === null) payload = data.slice(off + 13, off + 13 + length);
1308
1376
 
1309
1377
  records.push({
1310
1378
  type: type,
@@ -186,6 +186,20 @@ function createSecureContext(options) {
186
186
  if (!privateKey) throw new Error('createSecureContext: missing private key.');
187
187
  }
188
188
 
189
+ // The key must be the certificate's — node:tls throws on this too
190
+ // (KEY_VALUES_MISMATCH). Unchecked, a renewal that paired the new key with
191
+ // the old certificate went in without a word, and every handshake after it
192
+ // failed at the client's signature check.
193
+ if (keyObj && certObjs.length > 0) {
194
+ let fromKey = crypto.createPublicKey(keyObj).export({ type: 'spki', format: 'der' });
195
+ let fromCert = certObjs[0].publicKey.export({ type: 'spki', format: 'der' });
196
+ if (Buffer.compare(Buffer.from(fromKey), Buffer.from(fromCert)) !== 0) {
197
+ let e = new Error('createSecureContext: the private key does not match the certificate');
198
+ e.code = 'ERR_OSSL_X509_KEY_VALUES_MISMATCH';
199
+ throw e;
200
+ }
201
+ }
202
+
189
203
  // --- CA (Trust store) ---
190
204
  let ca = normalizeCA(options.ca);
191
205
 
@@ -1866,10 +1866,10 @@ function TLSSession(options){
1866
1866
  dbg('SRV-NST', 'issuing TLS 1.3 NST — cipher:', '0x' + context.selected_cipher_suite.toString(16),
1867
1867
  'hash:', hashName,
1868
1868
  'transcript len:', concatUint8Arrays(context.transcript).length);
1869
- dbg('SRV-NST', 'ticket_nonce:', hexPreview(ticket_nonce, 4),
1869
+ if (LEMON_DEBUG) dbg('SRV-NST', 'ticket_nonce:', hexPreview(ticket_nonce, 4),
1870
1870
  'age_add:', ticket_age_add,
1871
1871
  'lifetime:', ticket_lifetime);
1872
- dbg('SRV-NST', 'resumption_master_secret:', hexPreview(context.resumption_master_secret, 8),
1872
+ if (LEMON_DEBUG) dbg('SRV-NST', 'resumption_master_secret:', hexPreview(context.resumption_master_secret, 8),
1873
1873
  'derived psk:', hexPreview(psk, 8));
1874
1874
 
1875
1875
  // Ensure ticketKeys is 48 bytes (key_name + aes_key)
@@ -2319,7 +2319,7 @@ function TLSSession(options){
2319
2319
  context.ticket_offered = true;
2320
2320
  let pskBinder = message.pre_shared_key.binders ? message.pre_shared_key.binders[0] : null;
2321
2321
 
2322
- dbg('SRV-PSK', 'received identity:', hexPreview(pskIdentity.identity, 24),
2322
+ if (LEMON_DEBUG) dbg('SRV-PSK', 'received identity:', hexPreview(pskIdentity.identity, 24),
2323
2323
  'age:', (pskIdentity.age || 0) >>> 0,
2324
2324
  'received binder:', hexPreview(pskBinder, 16));
2325
2325
 
@@ -2337,7 +2337,7 @@ function TLSSession(options){
2337
2337
  });
2338
2338
  if (pskResult === undefined) pskResult = open_ticket_13(pskIdentity.identity);
2339
2339
 
2340
- dbg('SRV-PSK', 'pskResult:', pskResult ? `psk=${hexPreview(pskResult.psk, 8)} cipher=0x${pskResult.cipher?.toString(16)}` : 'null (decrypt failed)');
2340
+ if (LEMON_DEBUG) dbg('SRV-PSK', 'pskResult:', pskResult ? `psk=${hexPreview(pskResult.psk, 8)} cipher=0x${pskResult.cipher?.toString(16)}` : 'null (decrypt failed)');
2341
2341
 
2342
2342
  if (pskResult && pskResult.psk && !same_server_name(pskResult.sni, message.sni)) {
2343
2343
  dbg('SRV-PSK', 'ticket for another server name — full handshake');
@@ -2361,8 +2361,8 @@ function TLSSession(options){
2361
2361
  dbg('SRV-PSK', 'hash:', hashName, 'hashLen:', hashLen,
2362
2362
  'truncatedCH len:', truncatedCH.length,
2363
2363
  'full CH len:', data.length);
2364
- dbg('SRV-PSK', 'expected binder:', hexPreview(expectedBinder, 16));
2365
- dbg('SRV-PSK', 'received binder:', hexPreview(pskBinder, 16));
2364
+ if (LEMON_DEBUG) dbg('SRV-PSK', 'expected binder:', hexPreview(expectedBinder, 16));
2365
+ if (LEMON_DEBUG) dbg('SRV-PSK', 'received binder:', hexPreview(pskBinder, 16));
2366
2366
 
2367
2367
  // Constant-time binder comparison. A byte-by-byte early-exit compare
2368
2368
  // would let an attacker recover a valid binder via timing, one byte at
@@ -2972,7 +2972,7 @@ function TLSSession(options){
2972
2972
  let sentSid = context.local_session_id;
2973
2973
  let hasTicket = context.tls12_client_session.ticket && context.tls12_client_session.ticket.length > 0;
2974
2974
 
2975
- dbg('CLI-12RESUME', 'saved sid:', hexPreview(savedSid, 16),
2975
+ if (LEMON_DEBUG) dbg('CLI-12RESUME', 'saved sid:', hexPreview(savedSid, 16),
2976
2976
  'sent sid:', hexPreview(sentSid, 16),
2977
2977
  'received sid:', hexPreview(message.session_id, 16),
2978
2978
  'hasTicket:', hasTicket);
@@ -3435,10 +3435,10 @@ function TLSSession(options){
3435
3435
  dbg('CLI-NST', 'received TLS 1.3 NST — cipher:', '0x' + context.selected_cipher_suite.toString(16),
3436
3436
  'hash:', hashName,
3437
3437
  'transcript len:', concatUint8Arrays(context.transcript).length);
3438
- dbg('CLI-NST', 'ticket_nonce:', hexPreview(message.ticket_nonce, 4),
3438
+ if (LEMON_DEBUG) dbg('CLI-NST', 'ticket_nonce:', hexPreview(message.ticket_nonce, 4),
3439
3439
  'age_add:', message.ticket_age_add,
3440
3440
  'lifetime:', message.ticket_lifetime);
3441
- dbg('CLI-NST', 'resumption_master_secret:', hexPreview(context.resumption_master_secret, 8),
3441
+ if (LEMON_DEBUG) dbg('CLI-NST', 'resumption_master_secret:', hexPreview(context.resumption_master_secret, 8),
3442
3442
  'derived psk:', hexPreview(psk, 8));
3443
3443
 
3444
3444
  // Encode opaque client-side session Buffer (JSON — user is responsible for secure storage)
@@ -6597,17 +6597,17 @@ function TLSSession(options){
6597
6597
  let binder_key = derive_binder_key(hashName, context.psk_offered.psk, false, label_prefix());
6598
6598
  let binder = compute_psk_binder(hashName, binder_key, truncatedMessage, label_prefix());
6599
6599
 
6600
- dbg('CLI-PSK', 'ticket:', hexPreview(sessionData.ticket, 24),
6600
+ if (LEMON_DEBUG) dbg('CLI-PSK', 'ticket:', hexPreview(sessionData.ticket, 24),
6601
6601
  'cipher:', '0x' + sessionData.cipher.toString(16),
6602
6602
  'hash:', hashName);
6603
- dbg('CLI-PSK', 'psk:', hexPreview(sessionData.psk, 8),
6603
+ if (LEMON_DEBUG) dbg('CLI-PSK', 'psk:', hexPreview(sessionData.psk, 8),
6604
6604
  'age_add:', sessionData.age_add,
6605
6605
  'lifetime:', sessionData.lifetime,
6606
6606
  'ticketAge (ms):', ticketAge,
6607
6607
  'obfuscatedAge:', obfuscatedAge);
6608
6608
  dbg('CLI-PSK', 'truncatedMessage len:', truncatedMessage.length,
6609
6609
  'full CH len (after real binder):', 'see next');
6610
- dbg('CLI-PSK', 'sent binder:', hexPreview(binder, 16));
6610
+ if (LEMON_DEBUG) dbg('CLI-PSK', 'sent binder:', hexPreview(binder, 16));
6611
6611
 
6612
6612
  // Rebuild with real binder
6613
6613
  pskExt.value.binders = [binder];
@@ -0,0 +1,40 @@
1
+ // lemon-tls/crypto — Key schedule primitives: HKDF, the TLS 1.3 secrets, exporters, cipher-suite tables.
2
+ // Low-level: typed loosely; see the source for each signature.
3
+
4
+ export declare const DEFAULT_CIPHER_SUITES_TLS12: any;
5
+ export declare const DEFAULT_CIPHER_SUITES_TLS13: any;
6
+ export declare const LABEL_PREFIX_DTLS13: string;
7
+ export declare const LABEL_PREFIX_TLS13: string;
8
+ export declare const TLS_CIPHER_SUITES: any;
9
+ export declare function build_cert_verify_tbs(...args: any[]): any;
10
+ export declare function build_cert_verify_tbs_with_hash(...args: any[]): any;
11
+ export declare function build_hkdf_label(...args: any[]): any;
12
+ export declare function compute_psk_binder(...args: any[]): any;
13
+ export declare function default_cipher_suites(...args: any[]): any;
14
+ export declare function derive_app_traffic_secrets(...args: any[]): any;
15
+ export declare function derive_app_traffic_secrets_with_hash(...args: any[]): any;
16
+ export declare function derive_binder_key(...args: any[]): any;
17
+ export declare function derive_client_early_traffic_secret(...args: any[]): any;
18
+ export declare function derive_exporter_master_secret_with_hash(...args: any[]): any;
19
+ export declare function derive_handshake_traffic_secrets(...args: any[]): any;
20
+ export declare function derive_handshake_traffic_secrets_psk(...args: any[]): any;
21
+ export declare function derive_handshake_traffic_secrets_psk_with_hash(...args: any[]): any;
22
+ export declare function derive_handshake_traffic_secrets_with_hash(...args: any[]): any;
23
+ export declare function derive_psk(...args: any[]): any;
24
+ export declare function derive_resumption_master_secret(...args: any[]): any;
25
+ export declare function derive_resumption_master_secret_with_hash(...args: any[]): any;
26
+ export declare function derive_sn_key(...args: any[]): any;
27
+ export declare function getHashFn(...args: any[]): any;
28
+ export declare function getHashLen(...args: any[]): any;
29
+ export declare function get_handshake_finished(...args: any[]): any;
30
+ export declare function get_handshake_finished_with_hash(...args: any[]): any;
31
+ export declare function hkdf_expand(...args: any[]): any;
32
+ export declare function hkdf_expand_label(...args: any[]): any;
33
+ export declare function hkdf_extract(...args: any[]): any;
34
+ export declare function hmac(...args: any[]): any;
35
+ export declare function is_usable_cipher_suite(...args: any[]): any;
36
+ export declare function suite_matches_version(...args: any[]): any;
37
+ export declare function tls12_exporter(...args: any[]): any;
38
+ export declare function tls12_prf(...args: any[]): any;
39
+ export declare function tls13_exporter(...args: any[]): any;
40
+ export declare function tls_derive_from_master_secret_tls12(...args: any[]): any;
@@ -0,0 +1,40 @@
1
+ // lemon-tls/record — The record layer: AEAD keys and nonces, encrypt/decrypt for TLS and DTLS records.
2
+ // Low-level: typed loosely; see the source for each signature.
3
+
4
+ export declare const CT: any;
5
+ export declare function aesEcbEncrypt(...args: any[]): any;
6
+ export declare function buildAad12(...args: any[]): any;
7
+ export declare function buildDtlsAad12(...args: any[]): any;
8
+ export declare function buildDtlsAck(...args: any[]): any;
9
+ export declare function buildDtlsPlaintext(...args: any[]): any;
10
+ export declare function buildEncryptedDtls12(...args: any[]): any;
11
+ export declare function buildEncryptedDtls13(...args: any[]): any;
12
+ export declare function buildUnifiedHdr(...args: any[]): any;
13
+ export declare function decrypt12(...args: any[]): any;
14
+ export declare function decryptDtls12(...args: any[]): any;
15
+ export declare function decryptEncryptedDtls13(...args: any[]): any;
16
+ export declare function decryptRecord(...args: any[]): any;
17
+ export declare function decryptRecordWithAadView(...args: any[]): any;
18
+ export declare function deriveKeys(...args: any[]): any;
19
+ export declare function deriveKeys12(...args: any[]): any;
20
+ export declare function encrypt12(...args: any[]): any;
21
+ export declare function encryptCompleteRecord12(...args: any[]): any;
22
+ export declare function encryptCompleteRecord13(...args: any[]): any;
23
+ export declare function encryptDtls12(...args: any[]): any;
24
+ export declare function encryptRecord(...args: any[]): any;
25
+ export declare function getAeadAlgo(...args: any[]): any;
26
+ export declare function getChachaNonce12(...args: any[]): any;
27
+ export declare function getNonce(...args: any[]): any;
28
+ export declare function getNonce12(...args: any[]): any;
29
+ export declare function getNonceInto(...args: any[]): any;
30
+ export declare function isUnifiedHdr(...args: any[]): any;
31
+ export declare function maskRecordNumber(...args: any[]): any;
32
+ export declare function parseDtlsAck(...args: any[]): any;
33
+ export declare function parseDtlsDatagram(...args: any[]): any;
34
+ export declare function parseDtlsPlaintext(...args: any[]): any;
35
+ export declare function parseInnerPlaintext(...args: any[]): any;
36
+ export declare function parseUnifiedHdr(...args: any[]): any;
37
+ export declare function reconstructDtlsSeq(...args: any[]): any;
38
+ export declare function resolveReadEpoch(...args: any[]): any;
39
+ export declare function seqToBytes(...args: any[]): any;
40
+ export declare function writeRecord(...args: any[]): any;
@@ -0,0 +1,3 @@
1
+ // lemon-tls/session — the TLS state machine on its own, with no socket.
2
+ import { TLSSession } from '../index.js';
3
+ export default TLSSession;
@@ -0,0 +1,62 @@
1
+ // lemon-tls/wire — TLS and DTLS messages: parse_* and build_* for each handshake message, and the codes (TLS_EXT, TLS_ALERT, …).
2
+ // Low-level: typed loosely; see the source for each signature.
3
+
4
+ export declare const DTLS_VERSION: any;
5
+ export declare const HANDSHAKE_MESSAGE_MAX: number;
6
+ export declare const TLS13_HRR_RANDOM: any;
7
+ export declare const TLS_ALERT: any;
8
+ export declare const TLS_ALERT_LEVEL: any;
9
+ export declare const TLS_CONTENT_TYPE: any;
10
+ export declare const TLS_EXT: any;
11
+ export declare const TLS_MESSAGE_TYPE: any;
12
+ export declare const TLS_VERSION: any;
13
+ export declare function build_certificate(...args: any[]): any;
14
+ export declare function build_certificate_request(...args: any[]): any;
15
+ export declare function build_certificate_status(...args: any[]): any;
16
+ export declare function build_certificate_verify(...args: any[]): any;
17
+ export declare function build_client_key_exchange_ecdhe(...args: any[]): any;
18
+ export declare function build_client_key_exchange_rsa(...args: any[]): any;
19
+ export declare function build_compressed_certificate(...args: any[]): any;
20
+ export declare function build_dtls_handshake(...args: any[]): any;
21
+ export declare function build_extensions(...args: any[]): any;
22
+ export declare function build_hello(...args: any[]): any;
23
+ export declare function build_hello_retry_request(...args: any[]): any;
24
+ export declare function build_hello_verify_request(...args: any[]): any;
25
+ export declare function build_key_update(...args: any[]): any;
26
+ export declare function build_message(...args: any[]): any;
27
+ export declare function build_new_session_ticket(...args: any[]): any;
28
+ export declare function build_new_session_ticket_tls12(...args: any[]): any;
29
+ export declare function build_server_ecdh_params(...args: any[]): any;
30
+ export declare function build_server_key_exchange_ecdhe(...args: any[]): any;
31
+ export declare const exts: any;
32
+ export declare function is_tls12_wire(...args: any[]): any;
33
+ export declare function is_tls13_wire(...args: any[]): any;
34
+ export declare function parse_certificate(...args: any[]): any;
35
+ export declare function parse_certificate_request(...args: any[]): any;
36
+ export declare function parse_certificate_status(...args: any[]): any;
37
+ export declare function parse_certificate_verify(...args: any[]): any;
38
+ export declare function parse_client_key_exchange_ecdhe(...args: any[]): any;
39
+ export declare function parse_client_key_exchange_rsa(...args: any[]): any;
40
+ export declare function parse_compressed_certificate(...args: any[]): any;
41
+ export declare function parse_dtls_handshake(...args: any[]): any;
42
+ export declare function parse_extension_list(...args: any[]): any;
43
+ export declare function parse_extensions(...args: any[]): any;
44
+ export declare function parse_hello(...args: any[]): any;
45
+ export declare function parse_hello_verify_request(...args: any[]): any;
46
+ export declare function parse_key_update(...args: any[]): any;
47
+ export declare function parse_message(...args: any[]): any;
48
+ export declare function parse_new_session_ticket(...args: any[]): any;
49
+ export declare function parse_new_session_ticket_tls12(...args: any[]): any;
50
+ export declare function parse_server_key_exchange(...args: any[]): any;
51
+ export declare function r_bytes(...args: any[]): any;
52
+ export declare function r_u16(...args: any[]): any;
53
+ export declare function r_u24(...args: any[]): any;
54
+ export declare function r_u8(...args: any[]): any;
55
+ export declare function readVec(...args: any[]): any;
56
+ export declare function veclen(...args: any[]): any;
57
+ export declare function w_bytes(...args: any[]): any;
58
+ export declare function w_u16(...args: any[]): any;
59
+ export declare function w_u24(...args: any[]): any;
60
+ export declare function w_u48(...args: any[]): any;
61
+ export declare function w_u64(...args: any[]): any;
62
+ export declare function w_u8(...args: any[]): any;