quico 0.1.3 → 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.
@@ -0,0 +1,273 @@
1
+ /*
2
+ * quico: HTTP/3 and QUIC implementation for Node.js
3
+ * Copyright 2025 colocohen
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at:
8
+ *
9
+ * https://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * This file is part of the open-source project hosted at:
12
+ * https://github.com/colocohen/quico
13
+ */
14
+
15
+ import { TLSSession, createSecureContext as _lemonCreateSecureContext } from 'lemon-tls';
16
+ import nodeTls from 'node:tls';
17
+ import { DEBUG, Emitter } from './utils.js';
18
+ import { build_transport_params, parse_transport_params } from './transport.js';
19
+ import {
20
+ TLS_CIPHER_SUITES
21
+ } from './crypto.js';
22
+
23
+
24
+ function defaultTransportParams() {
25
+ return {
26
+ max_udp_payload_size: 65527,
27
+ max_idle_timeout: 30000,
28
+ initial_max_data: 1048576,
29
+ initial_max_stream_data_bidi_local: 262144,
30
+ initial_max_stream_data_bidi_remote: 262144,
31
+ initial_max_stream_data_uni: 131072,
32
+ initial_max_streams_bidi: 100,
33
+ initial_max_streams_uni: 3,
34
+ ack_delay_exponent: 3,
35
+ max_ack_delay: 25,
36
+ disable_active_migration: true,
37
+ active_connection_id_limit: 4,
38
+ max_datagram_frame_size: 65527
39
+ };
40
+ }
41
+
42
+
43
+ function TLSBridge(options) {
44
+ options = options || {};
45
+
46
+ var ev = Emitter();
47
+ var session = null;
48
+ var handshakeSecretsEmitted = false;
49
+ var appSecretsEmitted = false;
50
+ var helloHandled = false;
51
+ var isServer = !!options.isServer;
52
+ var originalDcid = options.originalDcid || new Uint8Array(0);
53
+ var localCid = options.localCid || new Uint8Array(0); // client's own SCID
54
+ var hostname = options.hostname || null;
55
+ // ALPN protocol(s) to advertise. Default ['h3']; normalize string → array.
56
+ var supportedAlpns = (function () {
57
+ var a = options.alpn || ['h3'];
58
+ return Array.isArray(a) ? a : [a];
59
+ })();
60
+
61
+ function feedMessage(data) {
62
+ if (!session) {
63
+ var sessionOpts = { isServer: isServer, SNICallback: options.SNICallback };
64
+ if (!isServer) {
65
+ sessionOpts.sessionId = new Uint8Array(0); // QUIC: no compatibility mode
66
+ if (hostname) sessionOpts.servername = hostname;
67
+ }
68
+ session = new TLSSession(sessionOpts);
69
+ setupSessionEvents();
70
+
71
+ // Client: set ALPN + transport params NOW, before setTimeout builds ClientHello
72
+ if (!isServer) {
73
+ var tp = defaultTransportParams();
74
+ tp.initial_source_connection_id = localCid;
75
+ var quicParams = build_transport_params(tp);
76
+
77
+ session.set_context({
78
+ local_supported_versions: [0x0304],
79
+ local_supported_alpns: supportedAlpns,
80
+ local_supported_groups: [29, 23, 24],
81
+ local_supported_cipher_suites: [0x1301, 0x1302],
82
+ local_extensions: [{ type: 0x39, data: quicParams }],
83
+ // Signature schemes offered to the peer (client: in the ClientHello;
84
+ // server: in CertificateRequest). Full modern list, ordered PSS/ECDSA/
85
+ // EdDSA first, legacy pkcs1 last (TLS 1.2 compat only — TLS 1.3 never
86
+ // signs pkcs1). Two bugs lived in the old pkcs1-only list:
87
+ // 1. Cloudflare zones with ECDSA-only certs reject the ClientHello
88
+ // outright (alert 40 handshake_failure — seen live against
89
+ // speed.cloudflare.com; claude.ai worked only because that zone
90
+ // also carries an RSA cert).
91
+ // 2. lemon-tls hardcodes THIS full list in the HRR CH2, and RFC 8446
92
+ // §4.1.2 requires CH2 to match CH1 exactly — offering a different
93
+ // list in CH1 made every HRR flow non-compliant.
94
+ local_supported_signature_algorithms: [
95
+ 0x0804, 0x0805, 0x0806, // rsa_pss_rsae_sha256/384/512
96
+ 0x0403, 0x0503, 0x0603, // ecdsa_secp256r1/384r1/521r1
97
+ 0x0807, 0x0808, // ed25519, ed448
98
+ 0x0401, 0x0501, 0x0601 // rsa_pkcs1 (TLS 1.2 legacy)
99
+ ],
100
+ });
101
+ helloHandled = true; // ClientHello will be generated from this set_context
102
+ }
103
+ }
104
+ session.message(data);
105
+ }
106
+
107
+ function setupSessionEvents() {
108
+
109
+ // Peer's QUIC transport parameters ride in the TLS quic_transport_parameters
110
+ // extension (type 0x39): client's in the ClientHello, server's in the
111
+ // EncryptedExtensions. lemon-tls surfaces every received handshake message via
112
+ // 'handshakeMessage' with the parsed extensions, so we extract the 0x39 data,
113
+ // parse it, and hand the params to the QUIC layer (which seeds remote_max_data,
114
+ // max_ack_delay, ack_delay_exponent, etc. — previously hardcoded defaults).
115
+ var peerParamsEmitted = false;
116
+ session.on('handshakeMessage', function (type, data, message) {
117
+ if (peerParamsEmitted || !message || !Array.isArray(message.extensions)) return;
118
+ for (var i = 0; i < message.extensions.length; i++) {
119
+ var e = message.extensions[i];
120
+ if (e && (e.type === 0x39 || e.type === 57) && e.data) {
121
+ try {
122
+ var parsed = parse_transport_params(e.data, 0);
123
+ peerParamsEmitted = true;
124
+ if (DEBUG) console.log('[tls] peer transport params parsed (' + e.data.length + 'B)');
125
+ ev.emit('peerTransportParams', parsed);
126
+ } catch (err) {
127
+ if (DEBUG) console.log('[tls] peer transport params parse failed: ' + err.message);
128
+ }
129
+ break;
130
+ }
131
+ }
132
+ });
133
+
134
+ session.on('hello', function () {
135
+ if (helloHandled) return; // prevent double-fire
136
+ helloHandled = true;
137
+ if (DEBUG) console.log('[tls] hello — configuring TLS');
138
+
139
+ // Transport params — shared defaults + role-specific fields
140
+ var tp = defaultTransportParams();
141
+
142
+ if (isServer) {
143
+ // Server must echo original DCID and provide SCID
144
+ tp.original_destination_connection_id = originalDcid;
145
+ tp.initial_source_connection_id = originalDcid;
146
+ tp.stateless_reset_token = new Uint8Array(16).fill(0xab); // TODO: random per connection
147
+ } else {
148
+ // Client: initial_source_connection_id = our SCID (not DCID!)
149
+ tp.initial_source_connection_id = localCid;
150
+ }
151
+
152
+ var quicParams = build_transport_params(tp);
153
+
154
+ var tlsContext = {
155
+ local_supported_versions: [0x0304],
156
+ local_supported_alpns: supportedAlpns,
157
+ local_supported_groups: [29, 23, 24],
158
+ local_supported_cipher_suites: [0x1301, 0x1302, 0xc02f, 0xc030, 0xcca8],
159
+ local_extensions: [
160
+ { type: 0x39, data: quicParams }
161
+ ],
162
+ // Signature schemes offered to the peer (client: in the ClientHello;
163
+ // server: in CertificateRequest). Full modern list, ordered PSS/ECDSA/
164
+ // EdDSA first, legacy pkcs1 last (TLS 1.2 compat only — TLS 1.3 never
165
+ // signs pkcs1). Two bugs lived in the old pkcs1-only list:
166
+ // 1. Cloudflare zones with ECDSA-only certs reject the ClientHello
167
+ // outright (alert 40 handshake_failure — seen live against
168
+ // speed.cloudflare.com; claude.ai worked only because that zone
169
+ // also carries an RSA cert).
170
+ // 2. lemon-tls hardcodes THIS full list in the HRR CH2, and RFC 8446
171
+ // §4.1.2 requires CH2 to match CH1 exactly — offering a different
172
+ // list in CH1 made every HRR flow non-compliant.
173
+ local_supported_signature_algorithms: [
174
+ 0x0804, 0x0805, 0x0806, // rsa_pss_rsae_sha256/384/512
175
+ 0x0403, 0x0503, 0x0603, // ecdsa_secp256r1/384r1/521r1
176
+ 0x0807, 0x0808, // ed25519, ed448
177
+ 0x0401, 0x0501, 0x0601 // rsa_pkcs1 (TLS 1.2 legacy)
178
+ ],
179
+ };
180
+
181
+ session.set_context(tlsContext);
182
+ });
183
+
184
+ session.on('message', function (epoch, seq, type, data) {
185
+ var quicEpoch;
186
+ if (epoch === 0) quicEpoch = 'initial';
187
+ else if (epoch === 1) quicEpoch = 'handshake';
188
+ else quicEpoch = 'app';
189
+
190
+ if (DEBUG) console.log('[tls] outgoing: epoch=' + quicEpoch + ' type=' + type + ' len=' + data.length);
191
+ ev.emit('send', quicEpoch, data);
192
+ });
193
+
194
+ // Native events from LemonTLS — no polling needed
195
+ session.on('handshakeSecrets', function (localSecret, remoteSecret) {
196
+ if (handshakeSecretsEmitted) return;
197
+ handshakeSecretsEmitted = true;
198
+ var cipher = session.getCipher();
199
+ var hashName = TLS_CIPHER_SUITES[cipher] ? TLS_CIPHER_SUITES[cipher].hash : 'sha256';
200
+ if (DEBUG) console.log('[tls] handshake secrets ready');
201
+ ev.emit('handshakeSecrets', { local: localSecret, remote: remoteSecret, cipher: cipher, hash: hashName });
202
+ });
203
+
204
+ session.on('appSecrets', function (localSecret, remoteSecret) {
205
+ if (appSecretsEmitted) return;
206
+ appSecretsEmitted = true;
207
+ var cipher = session.getCipher();
208
+ var hashName = TLS_CIPHER_SUITES[cipher] ? TLS_CIPHER_SUITES[cipher].hash : 'sha256';
209
+ if (DEBUG) console.log('[tls] app secrets ready');
210
+ ev.emit('appSecrets', { local: localSecret, remote: remoteSecret, cipher: cipher, hash: hashName });
211
+ });
212
+
213
+ session.on('secureConnect', function () {
214
+ if (DEBUG) console.log('[tls] secureConnect');
215
+ ev.emit('secureConnect');
216
+ });
217
+
218
+ session.on('keyUpdate', function (info) {
219
+ ev.emit('keyUpdate', info.direction, info.secret);
220
+ });
221
+ }
222
+
223
+ return {
224
+ on: function (name, fn) { ev.on(name, fn); },
225
+ off: function (name, fn) { ev.off(name, fn); },
226
+ feedMessage: feedMessage,
227
+ requestKeyUpdate: function () { if (session) session.requestKeyUpdate(true); },
228
+ getHandshakeSecrets: function () {
229
+ if (!session) return null;
230
+ var hs = session.getHandshakeSecrets();
231
+ if (!hs || !hs.localSecret) return null;
232
+ var cipher = session.getCipher();
233
+ var hashName = TLS_CIPHER_SUITES[cipher] ? TLS_CIPHER_SUITES[cipher].hash : 'sha256';
234
+ return { local: hs.localSecret, remote: hs.remoteSecret, cipher: cipher, hash: hashName };
235
+ },
236
+ getTrafficSecrets: function () {
237
+ if (!session) return null;
238
+ var secrets = session.getTrafficSecrets();
239
+ if (!secrets || !secrets.localAppSecret) return null;
240
+ var cipher = session.getCipher();
241
+ var hashName = TLS_CIPHER_SUITES[cipher] ? TLS_CIPHER_SUITES[cipher].hash : 'sha256';
242
+ return { local: secrets.localAppSecret, remote: secrets.remoteAppSecret, cipher: cipher, hash: hashName };
243
+ },
244
+ getCipher: function () { return session ? session.getCipher() : null; },
245
+ close: function () { if (session && session.close) session.close(); }
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Drop-in replacement for tls.createSecureContext.
251
+ * Builds both a lemon-tls context (for H3/QUIC) and a node:tls
252
+ * context (for HTTP/2 + HTTP/1.1 over TCP) from the same credentials.
253
+ */
254
+ function createSecureContext(options) {
255
+ var ctx = _lemonCreateSecureContext(options);
256
+ if (ctx && options) {
257
+ ctx._rawKey = options.key || null;
258
+ ctx._rawCert = options.cert || null;
259
+ ctx._rawCa = options.ca || null;
260
+ try {
261
+ var nodeOpts = {};
262
+ if (options.key) nodeOpts.key = options.key;
263
+ if (options.cert) nodeOpts.cert = options.cert;
264
+ if (options.ca) nodeOpts.ca = options.ca;
265
+ ctx._nodeContext = nodeTls.createSecureContext(nodeOpts);
266
+ } catch (e) {
267
+ ctx._nodeContext = null;
268
+ }
269
+ }
270
+ return ctx;
271
+ }
272
+
273
+ export { TLSBridge, createSecureContext };