lemon-tls 0.2.2 → 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 +258 -203
- package/index.d.ts +145 -14
- package/index.js +12 -0
- package/package.json +1 -1
- package/src/compat.js +290 -31
- package/src/crypto.js +127 -7
- package/src/record.js +408 -61
- package/src/session/message.js +27 -2
- package/src/session/ticket.js +185 -0
- package/src/tls_session.js +780 -94
- package/src/tls_socket.js +815 -249
- package/src/wire.js +25 -0
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'
|
|
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
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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.
|
|
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)
|
|
129
|
-
tls.createServer(options, callback)
|
|
130
|
-
tls.createSecureContext({ key, cert })
|
|
131
|
-
tls.
|
|
132
|
-
tls.
|
|
133
|
-
tls.
|
|
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` |
|
|
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
|
-
| `
|
|
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
|
|
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
|
|
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` | `(
|
|
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.
|
|
200
|
-
| `socket.
|
|
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.
|
|
206
|
-
| `socket.
|
|
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.
|
|
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 }`
|
|
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
|
|
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
|
|
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
|
|
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],
|
|
299
|
-
signatureAlgorithms: [0x0804],
|
|
300
|
-
prioritizeChaCha: true,
|
|
301
|
-
allowedCipherSuites: [0x1301, 0x1303],
|
|
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,
|
|
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
|
|
353
|
+
|
|
354
|
+
// Handshake timeout - DoS protection
|
|
313
355
|
tls.connect(443, 'host', { handshakeTimeout: 5000 });
|
|
314
|
-
|
|
315
|
-
// Max handshake size
|
|
356
|
+
|
|
357
|
+
// Max handshake size - prevents oversized certificate chains
|
|
316
358
|
tls.createServer({ key, cert, maxHandshakeSize: 65536 });
|
|
317
|
-
|
|
318
|
-
// Dynamic certificate selection (beyond SNI
|
|
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
|
|
359
|
-
| ✅ | TLS 1.2
|
|
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
|
-
| ✅ |
|
|
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`
|
|
369
|
-
| ✅ | `TLSSession`
|
|
370
|
-
| ✅ | `record.js`
|
|
371
|
-
| ✅ | Node.js `tls` compat —
|
|
372
|
-
| ✅ |
|
|
373
|
-
| ✅ |
|
|
374
|
-
| ✅ |
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
| ⏳ |
|
|
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
|
-
| ⏳ |
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
444
|
-
tls_socket.js
|
|
445
|
-
record.js
|
|
446
|
-
wire.js
|
|
447
|
-
crypto.js
|
|
448
|
-
compat.js
|
|
449
|
-
|
|
450
|
-
|
|
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
|
|
453
|
-
ecdh.js
|
|
454
|
-
message.js
|
|
455
|
-
|
|
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**
|