lemon-tls 0.7.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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;
@@ -31,16 +81,39 @@ export interface PeerCertificate {
31
81
  }
32
82
 
33
83
  export interface EphemeralKeyInfo {
34
- type: 'X25519' | 'ECDH' | 'DH';
84
+ /** 'TLSGroup' for a group that is not a plain curve, as Node reports it —
85
+ * X25519MLKEM768 among them (no size). Client side only; {} on a server. */
86
+ type: 'X25519' | 'ECDH' | 'DH' | 'TLSGroup';
35
87
  name?: string;
36
- size: number;
88
+ size?: number;
37
89
  }
38
90
 
39
- export interface JA3Result {
91
+ /** A TLS fingerprint: `hash` is the fingerprint, `raw` what was hashed. */
92
+ export interface FingerprintResult {
40
93
  hash: string;
41
94
  raw: string;
42
95
  }
43
96
 
97
+ export interface FingerprintOptions {
98
+ /** Transport character, for a caller driving the session from a transport
99
+ * it cannot see: 't' TCP (default), 'q' QUIC, 'd' DTLS. */
100
+ protocol?: 't' | 'q' | 'd';
101
+ }
102
+
103
+ /**
104
+ * Server: this connection's session ticket, from the first connection on.
105
+ * `data` is what the ticketData option put in the ticket — the one this
106
+ * connection resumed with, or, on a full handshake, the one made for the
107
+ * ticket issued to it (null until the server's Finished is sent).
108
+ * `offered && !resumed`: the client presented a ticket that was turned away
109
+ * (expired, issued before a restart, no compatible cipher).
110
+ */
111
+ export interface TicketInfo {
112
+ data: any;
113
+ resumed: boolean;
114
+ offered: boolean;
115
+ }
116
+
44
117
  export interface NegotiationResult {
45
118
  version: number | null;
46
119
  versionName: string | null;
@@ -84,21 +157,96 @@ export interface CertificateCallbackInfo {
84
157
  export interface TLSSocketOptions {
85
158
  isServer?: boolean;
86
159
  servername?: string;
87
- 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;
88
163
  minVersion?: 'TLSv1.2' | 'TLSv1.3';
89
164
  maxVersion?: 'TLSv1.2' | 'TLSv1.3';
90
165
  ALPNProtocols?: string[];
91
166
  rejectUnauthorized?: boolean;
92
- 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;
93
173
  ticketKeys?: Buffer;
174
+ /**
175
+ * Server: what to carry inside each session ticket issued. Called once per
176
+ * connection as ticketData(resumed) — `resumed` is the data of the ticket
177
+ * this connection resumed with, null on a full handshake. The return value
178
+ * (JSON-safe, at most 256 bytes serialized) rides in the ticket, encrypted
179
+ * with ticketKeys; read it back with getTicketInfo().
180
+ */
181
+ ticketData?: (resumed: any) => any;
94
182
  /**
95
183
  * Opaque serialized session from a previous 'session' event.
96
184
  * Pass this back to resume a TLS 1.2/1.3 session.
97
185
  */
98
186
  session?: Buffer | Uint8Array;
99
187
  requestCert?: boolean;
100
- cert?: Buffer | string;
101
- 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;
235
+
236
+ // TCP options, as in node:net. connect() passes them (and the rest of
237
+ // net.connect's: localAddress, localPort, family, lookup, timeout) to
238
+ // net.connect; createServer() applies noDelay/keepAlive to every
239
+ // connection it accepts. Defaults as in Node: off.
240
+ /** Disable Nagle's algorithm (TCP_NODELAY). Default: false. */
241
+ noDelay?: boolean;
242
+ /** Enable TCP keep-alive. Default: false. */
243
+ keepAlive?: boolean;
244
+ /** Milliseconds before the first keep-alive probe. Default: 0. */
245
+ keepAliveInitialDelay?: number;
246
+ localAddress?: string;
247
+ localPort?: number;
248
+ family?: number;
249
+ timeout?: number;
102
250
 
103
251
  // LemonTLS-only options
104
252
  noTickets?: boolean;
@@ -116,6 +264,27 @@ export interface TLSSocketOptions {
116
264
  customExtensions?: Array<{ type: number; data: Uint8Array }>;
117
265
  }
118
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
+
119
288
  // ======================== TLSSocket ========================
120
289
 
121
290
  export class TLSSocket extends Duplex {
@@ -124,7 +293,8 @@ export class TLSSocket extends Duplex {
124
293
  // ------- Node.js tls.TLSSocket compatible API -------
125
294
  getProtocol(): string | null;
126
295
  getCipher(): CipherInfo | null;
127
- getPeerCertificate(): PeerCertificate | null;
296
+ /** `detailed` is accepted for node:tls's signature; the leaf is described either way. */
297
+ getPeerCertificate(detailed?: boolean): PeerCertificate | null;
128
298
  /** Returns our LOCAL cert (what we presented). Empty object if none. */
129
299
  getCertificate(): PeerCertificate | {};
130
300
  /** Returns the peer cert as a native crypto.X509Certificate object (Node 15.9+). */
@@ -134,10 +304,16 @@ export class TLSSocket extends Duplex {
134
304
  isSessionReused(): boolean;
135
305
  getFinished(): Buffer | null;
136
306
  getPeerFinished(): Buffer | null;
137
- exportKeyingMaterial(length: number, label: string, context: Buffer): Buffer;
138
- 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). */
139
312
  disableRenegotiation(): void;
313
+ /** On sockets from connect() and createServer() (compat layer). */
140
314
  setServername(name: string): void;
315
+ /** The transport's bound address, as node:tls's. */
316
+ address(): { port?: number; family?: string; address?: string };
141
317
  /** Returns the opaque serialized session Buffer, or undefined. */
142
318
  getSession(): Buffer | undefined;
143
319
  /** Returns the TLS 1.2 session ticket as a Buffer, or undefined (incl. for TLS 1.3). */
@@ -179,7 +355,29 @@ export class TLSSocket extends Duplex {
179
355
  /** Returns the underlying TLSSession for low-level access. */
180
356
  session: TLSSession;
181
357
  readonly handshakeDuration: number | null;
182
- getJA3(): JA3Result | 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;
370
+ /** JA4 of the ClientHello — the published format, what fingerprint
371
+ * databases match. Changes when a client resumes (pre_shared_key). */
372
+ getJA4(opts?: FingerprintOptions): FingerprintResult | null;
373
+ /** JA5 of the ClientHello (ntop's draft): as JA4 without the extensions that
374
+ * vary per connection, plus supported_groups — the same for a client
375
+ * whether it resumed or not. */
376
+ getJA5(opts?: FingerprintOptions): FingerprintResult | null;
377
+ /** JA4S of the ServerHello. */
378
+ getJA4S(opts?: FingerprintOptions): FingerprintResult | null;
379
+ /** Server: this connection's session ticket — see TicketInfo. */
380
+ getTicketInfo(): TicketInfo;
183
381
  getSharedSecret(): Buffer | null;
184
382
  getNegotiationResult(): NegotiationResult | null;
185
383
  rekeySend(): void;
@@ -200,6 +398,13 @@ export class TLSSocket extends Duplex {
200
398
  on(event: 'certificateRequest', listener: (msg: any) => void): this;
201
399
  on(event: 'newSession', listener: (id: Buffer, data: Buffer, cb: () => void) => void): this;
202
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;
203
408
  on(event: 'error', listener: (err: Error) => void): this;
204
409
  on(event: 'close', listener: () => void): this;
205
410
  on(event: string, listener: (...args: any[]) => void): this;
@@ -218,8 +423,19 @@ export class Server extends EventEmitter {
218
423
  close(callback?: (err?: Error) => void): this;
219
424
  address(): { port: number; family: string; address: string } | string | null;
220
425
 
221
- /** Replace the server's key/cert at runtime (Let's Encrypt renewals, etc.). */
222
- 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;
223
439
  /** Returns the 48-byte TLS session ticket encryption keys. */
224
440
  getTicketKeys(): Buffer;
225
441
  /** Sets the 48-byte TLS session ticket encryption keys (for clustered deployments). */
@@ -231,6 +447,11 @@ export class Server extends EventEmitter {
231
447
  on(event: 'secureConnection', listener: (socket: TLSSocket) => void): this;
232
448
  on(event: 'tlsClientError', listener: (err: Error, socket: TLSSocket | null) => void): this;
233
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;
234
455
  on(event: 'newSession', listener: (id: Buffer, data: Buffer, cb: () => void) => void): this;
235
456
  on(event: 'resumeSession', listener: (id: Buffer, cb: (err: Error | null, data: Buffer | null) => void) => void): this;
236
457
  on(event: 'error', listener: (err: Error) => void): this;
@@ -246,6 +467,8 @@ export interface TLSSessionOptions {
246
467
  ALPNProtocols?: string[];
247
468
  SNICallback?: (servername: string, cb: (err: Error | null, ctx: SecureContext | null) => void) => void;
248
469
  ticketKeys?: Buffer;
470
+ /** See TLSSocketOptions.ticketData. */
471
+ ticketData?: (resumed: any) => any;
249
472
  session?: Buffer | Uint8Array | SessionTicketData;
250
473
  psk?: any;
251
474
  rejectUnauthorized?: boolean;
@@ -295,6 +518,8 @@ export class TLSSession extends EventEmitter {
295
518
  getCipher(): number | null;
296
519
  getALPN(): string | null;
297
520
  getPeerCertificate(): Array<{ cert: Buffer }> | null;
521
+ /** Our own certificate chain — the one this connection presents. */
522
+ getCertificate(): Array<{ cert: Buffer }> | null;
298
523
  getTrafficSecrets(): TrafficSecrets;
299
524
  getHandshakeSecrets(): HandshakeSecrets;
300
525
  exportKeyingMaterial(length: number, label: string, context: Uint8Array): Uint8Array;
@@ -302,7 +527,11 @@ export class TLSSession extends EventEmitter {
302
527
  getPeerFinished(): Buffer | null;
303
528
  getSharedSecret(): Buffer | null;
304
529
  getNegotiationResult(): NegotiationResult;
305
- getJA3(): JA3Result | null;
530
+ getJA4(opts?: FingerprintOptions): FingerprintResult | null;
531
+ getJA5(opts?: FingerprintOptions): FingerprintResult | null;
532
+ getJA4S(opts?: FingerprintOptions): FingerprintResult | null;
533
+ /** Server: this connection's session ticket — see TicketInfo. */
534
+ getTicketInfo(): TicketInfo;
306
535
  requestKeyUpdate(requestPeer?: boolean): void;
307
536
 
308
537
  // Events
@@ -325,19 +554,57 @@ export class TLSSession extends EventEmitter {
325
554
 
326
555
  // ======================== Module Functions ========================
327
556
 
328
- export function createSecureContext(options: { key: Buffer | string; cert: Buffer | string }): SecureContext;
557
+ export function createSecureContext(options: SecureContextOptions): SecureContext;
329
558
 
330
- 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;
331
560
  export function connect(port: number, host: string, callback?: () => void): TLSSocket;
332
- export function connect(options: TLSSocketOptions & { port: number; host?: string }, callback?: () => void): TLSSocket;
333
-
334
- export function createServer(
335
- options: TLSSocketOptions & { key?: Buffer | string; cert?: Buffer | string },
336
- connectionListener?: (socket: TLSSocket) => void
337
- ): 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;
338
592
 
339
593
  export function getCiphers(): string[];
340
594
 
595
+ /** Node-compat: the shipped CA bundle (PEM strings) — tls.rootCertificates. */
596
+ export const rootCertificates: readonly string[];
597
+ /**
598
+ * Node-compat: tls.getCACertificates([type]). 'default' and 'bundled' return
599
+ * the CA set clients verify against when given no `ca` (the shipped bundle,
600
+ * or what setDefaultCACertificates put in its place); 'extra' and 'system'
601
+ * are not sourced here and return [].
602
+ */
603
+ export function getCACertificates(type?: 'default' | 'bundled' | 'extra' | 'system'): string[];
604
+ /** Node-compat: tls.setDefaultCACertificates(certs) — replaces that set for
605
+ * every later connection that passes no `ca`. Throws if not an array. */
606
+ export function setDefaultCACertificates(certs: string[]): void;
607
+
341
608
  /**
342
609
  * Validates that the peer certificate matches the hostname (RFC 6125).
343
610
  * Returns undefined on success, or an Error on mismatch.
@@ -363,6 +630,24 @@ export declare const crypto: {
363
630
  hkdf_expand: (hash: string, prk: Uint8Array, info: Uint8Array, length: number) => Uint8Array;
364
631
  hkdf_expand_label: (hash: string, secret: Uint8Array, label: string, context: Uint8Array, length: number) => Uint8Array;
365
632
  hmac: (hash: string, key: Uint8Array, data: Uint8Array) => Uint8Array;
633
+ /** TLS 1.2 PRF (RFC 5246 §5). */
634
+ tls12_prf: (secret: Uint8Array, label: string, seed: Uint8Array, length: number, hash: string) => Uint8Array;
635
+ /** RFC 5705 exporter over a TLS 1.2 master secret. */
636
+ tls12_exporter: (hash: string, masterSecret: Uint8Array, label: string, clientRandom: Uint8Array,
637
+ serverRandom: Uint8Array, context: Uint8Array | null, length: number) => Uint8Array;
638
+ /** RFC 8446 §7.5 exporter; labelPrefix defaults to TLS 1.3's ('tls13 '). */
639
+ tls13_exporter: (hash: string, exporterMasterSecret: Uint8Array, label: string,
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
+ };
366
651
  };
367
652
 
368
653
  export declare const wire: any;
@@ -398,6 +683,9 @@ declare const _default: {
398
683
  createServer: typeof createServer;
399
684
  getCiphers: typeof getCiphers;
400
685
  checkServerIdentity: typeof checkServerIdentity;
686
+ getCACertificates: typeof getCACertificates;
687
+ setDefaultCACertificates: typeof setDefaultCACertificates;
688
+ rootCertificates: typeof rootCertificates;
401
689
  DEFAULT_MIN_VERSION: string;
402
690
  DEFAULT_MAX_VERSION: string;
403
691
  DEFAULT_CIPHERS: string;
@@ -405,10 +693,22 @@ declare const _default: {
405
693
  crypto: typeof crypto;
406
694
  wire: typeof wire;
407
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;
408
708
  DTLSSession: typeof DTLSSession;
409
709
  DTLSSocket: typeof DTLSSocket;
410
710
  createDTLSServer: typeof createDTLSServer;
411
711
  connectDTLS: typeof connectDTLS;
412
712
  };
413
713
 
414
- export default _default;
714
+ export default _default;
package/index.js CHANGED
@@ -21,11 +21,74 @@ import {
21
21
  Server,
22
22
  getCiphers,
23
23
  checkServerIdentity,
24
+ getCACertificates,
25
+ setDefaultCACertificates,
24
26
  DEFAULT_MIN_VERSION,
25
27
  DEFAULT_MAX_VERSION,
26
28
  DEFAULT_CIPHERS,
27
29
  DEFAULT_ECDH_CURVE,
28
30
  } from './src/compat.js';
31
+ import { parseCiphers, parseEcdhCurve, parseSigalgs, parseTlsVersion } from './src/node_options.js';
32
+ import { certificateObject } from './src/utils.js';
33
+ import { parse_cert_compression, precompress_certificate_body, supported_cert_compression } from './src/session/cert_compression.js';
34
+ import { build_tls_message } from './src/session/message.js';
35
+ import { client_session_info } from './src/session/ticket.js';
36
+
37
+ /** A saved session ('session' event's Buffer) described without its secrets:
38
+ * { version, servername, alpn, created, expiresAt, earlyData }, or null. */
39
+ function sessionInfo(blob) { return client_session_info(blob); }
40
+ import { SERVER_DEFAULT_GROUPS } from './src/session/ecdh.js';
41
+ import { default_signature_schemes } from './src/session/signing.js';
42
+
43
+ /** The signature schemes offered by default (signature_algorithms), in
44
+ * order, as code points: the modern ones, ML-DSA where this Node.js has it,
45
+ * then the legacy ones (accepted on certificates, never used in a TLS 1.3
46
+ * CertificateVerify). A copy. */
47
+ const DEFAULT_SIGNATURE_SCHEMES = Object.freeze(default_signature_schemes());
48
+
49
+ /** The key-exchange groups a server offers by default, in its preference:
50
+ * every post-quantum group this Node.js has (the hybrids, then ML-KEM
51
+ * alone), then X25519, P-256, P-384 — as code points. A copy. */
52
+ const DEFAULT_SERVER_GROUPS = Object.freeze(SERVER_DEFAULT_GROUPS.slice());
53
+
54
+ /** certificateCompression (RFC 8879): ['brotli', 'zlib', 'zstd'] in order,
55
+ * true (all this build has) or false (none) → algorithm codes; null when
56
+ * unset (the default). Throws on a name it does not know, so a server can
57
+ * check the option when it is made rather than at the first handshake. */
58
+ function parseCertificateCompression(value) { return parse_cert_compression(value); }
59
+
60
+ /** A server's certificate chain compressed now (RFC 8879), in the
61
+ * background, for each algorithm it may be sent in — `algorithms` as the
62
+ * certificateCompression option takes them (unset: all this build has) —
63
+ * so the first TLS 1.3 handshake to send it does not compress it while
64
+ * its client waits. `secureContext`: from createSecureContext(). Optional:
65
+ * without it the first handshake compresses, and caches, as before.
66
+ * callback() when done. */
67
+ function precompressCertificate(secureContext, algorithms, callback) {
68
+ if (typeof algorithms === 'function') { callback = algorithms; algorithms = undefined; }
69
+ const algs = parse_cert_compression(algorithms) || supported_cert_compression();
70
+ // Its chain, and those of its other key/certificate pairs (key/cert arrays).
71
+ const chains = secureContext ? [secureContext.certificateChain].concat((secureContext.alternates || []).map((a) => a.certificateChain)) : [];
72
+ let left = 1;
73
+ const done = () => { if (--left === 0 && callback) callback(); };
74
+ for (const chain of chains) {
75
+ if (!Array.isArray(chain) || chain.length === 0 || algs.length === 0) continue;
76
+ // The Certificate message's body as a TLS 1.3 server sends it: an empty
77
+ // request context, each entry without extensions (an OCSP response
78
+ // stapled to the leaf makes another body, compressed when it is sent).
79
+ const body = build_tls_message({ type: 'certificate', version: 0x0304, entries: chain, certificate_request_context: new Uint8Array(0) }).subarray(4);
80
+ left++;
81
+ precompress_certificate_body(body, algs, done);
82
+ }
83
+ process.nextTick(done);
84
+ }
85
+ import { generate_ech_key, prepare_ech_server } from './src/session/ech.js';
86
+ import { hpke_suite, setup_sender, setup_recipient, hpke_seal, hpke_open,
87
+ hpke_generate_key_pair, hpke_derive_key_pair } from './src/session/hpke.js';
88
+ import nodeTls from 'node:tls';
89
+
90
+ /** Node-compat: the shipped CA bundle, same array Node exposes. */
91
+ const rootCertificates = nodeTls.rootCertificates;
29
92
 
30
93
  // DTLS
31
94
  import DTLSSession from './src/dtls_session.js';
@@ -53,8 +116,40 @@ var crypto = {
53
116
  tls12_prf,
54
117
  tls12_exporter,
55
118
  tls13_exporter,
119
+ // HPKE (RFC 9180) — what ECH encrypts with; also the base of Oblivious
120
+ // HTTP and MLS.
121
+ hpke: {
122
+ suite: hpke_suite,
123
+ setupSender: setup_sender,
124
+ setupRecipient: setup_recipient,
125
+ seal: hpke_seal,
126
+ open: hpke_open,
127
+ generateKeyPair: hpke_generate_key_pair,
128
+ deriveKeyPair: hpke_derive_key_pair,
129
+ },
56
130
  };
57
131
 
132
+ /**
133
+ * A new Encrypted Client Hello key (RFC 9849) for a server:
134
+ * { config, privateKey, configList, configListBase64 }.
135
+ * Pass { keys: [{ config, privateKey }] } as the server's `ech` option and
136
+ * publish configListBase64 as `ech=` in the domain's HTTPS record.
137
+ */
138
+ function generateEchKey(options) {
139
+ return generate_ech_key(options);
140
+ }
141
+
142
+ /**
143
+ * Check a server's `ech` option — { keys: [{ config, privateKey, retry }] }
144
+ * — and throw, saying what is wrong, if it would not work: a config that
145
+ * does not parse, a private key that is not its config's. For a server
146
+ * built on TLSSession directly, which would otherwise learn of it on the
147
+ * first connection. createServer checks by itself.
148
+ */
149
+ function checkEchKeys(ech) {
150
+ prepare_ech_server(ech);
151
+ }
152
+
58
153
  export {
59
154
  TLSSocket,
60
155
  TLSSession,
@@ -64,6 +159,9 @@ export {
64
159
  Server,
65
160
  getCiphers,
66
161
  checkServerIdentity,
162
+ getCACertificates,
163
+ setDefaultCACertificates,
164
+ rootCertificates,
67
165
  DEFAULT_MIN_VERSION,
68
166
  DEFAULT_MAX_VERSION,
69
167
  DEFAULT_CIPHERS,
@@ -71,6 +169,19 @@ export {
71
169
  crypto,
72
170
  wire,
73
171
  record,
172
+ // node:tls option strings → code lists (node_options.js)
173
+ parseCiphers,
174
+ parseEcdhCurve,
175
+ parseSigalgs,
176
+ parseTlsVersion,
177
+ parseCertificateCompression,
178
+ precompressCertificate,
179
+ DEFAULT_SERVER_GROUPS,
180
+ DEFAULT_SIGNATURE_SCHEMES,
181
+ sessionInfo,
182
+ certificateObject,
183
+ generateEchKey,
184
+ checkEchKeys,
74
185
 
75
186
  // DTLS
76
187
  DTLSSession,
@@ -91,6 +202,9 @@ export default {
91
202
  Server,
92
203
  getCiphers,
93
204
  checkServerIdentity,
205
+ getCACertificates,
206
+ setDefaultCACertificates,
207
+ rootCertificates,
94
208
  DEFAULT_MIN_VERSION,
95
209
  DEFAULT_MAX_VERSION,
96
210
  DEFAULT_CIPHERS,
@@ -98,6 +212,18 @@ export default {
98
212
  crypto,
99
213
  wire,
100
214
  record,
215
+ parseCiphers,
216
+ parseEcdhCurve,
217
+ parseSigalgs,
218
+ parseTlsVersion,
219
+ parseCertificateCompression,
220
+ precompressCertificate,
221
+ DEFAULT_SERVER_GROUPS,
222
+ DEFAULT_SIGNATURE_SCHEMES,
223
+ sessionInfo,
224
+ certificateObject,
225
+ generateEchKey,
226
+ checkEchKeys,
101
227
 
102
228
  // DTLS
103
229
  DTLSSession,