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 +164 -42
- package/index.d.ts +399 -15
- package/package.json +7 -2
- package/src/ca_store.js +31 -0
- package/src/compat.js +64 -112
- package/src/crypto.js +21 -114
- package/src/dtls_session.js +1238 -412
- package/src/dtls_socket.js +318 -96
- package/src/node_options.js +154 -17
- package/src/record.js +293 -197
- package/src/secure_context.js +86 -42
- package/src/session/ecdh.js +140 -122
- package/src/session/ech.js +92 -32
- package/src/session/hpke.js +12 -44
- package/src/session/message.js +29 -9
- package/src/session/signing.js +80 -22
- package/src/session/ticket.js +5 -3
- package/src/session/x509.js +385 -0
- package/src/tls_session.js +2214 -895
- package/src/tls_socket.js +451 -452
- package/src/wire.js +271 -158
package/README.md
CHANGED
|
@@ -18,16 +18,20 @@
|
|
|
18
18
|
|
|
19
19
|
## โจ Features
|
|
20
20
|
|
|
21
|
-
* ๐ **Pure JavaScript** โ no OpenSSL, no native bindings.
|
|
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 |
|
|
293
|
-
| `key` | Buffer/string |
|
|
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
|
|
307
|
-
| `
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
`allowedCipherSuites`
|
|
330
|
-
|
|
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**
|
|
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
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
`
|
|
371
|
-
|
|
372
|
-
|
|
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.
|
|
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** โ
|
|
444
|
-
| `permuteExtensions` | Shuffle ClientHello extension order per connection, as Chrome 110+ does (default `false`).
|
|
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
|
|
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: [
|
|
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 /
|
|
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
|
-
| โ
|
|
|
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
|