lemon-tls 0.10.0 โ†’ 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,16 +18,20 @@
18
18
 
19
19
  ## โœจ Features
20
20
 
21
- * ๐Ÿ”’ **Pure JavaScript** โ€“ no OpenSSL, no native bindings. Zero dependencies.
21
+ * ๐Ÿ”’ **Pure JavaScript** โ€“ no OpenSSL, no native bindings. The cryptography is `node:crypto`'s; certificates and keys are read by `cryptographic-formats`, its one dependency.
22
22
  * โšก **TLS 1.3 (RFC 8446)** + **TLS 1.2** โ€“ both server and client.
23
23
  * ๐Ÿ“ก **DTLS 1.3 (RFC 9147)** + **DTLS 1.2** โ€“ server and client, with cookie exchange, retransmission, and **DTLS-SRTP** (RFC 5764) for WebRTC. Not a shim over TLS: the record layer, epochs, and sequence-number handling are implemented for datagrams.
24
24
  * ๐ŸŒ **Browser-tested** โ€“ verified interop with Chrome, curl, Node.js, openssl s_client, and msquic.
25
25
  * ๐Ÿ”‘ **Key Access** โ€“ read handshake secrets, traffic keys, ECDHE shared secret, and resumption data at any point.
26
26
  * ๐Ÿ” **Session Resumption** โ€“ session tickets + PSK with binder validation, with application data carried inside the ticket (`ticketData`, `getTicketInfo()`).
27
27
  * ๐Ÿ”„ **Key Update** โ€“ refresh traffic keys on long-lived TLS 1.3 connections.
28
- * ๐Ÿ”ƒ **HelloRetryRequest** โ€“ automatic group negotiation fallback (X25519, P-256, P-384).
28
+ * ๐Ÿ”ƒ **HelloRetryRequest** โ€“ automatic group negotiation fallback (X25519, P-256, P-384; a server takes X448 and P-521 too).
29
+ * ๐Ÿ— **External PSK (RFC 8446 ยง2.2)** โ€“ a key both sides hold from elsewhere authenticates the handshake, no certificates โ€” TLS 1.3 and DTLS 1.3, `node:tls`'s `pskCallback` or a `psk` option. Interop-tested with OpenSSL and `node:tls` both ways.
30
+ * ๐Ÿ“ฎ **DTLS Connection ID (RFC 9146, RFC 9147)** โ€“ a DTLS connection survives the peer's address changing (a NAT rebinding): `createDTLSServer` finds it by its CID.
31
+ * ๐Ÿ“ **record_size_limit (RFC 8449)** โ€“ for constrained peers: what they take is what they are sent.
32
+ * ๐Ÿชช **Several certificates per server** โ€“ `key` / `cert` arrays, as `node:tls`: an RSA and an ECDSA one, say โ€” per handshake, the one the client can verify (its signature algorithms, and in TLS 1.2 its suites and curves).
29
33
  * ๐Ÿ›ฐ **Post-quantum key exchange** โ€“ the hybrid **X25519MLKEM768** group, as Chrome, Firefox, Safari and Cloudflare negotiate it, wherever `node:crypto` has ML-KEM (Node 24.7+).
30
- * ๐Ÿ•ถ **Encrypted Client Hello (RFC 9849)** โ€“ server *and* client. The real ClientHello โ€” the site's name, the ALPN list โ€” goes encrypted with HPKE to a key published in DNS. Key generation, rotation, `retry_configs`, HelloRetryRequest, resumption inside ECH, and GREASE ECH. Tested against Go's `crypto/tls` in both directions.
34
+ * ๐Ÿ•ถ **Encrypted Client Hello (RFC 9849)** โ€“ server *and* client, over TLS 1.3 and DTLS 1.3. The real ClientHello โ€” the site's name, the ALPN list โ€” goes encrypted with HPKE to a key published in DNS. Key generation, rotation, `retry_configs`, HelloRetryRequest, resumption inside ECH, and GREASE ECH. Tested against Go's `crypto/tls` in both directions.
31
35
  * ๐Ÿ—œ **Certificate compression (RFC 8879)** โ€“ brotli, zlib and zstd, both directions; a public CA's chain often shrinks by half โ€” over QUIC that can save a round trip.
32
36
  * ๐Ÿ“„ **OCSP stapling** โ€“ the server staples (`'OCSPRequest'`, as `node:tls`), the client asks for it (`requestOCSP`, `'OCSPResponse'`).
33
37
  * ๐Ÿ“œ **Client Certificate Auth** โ€“ mutual TLS (mTLS) with `requestCert` / `cert` / `key` options.
@@ -109,6 +113,37 @@ const socket2 = tls.connect(8443, 'localhost', { session: savedSession }, () =>
109
113
  });
110
114
  ```
111
115
 
116
+ ### External PSK
117
+
118
+ A key both sides hold from elsewhere (provisioned, derived from a pairing),
119
+ in place of certificates โ€” TLS 1.3 and DTLS 1.3, with a fresh ECDHE secret
120
+ mixed in (psk_dhe_ke). The peer that proves it is authenticated by it:
121
+ `authorized` is `true` with no certificate at all. As in `node:tls`:
122
+
123
+ ```js
124
+ const KEYS = { 'device-17': Buffer.from('โ€ฆ32 bytesโ€ฆ', 'hex') };
125
+
126
+ const server = tls.createServer({
127
+ key, cert, // optional: for clients without a PSK
128
+ pskCallback: (socket, identity) => KEYS[identity] || null,
129
+ }, (socket) => console.log(socket.getPskIdentity().toString())); // 'device-17'
130
+
131
+ tls.connect({ host, port, pskCallback: () => ({ identity: 'device-17', psk: KEYS['device-17'] }) });
132
+ // or, without a callback: psk: { identity: 'device-17', psk: KEYS['device-17'] }
133
+ ```
134
+
135
+ SHA-256 unless said otherwise (`hash: 'sha384'`, on both sides โ€” the server
136
+ answers with `{ psk, hash }`). An identity the server does not know gives a
137
+ full handshake with its certificate, checked as usual. The PSK is TLS 1.3's:
138
+ a client offering only TLS 1.2 fails at once, saying so (TLS 1.2 PSK suites
139
+ are not carried). A server with no certificate takes PSK clients only โ€” any
140
+ other gets `handshake_failure`. A client given both `psk` and a saved
141
+ `session` offers the PSK, its credential; the server, offered several
142
+ identities (OpenSSL lists a ticket and a PSK), takes the first it knows.
143
+ DTLS 1.3 the same: `createDTLSServer` / `connectDTLS` take `pskCallback` as
144
+ above, `DTLSSession` as `TLSSession` does (`pskCallback(identity)`, the
145
+ identity as bytes).
146
+
112
147
  ### Mutual TLS (Client Certificate)
113
148
 
114
149
  ```js
@@ -284,13 +319,14 @@ High-level wrapper extending `stream.Duplex`, API-compatible with Node.js [`tls.
284
319
  | `minVersion` | string | `'TLSv1.2'` or `'TLSv1.3'` |
285
320
  | `maxVersion` | string | `'TLSv1.2'` or `'TLSv1.3'` |
286
321
  | `ALPNProtocols` | string[] | Offered ALPN protocols |
287
- | `rejectUnauthorized` | boolean | Validate peer certificate (default: `true`) |
322
+ | `rejectUnauthorized` | boolean | Validate peer certificate (default: `true`): the chain to a trust anchor, each certificate's validity, and what each may do (RFC 5280: basicConstraints and pathLenConstraint, keyUsage, extendedKeyUsage โ€” serverAuth for a server, clientAuth for a client โ€” name constraints โ€” on the subject, its emailAddress, every subjectAltName, and a leaf's common name when it has no DNS name, as OpenSSL โ€” critical extensions), no signature by MD5 or SHA-1 below the anchor (`CA_MD_TOO_WEAK`, as node:tls), then the name (subjectAltName only: no common-name fallback, no partial wildcards). No trust anchors: not authorized |
288
323
  | `ca` | Buffer/string | CA certificate(s) for validation |
289
324
  | `ticketKeys` | Buffer | 48-byte key for session ticket encryption (server) |
290
325
  | `session` | Buffer | Saved session blob from `'session'` event (client resumption) |
291
326
  | `requestCert` | boolean | Request client certificate (server) |
292
- | `cert` | Buffer/string | Client certificate PEM (for mTLS) |
293
- | `key` | Buffer/string | Client private key PEM (for mTLS) |
327
+ | `cert` | Buffer/string | Certificate PEM โ€” a client's for mTLS; a server's, or several as an array |
328
+ | `key` | Buffer/string | Private key PEM โ€” with several certificates, an array, each key paired with the chain of its public key; per handshake the server presents the one the client can verify |
329
+ | `pskCallback` | function | External PSK (TLS 1.3). **Client:** `(hint) => ({ psk, identity })` or `null` โ€” `hint` is always `null` (TLS 1.3 has none). **Server:** `(socket, identity) => psk` (or `{ psk, hash }`), `null` for an identity it does not know |
294
330
  | `ciphers`, `ecdhCurve`, `sigalgs` | string | As `node:tls` takes them (`createServer` / `connect`); a value naming nothing usable throws. `sigalgs` limits what the endpoint signs with, as well as what it offers and accepts |
295
331
  | `requestOCSP` | boolean | Client: ask for the server's stapled OCSP response; it comes as `'OCSPResponse'` |
296
332
  | `sessionTimeout` | number | Server: seconds a session stays good โ€” TLS 1.2 Session IDs, and the ticket lifetime unless `ticketLifetime` is given |
@@ -300,16 +336,18 @@ High-level wrapper extending `stream.Duplex`, API-compatible with Node.js [`tls.
300
336
  | Option | Type | Description |
301
337
  |---|---|---|
302
338
  | `sessionTickets` | boolean | Enable/disable session tickets (default: `true`) |
303
- | `signatureAlgorithms` | number[] | Per-connection sigalg list, e.g. `[0x0804]` for RSA-PSS only |
339
+ | `signatureAlgorithms` | number[] | Per-connection sigalg list, e.g. `[0x0804]` for RSA-PSS only. No SHA-1 scheme is offered, signed with or accepted for a handshake signature, at any version (RFC 9155): a list naming `0x0201` or `0x0203` throws, and a TLS 1.2 client that offers only SHA-1 โ€” or no `signature_algorithms` at all โ€” gets `handshake_failure` |
304
340
  | `groups` | number[] | Per-connection curves, e.g. `[0x001d]` for X25519 only |
305
341
  | `prioritizeChaCha` | boolean | Move ChaCha20-Poly1305 before AES in cipher preference |
306
- | `maxRecordSize` | number | Max plaintext per TLS record (default: 16384) |
307
- | `allowedCipherSuites` | number[] | Whitelist - only these ciphers are offered |
342
+ | `maxRecordSize` | number | Max plaintext per record of application data (default: 16384). Handshake messages always go in records of at most 2^14 bytes |
343
+ | `recordSizeLimit` | number / false | RFC 8449: the largest record this side takes โ€” 64 to 16385 (TLS 1.3 counts the type byte). A client sends it; a server answers a client's with this or the maximum. Records sent are cut to the peer's limit, a larger one received ends the connection (`record_overflow`). `false`: never sent nor answered |
344
+ | `psk` | object | Client: an external PSK without a callback โ€” `{ identity, psk, hash? }` (`hash`: `'sha256'`, the default, or `'sha384'`) |
345
+ | `allowedCipherSuites` | number[] | Only these suites, in this order of preference (see Supported cipher suites) |
308
346
  | `pins` | string[] | Certificate pinning: `['sha256/AAAA...']` |
309
347
  | `handshakeTimeout` | number | Abort handshake after N ms. `createServer`: `120000` by default, as in Node |
310
348
  | `maxHandshakeSize` | number | Max handshake bytes - DoS protection |
311
349
  | `certificateCallback` | function | Dynamic cert selection: `(info, cb) => cb(null, ctx)` |
312
- | `ech` | object / Buffer / string | Encrypted Client Hello (RFC 9849). **Server:** `{ keys: [{ config, privateKey, retry }] }` โ€” from `generateEchKey()`; `retry: false` keeps a key for decrypting without offering it in `retry_configs`. **Client:** the server's ECHConfigList, bytes or base64 (the `ech=` of its HTTPS record); the hello then offers TLS 1.3 only. A malformed list, or one with no config this client can use, throws |
350
+ | `ech` | object / Buffer / string | Encrypted Client Hello (RFC 9849). **Server:** `{ keys: [{ config, privateKey, retry }] }` โ€” from `generateEchKey()`; `retry: false` keeps a key for decrypting without offering it in `retry_configs`. **Client:** the server's ECHConfigList, bytes or base64 (the `ech=` of its HTTPS record); the hello then offers TLS 1.3 only (DTLS 1.3 over DTLS). A malformed list, or one with no config this client can use, throws โ€” fail closed: never a connection with the name in the clear |
313
351
  | `echGrease` | boolean | Client, without `ech`: send GREASE ECH (RFC 9849 ยง6.2), as browsers do (default `false`) |
314
352
  | `certificateCompression` | boolean / string[] | RFC 8879: `['brotli', 'zlib', 'zstd']`, `true` (all this Node has) or `false`. Server: what it compresses its chain with, for a client that offers it (default: all). Client: what it offers (default: none) |
315
353
  | `ticketLifetime` | number | Server: seconds a TLS 1.3 ticket is good for (default `7200`, capped at 7 days, RFC 8446 ยง4.6.1) |
@@ -324,12 +362,13 @@ certificate is asked for again.
324
362
 
325
363
  ### Supported cipher suites
326
364
 
327
- Sixteen suites are implemented; three are offered by default. Anything else has
328
- to be asked for explicitly via `local_supported_cipher_suites` or narrowed with
329
- `allowedCipherSuites` โ€” the default list stays small on purpose, because every
330
- extra suite widens the offer and changes your JA3/JA4.
365
+ Nine suites, all AEAD with forward secrecy: the three of TLS 1.3, and six of
366
+ TLS 1.2 with ECDHE. All are offered by default (the TLS 1.2 ones when TLS 1.2
367
+ is enabled). `allowedCipherSuites` (DTLS: `cipherSuites`) or `ciphers`
368
+ narrows the list; a list with none of these suites in it is refused where it
369
+ is given ("no cipher match"), as node:tls refuses `ciphers`.
331
370
 
332
- **TLS 1.3** โ€” offered by default
371
+ **TLS 1.3**
333
372
 
334
373
  | Code | Suite | AEAD | Hash |
335
374
  |---|---|---|---|
@@ -337,7 +376,7 @@ extra suite widens the offer and changes your JA3/JA4.
337
376
  | `0x1302` | `TLS_AES_256_GCM_SHA384` | AES-256-GCM | SHA-384 |
338
377
  | `0x1303` | `TLS_CHACHA20_POLY1305_SHA256` | ChaCha20-Poly1305 | SHA-256 |
339
378
 
340
- **TLS 1.2 โ€” AEAD** (opt in)
379
+ **TLS 1.2**
341
380
 
342
381
  | Code | Suite | Key exchange | AEAD |
343
382
  |---|---|---|---|
@@ -347,29 +386,22 @@ extra suite widens the offer and changes your JA3/JA4.
347
386
  | `0xC030` | `TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384` | ECDHE_RSA | AES-256-GCM |
348
387
  | `0xCCA8` | `TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256` | ECDHE_RSA | ChaCha20-Poly1305 |
349
388
  | `0xCCA9` | `TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256` | ECDHE_ECDSA | ChaCha20-Poly1305 |
350
- | `0xCCAA` | `TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256` | DHE_RSA | ChaCha20-Poly1305 |
351
- | `0x009C` | `TLS_RSA_WITH_AES_128_GCM_SHA256` | RSA | AES-128-GCM |
352
- | `0x009D` | `TLS_RSA_WITH_AES_256_GCM_SHA384` | RSA | AES-256-GCM |
353
-
354
- **TLS 1.2 โ€” CBC** (opt in; legacy, no forward secrecy on the RSA ones)
355
-
356
- | Code | Suite | Key exchange | Cipher | MAC |
357
- |---|---|---|---|---|
358
- | `0xC013` | `TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA` | ECDHE_RSA | AES-128-CBC | SHA-1 |
359
- | `0xC014` | `TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA` | ECDHE_RSA | AES-256-CBC | SHA-1 |
360
- | `0x003C` | `TLS_RSA_WITH_AES_128_CBC_SHA256` | RSA | AES-128-CBC | SHA-256 |
361
- | `0x003D` | `TLS_RSA_WITH_AES_256_CBC_SHA256` | RSA | AES-256-CBC | SHA-256 |
362
389
 
363
- The `0xCCAA` / `0x009C` / `0x009D` suites and the CBC block carry no forward
364
- secrecy with an RSA key exchange; they exist for interoperability with older
365
- peers, not as a recommendation.
390
+ RSA key transport (`0x009C`, `0x009D`), DHE (`0xCCAA`) and CBC (`0xC013`,
391
+ `0xC014`, `0x003C`, `0x003D`) are not supported: no forward secrecy, and
392
+ attacks of their own (Marvin, Lucky13). A client offering only those gets a
393
+ `handshake_failure` alert.
366
394
 
367
395
  ChaCha20-Poly1305 uses a full 12-byte IV with no explicit nonce (RFC 7905),
368
396
  unlike GCM โ€” the registry records that per suite.
369
397
 
370
- `prioritizeChaCha: true` moves the ChaCha suites ahead of AES. Note this
371
- changes your **JA3** but not your **JA4**: JA4 sorts the cipher list before
372
- hashing, so only *which* suites you offer matters there, never their order.
398
+ `allowedCipherSuites` (or `ciphers`) is also the **preference order**: the
399
+ suites are offered, and โ€” with `honorCipherOrder: true` on a server โ€” chosen,
400
+ in the order given, as node:tls takes `ciphers`. Without `honorCipherOrder` a
401
+ server takes the client's order (node:tls's default). `prioritizeChaCha: true`
402
+ moves the ChaCha suites ahead of AES. Neither changes your **JA4**: JA4 sorts
403
+ the cipher list before hashing, so only *which* suites you offer matters there,
404
+ never their order.
373
405
 
374
406
 
375
407
 
@@ -432,16 +464,18 @@ hashing, so only *which* suites you offer matters there, never their order.
432
464
  | `socket.isResumed` | Alias for `isSessionReused()` |
433
465
  | `socket.handshakeDuration` | Handshake time in ms |
434
466
  | `socket.echStatus` | Encrypted Client Hello. Server: `null` (not offered), `'accepted'`, `'rejected'` (the handshake ran as the public name and `retry_configs` went out). Client: `null` (no ECH), `'grease'`, `'accepted'`, `'rejected'` |
467
+ | `socket.echBackend` | Server, ECH split mode (RFC 9849 ยง3.1): `true` when the client-facing server in front of this one decrypted ECH and passed the inner hello on โ€” `echStatus` is `'accepted'` either way (the name went encrypted). DTLS: `isEchBackend()` |
468
+ | `socket.getPskIdentity()` | External PSK: the identity (`Buffer`) this connection was authenticated with โ€” or resumed from one that was โ€” else `null`. DTLS: the same, as bytes |
435
469
  | `socket.echAccepted` | `echStatus === 'accepted'` |
436
470
  | `socket.getTicketInfo()` | `{ data, resumed, offered, state }` โ€” the `ticketData` of the ticket this connection resumed with (server), whether it resumed, whether a ticket was offered |
437
- | `useDefaultCA` | When no `ca` is given, chain against the platform trust store instead of authorizing everything (default `true`). **Breaking change**: a self-signed or private-CA peer that previously connected with no `ca` configured now fails with `UNABLE_TO_GET_ISSUER_CERT`. That is the point โ€” the old behaviour reported a connection as verified while verifying nothing, with `rejectUnauthorized` defaulting to true. Set `false` to opt back out explicitly. DTLS defaults to `false`, since WebRTC peers are self-signed by design and authenticated by SDP fingerprint |
471
+ | `useDefaultCA` | When no `ca` is given, chain against the platform trust store instead of authorizing everything (default `true`). **Breaking change**: a self-signed or private-CA peer that previously connected with no `ca` configured now fails with `UNABLE_TO_GET_ISSUER_CERT`. That is the point โ€” the old behaviour reported a connection as verified while verifying nothing, with `rejectUnauthorized` defaulting to true. On both sides: a server asking for client certificates (`requestCert`) chains them against it too, as node:tls does. `false`: only `ca` โ€” and with no `ca` either, no peer is authorized (`DEPTH_ZERO_SELF_SIGNED_CERT` / `UNABLE_TO_GET_ISSUER_CERT_LOCALLY`); a caller that authenticates the peer another way says so with `rejectUnauthorized: false`. DTLS defaults to `false`, since WebRTC peers are self-signed by design and authenticated by SDP fingerprint |
438
472
  | `allowHalfOpen` | When `false` (the Node default, and now ours) the writable side ends automatically once the peer stops writing. This was hardcoded `true`, the opposite of Node โ€” code expecting the documented behaviour was left holding connections open |
439
473
  | `checkServerIdentity(hostname, cert)` | Replaces the built-in RFC 6125 identity check. Return `undefined` to accept, an `Error` to reject. Runs **after** chain verification, as Node documents โ€” a hook that only checks a pin cannot rescue a chain that failed |
440
474
  | `socket` | Run the handshake over an existing `Duplex` instead of dialling out. `host`/`port` are then ignored and nothing is connected for you, so the caller can speak cleartext first and upgrade โ€” STARTTLS for SMTP, IMAP, PostgreSQL |
441
475
  | `secureContext` | A context from `createSecureContext()`, reused across connections instead of re-parsing the same PEM each time. Explicit `secureContext` wins over `key`/`cert` |
442
476
  | `ALPNCallback` | `({ servername, protocols }) => protocol \| undefined`. Server-side, synchronous. Returning `undefined` refuses with `no_application_protocol`. Mutually exclusive with `ALPNProtocols` |
443
- | `grease` | Insert GREASE (RFC 8701) reserved values into the ClientHello, as browsers do (default `false`). Covers cipher suites, extension types, supported groups, signature algorithms, supported versions, ALPN, `psk_key_exchange_modes` and a dummy `key_share` entry. **Changes no fingerprint** โ€” JA3 and JA4 both strip GREASE โ€” but its *absence* reads as "not a browser". Any list you already GREASE'd by hand is left alone, so `grease: true` never collides with manual configuration. Safe against public HTTPS servers; leave off for DTLS/WebRTC peers until tested |
444
- | `permuteExtensions` | Shuffle ClientHello extension order per connection, as Chrome 110+ does (default `false`). Changes your **JA3** on every handshake; leaves your **JA4** byte-for-byte identical, since JA4 sorts before hashing. Order is chosen once and replayed on a post-HRR retry, per RFC 8446 ยง4.1.2 |
477
+ | `grease` | Insert GREASE (RFC 8701) reserved values into the ClientHello, as browsers do (default `false`). Covers cipher suites, extension types, supported groups, signature algorithms, supported versions, ALPN, `psk_key_exchange_modes` and a dummy `key_share` entry. **Changes no fingerprint** โ€” JA4 strips GREASE โ€” but its *absence* reads as "not a browser". Any list you already GREASE'd by hand is left alone, so `grease: true` never collides with manual configuration. Safe against public HTTPS servers; leave off for DTLS/WebRTC peers until tested |
478
+ | `permuteExtensions` | Shuffle ClientHello extension order per connection, as Chrome 110+ does (default `false`). Leaves your **JA4** byte-for-byte identical, since JA4 sorts before hashing. Order is chosen once and replayed on a post-HRR retry, per RFC 8446 ยง4.1.2 |
445
479
  | `socket.getJA4(opts?)` | The client's **JA4** fingerprint, `{ hash, raw }` (`t13d1516h2_8daaf6152771_e5627ece308c`). Available in either role, since both hellos are visible to both peers; `null` before the ClientHello has been seen. Pass `{ protocol: 'q' }` when driving the session from QUIC |
446
480
  | `socket.getJA5(opts?)` | **JA5**: JA4 without the extensions that describe the connection rather than the client (`pre_shared_key`, `session_ticket`, `early_data`, `padding`, `cookie`, `psk_key_exchange_modes`) โ€” the same client build has one JA5 whether it resumed or not โ€” and with the `supported_groups` it offers, which JA4 does not look at. `{ hash, raw }` |
447
481
  | `socket.getJA4S(opts?)` | The server's **JA4S** fingerprint, `{ hash, raw }` โ€” `null` until a ServerHello exists |
@@ -533,7 +567,18 @@ handling, cookie exchange):
533
567
  Retransmission under loss: in DTLS 1.3 an ACK that covers only part of a
534
568
  flight retires only those records โ€” the rest are resent when the timer
535
569
  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).
570
+ the peer ACKs it: the one flight acknowledged by nothing else (ยง7.1), not
571
+ even by the server's data, which can come before it has the Finished.
572
+
573
+ The timer doubles on each timeout, up to 60 s. Where it starts: with
574
+ `initialRetransmitTimeout` given, there โ€” a fixed schedule, whatever round
575
+ trip is measured; without it, at 1 s, and once a round trip has been
576
+ measured, from SRTT + 4ยทRTTVAR (RFC 6298), at least 100 ms, never sampled
577
+ from a resent flight. A
578
+ flight is given up only after six retransmissions *and* 60 s, so a short
579
+ timer (WebRTC uses 100 ms) does not end a handshake early on a lossy path.
580
+ Post-handshake records (tickets, KeyUpdate) are ACKed together, a quarter
581
+ of the timer after the first (ยง7.1).
537
582
 
538
583
  ```js
539
584
  import { DTLSSession, DTLSSocket, createDTLSServer, connectDTLS } from 'lemon-tls';
@@ -568,6 +613,44 @@ the `'clienthello'` event, plus `.tls` for direct access to the session.
568
613
  wrappers over `DTLSSession`, mirroring the TCP socket layer's shape.
569
614
  The same delegated methods and events are available on the socket.
570
615
 
616
+ **UDP socket buffers.** The sockets `createDTLSServer` and `connectDTLS` bind
617
+ get 4 MB receive and send buffers (`socketBufferSize`, bytes; `0` or `false`
618
+ for the OS default, 208 KB on Linux โ€” about 180 datagrams, which a burst
619
+ larger than one event-loop turn drains overflows: the kernel drops the rest,
620
+ silently). The kernel caps the size at `net.core.rmem_max` / `wmem_max`, so
621
+ on a server check them (`sysctl net.core.rmem_max`) and raise them if lower:
622
+
623
+ ```sh
624
+ sysctl -w net.core.rmem_max=4194304 net.core.wmem_max=4194304
625
+ printf 'net.core.rmem_max=4194304\nnet.core.wmem_max=4194304\n' > /etc/sysctl.d/90-udp-buffers.conf
626
+ ```
627
+
628
+ A socket you hand to `new DTLSSocket(udp, ...)` is yours and keeps its own.
629
+
630
+ **Connection ID** (RFC 9146 for DTLS 1.2, RFC 9147 ยง9 for 1.3). A peer
631
+ behind a NAT may come back from another port; without a CID its records are
632
+ no connection's. With `connectionId: true` on both sides, the client puts
633
+ the server's CID (8 random bytes, one per connection) in every protected
634
+ record, and `createDTLSServer` finds the connection by it from any address:
635
+
636
+ ```js
637
+ const server = createDTLSServer({ key, cert, connectionId: true }, (socket) => {
638
+ socket.on('remoteAddressChange', ({ address, port }) => {
639
+ // The newest authenticated record yet came from there. Not taken over by
640
+ // itself: RFC 9146 ยง6 asks for a way of knowing the peer is there first โ€”
641
+ // the application's (it answers, say), then:
642
+ socket.setRemoteAddress(address, port);
643
+ });
644
+ });
645
+ connectDTLS({ host, port, connectionId: true });
646
+ ```
647
+
648
+ A client asks for an empty CID (it needs none: the server does not move);
649
+ `connectionId` may also be the CID itself (`DTLSSession`). `getConnectionIds()`
650
+ gives `{ local, peer }`. In DTLS 1.3 a peer's NewConnectionId with
651
+ `cid_immediate` moves the CID we send; RequestConnectionId is answered by
652
+ keeping the one agreed (the RFC allows it).
653
+
571
654
  ### Record Layer Module
572
655
 
573
656
  Shared encrypt/decrypt primitives for QUIC, DTLS, and custom transport consumers:
@@ -650,7 +733,7 @@ const socket = tls.connect(443, 'api.example.com', {
650
733
  groups: [0x001d], // X25519 only (Node: ecdhCurve is global)
651
734
  signatureAlgorithms: [0x0804], // RSA-PSS-SHA256 only (Node: no control)
652
735
  prioritizeChaCha: true, // ChaCha20 before AES (Node: no control)
653
- allowedCipherSuites: [0x1301, 0x1303], // whitelist (Node: string-based, error-prone)
736
+ allowedCipherSuites: [0x1303, 0x1301], // only these, ChaCha first (Node: string-based, error-prone)
654
737
  });
655
738
 
656
739
  // Disable session tickets (in Node.js requires openssl.cnf)
@@ -722,6 +805,38 @@ LemonTLS is verified against real-world TLS implementations:
722
805
  | **msquic** | (via QUICO) | 1.3 | HRR + P-384 + AES-256-GCM-SHA384 tested |
723
806
  | **Go `crypto/tls`** | Client / Server | 1.3 | **ECH** both ways: accepted, `retry_configs` then accepted with them, HelloRetryRequest; GREASE ECH (our client), resumption inside ECH (our server) |
724
807
 
808
+ ### Certificate validation, against node:tls
809
+
810
+ `tests/test_x509_chain.js` runs 71 cases, the chains made by `openssl ca` โ€” trust and
811
+ shape, validity windows, what a CA and a leaf may do, name constraints of
812
+ every kind, RSA / RSA-PSS / Ed25519 / P-384, two CAs of one name โ€” and
813
+ connects to each with a node:tls client and a LemonTLS client (and checks
814
+ client certificates with both servers), comparing `authorizationError`.
815
+ Where node:tls reports `UNSPECIFIED` (codes missing from its table: name
816
+ constraints, critical extensions, weak digests), `openssl verify` on the
817
+ same chain is the reference. `tests/test_keys_groups.js` does the same for
818
+ private keys in every encoding and the key-exchange groups, against node:tls
819
+ and `openssl s_client`.
820
+
821
+ The certificate format โ€” DER, extensions, names, name constraints, host
822
+ names โ€” is `cryptographic-formats`'s; the
823
+ verifier's decisions on top of it, and the signature checks (`node:crypto`),
824
+ are `src/session/x509.js`.
825
+
826
+ Where LemonTLS answers differently from node:tls, on purpose:
827
+
828
+ | Case | node:tls | LemonTLS |
829
+ |---|---|---|
830
+ | An intermediate given as `ca` | not a root: refused | any certificate in `ca` is an anchor |
831
+ | The peer's own leaf given as `ca` | refused | pinned: authorized |
832
+ | A leaf that is a CA (`cA: TRUE`) | authorized | `INVALID_PURPOSE` (CA/B Forum BR 7.1.2.7.8) |
833
+ | A leaf whose keyUsage does not allow signing | `INVALID_PURPOSE` | the handshake ends (`unsupported_certificate`): the key signs it |
834
+ | A critical `nsCertType` | enforced by its own rules | `UNHANDLED_CRITICAL_EXTENSION`: not read here |
835
+ | `b.example.com.` under an excluded `.example.com` | outside it; the host check fails it | `EXCLUDED_VIOLATION`: the exclusion holds for both spellings |
836
+ | A URI name with an IPv6 host under a URI constraint | read as host `[2001`, passed | `UNSUPPORTED_NAME_SYNTAX` |
837
+ | Two CAs of one name without key identifiers, the wrong one first | `CERT_SIGNATURE_FAILURE` | the next one is tried |
838
+ | A `ca` bundle with a block that does not decode | stops there: the anchors after it are dropped | that block alone is skipped (named in the `LEMON_DEBUG` log; `createSecureContext()` returns it in `caSkipped`) |
839
+
725
840
  ## โšก Performance
726
841
 
727
842
  Benchmarks on Windows (Node v25.9.0, Lemonโ†”Lemon localhost, 10MB transfers, median of 25 iterations):
@@ -748,7 +863,7 @@ For a pure-JavaScript implementation with zero native dependencies, this is with
748
863
  | โœ… | TLS 1.3 - Server + Client |
749
864
  | โœ… | TLS 1.2 - Server + Client |
750
865
  | โœ… | AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305 |
751
- | โœ… | X25519 / P-256 / **P-384** key exchange |
866
+ | โœ… | X25519 / P-256 / P-384 / X448 / P-521 key exchange |
752
867
  | โœ… | RSA-PSS / ECDSA / RSA-PKCS#1 signatures |
753
868
  | โœ… | SNI, ALPN extensions |
754
869
  | โœ… | HelloRetryRequest (both client and server side) |
@@ -768,7 +883,7 @@ For a pure-JavaScript implementation with zero native dependencies, this is with
768
883
  | โœ… | `exportKeyingMaterial` โ€” RFC 5705 (1.2) + RFC 8446 ยง7.5 (1.3, proper two-stage `exporter_master_secret` derivation) |
769
884
  | โœ… | Peer-extension introspection (`getRemoteExtension(s)`) + custom `local_extensions` answered in the 1.2 ServerHello and 1.3 EncryptedExtensions |
770
885
  | โœ… | DTLS-SRTP keying flow (use_srtp negotiation + key export) verified end-to-end against rtp-packet |
771
- | โœ… | Zero dependencies - `node:crypto` only |
886
+ | โœ… | `node:crypto` for the cryptography; certificate and key formats from `cryptographic-formats` |
772
887
  | โœ… | **72 automated tests** (compat, resumption, data transfer) |
773
888
  | โœ… | Browser-tested (Chrome) |
774
889
  | โœ… | Encrypted Client Hello (RFC 9849) โ€” server and client, GREASE ECH |
@@ -777,6 +892,10 @@ For a pure-JavaScript implementation with zero native dependencies, this is with
777
892
  | โœ… | Certificate compression (RFC 8879) โ€” brotli, zlib, zstd |
778
893
  | โœ… | OCSP stapling โ€” server (`'OCSPRequest'`) and client (`requestOCSP`) |
779
894
  | โœ… | Application data in session tickets (`ticketData`, `getTicketInfo()`) |
895
+ | โœ… | External PSK (TLS 1.3 / DTLS 1.3, psk_dhe_ke) |
896
+ | โœ… | DTLS Connection ID (RFC 9146, RFC 9147) โ€” with the server finding a moved peer |
897
+ | โœ… | record_size_limit (RFC 8449) |
898
+ | โœ… | Several certificates per server, chosen per handshake |
780
899
 
781
900
  ### โณ Planned
782
901
 
@@ -828,12 +947,15 @@ src/
828
947
  checkServerIdentity, Server)
829
948
  node_options.js - node:tls option strings (ciphers, ecdhCurve, sigalgs)
830
949
  โ†’ code-point lists
831
- secure_context.js - PEM/DER cert/key loading
950
+ secure_context.js - PEM/DER cert/key loading (cryptographic-formats reads them)
951
+ ca_store.js - the default trust anchors (Node's CA bundle)
832
952
  utils.js - array helpers, certificateObject
833
953
  dtls_session.js - DTLS state machine
834
954
  dtls_socket.js - DTLS socket wrapper (UDP transport)
835
955
  session/
836
956
  signing.js - signature scheme selection + signing
957
+ x509.js - certificates (read by cryptographic-formats), issuer and
958
+ signature checks, RFC 5280 path checks, host names
837
959
  ecdh.js - X25519 / P-256 / P-384 key exchange
838
960
  message.js - high-level message build/parse
839
961
  ticket.js - session ticket encryption