lemon-tls 0.6.0 → 0.9.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 +253 -20
- package/index.d.ts +94 -6
- package/index.js +126 -0
- package/package.json +2 -7
- package/src/compat.js +316 -45
- package/src/crypto.js +33 -3
- package/src/dtls_session.js +477 -41
- package/src/dtls_socket.js +32 -0
- package/src/node_options.js +138 -0
- package/src/record.js +210 -27
- package/src/secure_context.js +49 -4
- package/src/session/cert_compression.js +157 -0
- package/src/session/ecdh.js +237 -10
- package/src/session/ech.js +715 -0
- package/src/session/hpke.js +414 -0
- package/src/session/message.js +44 -5
- package/src/session/signing.js +56 -3
- package/src/session/ticket.js +44 -28
- package/src/tls_session.js +3003 -217
- package/src/tls_socket.js +839 -149
- package/src/utils.js +75 -2
- package/src/wire.js +220 -13
package/README.md
CHANGED
|
@@ -11,29 +11,32 @@
|
|
|
11
11
|
<a href="https://www.npmjs.com/package/lemon-tls">
|
|
12
12
|
<img src="https://img.shields.io/npm/v/lemon-tls?color=blue" alt="npm">
|
|
13
13
|
</a>
|
|
14
|
-
<img src="https://img.shields.io/badge/status-in%20development-yellow" alt="status">
|
|
15
14
|
<img src="https://img.shields.io/github/license/colocohen/lemon-tls?color=brightgreen" alt="license">
|
|
16
15
|
</p>
|
|
17
16
|
|
|
18
17
|
---
|
|
19
18
|
|
|
20
|
-
> **⚠️ Project status: *Active development*.**
|
|
21
|
-
> APIs may change without notice until we reach v1.0.
|
|
22
|
-
> Use at your own risk and please report issues!
|
|
23
|
-
|
|
24
19
|
## ✨ Features
|
|
25
20
|
|
|
26
21
|
* 🔒 **Pure JavaScript** – no OpenSSL, no native bindings. Zero dependencies.
|
|
27
22
|
* ⚡ **TLS 1.3 (RFC 8446)** + **TLS 1.2** – both server and client.
|
|
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.
|
|
28
24
|
* 🌐 **Browser-tested** – verified interop with Chrome, curl, Node.js, openssl s_client, and msquic.
|
|
29
25
|
* 🔑 **Key Access** – read handshake secrets, traffic keys, ECDHE shared secret, and resumption data at any point.
|
|
30
|
-
* 🔁 **Session Resumption** – session tickets + PSK with binder validation.
|
|
26
|
+
* 🔁 **Session Resumption** – session tickets + PSK with binder validation, with application data carried inside the ticket (`ticketData`, `getTicketInfo()`).
|
|
31
27
|
* 🔄 **Key Update** – refresh traffic keys on long-lived TLS 1.3 connections.
|
|
32
28
|
* 🔃 **HelloRetryRequest** – automatic group negotiation fallback (X25519, P-256, P-384).
|
|
29
|
+
* 🛰 **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.
|
|
31
|
+
* 🗜 **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
|
+
* 📄 **OCSP stapling** – the server staples (`'OCSPRequest'`, as `node:tls`), the client asks for it (`requestOCSP`, `'OCSPResponse'`).
|
|
33
33
|
* 📜 **Client Certificate Auth** – mutual TLS (mTLS) with `requestCert` / `cert` / `key` options.
|
|
34
|
-
* 🛡 **Designed for extensibility** – exposes cryptographic keys and record-layer primitives for QUIC
|
|
34
|
+
* 🛡 **Designed for extensibility** – exposes cryptographic keys and record-layer primitives for QUIC or custom transports (DTLS ships implemented, see above).
|
|
35
35
|
* 🧩 **Two API levels** – high-level `TLSSocket` (drop-in Node.js Duplex stream) and low-level `TLSSession` (state machine only, you handle the transport).
|
|
36
|
-
*
|
|
36
|
+
* 🔍 **TLS fingerprinting, built in** – `getJA4()`, `getJA5()` and `getJA4S()`: the client's fingerprint, its resumption-stable variant, and the server's. Each is available **in either role**, because both hellos are visible to both peers. Verified byte-for-byte against independent implementations. Node cannot do this at all: OpenSSL parses the ClientHello and discards the offer, so other libraries re-read the raw bytes off the socket in parallel — here the data is already in hand.
|
|
37
|
+
* 🎭 **Handshake shaping** – `permuteExtensions` (Chrome 110+ extension-order randomisation), `grease` (RFC 8701, across all seven insertion points) and `echGrease` (GREASE ECH, RFC 9849 §6.2). All opt-in, all collision-safe against anything you configured by hand.
|
|
38
|
+
* 🧮 **HPKE (RFC 9180)** – exported as a module of its own (`crypto.hpke`): all four modes, the DHKEMs over X25519, X448, P-256, P-384 and P-521 — what ECH is built on, and the base of Oblivious HTTP and MLS.
|
|
39
|
+
* 🔧 **Beyond Node.js** – per-connection cipher/sigalg/group selection, certificate pinning, `ALPNCallback`, `setKeyCert`, and more options that are impossible or require `openssl.cnf` hacks in Node.js.
|
|
37
40
|
* 📘 **TypeScript support** – full `.d.ts` bundled, type-checked in strict mode.
|
|
38
41
|
|
|
39
42
|
## 📦 Installation
|
|
@@ -122,6 +125,91 @@ const socket = tls.connect(8443, 'localhost', {
|
|
|
122
125
|
});
|
|
123
126
|
```
|
|
124
127
|
|
|
128
|
+
### Encrypted Client Hello (ECH)
|
|
129
|
+
|
|
130
|
+
Without ECH the first message of every TLS connection — the ClientHello, with the
|
|
131
|
+
site's name — crosses the network in the clear. With ECH (RFC 9849) the client sends
|
|
132
|
+
the real ClientHello encrypted, inside an outer one that names only a shared
|
|
133
|
+
*public name*, to a key the server publishes in DNS (the `ech=` key of its HTTPS
|
|
134
|
+
record, RFC 9460).
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
import tls from 'lemon-tls';
|
|
138
|
+
|
|
139
|
+
// Server: a key — keep k.config and k.privateKey across restarts,
|
|
140
|
+
// publish k.configListBase64 as ech= in the domain's HTTPS record.
|
|
141
|
+
const k = tls.generateEchKey({ publicName: 'ech.example.com' });
|
|
142
|
+
|
|
143
|
+
const server = tls.createServer({
|
|
144
|
+
key, cert, // must cover ech.example.com too (see below)
|
|
145
|
+
ech: { keys: [{ config: k.config, privateKey: k.privateKey }] },
|
|
146
|
+
}, (socket) => {
|
|
147
|
+
console.log(socket.servername, socket.echStatus); // 'www.example.com' 'accepted'
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Client: the server's key list, from DNS.
|
|
151
|
+
const socket = tls.connect({
|
|
152
|
+
host: 'www.example.com', port: 443, servername: 'www.example.com',
|
|
153
|
+
ech: k.configListBase64,
|
|
154
|
+
}, () => console.log(socket.echStatus)); // 'accepted'
|
|
155
|
+
|
|
156
|
+
socket.on('error', (err) => {
|
|
157
|
+
if (err.code === 'ERR_ECH_REJECTED' && err.retryConfigs) {
|
|
158
|
+
// The server's keys changed: connect again with err.retryConfigs.
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**When the server cannot decrypt** (a key it no longer holds), it completes the
|
|
164
|
+
handshake as the public name — so its certificate must cover that name too — and
|
|
165
|
+
sends its current keys (`retry_configs`). The client checks that certificate against
|
|
166
|
+
the public name, sends `ech_required`, and fails with `ERR_ECH_REJECTED`:
|
|
167
|
+
`err.retryConfigs` is the server's new ECHConfigList (given only when the server was
|
|
168
|
+
authenticated), or `null` — the server has turned ECH off. There is no automatic
|
|
169
|
+
fallback to a hello in the clear: that is the application's decision.
|
|
170
|
+
|
|
171
|
+
**What the client does.** Two hellos go in one:
|
|
172
|
+
- **Inner:** the real one, sealed with HPKE.
|
|
173
|
+
- **Outer:** in the clear, naming the public name.
|
|
174
|
+
|
|
175
|
+
Extensions the two share, the `key_share` above all, are sent once: the inner hello
|
|
176
|
+
names them (`ech_outer_extensions`). The inner hello is padded so its length tells
|
|
177
|
+
little about the name. The server says which hello it answered with 8 bytes of its
|
|
178
|
+
random (or of its HelloRetryRequest) that only a holder of the inner hello can compute.
|
|
179
|
+
|
|
180
|
+
**Also covered:**
|
|
181
|
+
- HelloRetryRequest
|
|
182
|
+
- A resumed session inside the inner hello
|
|
183
|
+
- `echGrease: true`: an ECH extension no server can decrypt, as Chrome and Firefox send, so that clients using ECH do not stand out.
|
|
184
|
+
|
|
185
|
+
**Key rotation** — the new key in DNS first, then on the server, the old one kept with
|
|
186
|
+
`retry: false` until the old record's TTL has passed:
|
|
187
|
+
|
|
188
|
+
```js
|
|
189
|
+
server.setEchKeys({ keys: [
|
|
190
|
+
{ config: next.config, privateKey: next.privateKey },
|
|
191
|
+
{ config: prev.config, privateKey: prev.privateKey, retry: false },
|
|
192
|
+
] });
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
A key is not certified by anyone — its trust comes from DNS. A substituted key could
|
|
196
|
+
reveal which name was asked for, never let anyone impersonate the site: the
|
|
197
|
+
certificate is still checked against that name. TLS 1.3 only, not DTLS.
|
|
198
|
+
|
|
199
|
+
### OCSP Stapling
|
|
200
|
+
|
|
201
|
+
```js
|
|
202
|
+
// Server — as node:tls: asked when a client requests status. Fetch and cache
|
|
203
|
+
// responses ahead of time; the handshake waits for the callback.
|
|
204
|
+
server.on('OCSPRequest', (certificate, issuer, callback) => {
|
|
205
|
+
callback(null, cachedOcspResponseFor(certificate)); // DER Buffer, or null for none
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// Client
|
|
209
|
+
const socket = tls.connect({ host, port, requestOCSP: true });
|
|
210
|
+
socket.on('OCSPResponse', (response) => { /* DER Buffer — check it, e.g. with the `ocsp` package */ });
|
|
211
|
+
```
|
|
212
|
+
|
|
125
213
|
## 📚 API
|
|
126
214
|
|
|
127
215
|
### Module-Level Functions
|
|
@@ -137,7 +225,22 @@ tls.getCiphers() // ['tls_aes_128_gcm_sha256',
|
|
|
137
225
|
tls.DEFAULT_MIN_VERSION // 'TLSv1.2'
|
|
138
226
|
tls.DEFAULT_MAX_VERSION // 'TLSv1.3'
|
|
139
227
|
tls.DEFAULT_CIPHERS // 'TLS_AES_256_GCM_SHA384:...'
|
|
228
|
+
tls.rootCertificates // the shipped CA bundle (~144 PEMs)
|
|
229
|
+
tls.getCACertificates('default') // same, as a copy
|
|
230
|
+
tls.setDefaultCACertificates([...]) // replace it for later connections
|
|
140
231
|
tls.DEFAULT_ECDH_CURVE // 'auto'
|
|
232
|
+
|
|
233
|
+
// node:tls's option strings → the code lists lemon-tls works with (what
|
|
234
|
+
// createServer / connect do with `ciphers`, `ecdhCurve`, `sigalgs`)
|
|
235
|
+
tls.parseCiphers('TLS_AES_256_GCM_SHA384:ECDHE-RSA-AES128-GCM-SHA256') // [0x1302, 0xc02f]
|
|
236
|
+
tls.parseEcdhCurve('X25519:P-256') // [0x001d, 0x0017]; 'auto' → null (the default)
|
|
237
|
+
tls.parseSigalgs('ECDSA+SHA256:rsa_pss_rsae_sha256') // [0x0403, 0x0804]
|
|
238
|
+
tls.certificateObject(der) // a DER certificate as getPeerCertificate() describes it
|
|
239
|
+
|
|
240
|
+
// Encrypted Client Hello (RFC 9849)
|
|
241
|
+
tls.generateEchKey({ publicName, configId?, maxNameLength?, kem? })
|
|
242
|
+
// → { config, privateKey, configList, configListBase64 }
|
|
243
|
+
tls.checkEchKeys({ keys: [{ config, privateKey, retry }] }) // throws, saying what is wrong
|
|
141
244
|
```
|
|
142
245
|
|
|
143
246
|
### `tls.Server` (returned by `createServer`)
|
|
@@ -148,6 +251,8 @@ server.close(callback?) // Stop accepting new connect
|
|
|
148
251
|
server.setSecureContext({ key, cert }) // Runtime cert rotation (Let's Encrypt, etc.)
|
|
149
252
|
server.getTicketKeys() // 48-byte Buffer (ticket encryption keys)
|
|
150
253
|
server.setTicketKeys(keys) // For clustered deployments
|
|
254
|
+
server.getEchKeys() // the current `ech` option
|
|
255
|
+
server.setEchKeys(ech) // ECH key rotation; a bad key throws, the old set stays
|
|
151
256
|
server.address() // { port, family, address }
|
|
152
257
|
```
|
|
153
258
|
|
|
@@ -160,6 +265,7 @@ server.address() // { port, family, address }
|
|
|
160
265
|
| `keylog` | `(line, socket)` | SSLKEYLOGFILE-format line (for Wireshark) |
|
|
161
266
|
| `newSession` | `(id, data, cb)` | Store a TLS 1.2 Session ID (for custom session stores) |
|
|
162
267
|
| `resumeSession` | `(id, cb)` | Look up a TLS 1.2 Session ID (for custom session stores) |
|
|
268
|
+
| `OCSPRequest` | `(certificate, issuer, callback)` | A client asked for the certificate's OCSP status (as `node:tls`). `certificate` and `issuer` are DER Buffers (`issuer` `null` when the chain has none); `callback(null, response)` staples a DER OCSP response, `callback(null, null)` none, `callback(err)` fails the handshake. Without a listener nothing is stapled |
|
|
163
269
|
| `error` / `close` | — | Transport-level |
|
|
164
270
|
|
|
165
271
|
### `TLSSocket`
|
|
@@ -185,6 +291,9 @@ High-level wrapper extending `stream.Duplex`, API-compatible with Node.js [`tls.
|
|
|
185
291
|
| `requestCert` | boolean | Request client certificate (server) |
|
|
186
292
|
| `cert` | Buffer/string | Client certificate PEM (for mTLS) |
|
|
187
293
|
| `key` | Buffer/string | Client private key PEM (for mTLS) |
|
|
294
|
+
| `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
|
+
| `requestOCSP` | boolean | Client: ask for the server's stapled OCSP response; it comes as `'OCSPResponse'` |
|
|
296
|
+
| `sessionTimeout` | number | Server: seconds a session stays good — TLS 1.2 Session IDs, and the ticket lifetime unless `ticketLifetime` is given |
|
|
188
297
|
|
|
189
298
|
**LemonTLS-only (not available in Node.js):**
|
|
190
299
|
|
|
@@ -197,9 +306,72 @@ High-level wrapper extending `stream.Duplex`, API-compatible with Node.js [`tls.
|
|
|
197
306
|
| `maxRecordSize` | number | Max plaintext per TLS record (default: 16384) |
|
|
198
307
|
| `allowedCipherSuites` | number[] | Whitelist - only these ciphers are offered |
|
|
199
308
|
| `pins` | string[] | Certificate pinning: `['sha256/AAAA...']` |
|
|
200
|
-
| `handshakeTimeout` | number | Abort handshake after N ms |
|
|
309
|
+
| `handshakeTimeout` | number | Abort handshake after N ms. `createServer`: `120000` by default, as in Node |
|
|
201
310
|
| `maxHandshakeSize` | number | Max handshake bytes - DoS protection |
|
|
202
311
|
| `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 |
|
|
313
|
+
| `echGrease` | boolean | Client, without `ech`: send GREASE ECH (RFC 9849 §6.2), as browsers do (default `false`) |
|
|
314
|
+
| `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
|
+
| `ticketLifetime` | number | Server: seconds a TLS 1.3 ticket is good for (default `7200`, capped at 7 days, RFC 8446 §4.6.1) |
|
|
316
|
+
| `ticketData` | function | Server: `(resumed) => data` — JSON-safe data (≤ 256 bytes serialized) carried inside each ticket, encrypted with `ticketKeys`; `resumed` is the data of the ticket this connection resumed with (`null` on a full handshake). It comes back in `getTicketInfo()` on the connection that presents the ticket |
|
|
317
|
+
|
|
318
|
+
**Client certificates and resumption.** A resumed handshake carries no certificate, so the
|
|
319
|
+
ticket (or TLS 1.2 session) keeps the client's: on a resumed connection `authorized`,
|
|
320
|
+
`authorizationError` and `getPeerCertificate()` answer as on the full handshake that issued it.
|
|
321
|
+
A server that requires an authorized client (`requestCert` + `rejectUnauthorized`) resumes
|
|
322
|
+
only a ticket that says it had one — anything else is a full handshake, where the
|
|
323
|
+
certificate is asked for again.
|
|
324
|
+
|
|
325
|
+
### Supported cipher suites
|
|
326
|
+
|
|
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.
|
|
331
|
+
|
|
332
|
+
**TLS 1.3** — offered by default
|
|
333
|
+
|
|
334
|
+
| Code | Suite | AEAD | Hash |
|
|
335
|
+
|---|---|---|---|
|
|
336
|
+
| `0x1301` | `TLS_AES_128_GCM_SHA256` | AES-128-GCM | SHA-256 |
|
|
337
|
+
| `0x1302` | `TLS_AES_256_GCM_SHA384` | AES-256-GCM | SHA-384 |
|
|
338
|
+
| `0x1303` | `TLS_CHACHA20_POLY1305_SHA256` | ChaCha20-Poly1305 | SHA-256 |
|
|
339
|
+
|
|
340
|
+
**TLS 1.2 — AEAD** (opt in)
|
|
341
|
+
|
|
342
|
+
| Code | Suite | Key exchange | AEAD |
|
|
343
|
+
|---|---|---|---|
|
|
344
|
+
| `0xC02B` | `TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256` | ECDHE_ECDSA | AES-128-GCM |
|
|
345
|
+
| `0xC02C` | `TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384` | ECDHE_ECDSA | AES-256-GCM |
|
|
346
|
+
| `0xC02F` | `TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256` | ECDHE_RSA | AES-128-GCM |
|
|
347
|
+
| `0xC030` | `TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384` | ECDHE_RSA | AES-256-GCM |
|
|
348
|
+
| `0xCCA8` | `TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256` | ECDHE_RSA | ChaCha20-Poly1305 |
|
|
349
|
+
| `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
|
+
|
|
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.
|
|
366
|
+
|
|
367
|
+
ChaCha20-Poly1305 uses a full 12-byte IV with no explicit nonce (RFC 7905),
|
|
368
|
+
unlike GCM — the registry records that per suite.
|
|
369
|
+
|
|
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.
|
|
373
|
+
|
|
374
|
+
|
|
203
375
|
|
|
204
376
|
#### Events
|
|
205
377
|
|
|
@@ -210,9 +382,11 @@ High-level wrapper extending `stream.Duplex`, API-compatible with Node.js [`tls.
|
|
|
210
382
|
| `session` | `(Buffer)` | **Opaque session blob** — pass back to `connect({ session })` to resume |
|
|
211
383
|
| `keyUpdate` | `(direction)` | Traffic keys refreshed: `'send'` or `'receive'` |
|
|
212
384
|
| `keylog` | `(Buffer)` | SSLKEYLOGFILE-format line (for Wireshark) |
|
|
213
|
-
| `
|
|
385
|
+
| `secure` | `()` | Handshake complete. Fires on **both** sides and on sockets built with `new TLSSocket()` — unlike `secureConnect`, which Node does not emit there. The signal to use when wrapping an existing socket for STARTTLS |
|
|
386
|
+
| `clienthello` | `(raw, parsed)` | ClientHello received (server side). Fires **synchronously before the ServerHello is built** — use it for JA4/JA5 fingerprinting, or to inspect the client's offered extensions (`parsed.extensions` / `getRemoteExtension`) and answer them via `set_context({ local_extensions })` (e.g. DTLS-SRTP `use_srtp`) |
|
|
214
387
|
| `handshakeMessage` | `(type, raw, parsed)` | Every handshake message (debugging) |
|
|
215
388
|
| `certificateRequest` | `(msg)` | Server requested a client certificate |
|
|
389
|
+
| `OCSPResponse` | `(Buffer)` | Client, with `requestOCSP`: the server's stapled OCSP response (DER) |
|
|
216
390
|
| `error` | `(Error)` | TLS or transport error |
|
|
217
391
|
| `close` | `()` | Connection closed |
|
|
218
392
|
|
|
@@ -243,7 +417,7 @@ High-level wrapper extending `stream.Duplex`, API-compatible with Node.js [`tls.
|
|
|
243
417
|
| `socket.authorized` | `true` if peer certificate is valid |
|
|
244
418
|
| `socket.authorizationError` | Error string or `null` |
|
|
245
419
|
| `socket.alpnProtocol` | Negotiated ALPN protocol or `false` |
|
|
246
|
-
| `socket.servername` | SNI value (string or `false`) |
|
|
420
|
+
| `socket.servername` | SNI value (string or `false`). Server with ECH accepted: the name that was hidden; rejected: the public name |
|
|
247
421
|
| `socket.encrypted` | Always `true` |
|
|
248
422
|
| `socket.remoteAddress` / `.remotePort` | Peer address (delegated to transport) |
|
|
249
423
|
| `socket.setNoDelay()` / `.setKeepAlive()` / `.setTimeout()` | Transport delegation |
|
|
@@ -257,7 +431,27 @@ High-level wrapper extending `stream.Duplex`, API-compatible with Node.js [`tls.
|
|
|
257
431
|
| `socket.session` | Access the underlying `TLSSession` (low-level state machine) |
|
|
258
432
|
| `socket.isResumed` | Alias for `isSessionReused()` |
|
|
259
433
|
| `socket.handshakeDuration` | Handshake time in ms |
|
|
260
|
-
| `socket.
|
|
434
|
+
| `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'` |
|
|
435
|
+
| `socket.echAccepted` | `echStatus === 'accepted'` |
|
|
436
|
+
| `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 |
|
|
438
|
+
| `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
|
+
| `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
|
+
| `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
|
+
| `secureContext` | A context from `createSecureContext()`, reused across connections instead of re-parsing the same PEM each time. Explicit `secureContext` wins over `key`/`cert` |
|
|
442
|
+
| `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 |
|
|
445
|
+
| `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
|
+
| `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
|
+
| `socket.getJA4S(opts?)` | The server's **JA4S** fingerprint, `{ hash, raw }` — `null` until a ServerHello exists |
|
|
448
|
+
| `socket.getFinished()` / `getPeerFinished()` | The Finished verify_data, ours and the peer's — the `tls-unique` channel binding of RFC 5929. `undefined` before the handshake |
|
|
449
|
+
| `socket.isSessionReused()` | Whether this connection resumed an earlier session (same value as the `isResumed` property; Node spells it as a method) |
|
|
450
|
+
| `socket.address()` | `{ port, family, address }` of the underlying transport, `{}` when not connected |
|
|
451
|
+
| `socket.getEphemeralKeyInfo()` | `{ type, name, size }` for the negotiated key agreement. `null` on a server socket, as in Node |
|
|
452
|
+
| `server.injectConnection(stream)` | Feed an arbitrary `Duplex` into the server as if it had just connected. Node does this by emitting `'connection'` by hand; here it is a method, because the server emits that event itself and a self-listener would re-enter on every real connection |
|
|
453
|
+
| `server.addContext(host, ctx)` | Register a certificate for an SNI host or wildcard (`'*.example.com'`, or `'*'` as catch-all). Takes a `createSecureContext()` result or plain `{ key, cert }`. Exact matches beat wildcards; re-registering a host replaces it. Each context is compiled **once** — the hand-written `SNICallback` equivalent usually recompiles per connection, and often reads the cert off disk on the handshake path |
|
|
454
|
+
| `socket.setKeyCert(context)` | Swap the certificate and key for this one connection. Takes a `createSecureContext()` result or a plain `{ key, cert }`. Pairs with `ALPNCallback`, where the protocol is known but `SNICallback` has already run |
|
|
261
455
|
| `socket.getSharedSecret()` | ECDHE shared secret (Buffer) |
|
|
262
456
|
| `socket.getNegotiationResult()` | `{ version, cipher, group, sni, alpn, resumed, helloRetried, ... }` |
|
|
263
457
|
| `socket.rekeySend()` | Refresh outgoing encryption keys (TLS 1.3) |
|
|
@@ -306,15 +500,27 @@ session.on('secureConnect', () => {
|
|
|
306
500
|
session.requestKeyUpdate(true); // true = request peer to update too
|
|
307
501
|
session.on('keyUpdate', ({ direction, secret }) => { /* ... */ });
|
|
308
502
|
|
|
503
|
+
// Tickets carrying the protocol layer's own state (server, TLS 1.3) —
|
|
504
|
+
// apart from the application's ticketData, ≤ 128 bytes JSON. A QUIC stack
|
|
505
|
+
// keeps its path measurements here; sendTicket() issues another ticket
|
|
506
|
+
// now, after the handshake (OpenSSL's SSL_new_session_ticket), carrying it.
|
|
507
|
+
session.setTicketState({ rtt: 120, bw: 1250000 });
|
|
508
|
+
session.sendTicket();
|
|
509
|
+
// …and on the connection that resumes with it:
|
|
510
|
+
session.getTicketInfo(); // { data, resumed: true, offered: true, state: { rtt: 120, bw: 1250000 } }
|
|
511
|
+
|
|
309
512
|
// PSK callback - full control over ticket validation (server)
|
|
310
513
|
session.on('psk', (identity, callback) => {
|
|
311
514
|
const psk = myTicketStore.lookup(identity);
|
|
312
515
|
callback(psk ? { psk, cipher: 0x1301 } : null);
|
|
313
516
|
});
|
|
314
517
|
|
|
315
|
-
//
|
|
518
|
+
// Fingerprinting (server) — fires before the ServerHello is built, so this is
|
|
519
|
+
// where you drop a connection without spending a full handshake on it.
|
|
316
520
|
session.on('clienthello', (raw, parsed) => {
|
|
317
|
-
console.log(session.
|
|
521
|
+
console.log(session.getJA4().hash); // 't13d521100_b262b3658495_8e6e362c5eac'
|
|
522
|
+
// getJA4S() is still null here — no ServerHello exists yet. Ask again
|
|
523
|
+
// later in the session and it will be there.
|
|
318
524
|
});
|
|
319
525
|
```
|
|
320
526
|
|
|
@@ -469,10 +675,20 @@ tls.createServer({
|
|
|
469
675
|
socket.on('keylog', (line) => fs.appendFileSync('keys.log', line));
|
|
470
676
|
// Wireshark: Edit → Preferences → TLS → Pre-Master-Secret log filename → keys.log
|
|
471
677
|
|
|
472
|
-
//
|
|
678
|
+
// Fingerprinting — server-side bot detection
|
|
473
679
|
server.on('secureConnection', (socket) => {
|
|
474
|
-
const
|
|
475
|
-
console.log(
|
|
680
|
+
const ja4 = socket.getJA4();
|
|
681
|
+
console.log(ja4.hash); // 't13d1516h2_8daaf6152771_e5627ece308c' — the client
|
|
682
|
+
console.log(ja4.raw); // unhashed form: shows which ciphers actually differ
|
|
683
|
+
console.log(socket.getJA5().hash); // the same client, resumed or not
|
|
684
|
+
console.log(socket.getJA4S().hash); // 't130200_1301_a56c5b993250' — how WE look to them
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
// Client side: the same calls, the same values
|
|
688
|
+
const socket = connect({ host: 'example.com', port: 443 });
|
|
689
|
+
socket.on('secureConnect', () => {
|
|
690
|
+
console.log(socket.getJA4().hash); // how this library looks to the server
|
|
691
|
+
console.log(socket.getJA4S().hash); // which server stack answered
|
|
476
692
|
});
|
|
477
693
|
|
|
478
694
|
// Full negotiation result
|
|
@@ -499,6 +715,7 @@ LemonTLS is verified against real-world TLS implementations:
|
|
|
499
715
|
| **Node.js `tls`** | Client / Server | 1.2 / 1.3 | Bidirectional interop + session resumption |
|
|
500
716
|
| **openssl s_client** | Client | 1.2 / 1.3 | All supported ciphers & groups |
|
|
501
717
|
| **msquic** | (via QUICO) | 1.3 | HRR + P-384 + AES-256-GCM-SHA384 tested |
|
|
718
|
+
| **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) |
|
|
502
719
|
|
|
503
720
|
## ⚡ Performance
|
|
504
721
|
|
|
@@ -549,6 +766,12 @@ For a pure-JavaScript implementation with zero native dependencies, this is with
|
|
|
549
766
|
| ✅ | Zero dependencies - `node:crypto` only |
|
|
550
767
|
| ✅ | **72 automated tests** (compat, resumption, data transfer) |
|
|
551
768
|
| ✅ | Browser-tested (Chrome) |
|
|
769
|
+
| ✅ | Encrypted Client Hello (RFC 9849) — server and client, GREASE ECH |
|
|
770
|
+
| ✅ | HPKE (RFC 9180) — all modes, five KEMs |
|
|
771
|
+
| ✅ | Hybrid post-quantum key exchange — X25519MLKEM768 |
|
|
772
|
+
| ✅ | Certificate compression (RFC 8879) — brotli, zlib, zstd |
|
|
773
|
+
| ✅ | OCSP stapling — server (`'OCSPRequest'`) and client (`requestOCSP`) |
|
|
774
|
+
| ✅ | Application data in session tickets (`ticketData`, `getTicketInfo()`) |
|
|
552
775
|
|
|
553
776
|
### ⏳ Planned
|
|
554
777
|
|
|
@@ -557,7 +780,6 @@ For a pure-JavaScript implementation with zero native dependencies, this is with
|
|
|
557
780
|
| ⏳ | Loss detection + PTO (for QUIC integration) | Buffer ready, needs timers |
|
|
558
781
|
| ⏳ | 0-RTT Early Data | Risky (replay attacks), low priority |
|
|
559
782
|
| ⏳ | Full certificate chain validation | Including CA/revocation checks |
|
|
560
|
-
| ⏳ | OCSP stapling | Rare in modern web, low priority |
|
|
561
783
|
| ⏳ | Fuzz testing | Security hardening |
|
|
562
784
|
|
|
563
785
|
### Compatibility Test Summary
|
|
@@ -599,15 +821,20 @@ src/
|
|
|
599
821
|
crypto.js - key schedule (HKDF, PRF, resumption primitives)
|
|
600
822
|
compat.js - Node.js tls API wrappers (connect, createServer,
|
|
601
823
|
checkServerIdentity, Server)
|
|
824
|
+
node_options.js - node:tls option strings (ciphers, ecdhCurve, sigalgs)
|
|
825
|
+
→ code-point lists
|
|
602
826
|
secure_context.js - PEM/DER cert/key loading
|
|
603
|
-
utils.js - array helpers
|
|
827
|
+
utils.js - array helpers, certificateObject
|
|
604
828
|
dtls_session.js - DTLS state machine
|
|
605
829
|
dtls_socket.js - DTLS socket wrapper (UDP transport)
|
|
606
830
|
session/
|
|
607
831
|
signing.js - signature scheme selection + signing
|
|
608
832
|
ecdh.js - X25519 / P-256 / P-384 key exchange
|
|
609
833
|
message.js - high-level message build/parse
|
|
610
|
-
ticket.js -
|
|
834
|
+
ticket.js - session ticket encryption
|
|
835
|
+
ech.js - Encrypted Client Hello: configs, keys, both sides
|
|
836
|
+
hpke.js - HPKE (RFC 9180)
|
|
837
|
+
cert_compression.js - certificate compression (RFC 8879)
|
|
611
838
|
```
|
|
612
839
|
|
|
613
840
|
## 🤝 Contributing
|
|
@@ -630,6 +857,12 @@ You can support ongoing development via [GitHub Sponsors](https://github.com/spo
|
|
|
630
857
|
* [RFC 6125 – Hostname Verification](https://datatracker.ietf.org/doc/html/rfc6125)
|
|
631
858
|
* [RFC 5705 – Exported Keying Material](https://datatracker.ietf.org/doc/html/rfc5705)
|
|
632
859
|
* [RFC 5077 – Stateless Session Resumption (TLS 1.2 tickets)](https://datatracker.ietf.org/doc/html/rfc5077)
|
|
860
|
+
* [RFC 9849 – TLS Encrypted Client Hello](https://datatracker.ietf.org/doc/html/rfc9849)
|
|
861
|
+
* [RFC 9180 – Hybrid Public Key Encryption (HPKE)](https://datatracker.ietf.org/doc/html/rfc9180)
|
|
862
|
+
* [RFC 9460 – SVCB and HTTPS DNS records](https://datatracker.ietf.org/doc/html/rfc9460)
|
|
863
|
+
* [RFC 8879 – TLS Certificate Compression](https://datatracker.ietf.org/doc/html/rfc8879)
|
|
864
|
+
* [RFC 8701 – GREASE](https://datatracker.ietf.org/doc/html/rfc8701)
|
|
865
|
+
* [draft-ietf-tls-ecdhe-mlkem – Hybrid ML-KEM key agreement](https://datatracker.ietf.org/doc/draft-ietf-tls-ecdhe-mlkem/)
|
|
633
866
|
|
|
634
867
|
## 📜 License
|
|
635
868
|
|
package/index.d.ts
CHANGED
|
@@ -31,16 +31,39 @@ export interface PeerCertificate {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export interface EphemeralKeyInfo {
|
|
34
|
-
|
|
34
|
+
/** 'TLSGroup' for a group that is not a plain curve, as Node reports it —
|
|
35
|
+
* X25519MLKEM768 among them (no size). Client side only; {} on a server. */
|
|
36
|
+
type: 'X25519' | 'ECDH' | 'DH' | 'TLSGroup';
|
|
35
37
|
name?: string;
|
|
36
|
-
size
|
|
38
|
+
size?: number;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
/** A TLS fingerprint: `hash` is the fingerprint, `raw` what was hashed. */
|
|
42
|
+
export interface FingerprintResult {
|
|
40
43
|
hash: string;
|
|
41
44
|
raw: string;
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
export interface FingerprintOptions {
|
|
48
|
+
/** Transport character, for a caller driving the session from a transport
|
|
49
|
+
* it cannot see: 't' TCP (default), 'q' QUIC, 'd' DTLS. */
|
|
50
|
+
protocol?: 't' | 'q' | 'd';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Server: this connection's session ticket, from the first connection on.
|
|
55
|
+
* `data` is what the ticketData option put in the ticket — the one this
|
|
56
|
+
* connection resumed with, or, on a full handshake, the one made for the
|
|
57
|
+
* ticket issued to it (null until the server's Finished is sent).
|
|
58
|
+
* `offered && !resumed`: the client presented a ticket that was turned away
|
|
59
|
+
* (expired, issued before a restart, no compatible cipher).
|
|
60
|
+
*/
|
|
61
|
+
export interface TicketInfo {
|
|
62
|
+
data: any;
|
|
63
|
+
resumed: boolean;
|
|
64
|
+
offered: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
44
67
|
export interface NegotiationResult {
|
|
45
68
|
version: number | null;
|
|
46
69
|
versionName: string | null;
|
|
@@ -91,6 +114,14 @@ export interface TLSSocketOptions {
|
|
|
91
114
|
rejectUnauthorized?: boolean;
|
|
92
115
|
ca?: Buffer | string;
|
|
93
116
|
ticketKeys?: Buffer;
|
|
117
|
+
/**
|
|
118
|
+
* Server: what to carry inside each session ticket issued. Called once per
|
|
119
|
+
* connection as ticketData(resumed) — `resumed` is the data of the ticket
|
|
120
|
+
* this connection resumed with, null on a full handshake. The return value
|
|
121
|
+
* (JSON-safe, at most 256 bytes serialized) rides in the ticket, encrypted
|
|
122
|
+
* with ticketKeys; read it back with getTicketInfo().
|
|
123
|
+
*/
|
|
124
|
+
ticketData?: (resumed: any) => any;
|
|
94
125
|
/**
|
|
95
126
|
* Opaque serialized session from a previous 'session' event.
|
|
96
127
|
* Pass this back to resume a TLS 1.2/1.3 session.
|
|
@@ -100,6 +131,21 @@ export interface TLSSocketOptions {
|
|
|
100
131
|
cert?: Buffer | string;
|
|
101
132
|
key?: Buffer | string;
|
|
102
133
|
|
|
134
|
+
// TCP options, as in node:net. connect() passes them (and the rest of
|
|
135
|
+
// net.connect's: localAddress, localPort, family, lookup, timeout) to
|
|
136
|
+
// net.connect; createServer() applies noDelay/keepAlive to every
|
|
137
|
+
// connection it accepts. Defaults as in Node: off.
|
|
138
|
+
/** Disable Nagle's algorithm (TCP_NODELAY). Default: false. */
|
|
139
|
+
noDelay?: boolean;
|
|
140
|
+
/** Enable TCP keep-alive. Default: false. */
|
|
141
|
+
keepAlive?: boolean;
|
|
142
|
+
/** Milliseconds before the first keep-alive probe. Default: 0. */
|
|
143
|
+
keepAliveInitialDelay?: number;
|
|
144
|
+
localAddress?: string;
|
|
145
|
+
localPort?: number;
|
|
146
|
+
family?: number;
|
|
147
|
+
timeout?: number;
|
|
148
|
+
|
|
103
149
|
// LemonTLS-only options
|
|
104
150
|
noTickets?: boolean;
|
|
105
151
|
sessionTickets?: boolean;
|
|
@@ -179,7 +225,17 @@ export class TLSSocket extends Duplex {
|
|
|
179
225
|
/** Returns the underlying TLSSession for low-level access. */
|
|
180
226
|
session: TLSSession;
|
|
181
227
|
readonly handshakeDuration: number | null;
|
|
182
|
-
|
|
228
|
+
/** JA4 of the ClientHello — the published format, what fingerprint
|
|
229
|
+
* databases match. Changes when a client resumes (pre_shared_key). */
|
|
230
|
+
getJA4(opts?: FingerprintOptions): FingerprintResult | null;
|
|
231
|
+
/** JA5 of the ClientHello (ntop's draft): as JA4 without the extensions that
|
|
232
|
+
* vary per connection, plus supported_groups — the same for a client
|
|
233
|
+
* whether it resumed or not. */
|
|
234
|
+
getJA5(opts?: FingerprintOptions): FingerprintResult | null;
|
|
235
|
+
/** JA4S of the ServerHello. */
|
|
236
|
+
getJA4S(opts?: FingerprintOptions): FingerprintResult | null;
|
|
237
|
+
/** Server: this connection's session ticket — see TicketInfo. */
|
|
238
|
+
getTicketInfo(): TicketInfo;
|
|
183
239
|
getSharedSecret(): Buffer | null;
|
|
184
240
|
getNegotiationResult(): NegotiationResult | null;
|
|
185
241
|
rekeySend(): void;
|
|
@@ -246,6 +302,8 @@ export interface TLSSessionOptions {
|
|
|
246
302
|
ALPNProtocols?: string[];
|
|
247
303
|
SNICallback?: (servername: string, cb: (err: Error | null, ctx: SecureContext | null) => void) => void;
|
|
248
304
|
ticketKeys?: Buffer;
|
|
305
|
+
/** See TLSSocketOptions.ticketData. */
|
|
306
|
+
ticketData?: (resumed: any) => any;
|
|
249
307
|
session?: Buffer | Uint8Array | SessionTicketData;
|
|
250
308
|
psk?: any;
|
|
251
309
|
rejectUnauthorized?: boolean;
|
|
@@ -295,6 +353,8 @@ export class TLSSession extends EventEmitter {
|
|
|
295
353
|
getCipher(): number | null;
|
|
296
354
|
getALPN(): string | null;
|
|
297
355
|
getPeerCertificate(): Array<{ cert: Buffer }> | null;
|
|
356
|
+
/** Our own certificate chain — the one this connection presents. */
|
|
357
|
+
getCertificate(): Array<{ cert: Buffer }> | null;
|
|
298
358
|
getTrafficSecrets(): TrafficSecrets;
|
|
299
359
|
getHandshakeSecrets(): HandshakeSecrets;
|
|
300
360
|
exportKeyingMaterial(length: number, label: string, context: Uint8Array): Uint8Array;
|
|
@@ -302,7 +362,11 @@ export class TLSSession extends EventEmitter {
|
|
|
302
362
|
getPeerFinished(): Buffer | null;
|
|
303
363
|
getSharedSecret(): Buffer | null;
|
|
304
364
|
getNegotiationResult(): NegotiationResult;
|
|
305
|
-
|
|
365
|
+
getJA4(opts?: FingerprintOptions): FingerprintResult | null;
|
|
366
|
+
getJA5(opts?: FingerprintOptions): FingerprintResult | null;
|
|
367
|
+
getJA4S(opts?: FingerprintOptions): FingerprintResult | null;
|
|
368
|
+
/** Server: this connection's session ticket — see TicketInfo. */
|
|
369
|
+
getTicketInfo(): TicketInfo;
|
|
306
370
|
requestKeyUpdate(requestPeer?: boolean): void;
|
|
307
371
|
|
|
308
372
|
// Events
|
|
@@ -338,6 +402,19 @@ export function createServer(
|
|
|
338
402
|
|
|
339
403
|
export function getCiphers(): string[];
|
|
340
404
|
|
|
405
|
+
/** Node-compat: the shipped CA bundle (PEM strings) — tls.rootCertificates. */
|
|
406
|
+
export const rootCertificates: readonly string[];
|
|
407
|
+
/**
|
|
408
|
+
* Node-compat: tls.getCACertificates([type]). 'default' and 'bundled' return
|
|
409
|
+
* the CA set clients verify against when given no `ca` (the shipped bundle,
|
|
410
|
+
* or what setDefaultCACertificates put in its place); 'extra' and 'system'
|
|
411
|
+
* are not sourced here and return [].
|
|
412
|
+
*/
|
|
413
|
+
export function getCACertificates(type?: 'default' | 'bundled' | 'extra' | 'system'): string[];
|
|
414
|
+
/** Node-compat: tls.setDefaultCACertificates(certs) — replaces that set for
|
|
415
|
+
* every later connection that passes no `ca`. Throws if not an array. */
|
|
416
|
+
export function setDefaultCACertificates(certs: string[]): void;
|
|
417
|
+
|
|
341
418
|
/**
|
|
342
419
|
* Validates that the peer certificate matches the hostname (RFC 6125).
|
|
343
420
|
* Returns undefined on success, or an Error on mismatch.
|
|
@@ -363,6 +440,14 @@ export declare const crypto: {
|
|
|
363
440
|
hkdf_expand: (hash: string, prk: Uint8Array, info: Uint8Array, length: number) => Uint8Array;
|
|
364
441
|
hkdf_expand_label: (hash: string, secret: Uint8Array, label: string, context: Uint8Array, length: number) => Uint8Array;
|
|
365
442
|
hmac: (hash: string, key: Uint8Array, data: Uint8Array) => Uint8Array;
|
|
443
|
+
/** TLS 1.2 PRF (RFC 5246 §5). */
|
|
444
|
+
tls12_prf: (secret: Uint8Array, label: string, seed: Uint8Array, length: number, hash: string) => Uint8Array;
|
|
445
|
+
/** RFC 5705 exporter over a TLS 1.2 master secret. */
|
|
446
|
+
tls12_exporter: (hash: string, masterSecret: Uint8Array, label: string, clientRandom: Uint8Array,
|
|
447
|
+
serverRandom: Uint8Array, context: Uint8Array | null, length: number) => Uint8Array;
|
|
448
|
+
/** RFC 8446 §7.5 exporter; labelPrefix defaults to TLS 1.3's ('tls13 '). */
|
|
449
|
+
tls13_exporter: (hash: string, exporterMasterSecret: Uint8Array, label: string,
|
|
450
|
+
context: Uint8Array | null, length: number, labelPrefix?: string) => Uint8Array;
|
|
366
451
|
};
|
|
367
452
|
|
|
368
453
|
export declare const wire: any;
|
|
@@ -398,6 +483,9 @@ declare const _default: {
|
|
|
398
483
|
createServer: typeof createServer;
|
|
399
484
|
getCiphers: typeof getCiphers;
|
|
400
485
|
checkServerIdentity: typeof checkServerIdentity;
|
|
486
|
+
getCACertificates: typeof getCACertificates;
|
|
487
|
+
setDefaultCACertificates: typeof setDefaultCACertificates;
|
|
488
|
+
rootCertificates: typeof rootCertificates;
|
|
401
489
|
DEFAULT_MIN_VERSION: string;
|
|
402
490
|
DEFAULT_MAX_VERSION: string;
|
|
403
491
|
DEFAULT_CIPHERS: string;
|
|
@@ -411,4 +499,4 @@ declare const _default: {
|
|
|
411
499
|
connectDTLS: typeof connectDTLS;
|
|
412
500
|
};
|
|
413
501
|
|
|
414
|
-
export default _default;
|
|
502
|
+
export default _default;
|