lemon-tls 0.7.0 → 0.10.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,414 @@
1
+ /**
2
+ * HPKE — Hybrid Public Key Encryption (RFC 9180).
3
+ *
4
+ * Encrypting to a public key: the sender makes an ephemeral key pair, a
5
+ * Diffie-Hellman with the recipient's public key gives a shared secret, and
6
+ * from it come an AEAD key and nonce for any number of messages — while
7
+ * only the ephemeral public key (`enc`) has to travel with them. TLS
8
+ * Encrypted Client Hello (ECH) encrypts the real ClientHello this way, to
9
+ * the key a server publishes in DNS; MLS and Oblivious HTTP use it too.
10
+ *
11
+ * All four modes (base, psk, auth, auth_psk); the KEMs, KDFs and AEADs of
12
+ * the RFC's registry that node:crypto has:
13
+ *
14
+ * KEM 0x0010 DHKEM(P-256, HKDF-SHA256) 0x0011 DHKEM(P-384, HKDF-SHA384)
15
+ * 0x0012 DHKEM(P-521, HKDF-SHA512) 0x0020 DHKEM(X25519, HKDF-SHA256)
16
+ * 0x0021 DHKEM(X448, HKDF-SHA512)
17
+ * KDF 0x0001 HKDF-SHA256 0x0002 HKDF-SHA384 0x0003 HKDF-SHA512
18
+ * AEAD 0x0001 AES-128-GCM 0x0002 AES-256-GCM 0x0003 ChaCha20-Poly1305
19
+ * 0xFFFF export-only
20
+ *
21
+ * Synchronous, like the rest of the handshake's crypto.
22
+ */
23
+
24
+ import crypto from 'node:crypto';
25
+ import { hkdf_extract, hkdf_expand } from '../crypto.js';
26
+ import { x25519_get_public_key, x25519_get_shared_secret } from './ecdh.js';
27
+
28
+ var MODE_BASE = 0x00, MODE_PSK = 0x01, MODE_AUTH = 0x02, MODE_AUTH_PSK = 0x03;
29
+
30
+ var te = new TextEncoder();
31
+ function u8(s) { return te.encode(s); }
32
+ function i2osp(n, len) {
33
+ var out = new Uint8Array(len);
34
+ for (var i = len - 1; i >= 0; i--) { out[i] = n & 0xff; n = Math.floor(n / 256); }
35
+ return out;
36
+ }
37
+ function concat() {
38
+ var len = 0, i;
39
+ for (i = 0; i < arguments.length; i++) len += arguments[i].length;
40
+ var out = new Uint8Array(len), off = 0;
41
+ for (i = 0; i < arguments.length; i++) { out.set(arguments[i], off); off += arguments[i].length; }
42
+ return out;
43
+ }
44
+ function hexToBig(h) { return BigInt('0x' + h); }
45
+ function bytesToBig(b) { return b.length ? BigInt('0x' + Buffer.from(b).toString('hex')) : 0n; }
46
+
47
+ var HPKE_V1 = u8('HPKE-v1');
48
+ var EMPTY = new Uint8Array(0);
49
+
50
+
51
+ // ============================================================
52
+ // Labeled HKDF (RFC 9180 §4)
53
+ // ============================================================
54
+
55
+ function labeled_extract(hash, suite_id, salt, label, ikm) {
56
+ return hkdf_extract(hash, salt, concat(HPKE_V1, suite_id, u8(label), ikm));
57
+ }
58
+
59
+ function labeled_expand(hash, suite_id, prk, label, info, L) {
60
+ return hkdf_expand(hash, prk, concat(i2osp(L, 2), HPKE_V1, suite_id, u8(label), info), L);
61
+ }
62
+
63
+
64
+ // ============================================================
65
+ // KEMs: DH groups (RFC 9180 §4.1, §7.1)
66
+ // ============================================================
67
+
68
+ // The two Montgomery curves, through node:crypto's raw-key DER wrapping.
69
+ var X448_PKCS8 = Buffer.from('3046020100300506032b656f043a0438', 'hex');
70
+ var X448_SPKI = Buffer.from('3042300506032b656f033900', 'hex');
71
+
72
+ function x448_public_key(sk) {
73
+ var priv = crypto.createPrivateKey({ key: Buffer.concat([X448_PKCS8, Buffer.from(sk)]), format: 'der', type: 'pkcs8' });
74
+ return new Uint8Array(crypto.createPublicKey(priv).export({ type: 'spki', format: 'der' }).subarray(X448_SPKI.length));
75
+ }
76
+ function x448_shared(sk, pk) {
77
+ var priv = crypto.createPrivateKey({ key: Buffer.concat([X448_PKCS8, Buffer.from(sk)]), format: 'der', type: 'pkcs8' });
78
+ var pub = crypto.createPublicKey({ key: Buffer.concat([X448_SPKI, Buffer.from(pk)]), format: 'der', type: 'spki' });
79
+ var dh = new Uint8Array(crypto.diffieHellman({ privateKey: priv, publicKey: pub }));
80
+ // RFC 9180 §7.1.4 / RFC 7748 §6.2: an all-zero result means a low-order
81
+ // public key — the sender would know the "secret". Constant time.
82
+ var acc = 0;
83
+ for (var i = 0; i < dh.length; i++) acc |= dh[i];
84
+ if (acc === 0) throw new Error('HPKE: X448 shared secret is all zero (invalid public key)');
85
+ return dh;
86
+ }
87
+
88
+ // The NIST curves: a private key is a scalar below the group order;
89
+ // DeriveKeyPair draws candidates until one is (§7.1.3).
90
+ function nist(curve, order, bitmask) {
91
+ return {
92
+ order: order,
93
+ bitmask: bitmask,
94
+ public_key: function (sk) {
95
+ var e = crypto.createECDH(curve); e.setPrivateKey(Buffer.from(sk));
96
+ return new Uint8Array(e.getPublicKey(null, 'uncompressed'));
97
+ },
98
+ // computeSecret validates the peer point (on the curve, not infinity)
99
+ // and gives the x-coordinate: the DH value of §7.1.
100
+ shared: function (sk, pk) {
101
+ var e = crypto.createECDH(curve); e.setPrivateKey(Buffer.from(sk));
102
+ return new Uint8Array(e.computeSecret(Buffer.from(pk)));
103
+ }
104
+ };
105
+ }
106
+
107
+ var KEMS = {};
108
+ KEMS[0x0010] = Object.assign(nist('prime256v1',
109
+ hexToBig('ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551'), 0xff),
110
+ { id: 0x0010, name: 'DHKEM(P-256, HKDF-SHA256)', hash: 'sha256', Nsecret: 32, Nenc: 65, Npk: 65, Nsk: 32 });
111
+ KEMS[0x0011] = Object.assign(nist('secp384r1',
112
+ hexToBig('ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973'), 0xff),
113
+ { id: 0x0011, name: 'DHKEM(P-384, HKDF-SHA384)', hash: 'sha384', Nsecret: 48, Nenc: 97, Npk: 97, Nsk: 48 });
114
+ KEMS[0x0012] = Object.assign(nist('secp521r1',
115
+ hexToBig('01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409'), 0x01),
116
+ { id: 0x0012, name: 'DHKEM(P-521, HKDF-SHA512)', hash: 'sha512', Nsecret: 64, Nenc: 133, Npk: 133, Nsk: 66 });
117
+ KEMS[0x0020] = { id: 0x0020, name: 'DHKEM(X25519, HKDF-SHA256)', hash: 'sha256', Nsecret: 32, Nenc: 32, Npk: 32, Nsk: 32,
118
+ public_key: x25519_get_public_key,
119
+ shared: x25519_get_shared_secret }; // rejects the all-zero result itself
120
+ KEMS[0x0021] = { id: 0x0021, name: 'DHKEM(X448, HKDF-SHA512)', hash: 'sha512', Nsecret: 64, Nenc: 56, Npk: 56, Nsk: 56,
121
+ public_key: x448_public_key,
122
+ shared: x448_shared };
123
+
124
+ function kem_suite_id(kem) { return concat(u8('KEM'), i2osp(kem.id, 2)); }
125
+
126
+ /** §7.1.3: a key pair from `ikm` (at least Nsk bytes of entropy). */
127
+ function kem_derive_key_pair(kem, ikm) {
128
+ var sid = kem_suite_id(kem);
129
+ var prk = labeled_extract(kem.hash, sid, EMPTY, 'dkp_prk', ikm);
130
+ var sk;
131
+ if (!kem.order) {
132
+ sk = labeled_expand(kem.hash, sid, prk, 'sk', EMPTY, kem.Nsk);
133
+ } else {
134
+ for (var counter = 0; ; counter++) {
135
+ if (counter > 255) throw new Error('HPKE: DeriveKeyPair found no key');
136
+ var bytes = labeled_expand(kem.hash, sid, prk, 'candidate', i2osp(counter, 1), kem.Nsk);
137
+ bytes[0] &= kem.bitmask;
138
+ var n = bytesToBig(bytes);
139
+ if (n !== 0n && n < kem.order) { sk = bytes; break; }
140
+ }
141
+ }
142
+ return { private_key: sk, public_key: kem.public_key(sk) };
143
+ }
144
+
145
+ function kem_generate_key_pair(kem) {
146
+ return kem_derive_key_pair(kem, new Uint8Array(crypto.randomBytes(kem.Nsk)));
147
+ }
148
+
149
+ function kem_check_public_key(kem, pk) {
150
+ if (!(pk instanceof Uint8Array) || pk.length !== kem.Npk) throw new Error('HPKE: public key must be ' + kem.Npk + ' bytes for ' + kem.name);
151
+ if (kem.order && pk[0] !== 0x04) throw new Error('HPKE: public key is not an uncompressed point');
152
+ }
153
+
154
+ function kem_extract_and_expand(kem, dh, kem_context) {
155
+ var sid = kem_suite_id(kem);
156
+ var prk = labeled_extract(kem.hash, sid, EMPTY, 'eae_prk', dh);
157
+ return labeled_expand(kem.hash, sid, prk, 'shared_secret', kem_context, kem.Nsecret);
158
+ }
159
+
160
+ // §4.1 Encap / Decap, and their authenticated forms. `ephemeral` replaces
161
+ // the random key pair — for the RFC's test vectors only.
162
+ function kem_encap(kem, pkR, skS, ephemeral) {
163
+ kem_check_public_key(kem, pkR);
164
+ var e = ephemeral || kem_generate_key_pair(kem);
165
+ var dh = kem.shared(e.private_key, pkR);
166
+ var kem_context = concat(e.public_key, pkR);
167
+ if (skS) {
168
+ dh = concat(dh, kem.shared(skS, pkR));
169
+ kem_context = concat(kem_context, kem.public_key(skS));
170
+ }
171
+ return { shared_secret: kem_extract_and_expand(kem, dh, kem_context), enc: e.public_key };
172
+ }
173
+
174
+ function kem_decap(kem, enc, skR, pkS, pkR) {
175
+ kem_check_public_key(kem, enc);
176
+ var dh = kem.shared(skR, enc);
177
+ var kem_context = concat(enc, pkR || kem.public_key(skR));
178
+ if (pkS) {
179
+ kem_check_public_key(kem, pkS);
180
+ dh = concat(dh, kem.shared(skR, pkS));
181
+ kem_context = concat(kem_context, pkS);
182
+ }
183
+ return kem_extract_and_expand(kem, dh, kem_context);
184
+ }
185
+
186
+
187
+ // ============================================================
188
+ // KDFs and AEADs (RFC 9180 §7.2, §7.3)
189
+ // ============================================================
190
+
191
+ var KDFS = {
192
+ 0x0001: { id: 0x0001, hash: 'sha256', Nh: 32 },
193
+ 0x0002: { id: 0x0002, hash: 'sha384', Nh: 48 },
194
+ 0x0003: { id: 0x0003, hash: 'sha512', Nh: 64 }
195
+ };
196
+
197
+ var AEADS = {
198
+ 0x0001: { id: 0x0001, algo: 'aes-128-gcm', Nk: 16, Nn: 12, Nt: 16 },
199
+ 0x0002: { id: 0x0002, algo: 'aes-256-gcm', Nk: 32, Nn: 12, Nt: 16 },
200
+ 0x0003: { id: 0x0003, algo: 'chacha20-poly1305', Nk: 32, Nn: 12, Nt: 16 },
201
+ 0xffff: { id: 0xffff, algo: null, Nk: 0, Nn: 0, Nt: 0 } // export-only
202
+ };
203
+
204
+ function aead_seal(aead, key, nonce, aad, pt) {
205
+ var c = crypto.createCipheriv(aead.algo, key, nonce, { authTagLength: aead.Nt });
206
+ c.setAAD(aad);
207
+ var a = c.update(pt), b = c.final();
208
+ return new Uint8Array(Buffer.concat([a, b, c.getAuthTag()]));
209
+ }
210
+
211
+ function aead_open(aead, key, nonce, aad, ct) {
212
+ if (ct.length < aead.Nt) return null;
213
+ var d = crypto.createDecipheriv(aead.algo, key, nonce, { authTagLength: aead.Nt });
214
+ d.setAAD(aad);
215
+ d.setAuthTag(ct.subarray(ct.length - aead.Nt));
216
+ try {
217
+ var a = d.update(ct.subarray(0, ct.length - aead.Nt)), b = d.final();
218
+ return new Uint8Array(Buffer.concat([a, b]));
219
+ } catch (e) { return null; }
220
+ }
221
+
222
+
223
+ // ============================================================
224
+ // Suites and the key schedule (RFC 9180 §5.1)
225
+ // ============================================================
226
+
227
+ /**
228
+ * A cipher suite by its registry ids. Throws on one this build does not
229
+ * have. `{ kem, kdf, aead, id: suite_id, Nenc, … }`.
230
+ */
231
+ function hpke_suite(kem_id, kdf_id, aead_id) {
232
+ var kem = KEMS[kem_id], kdf = KDFS[kdf_id], aead = AEADS[aead_id];
233
+ if (!kem) throw new Error('HPKE: unsupported KEM 0x' + (kem_id >>> 0).toString(16));
234
+ if (!kdf) throw new Error('HPKE: unsupported KDF 0x' + (kdf_id >>> 0).toString(16));
235
+ if (!aead) throw new Error('HPKE: unsupported AEAD 0x' + (aead_id >>> 0).toString(16));
236
+ return {
237
+ kem: kem, kdf: kdf, aead: aead,
238
+ suite_id: concat(u8('HPKE'), i2osp(kem.id, 2), i2osp(kdf.id, 2), i2osp(aead.id, 2)),
239
+ Nenc: kem.Nenc, Npk: kem.Npk, Nsk: kem.Nsk, Nk: aead.Nk, Nn: aead.Nn, Nt: aead.Nt
240
+ };
241
+ }
242
+
243
+ function is_supported_hpke_suite(kem_id, kdf_id, aead_id) {
244
+ return !!(KEMS[kem_id] && KDFS[kdf_id] && AEADS[aead_id]);
245
+ }
246
+
247
+ function verify_psk_inputs(mode, psk, psk_id) {
248
+ var got_psk = psk.length > 0, got_id = psk_id.length > 0;
249
+ if (got_psk !== got_id) throw new Error('HPKE: psk and psk_id must be given together');
250
+ var needs = (mode === MODE_PSK || mode === MODE_AUTH_PSK);
251
+ if (got_psk !== needs) throw new Error(needs ? 'HPKE: this mode needs a psk' : 'HPKE: a psk was given to a mode without one');
252
+ // §5.1.1: at least 32 bytes of entropy
253
+ if (got_psk && psk.length < 32) throw new Error('HPKE: psk must be at least 32 bytes');
254
+ }
255
+
256
+ function key_schedule(suite, mode, shared_secret, info, psk, psk_id, role) {
257
+ verify_psk_inputs(mode, psk, psk_id);
258
+ var h = suite.kdf.hash, sid = suite.suite_id;
259
+ var psk_id_hash = labeled_extract(h, sid, EMPTY, 'psk_id_hash', psk_id);
260
+ var info_hash = labeled_extract(h, sid, EMPTY, 'info_hash', info);
261
+ var key_schedule_context = concat(new Uint8Array([mode]), psk_id_hash, info_hash);
262
+ var secret = labeled_extract(h, sid, shared_secret, 'secret', psk);
263
+
264
+ var key = EMPTY, base_nonce = EMPTY;
265
+ if (suite.aead.algo) {
266
+ key = labeled_expand(h, sid, secret, 'key', key_schedule_context, suite.Nk);
267
+ base_nonce = labeled_expand(h, sid, secret, 'base_nonce', key_schedule_context, suite.Nn);
268
+ }
269
+ var exporter_secret = labeled_expand(h, sid, secret, 'exp', key_schedule_context, suite.kdf.Nh);
270
+ return new HpkeContext(suite, role, key, base_nonce, exporter_secret);
271
+ }
272
+
273
+
274
+ // ============================================================
275
+ // The encryption context (RFC 9180 §5.2, §5.3)
276
+ // ============================================================
277
+
278
+ function HpkeContext(suite, role, key, base_nonce, exporter_secret) {
279
+ this.suite = suite;
280
+ this.role = role; // 'sender' | 'recipient'
281
+ this.key = key;
282
+ this.base_nonce = base_nonce;
283
+ this.exporter_secret = exporter_secret;
284
+ this.seq = 0;
285
+ }
286
+
287
+ HpkeContext.prototype._nonce = function () {
288
+ // base_nonce XOR seq, seq big-endian in the nonce's low bytes. 2^53
289
+ // messages is far past any use; seal/open refuse beyond it.
290
+ if (this.seq >= Number.MAX_SAFE_INTEGER) throw new Error('HPKE: message limit reached');
291
+ var n = this.base_nonce.slice(0), s = i2osp(this.seq, n.length);
292
+ for (var i = 0; i < n.length; i++) n[i] ^= s[i];
293
+ return n;
294
+ };
295
+
296
+ /** Encrypt the next message. Sender only. */
297
+ HpkeContext.prototype.seal = function (aad, pt) {
298
+ if (this.role !== 'sender') throw new Error('HPKE: seal on a recipient context');
299
+ if (!this.suite.aead.algo) throw new Error('HPKE: export-only suite cannot seal');
300
+ var ct = aead_seal(this.suite.aead, this.key, this._nonce(), aad || EMPTY, pt);
301
+ this.seq++;
302
+ return ct;
303
+ };
304
+
305
+ /**
306
+ * Decrypt the next message: the plaintext, or null if it does not open —
307
+ * and then the sequence does not advance, so the next valid message still
308
+ * does. Recipient only.
309
+ */
310
+ HpkeContext.prototype.open = function (aad, ct) {
311
+ if (this.role !== 'recipient') throw new Error('HPKE: open on a sender context');
312
+ if (!this.suite.aead.algo) throw new Error('HPKE: export-only suite cannot open');
313
+ var pt = aead_open(this.suite.aead, this.key, this._nonce(), aad || EMPTY, ct);
314
+ if (pt) this.seq++;
315
+ return pt;
316
+ };
317
+
318
+ /** §5.3: a secret of `L` bytes for `exporter_context`. */
319
+ HpkeContext.prototype.export = function (exporter_context, L) {
320
+ if (!(L >= 0) || L > 255 * this.suite.kdf.Nh) throw new Error('HPKE: export length out of range');
321
+ return labeled_expand(this.suite.kdf.hash, this.suite.suite_id, this.exporter_secret, 'sec', exporter_context || EMPTY, L);
322
+ };
323
+
324
+
325
+ // ============================================================
326
+ // Setup (RFC 9180 §5.1.1 - §5.1.4)
327
+ // ============================================================
328
+
329
+ function opt_bytes(v) { return v ? (v instanceof Uint8Array ? v : new Uint8Array(v)) : EMPTY; }
330
+
331
+ function mode_of(opts) {
332
+ var psk = !!(opts && opts.psk), auth = !!(opts && (opts.sender_private_key || opts.sender_public_key));
333
+ return auth ? (psk ? MODE_AUTH_PSK : MODE_AUTH) : (psk ? MODE_PSK : MODE_BASE);
334
+ }
335
+
336
+ /**
337
+ * The sender's side: `{ enc, context }`. `enc` goes to the recipient with
338
+ * the messages; `context.seal()` encrypts them.
339
+ *
340
+ * opts (all optional): `psk` + `psk_id` (psk modes), `sender_private_key`
341
+ * (auth modes), `ephemeral` — a `{ private_key, public_key }` to use in
342
+ * place of a fresh one, for test vectors only.
343
+ */
344
+ function setup_sender(suite, recipient_public_key, info, opts) {
345
+ opts = opts || {};
346
+ var mode = mode_of(opts);
347
+ var r = kem_encap(suite.kem, recipient_public_key, opts.sender_private_key || null, opts.ephemeral || null);
348
+ var ctx = key_schedule(suite, mode, r.shared_secret, opt_bytes(info), opt_bytes(opts.psk), opt_bytes(opts.psk_id), 'sender');
349
+ return { enc: r.enc, context: ctx };
350
+ }
351
+
352
+ /**
353
+ * The recipient's side: the context to `open()` with. Throws when `enc` is
354
+ * not a valid public key; a wrong key or info shows as `open()` → null.
355
+ *
356
+ * opts: `psk` + `psk_id`, `sender_public_key` (auth modes),
357
+ * `recipient_public_key` — the public key of `recipient_private_key`, when
358
+ * known, saving a scalar multiplication per setup (it must be that key's).
359
+ */
360
+ function setup_recipient(suite, enc, recipient_private_key, info, opts) {
361
+ opts = opts || {};
362
+ var mode = mode_of(opts);
363
+ var ss = kem_decap(suite.kem, opt_bytes(enc), recipient_private_key, opts.sender_public_key || null,
364
+ opts.recipient_public_key || null);
365
+ return key_schedule(suite, mode, ss, opt_bytes(info), opt_bytes(opts.psk), opt_bytes(opts.psk_id), 'recipient');
366
+ }
367
+
368
+ /** Single-shot (§6.1): one message. `{ enc, ct }`. */
369
+ function hpke_seal(suite, recipient_public_key, info, aad, pt, opts) {
370
+ var s = setup_sender(suite, recipient_public_key, info, opts);
371
+ return { enc: s.enc, ct: s.context.seal(aad, pt) };
372
+ }
373
+
374
+ /** Single-shot (§6.1): the plaintext, or null. */
375
+ function hpke_open(suite, enc, recipient_private_key, info, aad, ct, opts) {
376
+ var ctx;
377
+ try { ctx = setup_recipient(suite, enc, recipient_private_key, info, opts); }
378
+ catch (e) { return null; }
379
+ return ctx.open(aad, ct);
380
+ }
381
+
382
+ /** A new key pair for `kem_id`: `{ private_key, public_key }`, raw bytes. */
383
+ function hpke_generate_key_pair(kem_id) {
384
+ var kem = KEMS[kem_id];
385
+ if (!kem) throw new Error('HPKE: unsupported KEM 0x' + (kem_id >>> 0).toString(16));
386
+ return kem_generate_key_pair(kem);
387
+ }
388
+
389
+ /** The key pair `ikm` determines (§7.1.3). */
390
+ function hpke_derive_key_pair(kem_id, ikm) {
391
+ var kem = KEMS[kem_id];
392
+ if (!kem) throw new Error('HPKE: unsupported KEM 0x' + (kem_id >>> 0).toString(16));
393
+ return kem_derive_key_pair(kem, opt_bytes(ikm));
394
+ }
395
+
396
+ /** The public key of a private key. */
397
+ function hpke_public_key(kem_id, private_key) {
398
+ var kem = KEMS[kem_id];
399
+ if (!kem) throw new Error('HPKE: unsupported KEM 0x' + (kem_id >>> 0).toString(16));
400
+ return kem.public_key(private_key);
401
+ }
402
+
403
+ export {
404
+ MODE_BASE, MODE_PSK, MODE_AUTH, MODE_AUTH_PSK,
405
+ hpke_suite,
406
+ is_supported_hpke_suite,
407
+ setup_sender,
408
+ setup_recipient,
409
+ hpke_seal,
410
+ hpke_open,
411
+ hpke_generate_key_pair,
412
+ hpke_derive_key_pair,
413
+ hpke_public_key,
414
+ };
@@ -9,7 +9,7 @@ import * as wire from '../wire.js';
9
9
  /**
10
10
  * Normalize a parsed hello into a flat object with named extension fields.
11
11
  */
12
- function normalize_hello(hello) {
12
+ function normalize_hello(hello, kind) {
13
13
  let out = {};
14
14
 
15
15
  for (let key in hello) {
@@ -28,9 +28,16 @@ function normalize_hello(hello) {
28
28
  out.alpn = value;
29
29
  } else if (name === 'KEY_SHARE') {
30
30
  if (!('key_groups' in out)) out.key_groups = [];
31
- if (!('supported_groups' in out)) out.supported_groups = [];
31
+ // A ServerHello's key_share names the group chosen, which the
32
+ // client's code reads as its supported_groups. A ClientHello's
33
+ // shares are NOT added to its supported_groups: they came only from
34
+ // that extension. Merged, the result depended on which extension came
35
+ // first (supported_groups after key_share replaced the list; before
36
+ // it, the shares' groups were appended), and a share for a group the
37
+ // client never listed looked listed (RFC 8446 §4.2.8).
38
+ if (kind !== 'client' && !('supported_groups' in out)) out.supported_groups = [];
32
39
  for (let i2 = 0; i2 < value.length; i2++) {
33
- if (out.supported_groups.indexOf(value[i2].group) < 0) {
40
+ if (kind !== 'client' && out.supported_groups.indexOf(value[i2].group) < 0) {
34
41
  out.supported_groups.push(value[i2].group);
35
42
  }
36
43
  out.key_groups.push({
@@ -60,6 +67,8 @@ function normalize_hello(hello) {
60
67
  out.status_request = value;
61
68
  } else if (name === 'MAX_FRAGMENT_LENGTH') {
62
69
  out.max_fragment_length = value;
70
+ } else if (name === 'COMPRESS_CERTIFICATE') {
71
+ out.compress_certificate = value;
63
72
  } else if (name === 'CERTIFICATE_AUTHORITIES') {
64
73
  out.certificate_authorities = value;
65
74
  } else if (name === 'SCT') {
@@ -127,12 +136,21 @@ function build_tls_message(params) {
127
136
  } else if (params.type == 'certificate') {
128
137
  type = wire.TLS_MESSAGE_TYPE.CERTIFICATE;
129
138
  body = wire.build_certificate(params);
139
+ } else if (params.type == 'compressed_certificate') {
140
+ type = wire.TLS_MESSAGE_TYPE.COMPRESSED_CERTIFICATE;
141
+ body = wire.build_compressed_certificate(params);
142
+ } else if (params.type == 'certificate_status') {
143
+ type = wire.TLS_MESSAGE_TYPE.CERTIFICATE_STATUS;
144
+ body = wire.build_certificate_status(params.response);
130
145
  } else if (params.type == 'certificate_verify') {
131
146
  type = wire.TLS_MESSAGE_TYPE.CERTIFICATE_VERIFY;
132
147
  body = wire.build_certificate_verify(params.scheme, params.signature);
133
148
  } else if (params.type == 'finished') {
134
149
  type = wire.TLS_MESSAGE_TYPE.FINISHED;
135
150
  body = params.data;
151
+ } else if (params.type == 'end_of_early_data') {
152
+ type = wire.TLS_MESSAGE_TYPE.END_OF_EARLY_DATA;
153
+ body = new Uint8Array(0);
136
154
  } else if (params.type == 'key_update') {
137
155
  type = wire.TLS_MESSAGE_TYPE.KEY_UPDATE;
138
156
  body = wire.build_key_update(params.request_update);
@@ -167,7 +185,7 @@ function parse_tls_message(data, negotiatedVersion) {
167
185
  if (message.type == wire.TLS_MESSAGE_TYPE.CLIENT_HELLO || message.type == wire.TLS_MESSAGE_TYPE.SERVER_HELLO) {
168
186
  let kind = (message.type == wire.TLS_MESSAGE_TYPE.CLIENT_HELLO) ? 'client' : 'server';
169
187
  let hello = wire.parse_hello({ kind: kind, body: message.body });
170
- out = normalize_hello(hello);
188
+ out = normalize_hello(hello, kind);
171
189
 
172
190
  } else if (message.type == wire.TLS_MESSAGE_TYPE.SERVER_KEY_EXCHANGE) {
173
191
  out = wire.parse_server_key_exchange(message.body);
@@ -192,6 +210,17 @@ function parse_tls_message(data, negotiatedVersion) {
192
210
  out = wire.parse_certificate(message.body);
193
211
  out.type = 'certificate';
194
212
 
213
+ } else if (message.type == wire.TLS_MESSAGE_TYPE.COMPRESSED_CERTIFICATE) {
214
+ // RFC 8879: decompressed by the session, which knows what it offered.
215
+ out = wire.parse_compressed_certificate(message.body);
216
+ out.type = 'compressed_certificate';
217
+
218
+ } else if (message.type == wire.TLS_MESSAGE_TYPE.CERTIFICATE_STATUS) {
219
+ // TLS 1.2: the stapled OCSP response, between Certificate and
220
+ // ServerKeyExchange (RFC 6066 §8).
221
+ out = wire.parse_certificate_status(message.body);
222
+ out.type = 'certificate_status';
223
+
195
224
  } else if (message.type == wire.TLS_MESSAGE_TYPE.CERTIFICATE_VERIFY) {
196
225
  // Decode scheme + signature so the session can actually VERIFY it.
197
226
  // Previously only the raw body was handed up and no caller ever checked
@@ -219,6 +248,12 @@ function parse_tls_message(data, negotiatedVersion) {
219
248
  }
220
249
  out.type = 'new_session_ticket';
221
250
 
251
+ } else if (message.type == wire.TLS_MESSAGE_TYPE.END_OF_EARLY_DATA) {
252
+ // TLS over TCP, after early data (RFC 8446 §4.5): an empty body.
253
+ if (message.body.length !== 0) { let e = new Error('EndOfEarlyData with a body'); e.alertDesc = wire.TLS_ALERT.DECODE_ERROR; throw e; }
254
+ out.type = 'end_of_early_data';
255
+ out.body = message.body;
256
+
222
257
  } else if (message.type == wire.TLS_MESSAGE_TYPE.KEY_UPDATE) {
223
258
  out = wire.parse_key_update(message.body);
224
259
  out.type = 'key_update';
@@ -36,6 +36,27 @@ const SIG_SCHEMES_MODERN = [
36
36
  0x0804, 0x0805, 0x0806, // rsa_pss_rsae_sha256/384/512
37
37
  ];
38
38
 
39
+ // ML-DSA (FIPS 204), post-quantum signatures, TLS 1.3 only
40
+ // (draft-ietf-tls-mldsa): mldsa44, mldsa65, mldsa87 — where node:crypto has
41
+ // them (Node 24.7+, OpenSSL 3.5). Offered after the classical schemes: a
42
+ // server holding both an ML-DSA and a classical certificate (key/cert
43
+ // arrays) follows the client's order, and an ML-DSA chain is 3–4× the size
44
+ // of an ECDSA one — kilobytes more in every handshake — so a client gets it
45
+ // when it asks for it first. Offered at all so that a server with ML-DSA
46
+ // alone can be verified.
47
+ const MLDSA_SCHEMES = { 0x0904: 'ml-dsa-44', 0x0905: 'ml-dsa-65', 0x0906: 'ml-dsa-87' };
48
+ const MLDSA_AVAILABLE = (function () {
49
+ try {
50
+ for (const code in MLDSA_SCHEMES) {
51
+ let kp = crypto.generateKeyPairSync(MLDSA_SCHEMES[code]);
52
+ let sig = crypto.sign(null, Buffer.from('x'), kp.privateKey);
53
+ if (!crypto.verify(null, Buffer.from('x'), kp.publicKey, sig)) return false;
54
+ }
55
+ return true;
56
+ } catch (e) { return false; }
57
+ })();
58
+ const SIG_SCHEMES_MLDSA = MLDSA_AVAILABLE ? [0x0904, 0x0905, 0x0906] : [];
59
+
39
60
  const SIG_SCHEMES_LEGACY_PKCS1 = [
40
61
  0x0401, 0x0501, 0x0601, // rsa_pkcs1_sha256/384/512
41
62
  ];
@@ -73,6 +94,7 @@ const SIG_SCHEMES_LEGACY_SHA1 = [
73
94
  */
74
95
  function default_signature_schemes() {
75
96
  return SIG_SCHEMES_MODERN
97
+ .concat(SIG_SCHEMES_MLDSA)
76
98
  .concat(SIG_SCHEMES_LEGACY_PKCS1)
77
99
  .concat(SIG_SCHEMES_LEGACY_SHA1);
78
100
  }
@@ -98,6 +120,13 @@ function scheme_info(version, scheme) {
98
120
  // verify_with_scheme share this function, so signing and verification can
99
121
  // never drift apart.
100
122
 
123
+ // ML-DSA: TLS 1.3 only (draft-ietf-tls-mldsa §3), pure ML-DSA with an
124
+ // empty context string — the whole TLS 1.3 signature input, unhashed.
125
+ if (MLDSA_SCHEMES[scheme]) {
126
+ if (!MLDSA_AVAILABLE || version !== TLS_VERSION_TLS1_3) return null;
127
+ return { hash: null, sig: 'mldsa', isPSS: false };
128
+ }
129
+
101
130
  // EdDSA
102
131
  if (scheme === 0x0807 || scheme === 0x0808) {
103
132
  return { hash: null, sig: 'eddsa', isPSS: false };
@@ -129,8 +158,13 @@ function scheme_info(version, scheme) {
129
158
 
130
159
  /**
131
160
  * Pick the best signature scheme given the server's key type and client's supported list.
161
+ * `localAllowed`, when given and not empty: our own list — only a scheme in
162
+ * both is used (OpenSSL's "shared sigalgs"). Without it, the `sigalgs` a
163
+ * server was configured with limited what it offered and accepted, and not
164
+ * what it signed with: an ECDSA key restricted to RSA-PSS still signed with
165
+ * ECDSA.
132
166
  */
133
- function pick_scheme(version, certKeyObj, peerSupported) {
167
+ function pick_scheme(version, certKeyObj, peerSupported, localAllowed) {
134
168
  // Iterate the PEER's list, in the peer's own order.
135
169
  //
136
170
  // RFC 8446 §4.2.3: signature_algorithms is "in descending order of
@@ -151,6 +185,7 @@ function pick_scheme(version, certKeyObj, peerSupported) {
151
185
  for (let i = 0; i < peerSupported.length; i++) {
152
186
  let s2 = peerSupported[i];
153
187
  if (typeof s2 !== 'number') continue;
188
+ if (Array.isArray(localAllowed) && localAllowed.length > 0 && localAllowed.indexOf(s2) < 0) continue;
154
189
  if (!scheme_matches_key(version, s2, certKeyObj)) continue;
155
190
  picked = s2;
156
191
  break;
@@ -168,7 +203,7 @@ function sign_with_scheme(version, scheme, tbs, certKeyObj) {
168
203
  let info = scheme_info(version, scheme);
169
204
  if (!info) return null;
170
205
 
171
- if (info.sig === 'eddsa') {
206
+ if (info.sig === 'eddsa' || info.sig === 'mldsa') {
172
207
  return new Uint8Array(crypto.sign(null, tbs, certKeyObj));
173
208
  }
174
209
 
@@ -213,6 +248,7 @@ function sign_with_scheme(version, scheme, tbs, certKeyObj) {
213
248
  * CertificateVerify (scheme_info returns null there)
214
249
  * ecdsa_* → EC key on exactly the curve the scheme names
215
250
  * ed25519/ed448 → the matching Edwards key
251
+ * mldsa44/65/87 → the ML-DSA key of that parameter set
216
252
  * Returns false for any scheme unusable at this version, so callers get
217
253
  * version filtering for free.
218
254
  */
@@ -247,6 +283,8 @@ function scheme_matches_key(version, scheme, keyObj) {
247
283
  return want === null || curve === want;
248
284
  }
249
285
 
286
+ if (info.sig === 'mldsa') return kt === MLDSA_SCHEMES[scheme];
287
+
250
288
  if (info.sig === 'eddsa') {
251
289
  if (scheme === 0x0807) return kt === 'ed25519';
252
290
  if (scheme === 0x0808) return kt === 'ed448';
@@ -277,7 +315,7 @@ function verify_with_scheme(version, scheme, tbs, publicKey, signature) {
277
315
  if (!scheme_matches_key(version, scheme, publicKey)) return false;
278
316
 
279
317
  try {
280
- if (info.sig === 'eddsa') {
318
+ if (info.sig === 'eddsa' || info.sig === 'mldsa') {
281
319
  return crypto.verify(null, tbs, publicKey, signature);
282
320
  }
283
321
 
@@ -310,7 +348,20 @@ function verify_with_scheme(version, scheme, tbs, publicKey, signature) {
310
348
  return false;
311
349
  }
312
350
 
351
+ // IANA's names (RFC 8446 §4.2.3; draft-ietf-tls-mldsa).
352
+ const SCHEME_NAMES = {
353
+ 0x0401: 'rsa_pkcs1_sha256', 0x0501: 'rsa_pkcs1_sha384', 0x0601: 'rsa_pkcs1_sha512',
354
+ 0x0403: 'ecdsa_secp256r1_sha256', 0x0503: 'ecdsa_secp384r1_sha384', 0x0603: 'ecdsa_secp521r1_sha512',
355
+ 0x0804: 'rsa_pss_rsae_sha256', 0x0805: 'rsa_pss_rsae_sha384', 0x0806: 'rsa_pss_rsae_sha512',
356
+ 0x0807: 'ed25519', 0x0808: 'ed448',
357
+ 0x0809: 'rsa_pss_pss_sha256', 0x080a: 'rsa_pss_pss_sha384', 0x080b: 'rsa_pss_pss_sha512',
358
+ 0x0904: 'mldsa44', 0x0905: 'mldsa65', 0x0906: 'mldsa87',
359
+ 0x0201: 'rsa_pkcs1_sha1', 0x0203: 'ecdsa_sha1',
360
+ };
361
+ function scheme_name(scheme) { return SCHEME_NAMES[scheme] || null; }
362
+
313
363
  export {
364
+ scheme_name,
314
365
  scheme_info,
315
366
  pick_scheme,
316
367
  sign_with_scheme,
@@ -318,5 +369,7 @@ export {
318
369
  SIG_SCHEMES_MODERN,
319
370
  SIG_SCHEMES_LEGACY_PKCS1,
320
371
  SIG_SCHEMES_LEGACY_SHA1,
372
+ SIG_SCHEMES_MLDSA,
373
+ MLDSA_AVAILABLE,
321
374
  default_signature_schemes
322
375
  };