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 +5 -0
- package/index.cjs +9 -7
- package/index.d.ts +229 -17
- package/package.json +39 -6
- package/src/compat.js +12 -4
- package/src/dtls_session.js +79 -11
- package/src/record.js +120 -52
- package/src/secure_context.js +14 -0
- package/src/tls_session.js +12 -12
- package/types/crypto.d.ts +40 -0
- package/types/record.d.ts +40 -0
- package/types/session.d.ts +3 -0
- package/types/wire.d.ts +62 -0
package/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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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?:
|
|
132
|
-
key?:
|
|
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
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
278
|
-
|
|
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:
|
|
557
|
+
export function createSecureContext(options: SecureContextOptions): SecureContext;
|
|
393
558
|
|
|
394
|
-
export function connect(port: number, host: string, options?:
|
|
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:
|
|
397
|
-
|
|
398
|
-
export function createServer(
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "TLS 1.3/1.2 DTLS 1.3/1.2 implementation for Node.js - full control over cryptographic keys, record layer, and handshake. Drop-in replacement for node:tls with advanced options impossible in OpenSSL.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"types": "index.d.ts",
|
|
8
|
+
"typesVersions": {
|
|
9
|
+
"*": {
|
|
10
|
+
"wire": [
|
|
11
|
+
"types/wire.d.ts"
|
|
12
|
+
],
|
|
13
|
+
"crypto": [
|
|
14
|
+
"types/crypto.d.ts"
|
|
15
|
+
],
|
|
16
|
+
"record": [
|
|
17
|
+
"types/record.d.ts"
|
|
18
|
+
],
|
|
19
|
+
"session": [
|
|
20
|
+
"types/session.d.ts"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
8
24
|
"exports": {
|
|
9
25
|
".": {
|
|
10
26
|
"types": "./index.d.ts",
|
|
@@ -12,22 +28,35 @@
|
|
|
12
28
|
"require": "./index.cjs",
|
|
13
29
|
"default": "./index.js"
|
|
14
30
|
},
|
|
15
|
-
"./wire":
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
31
|
+
"./wire": {
|
|
32
|
+
"types": "./types/wire.d.ts",
|
|
33
|
+
"default": "./src/wire.js"
|
|
34
|
+
},
|
|
35
|
+
"./crypto": {
|
|
36
|
+
"types": "./types/crypto.d.ts",
|
|
37
|
+
"default": "./src/crypto.js"
|
|
38
|
+
},
|
|
39
|
+
"./record": {
|
|
40
|
+
"types": "./types/record.d.ts",
|
|
41
|
+
"default": "./src/record.js"
|
|
42
|
+
},
|
|
43
|
+
"./session": {
|
|
44
|
+
"types": "./types/session.d.ts",
|
|
45
|
+
"default": "./src/tls_session.js"
|
|
46
|
+
}
|
|
19
47
|
},
|
|
20
48
|
"files": [
|
|
21
49
|
"index.js",
|
|
22
50
|
"index.cjs",
|
|
23
51
|
"index.d.ts",
|
|
52
|
+
"types/",
|
|
24
53
|
"src/",
|
|
25
54
|
"README.md",
|
|
26
55
|
"LICENSE",
|
|
27
56
|
"lemontls.svg"
|
|
28
57
|
],
|
|
29
58
|
"engines": {
|
|
30
|
-
"node": ">=
|
|
59
|
+
"node": ">=24.0.0"
|
|
31
60
|
},
|
|
32
61
|
"keywords": [
|
|
33
62
|
"tls",
|
|
@@ -70,6 +99,10 @@
|
|
|
70
99
|
"alpn",
|
|
71
100
|
"ja3",
|
|
72
101
|
"ja3-fingerprint",
|
|
102
|
+
"ja4",
|
|
103
|
+
"ja4-fingerprint",
|
|
104
|
+
"ja5",
|
|
105
|
+
"ja5-fingerprint",
|
|
73
106
|
"fingerprint",
|
|
74
107
|
"keylog",
|
|
75
108
|
"wireshark",
|
package/src/compat.js
CHANGED
|
@@ -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)
|
|
637
|
-
|
|
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
|
/**
|
package/src/dtls_session.js
CHANGED
|
@@ -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
|
-
|
|
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 === '
|
|
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
|
-
|
|
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 === '
|
|
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
|
-
//
|
|
1650
|
-
//
|
|
1651
|
-
//
|
|
1652
|
-
//
|
|
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
|
-
|
|
1704
|
-
|
|
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.
|
|
789
|
-
let tag = fragment.
|
|
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.
|
|
806
|
-
let tag = fragment.
|
|
807
|
-
let ct = fragment.
|
|
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
|
-
|
|
823
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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] =
|
|
944
|
-
aad[2] =
|
|
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:
|
|
955
|
-
let
|
|
956
|
-
|
|
957
|
-
let
|
|
958
|
-
|
|
959
|
-
let
|
|
960
|
-
|
|
961
|
-
|
|
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,
|
|
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
|
-
|
|
976
|
-
|
|
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(
|
|
1027
|
+
' seqPlain=' + _h(rn0) + _h(rn1) +
|
|
983
1028
|
' seqMasked=' + _h(encRn[0]) + _h(encRn[1]) +
|
|
984
1029
|
' declaredLen=' + ciphertext.length +
|
|
985
|
-
' aadLen=' +
|
|
1030
|
+
' aadLen=' + aad.length +
|
|
986
1031
|
' snKey=' + (keys.snKey ? 'set' : 'MISSING'));
|
|
987
|
-
}
|
|
988
|
-
}
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
1190
|
-
|
|
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 =
|
|
1194
|
-
if (back >=
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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 =
|
|
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(
|
|
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,
|
package/src/secure_context.js
CHANGED
|
@@ -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
|
|
package/src/tls_session.js
CHANGED
|
@@ -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;
|
package/types/wire.d.ts
ADDED
|
@@ -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;
|