lemon-tls 0.1.1 → 0.2.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 +431 -119
- package/index.cjs +28 -0
- package/index.d.ts +283 -0
- package/index.js +70 -18
- package/lemontls.svg +1 -1
- package/package.json +120 -62
- package/src/compat.js +235 -0
- package/src/crypto.js +580 -0
- package/src/record.js +232 -0
- package/{secure_context.js → src/secure_context.js} +196 -196
- package/src/session/ecdh.js +61 -0
- package/src/session/message.js +203 -0
- package/src/session/signing.js +129 -0
- package/src/tls_session.js +2204 -0
- package/src/tls_socket.js +877 -0
- package/{utils.js → src/utils.js} +100 -88
- package/{wire.js → src/wire.js} +1499 -1668
- package/crypto.js +0 -391
- package/tls_server.js +0 -0
- package/tls_session.js +0 -1440
- package/tls_socket.js +0 -453
package/README.md
CHANGED
|
@@ -16,151 +16,464 @@
|
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
---
|
|
19
|
-
|
|
20
|
-
> **⚠️ Project status:
|
|
19
|
+
|
|
20
|
+
> **⚠️ Project status: *Active development*.**
|
|
21
21
|
> APIs may change without notice until we reach v1.0.
|
|
22
22
|
> Use at your own risk and please report issues!
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
|
|
26
24
|
## ✨ Features
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
|
|
26
|
+
* 🔒 **Pure JavaScript** – no OpenSSL, no native bindings. Zero dependencies.
|
|
27
|
+
* ⚡ **TLS 1.3 (RFC 8446)** + **TLS 1.2** – both server and client.
|
|
28
|
+
* 🔑 **Key Access** – read handshake secrets, traffic keys, ECDHE shared secret, and resumption data at any point.
|
|
29
|
+
* 🔁 **Session Resumption** – session tickets + PSK with binder validation.
|
|
30
|
+
* 🔄 **Key Update** – refresh traffic keys on long-lived TLS 1.3 connections.
|
|
31
|
+
* 🔃 **HelloRetryRequest** – automatic group negotiation fallback.
|
|
32
|
+
* 📜 **Client Certificate Auth** – mutual TLS (mTLS) with `requestCert` / `cert` / `key` options.
|
|
33
|
+
* 🛡 **Designed for extensibility** – exposes cryptographic keys and record-layer primitives for QUIC, DTLS, or custom transports.
|
|
34
|
+
* 🧩 **Two API levels** – high-level `TLSSocket` (drop-in Node.js Duplex stream) and low-level `TLSSession` (state machine only, you handle the transport).
|
|
35
|
+
* 🔧 **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
|
+
|
|
36
37
|
## 📦 Installation
|
|
37
|
-
|
|
38
|
+
|
|
39
|
+
```
|
|
38
40
|
npm i lemon-tls
|
|
39
41
|
```
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
|
|
43
|
+
## 🚀 Quick Start
|
|
44
|
+
|
|
45
|
+
### Drop-in Node.js Replacement
|
|
46
|
+
|
|
44
47
|
```js
|
|
45
|
-
import
|
|
48
|
+
import tls from 'lemon-tls'; // not 'node:tls' — same API
|
|
46
49
|
import fs from 'node:fs';
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
50
|
+
|
|
51
|
+
// Server
|
|
52
|
+
const server = tls.createServer({
|
|
53
|
+
key: fs.readFileSync('server.key'),
|
|
54
|
+
cert: fs.readFileSync('server.crt'),
|
|
55
|
+
}, (socket) => {
|
|
56
|
+
console.log('Protocol:', socket.getProtocol());
|
|
57
|
+
console.log('Cipher:', socket.getCipher().name);
|
|
58
|
+
socket.write('Hello from LemonTLS!\n');
|
|
59
|
+
});
|
|
60
|
+
server.listen(8443);
|
|
61
|
+
|
|
62
|
+
// Client
|
|
63
|
+
const socket = tls.connect(8443, 'localhost', { rejectUnauthorized: false }, () => {
|
|
64
|
+
socket.write('Hello from client!\n');
|
|
65
|
+
});
|
|
66
|
+
socket.on('data', (d) => console.log(d.toString()));
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Low-Level: TLSSocket with TCP
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
import net from 'node:net';
|
|
73
|
+
import { TLSSocket, createSecureContext } from 'lemon-tls';
|
|
74
|
+
|
|
75
|
+
const server = net.createServer((tcp) => {
|
|
76
|
+
const socket = new TLSSocket(tcp, {
|
|
77
|
+
isServer: true,
|
|
78
|
+
SNICallback: (servername, cb) => {
|
|
79
|
+
cb(null, createSecureContext({
|
|
80
|
+
key: fs.readFileSync('server.key'),
|
|
81
|
+
cert: fs.readFileSync('server.crt'),
|
|
62
82
|
}));
|
|
63
83
|
}
|
|
64
84
|
});
|
|
65
|
-
|
|
66
|
-
socket.on('
|
|
67
|
-
console.log('[SRV] secure handshake established');
|
|
68
|
-
|
|
69
|
-
socket.write(new TextEncoder().encode('hi'));
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
socket.on('data', function(c){
|
|
73
|
-
// echo
|
|
74
|
-
socket.write(c);
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
socket.on('error', function(e){ console.error('[SRV TLS ERROR]', e); });
|
|
78
|
-
socket.on('close', function(){ console.log('[SRV] closed'); });
|
|
85
|
+
socket.on('secureConnect', () => socket.write('hi\n'));
|
|
86
|
+
socket.on('data', (d) => console.log('Got:', d.toString()));
|
|
79
87
|
});
|
|
80
|
-
|
|
81
|
-
server.listen(8443, function(){ console.log('[SRV] listening 8443'); });
|
|
82
|
-
|
|
88
|
+
server.listen(8443);
|
|
83
89
|
```
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
|
|
91
|
+
### Session Resumption (PSK)
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
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)
|
|
100
|
+
const socket2 = tls.connect(8443, 'localhost', { session: savedSession }, () => {
|
|
101
|
+
console.log('Resumed:', socket2.isResumed); // true
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Mutual TLS (Client Certificate)
|
|
106
|
+
|
|
107
|
+
```js
|
|
108
|
+
// Server: request client certificate
|
|
109
|
+
const server = tls.createServer({
|
|
110
|
+
key: serverKey, cert: serverCert,
|
|
111
|
+
requestCert: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Client: provide certificate
|
|
115
|
+
const socket = tls.connect(8443, 'localhost', {
|
|
116
|
+
cert: fs.readFileSync('client.crt'),
|
|
117
|
+
key: fs.readFileSync('client.key'),
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
87
121
|
## 📚 API
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
122
|
+
|
|
123
|
+
### Module-Level Functions
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
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'
|
|
134
|
+
```
|
|
135
|
+
|
|
97
136
|
### `TLSSocket`
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
137
|
+
|
|
138
|
+
High-level wrapper extending `stream.Duplex`, API-compatible with Node.js [`tls.TLSSocket`](https://nodejs.org/api/tls.html#class-tlstlssocket).
|
|
139
|
+
|
|
140
|
+
#### Constructor Options
|
|
141
|
+
|
|
142
|
+
**Standard (Node.js compatible):**
|
|
143
|
+
|
|
144
|
+
| Option | Type | Description |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| `isServer` | boolean | Server or client mode |
|
|
147
|
+
| `servername` | string | SNI hostname (client) |
|
|
148
|
+
| `SNICallback` | function | `(servername, cb) => cb(null, secureContext)` (server) |
|
|
149
|
+
| `minVersion` | string | `'TLSv1.2'` or `'TLSv1.3'` |
|
|
150
|
+
| `maxVersion` | string | `'TLSv1.2'` or `'TLSv1.3'` |
|
|
151
|
+
| `ALPNProtocols` | string[] | Offered ALPN protocols |
|
|
152
|
+
| `rejectUnauthorized` | boolean | Validate peer certificate (default: `true`) |
|
|
153
|
+
| `ca` | Buffer/string | CA certificate(s) for validation |
|
|
154
|
+
| `ticketKeys` | Buffer | 48-byte key for session ticket encryption (server) |
|
|
155
|
+
| `session` | object | Saved ticket data from `'session'` event (client resumption) |
|
|
156
|
+
| `requestCert` | boolean | Request client certificate (server) |
|
|
157
|
+
| `cert` | Buffer/string | Client certificate PEM (for mTLS) |
|
|
158
|
+
| `key` | Buffer/string | Client private key PEM (for mTLS) |
|
|
159
|
+
|
|
160
|
+
**LemonTLS-only (not available in Node.js):**
|
|
161
|
+
|
|
162
|
+
| Option | Type | Description |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `noTickets` | boolean | Disable session tickets (in Node.js requires `openssl.cnf`) |
|
|
165
|
+
| `signatureAlgorithms` | number[] | Per-connection sigalg list, e.g. `[0x0804]` for RSA-PSS only |
|
|
166
|
+
| `groups` | number[] | Per-connection curves, e.g. `[0x001d]` for X25519 only |
|
|
167
|
+
| `prioritizeChaCha` | boolean | Move ChaCha20-Poly1305 before AES in cipher preference |
|
|
168
|
+
| `maxRecordSize` | number | Max plaintext per TLS record (default: 16384) |
|
|
169
|
+
| `allowedCipherSuites` | number[] | Whitelist — only these ciphers are offered |
|
|
170
|
+
| `pins` | string[] | Certificate pinning: `['sha256/AAAA...']` |
|
|
171
|
+
| `handshakeTimeout` | number | Abort handshake after N ms |
|
|
172
|
+
| `maxHandshakeSize` | number | Max handshake bytes — DoS protection |
|
|
173
|
+
| `certificateCallback` | function | Dynamic cert selection: `(info, cb) => cb(null, ctx)` |
|
|
174
|
+
|
|
175
|
+
#### Events
|
|
176
|
+
|
|
177
|
+
| Event | Callback | Description |
|
|
178
|
+
|---|---|---|
|
|
179
|
+
| `secureConnect` | `()` | Handshake complete, data can flow |
|
|
180
|
+
| `data` | `(Buffer)` | Decrypted application data received |
|
|
181
|
+
| `session` | `(ticketData)` | New session ticket available for resumption |
|
|
182
|
+
| `keyUpdate` | `(direction)` | Traffic keys refreshed: `'send'` or `'receive'` |
|
|
183
|
+
| `keylog` | `(Buffer)` | SSLKEYLOGFILE-format line (for Wireshark) |
|
|
184
|
+
| `clienthello` | `(raw, parsed)` | Raw ClientHello received (server, for JA3) |
|
|
185
|
+
| `handshakeMessage` | `(type, raw, parsed)` | Every handshake message (debugging) |
|
|
186
|
+
| `certificateRequest` | `(msg)` | Server requested a client certificate |
|
|
187
|
+
| `error` | `(Error)` | TLS or transport error |
|
|
188
|
+
| `close` | `()` | Connection closed |
|
|
189
|
+
|
|
190
|
+
#### Properties & Methods
|
|
191
|
+
|
|
192
|
+
**Node.js compatible:**
|
|
193
|
+
|
|
194
|
+
| | |
|
|
195
|
+
|---|---|
|
|
196
|
+
| `socket.getProtocol()` | `'TLSv1.3'` or `'TLSv1.2'` |
|
|
197
|
+
| `socket.getCipher()` | `{ name, standardName, version }` |
|
|
198
|
+
| `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) |
|
|
201
|
+
| `socket.authorized` | `true` if peer certificate is valid |
|
|
202
|
+
| `socket.authorizationError` | Error string or `null` |
|
|
203
|
+
| `socket.alpnProtocol` | Negotiated ALPN protocol or `false` |
|
|
204
|
+
| `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 }` |
|
|
209
|
+
| `socket.write(data)` | Send encrypted application data |
|
|
210
|
+
| `socket.end()` | Send `close_notify` alert and close |
|
|
211
|
+
|
|
212
|
+
**LemonTLS-only:**
|
|
213
|
+
|
|
214
|
+
| | |
|
|
215
|
+
|---|---|
|
|
216
|
+
| `socket.getSession()` | Access the underlying `TLSSession` |
|
|
217
|
+
| `socket.handshakeDuration` | Handshake time in ms |
|
|
218
|
+
| `socket.getJA3()` | `{ hash, raw }` — JA3 fingerprint (server-side) |
|
|
219
|
+
| `socket.getSharedSecret()` | ECDHE shared secret (Buffer) |
|
|
220
|
+
| `socket.getNegotiationResult()` | `{ version, cipher, group, sni, alpn, resumed, helloRetried, ... }` |
|
|
221
|
+
| `socket.rekeySend()` | Refresh outgoing encryption keys (TLS 1.3) |
|
|
222
|
+
| `socket.rekeyBoth()` | Refresh keys for both directions (TLS 1.3) |
|
|
223
|
+
|
|
224
|
+
### `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
|
+
|
|
228
|
+
This is the API to use for QUIC, DTLS, or any custom transport.
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
import { TLSSession } from 'lemon-tls';
|
|
232
|
+
|
|
233
|
+
const session = new TLSSession({ isServer: true });
|
|
234
|
+
|
|
235
|
+
// Feed incoming handshake bytes from your transport:
|
|
236
|
+
session.message(handshakeBytes);
|
|
237
|
+
|
|
238
|
+
// Session tells you what to send:
|
|
239
|
+
session.on('message', (epoch, seq, type, data) => {
|
|
240
|
+
// epoch: 0=cleartext, 1=handshake-encrypted, 2=app-encrypted
|
|
241
|
+
myTransport.send(data);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
session.on('hello', () => {
|
|
245
|
+
session.set_context({
|
|
246
|
+
local_supported_versions: [0x0304],
|
|
247
|
+
local_supported_cipher_suites: [0x1301, 0x1302, 0x1303],
|
|
248
|
+
local_cert_chain: myCerts,
|
|
249
|
+
cert_private_key: myKey,
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
session.on('secureConnect', () => {
|
|
254
|
+
const secrets = session.getTrafficSecrets();
|
|
255
|
+
const result = session.getNegotiationResult();
|
|
256
|
+
console.log(session.handshakeDuration, 'ms');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Key Update
|
|
260
|
+
session.requestKeyUpdate(true); // true = request peer to update too
|
|
261
|
+
session.on('keyUpdate', ({ direction, secret }) => { /* ... */ });
|
|
262
|
+
|
|
263
|
+
// PSK callback — full control over ticket validation (server)
|
|
264
|
+
session.on('psk', (identity, callback) => {
|
|
265
|
+
const psk = myTicketStore.lookup(identity);
|
|
266
|
+
callback(psk ? { psk, cipher: 0x1301 } : null);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// JA3 fingerprinting (server)
|
|
270
|
+
session.on('clienthello', (raw, parsed) => {
|
|
271
|
+
console.log(session.getJA3()); // { hash: 'abc...', raw: '769,47-53,...' }
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Record Layer Module
|
|
276
|
+
|
|
277
|
+
Shared encrypt/decrypt primitives for QUIC, DTLS, and custom transport consumers:
|
|
278
|
+
|
|
279
|
+
```js
|
|
280
|
+
import { deriveKeys, encryptRecord, decryptRecord, getNonce, getAeadAlgo }
|
|
281
|
+
from 'lemon-tls/record';
|
|
282
|
+
|
|
283
|
+
const { key, iv } = deriveKeys(trafficSecret, cipherSuite);
|
|
284
|
+
const nonce = getNonce(iv, sequenceNumber);
|
|
285
|
+
const algo = getAeadAlgo(cipherSuite); // 'aes-128-gcm' | 'chacha20-poly1305'
|
|
286
|
+
const encrypted = encryptRecord(contentType, plaintext, key, nonce, algo);
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## 🔧 Advanced Options (Not Available in Node.js)
|
|
290
|
+
|
|
291
|
+
LemonTLS gives you control that Node.js doesn't expose — without `openssl.cnf` hacks:
|
|
292
|
+
|
|
293
|
+
```js
|
|
294
|
+
import tls from 'lemon-tls';
|
|
295
|
+
|
|
296
|
+
// Per-connection cipher/group/sigalg selection (impossible in Node.js)
|
|
297
|
+
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)
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Disable session tickets (in Node.js requires openssl.cnf)
|
|
305
|
+
tls.createServer({ key, cert, noTickets: true });
|
|
306
|
+
|
|
307
|
+
// Certificate pinning
|
|
308
|
+
tls.connect(443, 'bank.example.com', {
|
|
309
|
+
pins: ['sha256/YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg='],
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Handshake timeout — DoS protection
|
|
313
|
+
tls.connect(443, 'host', { handshakeTimeout: 5000 });
|
|
314
|
+
|
|
315
|
+
// Max handshake size — prevents oversized certificate chains
|
|
316
|
+
tls.createServer({ key, cert, maxHandshakeSize: 65536 });
|
|
317
|
+
|
|
318
|
+
// Dynamic certificate selection (beyond SNI — based on cipher, version, extensions)
|
|
319
|
+
tls.createServer({
|
|
320
|
+
certificateCallback: (info, cb) => {
|
|
321
|
+
// info = { servername, version, ciphers, sigalgs, groups, alpns }
|
|
322
|
+
const ctx = pickCertFor(info);
|
|
323
|
+
cb(null, ctx);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Wireshark debugging
|
|
328
|
+
socket.on('keylog', (line) => fs.appendFileSync('keys.log', line));
|
|
329
|
+
// Wireshark: Edit → Preferences → TLS → Pre-Master-Secret log filename → keys.log
|
|
330
|
+
|
|
331
|
+
// JA3 fingerprinting (server-side bot detection)
|
|
332
|
+
server.on('secureConnection', (socket) => {
|
|
333
|
+
const ja3 = socket.getJA3();
|
|
334
|
+
console.log(ja3.hash); // 'e7d705a3286e19ea42f587b344ee6865'
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Full negotiation result
|
|
338
|
+
socket.on('secureConnect', () => {
|
|
339
|
+
console.log(socket.getNegotiationResult());
|
|
340
|
+
// { version: 0x0304, versionName: 'TLSv1.3', cipher: 0x1301,
|
|
341
|
+
// cipherName: 'TLS_AES_128_GCM_SHA256', group: 0x001d, groupName: 'X25519',
|
|
342
|
+
// sni: 'example.com', alpn: 'h2', resumed: false, helloRetried: false,
|
|
343
|
+
// handshakeDuration: 23 }
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
// ECDHE shared secret access (for research)
|
|
347
|
+
console.log(socket.getSharedSecret()); // Buffer<...>
|
|
348
|
+
```
|
|
349
|
+
|
|
106
350
|
## 🛣 Roadmap
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
351
|
+
|
|
352
|
+
✅ = Completed 🔄 = Implemented, needs testing ⏳ = Planned
|
|
353
|
+
|
|
111
354
|
### ✅ Completed
|
|
355
|
+
|
|
112
356
|
| Status | Item |
|
|
113
|
-
|
|
114
|
-
| ✅ | TLS 1.3
|
|
115
|
-
| ✅ |
|
|
116
|
-
|
|
117
|
-
|
|
357
|
+
|---|---|
|
|
358
|
+
| ✅ | TLS 1.3 — Server + Client |
|
|
359
|
+
| ✅ | TLS 1.2 — Server + Client |
|
|
360
|
+
| ✅ | AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305 |
|
|
361
|
+
| ✅ | X25519 / P-256 key exchange |
|
|
362
|
+
| ✅ | RSA-PSS / ECDSA signatures |
|
|
363
|
+
| ✅ | SNI, ALPN extensions |
|
|
364
|
+
| ✅ | Session tickets + PSK resumption (TLS 1.3) |
|
|
365
|
+
| ✅ | Extended Master Secret (RFC 7627, TLS 1.2) |
|
|
366
|
+
| ✅ | Certificate validation (dates, hostname, CA chain) |
|
|
367
|
+
| ✅ | 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
|
+
|
|
118
378
|
| Status | Item | Notes |
|
|
119
|
-
|
|
120
|
-
| 🔄 |
|
|
121
|
-
| 🔄 | TLS 1.
|
|
122
|
-
| 🔄 |
|
|
123
|
-
|
|
124
|
-
| 🔄 | ALPN & SNI extensions | API design ongoing |
|
|
125
|
-
| 🔄 | API alignment with Node.js `tls.TLSSocket` | Migration tests in progress |
|
|
126
|
-
| 🔄 | Modularization of key schedule & record layer | For reuse in QUIC/DTLS |
|
|
127
|
-
|
|
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
|
+
|
|
128
384
|
### ⏳ Planned
|
|
385
|
+
|
|
129
386
|
| Status | Item | Notes |
|
|
130
|
-
|
|
131
|
-
| ⏳ | DTLS
|
|
387
|
+
|---|---|---|
|
|
388
|
+
| ⏳ | DTLS 1.2/1.3 | Datagram TLS over UDP |
|
|
389
|
+
| ⏳ | 0-RTT Early Data | Risky (replay attacks), low priority |
|
|
132
390
|
| ⏳ | Full certificate chain validation | Including revocation checks |
|
|
133
|
-
| ⏳ | Browser compatibility | Via WebCrypto integration |
|
|
134
|
-
| ⏳ | End-to-end interoperability tests | Against OpenSSL, rustls |
|
|
135
|
-
| ⏳ | Benchmarks & performance tuning | Resource usage, throughput |
|
|
136
|
-
| ⏳ | Fuzz testing & robustness checks | To improve security |
|
|
137
|
-
| ⏳ | Developer documentation & API reference | For easier onboarding |
|
|
138
391
|
| ⏳ | TypeScript typings | Type safety and IDE integration |
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
392
|
+
| ⏳ | Benchmarks & performance tuning | Throughput, memory |
|
|
393
|
+
| ⏳ | 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
|
+
|
|
432
|
+
```
|
|
433
|
+
https://localhost:19600/
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
Requires: Node.js ≥ 16, OpenSSL in PATH.
|
|
437
|
+
|
|
438
|
+
## 📁 Project Structure
|
|
439
|
+
|
|
440
|
+
```
|
|
441
|
+
index.js — exports: TLSSocket, TLSSession, connect, createServer, crypto, wire, record
|
|
442
|
+
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
|
|
451
|
+
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
|
|
459
|
+
```
|
|
460
|
+
|
|
146
461
|
## 🤝 Contributing
|
|
462
|
+
|
|
147
463
|
Pull requests are welcome!
|
|
148
464
|
Please open an issue before submitting major changes.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
465
|
+
|
|
152
466
|
## 💖 Sponsors
|
|
467
|
+
|
|
153
468
|
This project is part of the [colocohen](https://github.com/colocohen) Node.js infrastructure stack (QUIC, WebRTC, DNSSEC, TLS, and more).
|
|
154
|
-
You can support ongoing development via [GitHub Sponsors](https://github.com/sponsors/colocohen).
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
469
|
+
You can support ongoing development via [GitHub Sponsors](https://github.com/sponsors/colocohen).
|
|
470
|
+
|
|
471
|
+
## 📚 References
|
|
472
|
+
|
|
473
|
+
* [RFC 8446 – TLS 1.3](https://datatracker.ietf.org/doc/html/rfc8446)
|
|
474
|
+
* [RFC 5246 – TLS 1.2](https://datatracker.ietf.org/doc/html/rfc5246)
|
|
475
|
+
* [RFC 7627 – Extended Master Secret](https://datatracker.ietf.org/doc/html/rfc7627)
|
|
476
|
+
|
|
164
477
|
## 📜 License
|
|
165
478
|
|
|
166
479
|
**Apache License 2.0**
|
|
@@ -179,5 +492,4 @@ distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
179
492
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
180
493
|
See the License for the specific language governing permissions and
|
|
181
494
|
limitations under the License.
|
|
182
|
-
```
|
|
183
|
-
|
|
495
|
+
```
|
package/index.cjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CJS wrapper for lemon-tls.
|
|
3
|
+
*
|
|
4
|
+
* On Node 22+, require('lemon-tls') works natively with the ESM module.
|
|
5
|
+
* On Node 16-21, this wrapper provides require() support via dynamic import.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* const tls = require('lemon-tls'); // sync on Node 22+
|
|
9
|
+
* const tls = await require('lemon-tls'); // async on Node 16-21
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
let cached = null;
|
|
13
|
+
|
|
14
|
+
async function load() {
|
|
15
|
+
if (!cached) cached = await import('./index.js');
|
|
16
|
+
return cached.default || cached;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Try sync require first (Node 22+)
|
|
20
|
+
try {
|
|
21
|
+
const m = require('./index.js');
|
|
22
|
+
module.exports = m.default || m;
|
|
23
|
+
} catch (e) {
|
|
24
|
+
// Node 16-21: return a promise that resolves to the module
|
|
25
|
+
// Users need: const tls = await require('lemon-tls');
|
|
26
|
+
module.exports = load();
|
|
27
|
+
module.exports.__esModule = true;
|
|
28
|
+
}
|