lemon-tls 0.2.1 → 0.3.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
@@ -16,38 +16,40 @@
16
16
  </p>
17
17
 
18
18
  ---
19
-
20
- > **⚠️ Project status: *Active development*.**
21
- > APIs may change without notice until we reach v1.0.
19
+
20
+ > **⚠️ Project status: *Active development*.**
21
+ > APIs may change without notice until we reach v1.0.
22
22
  > Use at your own risk and please report issues!
23
-
23
+
24
24
  ## ✨ Features
25
-
25
+
26
26
  * 🔒 **Pure JavaScript** – no OpenSSL, no native bindings. Zero dependencies.
27
27
  * ⚡ **TLS 1.3 (RFC 8446)** + **TLS 1.2** – both server and client.
28
+ * 🌐 **Browser-tested** – verified interop with Chrome, curl, Node.js, openssl s_client, and msquic.
28
29
  * 🔑 **Key Access** – read handshake secrets, traffic keys, ECDHE shared secret, and resumption data at any point.
29
30
  * 🔁 **Session Resumption** – session tickets + PSK with binder validation.
30
31
  * 🔄 **Key Update** – refresh traffic keys on long-lived TLS 1.3 connections.
31
- * 🔃 **HelloRetryRequest** – automatic group negotiation fallback.
32
+ * 🔃 **HelloRetryRequest** – automatic group negotiation fallback (X25519, P-256, P-384).
32
33
  * 📜 **Client Certificate Auth** – mutual TLS (mTLS) with `requestCert` / `cert` / `key` options.
33
34
  * 🛡 **Designed for extensibility** – exposes cryptographic keys and record-layer primitives for QUIC, DTLS, or custom transports.
34
35
  * 🧩 **Two API levels** – high-level `TLSSocket` (drop-in Node.js Duplex stream) and low-level `TLSSession` (state machine only, you handle the transport).
35
36
  * 🔧 **Beyond Node.js** – per-connection cipher/sigalg/group selection, JA3 fingerprinting, certificate pinning, and more options that are impossible or require `openssl.cnf` hacks in Node.js.
36
-
37
+ * 📘 **TypeScript support** – full `.d.ts` bundled, type-checked in strict mode.
38
+
37
39
  ## 📦 Installation
38
-
40
+
39
41
  ```
40
42
  npm i lemon-tls
41
43
  ```
42
-
44
+
43
45
  ## 🚀 Quick Start
44
-
46
+
45
47
  ### Drop-in Node.js Replacement
46
-
48
+
47
49
  ```js
48
- import tls from 'lemon-tls'; // not 'node:tls' same API
50
+ import tls from 'lemon-tls'; // not 'node:tls' - same API
49
51
  import fs from 'node:fs';
50
-
52
+
51
53
  // Server
52
54
  const server = tls.createServer({
53
55
  key: fs.readFileSync('server.key'),
@@ -58,20 +60,21 @@ const server = tls.createServer({
58
60
  socket.write('Hello from LemonTLS!\n');
59
61
  });
60
62
  server.listen(8443);
61
-
63
+
62
64
  // Client
63
65
  const socket = tls.connect(8443, 'localhost', { rejectUnauthorized: false }, () => {
64
66
  socket.write('Hello from client!\n');
65
67
  });
66
68
  socket.on('data', (d) => console.log(d.toString()));
67
69
  ```
68
-
70
+
69
71
  ### Low-Level: TLSSocket with TCP
70
-
72
+
71
73
  ```js
72
74
  import net from 'node:net';
75
+ import fs from 'node:fs';
73
76
  import { TLSSocket, createSecureContext } from 'lemon-tls';
74
-
77
+
75
78
  const server = net.createServer((tcp) => {
76
79
  const socket = new TLSSocket(tcp, {
77
80
  isServer: true,
@@ -87,60 +90,86 @@ const server = net.createServer((tcp) => {
87
90
  });
88
91
  server.listen(8443);
89
92
  ```
90
-
93
+
91
94
  ### Session Resumption (PSK)
92
-
95
+
93
96
  ```js
94
97
  let savedSession = null;
95
-
96
- // First connection save the ticket
97
- socket.on('session', (ticketData) => { savedSession = ticketData; });
98
-
99
- // Second connection — resume (no certificate exchange, faster)
98
+
99
+ // First connection - the 'session' event emits an opaque Buffer (Node-compatible).
100
+ // Treat it as a blob: store it, pass it back don't introspect the bytes.
101
+ socket.on('session', (sessionBuffer) => { savedSession = sessionBuffer; });
102
+
103
+ // Second connection - resume (no certificate exchange, faster)
100
104
  const socket2 = tls.connect(8443, 'localhost', { session: savedSession }, () => {
101
- console.log('Resumed:', socket2.isResumed); // true
105
+ console.log('Resumed:', socket2.isSessionReused()); // true
102
106
  });
103
107
  ```
104
-
108
+
105
109
  ### Mutual TLS (Client Certificate)
106
-
110
+
107
111
  ```js
108
112
  // Server: request client certificate
109
113
  const server = tls.createServer({
110
114
  key: serverKey, cert: serverCert,
111
115
  requestCert: true,
112
116
  });
113
-
117
+
114
118
  // Client: provide certificate
115
119
  const socket = tls.connect(8443, 'localhost', {
116
120
  cert: fs.readFileSync('client.crt'),
117
121
  key: fs.readFileSync('client.key'),
118
122
  });
119
123
  ```
120
-
124
+
121
125
  ## 📚 API
122
-
126
+
123
127
  ### Module-Level Functions
124
-
128
+
125
129
  ```js
126
130
  import tls from 'lemon-tls';
127
-
128
- tls.connect(port, host, options, callback) // Node.js compatible
129
- tls.createServer(options, callback) // Node.js compatible
130
- tls.createSecureContext({ key, cert }) // PEM → { certificateChain, privateKey }
131
- tls.getCiphers() // ['tls_aes_128_gcm_sha256', ...]
132
- tls.DEFAULT_MIN_VERSION // 'TLSv1.2'
133
- tls.DEFAULT_MAX_VERSION // 'TLSv1.3'
131
+
132
+ tls.connect(port, host, options, callback) // Node.js compatible
133
+ tls.createServer(options, callback) // Node.js compatible — returns tls.Server
134
+ tls.createSecureContext({ key, cert }) // PEM → opaque SecureContext
135
+ tls.checkServerIdentity(hostname, cert) // RFC 6125 hostname verification
136
+ tls.getCiphers() // ['tls_aes_128_gcm_sha256', ...]
137
+ tls.DEFAULT_MIN_VERSION // 'TLSv1.2'
138
+ tls.DEFAULT_MAX_VERSION // 'TLSv1.3'
139
+ tls.DEFAULT_CIPHERS // 'TLS_AES_256_GCM_SHA384:...'
140
+ tls.DEFAULT_ECDH_CURVE // 'auto'
141
+ ```
142
+
143
+ ### `tls.Server` (returned by `createServer`)
144
+
145
+ ```js
146
+ server.listen(port, host?, callback?) // Start listening
147
+ server.close(callback?) // Stop accepting new connections
148
+ server.setSecureContext({ key, cert }) // Runtime cert rotation (Let's Encrypt, etc.)
149
+ server.getTicketKeys() // 48-byte Buffer (ticket encryption keys)
150
+ server.setTicketKeys(keys) // For clustered deployments
151
+ server.address() // { port, family, address }
134
152
  ```
135
-
153
+
154
+ **Server events:**
155
+
156
+ | Event | Callback | Description |
157
+ |---|---|---|
158
+ | `secureConnection` | `(socket)` | Handshake complete — handle the new connection |
159
+ | `tlsClientError` | `(err, socket)` | Client handshake failed |
160
+ | `keylog` | `(line, socket)` | SSLKEYLOGFILE-format line (for Wireshark) |
161
+ | `newSession` | `(id, data, cb)` | Store a TLS 1.2 Session ID (for custom session stores) |
162
+ | `resumeSession` | `(id, cb)` | Look up a TLS 1.2 Session ID (for custom session stores) |
163
+ | `error` / `close` | — | Transport-level |
164
+
136
165
  ### `TLSSocket`
137
-
166
+
138
167
  High-level wrapper extending `stream.Duplex`, API-compatible with Node.js [`tls.TLSSocket`](https://nodejs.org/api/tls.html#class-tlstlssocket).
139
-
168
+
140
169
  #### Constructor Options
141
-
170
+
142
171
  **Standard (Node.js compatible):**
143
-
172
+
144
173
  | Option | Type | Description |
145
174
  |---|---|---|
146
175
  | `isServer` | boolean | Server or client mode |
@@ -152,33 +181,33 @@ High-level wrapper extending `stream.Duplex`, API-compatible with Node.js [`tls.
152
181
  | `rejectUnauthorized` | boolean | Validate peer certificate (default: `true`) |
153
182
  | `ca` | Buffer/string | CA certificate(s) for validation |
154
183
  | `ticketKeys` | Buffer | 48-byte key for session ticket encryption (server) |
155
- | `session` | object | Saved ticket data from `'session'` event (client resumption) |
184
+ | `session` | Buffer | Saved session blob from `'session'` event (client resumption) |
156
185
  | `requestCert` | boolean | Request client certificate (server) |
157
186
  | `cert` | Buffer/string | Client certificate PEM (for mTLS) |
158
187
  | `key` | Buffer/string | Client private key PEM (for mTLS) |
159
-
188
+
160
189
  **LemonTLS-only (not available in Node.js):**
161
-
190
+
162
191
  | Option | Type | Description |
163
192
  |---|---|---|
164
- | `noTickets` | boolean | Disable session tickets (in Node.js requires `openssl.cnf`) |
193
+ | `sessionTickets` | boolean | Enable/disable session tickets (default: `true`) |
165
194
  | `signatureAlgorithms` | number[] | Per-connection sigalg list, e.g. `[0x0804]` for RSA-PSS only |
166
195
  | `groups` | number[] | Per-connection curves, e.g. `[0x001d]` for X25519 only |
167
196
  | `prioritizeChaCha` | boolean | Move ChaCha20-Poly1305 before AES in cipher preference |
168
197
  | `maxRecordSize` | number | Max plaintext per TLS record (default: 16384) |
169
- | `allowedCipherSuites` | number[] | Whitelist only these ciphers are offered |
198
+ | `allowedCipherSuites` | number[] | Whitelist - only these ciphers are offered |
170
199
  | `pins` | string[] | Certificate pinning: `['sha256/AAAA...']` |
171
200
  | `handshakeTimeout` | number | Abort handshake after N ms |
172
- | `maxHandshakeSize` | number | Max handshake bytes DoS protection |
201
+ | `maxHandshakeSize` | number | Max handshake bytes - DoS protection |
173
202
  | `certificateCallback` | function | Dynamic cert selection: `(info, cb) => cb(null, ctx)` |
174
-
203
+
175
204
  #### Events
176
-
205
+
177
206
  | Event | Callback | Description |
178
207
  |---|---|---|
179
208
  | `secureConnect` | `()` | Handshake complete, data can flow |
180
209
  | `data` | `(Buffer)` | Decrypted application data received |
181
- | `session` | `(ticketData)` | New session ticket available for resumption |
210
+ | `session` | `(Buffer)` | **Opaque session blob** pass back to `connect({ session })` to resume |
182
211
  | `keyUpdate` | `(direction)` | Traffic keys refreshed: `'send'` or `'receive'` |
183
212
  | `keylog` | `(Buffer)` | SSLKEYLOGFILE-format line (for Wireshark) |
184
213
  | `clienthello` | `(raw, parsed)` | Raw ClientHello received (server, for JA3) |
@@ -186,61 +215,74 @@ High-level wrapper extending `stream.Duplex`, API-compatible with Node.js [`tls.
186
215
  | `certificateRequest` | `(msg)` | Server requested a client certificate |
187
216
  | `error` | `(Error)` | TLS or transport error |
188
217
  | `close` | `()` | Connection closed |
189
-
218
+
190
219
  #### Properties & Methods
191
-
220
+
192
221
  **Node.js compatible:**
193
-
222
+
194
223
  | | |
195
224
  |---|---|
196
225
  | `socket.getProtocol()` | `'TLSv1.3'` or `'TLSv1.2'` |
197
226
  | `socket.getCipher()` | `{ name, standardName, version }` |
198
227
  | `socket.getPeerCertificate()` | `{ subject, issuer, valid_from, fingerprint256, raw, ... }` |
199
- | `socket.isResumed` | `true` if PSK resumption was used |
200
- | `socket.isSessionReused()` | Same as `isResumed` (Node.js compat) |
228
+ | `socket.getPeerX509Certificate()` | Native `crypto.X509Certificate` of the peer's leaf cert |
229
+ | `socket.getCertificate()` | Info about **our** local cert (mirror of `getPeerCertificate`) |
230
+ | `socket.getX509Certificate()` | Native `crypto.X509Certificate` of our local cert |
231
+ | `socket.getSession()` | Opaque serialized session as `Buffer` (or `undefined`) |
232
+ | `socket.getTLSTicket()` | TLS 1.2 raw ticket (`Buffer` or `undefined`) |
233
+ | `socket.getFinished()` | Local Finished verify_data (Buffer) |
234
+ | `socket.getPeerFinished()` | Peer Finished verify_data (Buffer) |
235
+ | `socket.getSharedSigalgs()` | Array of shared signature algorithm names (server-side) |
236
+ | `socket.getEphemeralKeyInfo()` | `{ type: 'X25519', size: 253 }` |
237
+ | `socket.exportKeyingMaterial(len, label, ctx)` | RFC 5705 keying material |
238
+ | `socket.isSessionReused()` | `true` if session was resumed |
239
+ | `socket.setMaxSendFragment(size)` | Cap outgoing record plaintext size `[512, 16384]` |
240
+ | `socket.setServername(name)` | Set SNI (client-side, before handshake) |
241
+ | `socket.disableRenegotiation()` | No-op stub (TLS 1.3 removed renegotiation) |
242
+ | `socket.enableTrace()` | No-op stub (use `keylog` / `handshakeMessage` for insight) |
201
243
  | `socket.authorized` | `true` if peer certificate is valid |
202
244
  | `socket.authorizationError` | Error string or `null` |
203
245
  | `socket.alpnProtocol` | Negotiated ALPN protocol or `false` |
246
+ | `socket.servername` | SNI value (string or `false`) |
204
247
  | `socket.encrypted` | Always `true` |
205
- | `socket.getFinished()` | Local Finished verify_data (Buffer) |
206
- | `socket.getPeerFinished()` | Peer Finished verify_data (Buffer) |
207
- | `socket.exportKeyingMaterial(len, label, ctx)` | RFC 5705 keying material |
208
- | `socket.getEphemeralKeyInfo()` | `{ type: 'X25519', size: 253 }` |
248
+ | `socket.remoteAddress` / `.remotePort` | Peer address (delegated to transport) |
249
+ | `socket.setNoDelay()` / `.setKeepAlive()` / `.setTimeout()` | Transport delegation |
209
250
  | `socket.write(data)` | Send encrypted application data |
210
251
  | `socket.end()` | Send `close_notify` alert and close |
211
-
252
+
212
253
  **LemonTLS-only:**
213
-
254
+
214
255
  | | |
215
256
  |---|---|
216
- | `socket.getSession()` | Access the underlying `TLSSession` |
257
+ | `socket.session` | Access the underlying `TLSSession` (low-level state machine) |
258
+ | `socket.isResumed` | Alias for `isSessionReused()` |
217
259
  | `socket.handshakeDuration` | Handshake time in ms |
218
- | `socket.getJA3()` | `{ hash, raw }` JA3 fingerprint (server-side) |
260
+ | `socket.getJA3()` | `{ hash, raw }` - JA3 fingerprint (server-side) |
219
261
  | `socket.getSharedSecret()` | ECDHE shared secret (Buffer) |
220
262
  | `socket.getNegotiationResult()` | `{ version, cipher, group, sni, alpn, resumed, helloRetried, ... }` |
221
263
  | `socket.rekeySend()` | Refresh outgoing encryption keys (TLS 1.3) |
222
264
  | `socket.rekeyBoth()` | Refresh keys for both directions (TLS 1.3) |
223
-
265
+
224
266
  ### `TLSSession`
225
-
226
- The **core state machine** for a TLS connection. Performs handshake, key derivation, and state management but does **no I/O**. You provide the transport.
227
-
267
+
268
+ The **core state machine** for a TLS connection. Performs handshake, key derivation, and state management - but does **no I/O**. You provide the transport.
269
+
228
270
  This is the API to use for QUIC, DTLS, or any custom transport.
229
-
271
+
230
272
  ```js
231
273
  import { TLSSession } from 'lemon-tls';
232
-
274
+
233
275
  const session = new TLSSession({ isServer: true });
234
-
276
+
235
277
  // Feed incoming handshake bytes from your transport:
236
278
  session.message(handshakeBytes);
237
-
279
+
238
280
  // Session tells you what to send:
239
281
  session.on('message', (epoch, seq, type, data) => {
240
282
  // epoch: 0=cleartext, 1=handshake-encrypted, 2=app-encrypted
241
283
  myTransport.send(data);
242
284
  });
243
-
285
+
244
286
  session.on('hello', () => {
245
287
  session.set_context({
246
288
  local_supported_versions: [0x0304],
@@ -249,73 +291,73 @@ session.on('hello', () => {
249
291
  cert_private_key: myKey,
250
292
  });
251
293
  });
252
-
294
+
253
295
  session.on('secureConnect', () => {
254
296
  const secrets = session.getTrafficSecrets();
255
297
  const result = session.getNegotiationResult();
256
298
  console.log(session.handshakeDuration, 'ms');
257
299
  });
258
-
300
+
259
301
  // Key Update
260
302
  session.requestKeyUpdate(true); // true = request peer to update too
261
303
  session.on('keyUpdate', ({ direction, secret }) => { /* ... */ });
262
-
263
- // PSK callback full control over ticket validation (server)
304
+
305
+ // PSK callback - full control over ticket validation (server)
264
306
  session.on('psk', (identity, callback) => {
265
307
  const psk = myTicketStore.lookup(identity);
266
308
  callback(psk ? { psk, cipher: 0x1301 } : null);
267
309
  });
268
-
310
+
269
311
  // JA3 fingerprinting (server)
270
312
  session.on('clienthello', (raw, parsed) => {
271
313
  console.log(session.getJA3()); // { hash: 'abc...', raw: '769,47-53,...' }
272
314
  });
273
315
  ```
274
-
316
+
275
317
  ### Record Layer Module
276
-
318
+
277
319
  Shared encrypt/decrypt primitives for QUIC, DTLS, and custom transport consumers:
278
-
320
+
279
321
  ```js
280
322
  import { deriveKeys, encryptRecord, decryptRecord, getNonce, getAeadAlgo }
281
323
  from 'lemon-tls/record';
282
-
324
+
283
325
  const { key, iv } = deriveKeys(trafficSecret, cipherSuite);
284
326
  const nonce = getNonce(iv, sequenceNumber);
285
327
  const algo = getAeadAlgo(cipherSuite); // 'aes-128-gcm' | 'chacha20-poly1305'
286
328
  const encrypted = encryptRecord(contentType, plaintext, key, nonce, algo);
287
329
  ```
288
-
330
+
289
331
  ## 🔧 Advanced Options (Not Available in Node.js)
290
-
291
- LemonTLS gives you control that Node.js doesn't expose without `openssl.cnf` hacks:
292
-
332
+
333
+ LemonTLS gives you control that Node.js doesn't expose - without `openssl.cnf` hacks:
334
+
293
335
  ```js
294
336
  import tls from 'lemon-tls';
295
-
337
+
296
338
  // Per-connection cipher/group/sigalg selection (impossible in Node.js)
297
339
  const socket = tls.connect(443, 'api.example.com', {
298
- groups: [0x001d], // X25519 only (Node: ecdhCurve is global)
299
- signatureAlgorithms: [0x0804], // RSA-PSS-SHA256 only (Node: no control)
300
- prioritizeChaCha: true, // ChaCha20 before AES (Node: no control)
301
- allowedCipherSuites: [0x1301, 0x1303], // whitelist (Node: string-based, error-prone)
340
+ groups: [0x001d], // X25519 only (Node: ecdhCurve is global)
341
+ signatureAlgorithms: [0x0804], // RSA-PSS-SHA256 only (Node: no control)
342
+ prioritizeChaCha: true, // ChaCha20 before AES (Node: no control)
343
+ allowedCipherSuites: [0x1301, 0x1303], // whitelist (Node: string-based, error-prone)
302
344
  });
303
-
345
+
304
346
  // Disable session tickets (in Node.js requires openssl.cnf)
305
- tls.createServer({ key, cert, noTickets: true });
306
-
347
+ tls.createServer({ key, cert, sessionTickets: false });
348
+
307
349
  // Certificate pinning
308
350
  tls.connect(443, 'bank.example.com', {
309
351
  pins: ['sha256/YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg='],
310
352
  });
311
-
312
- // Handshake timeout DoS protection
353
+
354
+ // Handshake timeout - DoS protection
313
355
  tls.connect(443, 'host', { handshakeTimeout: 5000 });
314
-
315
- // Max handshake size prevents oversized certificate chains
356
+
357
+ // Max handshake size - prevents oversized certificate chains
316
358
  tls.createServer({ key, cert, maxHandshakeSize: 65536 });
317
-
318
- // Dynamic certificate selection (beyond SNI based on cipher, version, extensions)
359
+
360
+ // Dynamic certificate selection (beyond SNI - based on cipher, version, extensions)
319
361
  tls.createServer({
320
362
  certificateCallback: (info, cb) => {
321
363
  // info = { servername, version, ciphers, sigalgs, groups, alpns }
@@ -323,17 +365,17 @@ tls.createServer({
323
365
  cb(null, ctx);
324
366
  }
325
367
  });
326
-
368
+
327
369
  // Wireshark debugging
328
370
  socket.on('keylog', (line) => fs.appendFileSync('keys.log', line));
329
371
  // Wireshark: Edit → Preferences → TLS → Pre-Master-Secret log filename → keys.log
330
-
372
+
331
373
  // JA3 fingerprinting (server-side bot detection)
332
374
  server.on('secureConnection', (socket) => {
333
375
  const ja3 = socket.getJA3();
334
376
  console.log(ja3.hash); // 'e7d705a3286e19ea42f587b344ee6865'
335
377
  });
336
-
378
+
337
379
  // Full negotiation result
338
380
  socket.on('secureConnect', () => {
339
381
  console.log(socket.getNegotiationResult());
@@ -342,138 +384,151 @@ socket.on('secureConnect', () => {
342
384
  // sni: 'example.com', alpn: 'h2', resumed: false, helloRetried: false,
343
385
  // handshakeDuration: 23 }
344
386
  });
345
-
387
+
346
388
  // ECDHE shared secret access (for research)
347
389
  console.log(socket.getSharedSecret()); // Buffer<...>
348
390
  ```
349
-
391
+
392
+ ## 🌐 Interoperability
393
+
394
+ LemonTLS is verified against real-world TLS implementations:
395
+
396
+ | Peer | Role | TLS Versions | Notes |
397
+ |---|---|---|---|
398
+ | **Chrome** (browser) | Client | 1.3 | Full HTTPS page loads, favicon, streaming 100KB responses under 3G throttling |
399
+ | **curl** | Client | 1.2 / 1.3 | Including `--curves P-384:X25519` to force HelloRetryRequest |
400
+ | **Node.js `tls`** | Client / Server | 1.2 / 1.3 | Bidirectional interop + session resumption |
401
+ | **openssl s_client** | Client | 1.2 / 1.3 | All supported ciphers & groups |
402
+ | **msquic** | (via QUICO) | 1.3 | HRR + P-384 + AES-256-GCM-SHA384 tested |
403
+
404
+ ## ⚡ Performance
405
+
406
+ Benchmarks on Windows (Node v25.9.0, Lemon↔Lemon localhost, 10MB transfers, median of 25 iterations):
407
+
408
+ | Metric | LemonTLS | Node native | Ratio |
409
+ |---|---|---|---|
410
+ | Upload TLS 1.2 | **459 MB/s** | 680 MB/s | 68% |
411
+ | Upload TLS 1.3 | **301 MB/s** | 640 MB/s | 47% |
412
+ | Download TLS 1.2 (cross-process) | **716 MB/s** | 870 MB/s | 82% |
413
+ | Echo bidirectional TLS 1.3 | **396 MB/s** | — | — |
414
+ | Small burst (100B × 2000) | **1.67M writes/s** | 2.7M writes/s | 62% |
415
+ | OpenSSL s_time handshakes/sec | **1,511** (TLS 1.3), **1,723** (TLS 1.2) | ~1800 | 85–95% |
416
+
417
+ For a pure-JavaScript implementation with zero native dependencies, this is within striking distance of OpenSSL on most paths.
418
+
350
419
  ## 🛣 Roadmap
351
-
420
+
352
421
  ✅ = Completed 🔄 = Implemented, needs testing ⏳ = Planned
353
-
422
+
354
423
  ### ✅ Completed
355
-
424
+
356
425
  | Status | Item |
357
426
  |---|---|
358
- | ✅ | TLS 1.3 Server + Client |
359
- | ✅ | TLS 1.2 Server + Client |
427
+ | ✅ | TLS 1.3 - Server + Client |
428
+ | ✅ | TLS 1.2 - Server + Client |
360
429
  | ✅ | AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305 |
361
- | ✅ | X25519 / P-256 key exchange |
362
- | ✅ | RSA-PSS / ECDSA signatures |
430
+ | ✅ | X25519 / P-256 / **P-384** key exchange |
431
+ | ✅ | RSA-PSS / ECDSA / RSA-PKCS#1 signatures |
363
432
  | ✅ | SNI, ALPN extensions |
433
+ | ✅ | HelloRetryRequest (both client and server side) |
364
434
  | ✅ | Session tickets + PSK resumption (TLS 1.3) |
435
+ | ✅ | Session ID / ticket resumption (TLS 1.2) |
365
436
  | ✅ | Extended Master Secret (RFC 7627, TLS 1.2) |
366
- | ✅ | Certificate validation (dates, hostname, CA chain) |
437
+ | ✅ | Key Update (TLS 1.3) |
438
+ | ✅ | Client Certificate Auth (mTLS) |
439
+ | ✅ | Certificate validation (dates, hostname via `checkServerIdentity`, CA chain) |
367
440
  | ✅ | Alert handling (close_notify, fatal alerts) |
368
- | ✅ | `TLSSocket` Node.js compatible Duplex stream |
369
- | ✅ | `TLSSession` raw state machine for QUIC/DTLS |
370
- | ✅ | `record.js` shared AEAD module for custom transports |
371
- | ✅ | Node.js `tls` compat — `connect()`, `createServer()`, `getCiphers()` |
372
- | ✅ | 27 Node.js API compatibility methods verified |
373
- | ✅ | Zero dependencies `node:crypto` only |
374
- | ✅ | 45 automated tests |
375
-
376
- ### 🔄 Implemented (Needs Testing)
377
-
378
- | Status | Item | Notes |
379
- |---|---|---|
380
- | 🔄 | HelloRetryRequest | Group negotiation fallback, transcript message_hash |
381
- | 🔄 | Key Update (TLS 1.3) | `rekeySend()` / `rekeyBoth()` for long-lived connections |
382
- | 🔄 | Client Certificate Auth | mTLS with `requestCert` / `cert` / `key` options |
383
-
441
+ | ✅ | `TLSSocket` - Node.js compatible Duplex stream |
442
+ | ✅ | `TLSSession` - raw state machine for QUIC/DTLS |
443
+ | ✅ | `record.js` - shared AEAD module for custom transports |
444
+ | ✅ | Node.js `tls` compat — **41 API methods/properties verified** |
445
+ | ✅ | TypeScript typings bundled (`index.d.ts`) |
446
+ | ✅ | DTLS 1.2 baseline (via `DTLSSocket` / `createDTLSServer`) |
447
+ | ✅ | Zero dependencies - `node:crypto` only |
448
+ | ✅ | **72 automated tests** (compat, resumption, data transfer) |
449
+ | | Browser-tested (Chrome) |
450
+
384
451
  ### ⏳ Planned
385
-
452
+
386
453
  | Status | Item | Notes |
387
454
  |---|---|---|
388
- | ⏳ | DTLS 1.2/1.3 | Datagram TLS over UDP |
455
+ | ⏳ | Loss detection + PTO (for QUIC integration) | Buffer ready, needs timers |
389
456
  | ⏳ | 0-RTT Early Data | Risky (replay attacks), low priority |
390
- | ⏳ | Full certificate chain validation | Including revocation checks |
391
- | ⏳ | TypeScript typings | Type safety and IDE integration |
392
- | ⏳ | Benchmarks & performance tuning | Throughput, memory |
457
+ | ⏳ | Full certificate chain validation | Including CA/revocation checks |
458
+ | ⏳ | OCSP stapling | Rare in modern web, low priority |
393
459
  | ⏳ | Fuzz testing | Security hardening |
394
-
395
- ## 🧪 Testing
396
-
397
- ```bash
398
- npm test # 11 tests — core TLS interop (OpenSSL)
399
- node tests/test_https.js # 7 tests — HTTPS server (browser + curl)
400
- node tests/test_compat.js # 27 tests — Node.js API compatibility
401
- ```
402
-
403
- ### Core Tests (`npm test`)
404
-
405
- ```
406
- Server tests (LemonTLS server ↔ openssl s_client):
407
- ✅ TLS 1.3 — handshake + bidirectional data
408
- ✅ TLS 1.2 — handshake + bidirectional data
409
- ✅ ChaCha20-Poly1305 — cipher negotiation
410
- ✅ Session ticket — sent to client
411
-
412
- Client tests (Node.js tls server ↔ LemonTLS client):
413
- ✅ TLS 1.3 — handshake + bidirectional data
414
- ✅ TLS 1.2 — handshake + bidirectional data
415
-
416
- Resumption (LemonTLS ↔ LemonTLS):
417
- ✅ PSK — full handshake → ticket → resumed connection
418
-
419
- Node.js compat API:
420
- ✅ tls.connect() / tls.createServer() / getCiphers()
421
- ✅ isSessionReused / getFinished / exportKeyingMaterial / ...
422
- ```
423
-
424
- ### HTTPS Integration Test
425
-
426
- ```bash
427
- node tests/test_https.js
428
- ```
429
-
430
- Starts a real HTTPS server powered by LemonTLS. After tests pass, open in your browser:
431
-
460
+
461
+ ### Compatibility Test Summary
462
+
432
463
  ```
433
- https://localhost:19600/
464
+ Module API: exports, getCiphers, createSecureContext,
465
+ DEFAULT_MIN_VERSION / MAX_VERSION / CIPHERS / ECDH_CURVE,
466
+ checkServerIdentity
467
+ tls.connect(): positional + options-object forms
468
+ tls.createServer(): options + callback + "secureConnection" event
469
+ TLSSocket methods: getProtocol, getCipher, getPeerCertificate,
470
+ getPeerX509Certificate, getCertificate, getX509Certificate,
471
+ getSession, getTLSTicket, getSharedSigalgs,
472
+ isSessionReused, getFinished, getPeerFinished,
473
+ exportKeyingMaterial, getEphemeralKeyInfo,
474
+ disableRenegotiation, enableTrace, setServername,
475
+ setMaxSendFragment
476
+ Server methods: setSecureContext, getTicketKeys / setTicketKeys
477
+ Properties: .encrypted, .authorized, .alpnProtocol, .servername
478
+ Transport delegation: remoteAddress, remotePort, setNoDelay, setKeepAlive,
479
+ setTimeout
480
+ Events: 'session' (Buffer), 'keylog', 'tlsClientError'
481
+ Stream behavior: write/read echo, pipe, 200KB record fragmentation
482
+ Resumption: connect({session}) round-trip with isSessionReused()
434
483
  ```
435
-
436
- Requires: Node.js ≥ 16, OpenSSL in PATH.
437
-
484
+
438
485
  ## 📁 Project Structure
439
-
486
+
440
487
  ```
441
- index.js exports: TLSSocket, TLSSession, connect, createServer, crypto, wire, record
488
+ index.js - ESM entry: TLSSocket, TLSSession, connect,
489
+ createServer, checkServerIdentity, crypto, wire, record
490
+ index.cjs - CommonJS wrapper
491
+ index.d.ts - TypeScript definitions
442
492
  src/
443
- tls_session.js TLS state machine (reactive set_context pattern)
444
- tls_socket.js Duplex stream wrapper, Node.js compatible API
445
- record.js shared AEAD encrypt/decrypt, key derivation
446
- wire.js binary encode/decode of all TLS messages + constants
447
- crypto.js key schedule (HKDF, PRF, resumption primitives)
448
- compat.js Node.js tls API wrappers (connect, createServer, etc.)
449
- secure_context.js — PEM/DER cert/key loading
450
- utils.js array helpers
493
+ tls_session.js - TLS state machine (reactive set_context pattern)
494
+ tls_socket.js - Duplex stream wrapper, Node.js compatible API
495
+ record.js - shared AEAD encrypt/decrypt, key derivation
496
+ wire.js - binary encode/decode of all TLS messages + constants
497
+ crypto.js - key schedule (HKDF, PRF, resumption primitives)
498
+ compat.js - Node.js tls API wrappers (connect, createServer,
499
+ checkServerIdentity, Server)
500
+ secure_context.js - PEM/DER cert/key loading
501
+ utils.js - array helpers
502
+ dtls_session.js - DTLS state machine
503
+ dtls_socket.js - DTLS socket wrapper (UDP transport)
451
504
  session/
452
- signing.js signature scheme selection + signing
453
- ecdh.js X25519/P-256 key exchange
454
- message.js high-level message build/parse
455
- tests/
456
- test_all.js — automated suite (npm test)
457
- test_https.js — HTTPS integration (stays running for browser)
458
- test_compat.js — Node.js API compatibility
505
+ signing.js - signature scheme selection + signing
506
+ ecdh.js - X25519 / P-256 / P-384 key exchange
507
+ message.js - high-level message build/parse
508
+ ticket.js - TLS 1.2 session ticket encryption
459
509
  ```
460
-
510
+
461
511
  ## 🤝 Contributing
462
-
463
- Pull requests are welcome!
512
+
513
+ Pull requests are welcome!
464
514
  Please open an issue before submitting major changes.
465
-
515
+
466
516
  ## 💖 Sponsors
467
-
468
- This project is part of the [colocohen](https://github.com/colocohen) Node.js infrastructure stack (QUIC, WebRTC, DNSSEC, TLS, and more).
517
+
518
+ This project is part of the [colocohen](https://github.com/colocohen) Node.js infrastructure stack (QUIC, WebRTC, DNSSEC, TLS, and more).
469
519
  You can support ongoing development via [GitHub Sponsors](https://github.com/sponsors/colocohen).
470
-
520
+
471
521
  ## 📚 References
472
-
522
+
473
523
  * [RFC 8446 – TLS 1.3](https://datatracker.ietf.org/doc/html/rfc8446)
474
524
  * [RFC 5246 – TLS 1.2](https://datatracker.ietf.org/doc/html/rfc5246)
525
+ * [RFC 6066 – TLS Extensions (SNI)](https://datatracker.ietf.org/doc/html/rfc6066)
526
+ * [RFC 7301 – ALPN](https://datatracker.ietf.org/doc/html/rfc7301)
475
527
  * [RFC 7627 – Extended Master Secret](https://datatracker.ietf.org/doc/html/rfc7627)
476
-
528
+ * [RFC 6125 – Hostname Verification](https://datatracker.ietf.org/doc/html/rfc6125)
529
+ * [RFC 5705 – Exported Keying Material](https://datatracker.ietf.org/doc/html/rfc5705)
530
+ * [RFC 5077 – Stateless Session Resumption (TLS 1.2 tickets)](https://datatracker.ietf.org/doc/html/rfc5077)
531
+
477
532
  ## 📜 License
478
533
 
479
534
  **Apache License 2.0**