lemon-tls 0.9.0 → 0.11.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.
@@ -280,19 +280,29 @@ function prepare_ech_server_uncached(option) {
280
280
 
281
281
  // ============================================================
282
282
  // ClientHello bytes (RFC 8446 §4.1.2), without the handshake header
283
+ //
284
+ // ECH is TLS 1.3's and DTLS 1.3's (RFC 9849 §1). Over DTLS the hello is
285
+ // DTLS's (RFC 9147 §5.3): a legacy_cookie<0..255> after the session id —
286
+ // the encoded inner hello (§5.1) and the outer one the AAD is made of
287
+ // (§5.2) the same. `dtls` says which, everywhere below: read as a TLS one,
288
+ // every field past the session id was a byte off ("trailing bytes after
289
+ // ClientHello" for every DTLS hello with ECH).
283
290
  // ============================================================
284
291
 
285
292
  /**
286
293
  * The fields of a ClientHello body with their positions — what rebuilding
287
294
  * the inner hello and zeroing the outer payload need, and what a parsed
288
- * message object no longer has.
295
+ * message object no longer has. `afterSid` is past the session id, `mid`
296
+ * (cipher_suites onwards) past the cookie too.
289
297
  */
290
- function scan_client_hello(body) {
298
+ function scan_client_hello(body, dtls) {
291
299
  var r = new Reader(body);
292
300
  r.u16(); // legacy_version
293
301
  var random = r.bytes(32);
294
302
  var sidStart = r.o; var session_id = r.vec8();
295
303
  var afterSid = r.o;
304
+ var cookie = dtls ? r.vec8() : null; // DTLS legacy_cookie
305
+ var mid = r.o;
296
306
  r.vec16(); // cipher_suites
297
307
  r.vec8(); // legacy_compression_methods
298
308
  var extsStart = r.o;
@@ -306,7 +316,7 @@ function scan_client_hello(body) {
306
316
  exts.push({ type: type, start: base + at, end: base + er.o, data: data, dataStart: base + at + 4 });
307
317
  }
308
318
  }
309
- return { random: random, session_id: session_id, sidStart: sidStart, afterSid: afterSid, extsStart: extsStart, exts: exts };
319
+ return { random: random, session_id: session_id, sidStart: sidStart, afterSid: afterSid, cookie: cookie, mid: mid, extsStart: extsStart, exts: exts };
310
320
  }
311
321
 
312
322
  function find_ext(scan, type) {
@@ -347,12 +357,13 @@ function outer_aad(body, ext, ech) {
347
357
  * outer extensions it names. In linear time, as §5.1 asks: one forward walk
348
358
  * over the outer extensions for the whole reference list.
349
359
  */
350
- function decode_inner(encoded, outerBody, outerScan) {
360
+ function decode_inner(encoded, outerBody, outerScan, dtls) {
351
361
  var r = new Reader(encoded);
352
362
  r.u16(); r.bytes(32);
353
363
  var sidLen = r.u8();
354
364
  if (sidLen !== 0) throw eccError('EncodedClientHelloInner session id not empty');
355
365
  var afterSid = r.o;
366
+ if (dtls) r.vec8(); // its legacy_cookie, kept as it is
356
367
  r.vec16(); r.vec8();
357
368
  var extsStart = r.o;
358
369
  var extBlock = r.vec16();
@@ -384,7 +395,7 @@ function decode_inner(encoded, outerBody, outerScan) {
384
395
  var newExts = concat(parts);
385
396
  var sid = outerScan.session_id;
386
397
  var head = encoded.subarray(0, afterSid - 1); // legacy_version + random
387
- var mid = encoded.subarray(afterSid, extsStart); // cipher_suites + compression
398
+ var mid = encoded.subarray(afterSid, extsStart); // (DTLS: cookie +) cipher_suites + compression
388
399
  var extLen = new Uint8Array(2); put16(extLen, 0, newExts.length);
389
400
  return concat([head, new Uint8Array([sid.length]), sid, mid, extLen, newExts]);
390
401
  }
@@ -401,20 +412,23 @@ function wrap_client_hello(body) {
401
412
  * §7.1: the inner hello must carry the inner ECH extension and offer
402
413
  * nothing below TLS 1.3.
403
414
  */
404
- function check_inner(innerBody) {
405
- var scan = scan_client_hello(innerBody);
415
+ function check_inner(innerBody, dtls) {
416
+ var scan = scan_client_hello(innerBody, dtls);
406
417
  var e = find_ext(scan, EXT_ECH);
407
418
  if (!e || parse_ech_ext(e.data).type !== 1) throw eccError('ClientHelloInner without the inner encrypted_client_hello extension');
408
419
  var sv = find_ext(scan, EXT_SUPPORTED_VERSIONS);
409
- if (!sv) throw eccError('ClientHelloInner offers TLS 1.2');
420
+ if (!sv) throw eccError('ClientHelloInner offers ' + (dtls ? 'DTLS' : 'TLS') + ' 1.2');
410
421
  var r = new Reader(sv.data), list = r.vec8();
411
422
  if (list.length === 0 || list.length % 2) throw eccError('ClientHelloInner supported_versions malformed', ALERT_DECODE_ERROR);
412
423
  for (var i = 0; i < list.length; i += 2) {
413
424
  var v = u16(list, i);
414
425
  // 0x?a?a is GREASE (RFC 8701). TLS 1.3 and up: 0x0304 to 0xfdff — the
415
- // 0xfexx codes are DTLS, never a TLS version.
426
+ // 0xfexx codes are DTLS, never a TLS version. DTLS 1.3 and up, over
427
+ // DTLS: 0xfefc and down (DTLS versions count downwards), never 1.2
428
+ // (0xfefd) or 1.0 (0xfeff).
416
429
  if ((v & 0x0f0f) === 0x0a0a) continue;
417
- if (v < 0x0304 || v >= 0xfe00) throw eccError('ClientHelloInner offers a version other than TLS 1.3 and up (0x' + v.toString(16) + ')');
430
+ var ok = dtls ? (v >= 0xfe00 && v <= 0xfefc) : (v >= 0x0304 && v < 0xfe00);
431
+ if (!ok) throw eccError('ClientHelloInner offers a version other than ' + (dtls ? 'DTLS' : 'TLS') + ' 1.3 and up (0x' + v.toString(16) + ')');
418
432
  }
419
433
  return scan;
420
434
  }
@@ -424,6 +438,41 @@ function check_inner(innerBody) {
424
438
  // The server's side (§7.1, §7.1.1)
425
439
  // ============================================================
426
440
 
441
+ // RFC 8446 §4.1.2: a second ClientHello repeats the first one's random —
442
+ // for ECH the inner one's, over which the confirmation (§7.2) is computed.
443
+ function same_inner_random(state, random) {
444
+ if (!Buffer.from(random).equals(Buffer.from(state.inner_random))) {
445
+ throw eccError('second ClientHelloInner changed its random');
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Split mode, the backend's side (§7.2): a server with no ECH keys of its
451
+ * own. A ClientHello carrying the INNER extension is a ClientHelloInner a
452
+ * client-facing server decrypted and passed on; the backend confirms ECH
453
+ * acceptance as a server that decrypted it itself would (§7.2, §7.2.1), so
454
+ * it takes the same state: status 'accepted', inner_random.
455
+ * `extensions` is the parsed hello's list ({ type, data }). Only the inner
456
+ * type is the backend's business; an outer extension, which only a holder of
457
+ * the keys could open, is left alone. A second ClientHello (after a
458
+ * HelloRetryRequest) of an accepted one is a ClientHelloInner again (§7.2.1)
459
+ * with the first one's random (RFC 8446 §4.1.2). Throws an eccError.
460
+ */
461
+ function backend_client_hello(state, extensions, random, isSecond) {
462
+ var ext = null;
463
+ for (var i = 0; i < extensions.length; i++) if (extensions[i] && extensions[i].type === EXT_ECH) { ext = extensions[i]; break; }
464
+ var data = ext ? (ext.data || new Uint8Array(0)) : null;
465
+ var inner = !!data && data.length >= 1 && data[0] === 1;
466
+ if (inner) parse_ech_ext(data); // not empty: decode_error
467
+ if (!isSecond) {
468
+ if (inner) { state.status = 'accepted'; state.backend = true; state.inner_random = new Uint8Array(random); }
469
+ return;
470
+ }
471
+ if (!state.backend) return;
472
+ if (!inner) throw eccError('second ClientHello without the inner encrypted_client_hello extension');
473
+ same_inner_random(state, random);
474
+ }
475
+
427
476
  /**
428
477
  * A ClientHello as it arrived (handshake header included) → what the
429
478
  * handshake should continue with. `state` is per connection, `{}` at
@@ -435,9 +484,9 @@ function check_inner(innerBody) {
435
484
  * `state.status` is 'rejected' and retry_configs are due). Throws, with
436
485
  * `.alertDesc`, where §5.1 / §7.1 / §7.1.1 require the connection to end.
437
486
  */
438
- function server_process_client_hello(server, state, data, isSecond) {
487
+ function server_process_client_hello(server, state, data, isSecond, dtls) {
439
488
  var body = data.subarray(4);
440
- var scan = scan_client_hello(body);
489
+ var scan = scan_client_hello(body, dtls);
441
490
  var ext = find_ext(scan, EXT_ECH);
442
491
 
443
492
  if (isSecond) {
@@ -450,13 +499,11 @@ function server_process_client_hello(server, state, data, isSecond) {
450
499
  }
451
500
  var pt2 = state.context.open(outer_aad(body, ext, ech2), ech2.payload);
452
501
  if (!pt2) throw eccError('second ClientHelloInner does not decrypt', ALERT_DECRYPT_ERROR);
453
- var inner2 = decode_inner(pt2, body, scan);
454
- var scan2 = check_inner(inner2);
502
+ var inner2 = decode_inner(pt2, body, scan, dtls);
503
+ var scan2 = check_inner(inner2, dtls);
455
504
  // RFC 8446 §4.1.2: the second hello repeats the first one's random; the
456
505
  // ServerHello's confirmation (§7.2) is computed over it.
457
- if (!Buffer.from(scan2.random).equals(Buffer.from(state.inner_random))) {
458
- throw eccError('second ClientHelloInner changed its random');
459
- }
506
+ same_inner_random(state, scan2.random);
460
507
  return { data: wrap_client_hello(inner2) };
461
508
  }
462
509
 
@@ -487,8 +534,8 @@ function server_process_client_hello(server, state, data, isSecond) {
487
534
  if (!pt) continue;
488
535
 
489
536
  // Decrypted: from here a malformed inner hello is fatal (§5.1).
490
- var inner = decode_inner(pt, body, scan);
491
- var innerScan = check_inner(inner);
537
+ var inner = decode_inner(pt, body, scan, dtls);
538
+ var innerScan = check_inner(inner, dtls);
492
539
  state.status = 'accepted';
493
540
  state.context = ctx;
494
541
  state.kdf_id = ech.kdf_id; state.aead_id = ech.aead_id; state.config_id = ech.config_id;
@@ -595,9 +642,9 @@ function client_outer_ext_data(kdf_id, aead_id, config_id, enc, payloadLen) {
595
642
  * little of the name: the server_name padded to maximum_name_length, and
596
643
  * the whole to a multiple of 32.
597
644
  */
598
- function client_encode_inner(innerMsg, compressed, maxNameLength, sniLength) {
645
+ function client_encode_inner(innerMsg, compressed, maxNameLength, sniLength, dtls) {
599
646
  var body = innerMsg.subarray(4);
600
- var scan = scan_client_hello(body);
647
+ var scan = scan_client_hello(body, dtls);
601
648
  var parts = [], placed = false;
602
649
  for (var i = 0; i < scan.exts.length; i++) {
603
650
  var e = scan.exts[i];
@@ -615,7 +662,7 @@ function client_encode_inner(innerMsg, compressed, maxNameLength, sniLength) {
615
662
  }
616
663
  var exts = concat(parts);
617
664
  var head = body.subarray(0, scan.sidStart); // legacy_version + random
618
- var mid = body.subarray(scan.afterSid, scan.extsStart); // cipher_suites + compression
665
+ var mid = body.subarray(scan.afterSid, scan.extsStart); // (DTLS: cookie +) cipher_suites + compression
619
666
  var extLen = new Uint8Array(2); put16(extLen, 0, exts.length);
620
667
  var encoded = concat([head, new Uint8Array([0]), mid, extLen, exts]);
621
668
  var pad = sniLength != null ? Math.max(0, maxNameLength - sniLength) : 9 + maxNameLength;
@@ -623,14 +670,20 @@ function client_encode_inner(innerMsg, compressed, maxNameLength, sniLength) {
623
670
  return concat([encoded, new Uint8Array(pad)]);
624
671
  }
625
672
 
673
+ /** A ClientHello's (a handshake message) encrypted_client_hello extension data, as it went. */
674
+ function client_hello_ech_ext(msg, dtls) {
675
+ var e = find_ext(scan_client_hello(msg.subarray(4), dtls), EXT_ECH);
676
+ return e ? new Uint8Array(e.data) : null;
677
+ }
678
+
626
679
  /**
627
680
  * ClientHelloOuter (a handshake message, its ECH payload zero) → the same
628
681
  * with the payload: `encoded` sealed with AAD = the outer hello itself, as
629
682
  * it is now (§5.2).
630
683
  */
631
- function client_seal_outer(outerMsg, hpkeContext, encoded) {
684
+ function client_seal_outer(outerMsg, hpkeContext, encoded, dtls) {
632
685
  var body = outerMsg.subarray(4);
633
- var scan = scan_client_hello(body);
686
+ var scan = scan_client_hello(body, dtls);
634
687
  var ext = find_ext(scan, EXT_ECH);
635
688
  var ech = parse_ech_ext(ext.data);
636
689
  var ct = hpkeContext.seal(new Uint8Array(body), encoded);
@@ -644,18 +697,23 @@ function client_seal_outer(outerMsg, hpkeContext, encoded) {
644
697
  * §6.2: GREASE ECH — an encrypted_client_hello extension that decrypts
645
698
  * under no key, from a client with no config, so that one with a config
646
699
  * does not stand out. `g` is fixed per connection (config_id, payload
647
- * length); after a HelloRetryRequest, `enc` is empty and the payload new
648
- * random bytes of the same length.
700
+ * length), and so is the extension: made once, the same bytes every time —
701
+ * in a second ClientHello too, which "copies the entire
702
+ * 'encrypted_client_hello' extension from the first ClientHello" (§6.2).
703
+ * (An empty enc and a fresh payload after a HelloRetryRequest is real ECH's
704
+ * rule, §6.1.5, which GREASE does not follow; BoGo saw the change.)
649
705
  */
650
706
  function grease_ech_state() {
651
707
  var lens = [144, 176, 208, 240];
652
- return { config_id: crypto.randomBytes(1)[0], payload_len: lens[crypto.randomBytes(1)[0] & 3] + 16 };
708
+ return { config_id: crypto.randomBytes(1)[0], payload_len: lens[crypto.randomBytes(1)[0] & 3] + 16, ext: null };
653
709
  }
654
- function grease_ech_ext_data(g, second) {
655
- var enc = second ? new Uint8Array(0) : new Uint8Array(crypto.randomBytes(32));
656
- var d = client_outer_ext_data(0x0001, 0x0001, g.config_id, enc, g.payload_len);
657
- d.set(crypto.randomBytes(g.payload_len), d.length - g.payload_len);
658
- return d;
710
+ function grease_ech_ext_data(g) {
711
+ if (!g.ext) {
712
+ var d = client_outer_ext_data(0x0001, 0x0001, g.config_id, new Uint8Array(crypto.randomBytes(32)), g.payload_len);
713
+ d.set(crypto.randomBytes(g.payload_len), d.length - g.payload_len);
714
+ g.ext = d;
715
+ }
716
+ return g.ext;
659
717
  }
660
718
 
661
719
  /**
@@ -700,6 +758,7 @@ export {
700
758
  client_outer_ext_data,
701
759
  client_encode_inner,
702
760
  client_seal_outer,
761
+ client_hello_ech_ext,
703
762
  grease_ech_state,
704
763
  grease_ech_ext_data,
705
764
  hrr_ech_offset,
@@ -709,6 +768,7 @@ export {
709
768
  generate_ech_key,
710
769
  prepare_ech_server,
711
770
  server_process_client_hello,
771
+ backend_client_hello,
712
772
  accept_confirmation,
713
773
  scan_client_hello,
714
774
  parse_ech_ext,
@@ -23,7 +23,7 @@
23
23
 
24
24
  import crypto from 'node:crypto';
25
25
  import { hkdf_extract, hkdf_expand } from '../crypto.js';
26
- import { x25519_get_public_key, x25519_get_shared_secret } from './ecdh.js';
26
+ import { CURVES } from './ecdh.js';
27
27
 
28
28
  var MODE_BASE = 0x00, MODE_PSK = 0x01, MODE_AUTH = 0x02, MODE_AUTH_PSK = 0x03;
29
29
 
@@ -65,61 +65,29 @@ function labeled_expand(hash, suite_id, prk, label, info, L) {
65
65
  // KEMs: DH groups (RFC 9180 §4.1, §7.1)
66
66
  // ============================================================
67
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).
68
+ // The curves are ecdh.js's — TLS's key exchange uses the same ones. The
69
+ // NIST curves' private keys are scalars below the group order, which
70
+ // DeriveKeyPair draws candidates for (§7.1.3).
90
71
  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
- };
72
+ return { order: order, bitmask: bitmask, public_key: curve.public_key, shared: curve.shared };
105
73
  }
106
74
 
107
75
  var KEMS = {};
108
- KEMS[0x0010] = Object.assign(nist('prime256v1',
76
+ KEMS[0x0010] = Object.assign(nist(CURVES.p256,
109
77
  hexToBig('ffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551'), 0xff),
110
78
  { 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',
79
+ KEMS[0x0011] = Object.assign(nist(CURVES.p384,
112
80
  hexToBig('ffffffffffffffffffffffffffffffffffffffffffffffffc7634d81f4372ddf581a0db248b0a77aecec196accc52973'), 0xff),
113
81
  { 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',
82
+ KEMS[0x0012] = Object.assign(nist(CURVES.p521,
115
83
  hexToBig('01ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa51868783bf2f966b7fcc0148f709a5d03bb5c9b8899c47aebb6fb71e91386409'), 0x01),
116
84
  { id: 0x0012, name: 'DHKEM(P-521, HKDF-SHA512)', hash: 'sha512', Nsecret: 64, Nenc: 133, Npk: 133, Nsk: 66 });
117
85
  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
86
+ public_key: CURVES.x25519.public_key,
87
+ shared: CURVES.x25519.shared }; // rejects the all-zero result itself (§7.1.4)
120
88
  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 };
89
+ public_key: CURVES.x448.public_key,
90
+ shared: CURVES.x448.shared };
123
91
 
124
92
  function kem_suite_id(kem) { return concat(u8('KEM'), i2osp(kem.id, 2)); }
125
93
 
@@ -93,12 +93,24 @@ function normalize_hello(hello, kind) {
93
93
  }
94
94
  }
95
95
 
96
+ // The versions on offer (RFC 8446 §4.2.1). With a supported_versions
97
+ // extension, that list is the whole offer: legacy_version "MUST" then be
98
+ // ignored. It was added to the list — a hello offering only SSL 3.0 there
99
+ // came out as TLS 1.2, from its legacy 0x0303.
100
+ // Without the extension, TLS 1.2's rules (RFC 5246 Appendix E.1): the
101
+ // legacy_version is the client's highest, so any value from TLS 1.2 up —
102
+ // 0x0304, 0x0305, a future (6,3) — offers TLS 1.2, and nothing that
103
+ // §4.2.1 lets a server negotiate as 1.3 ("MUST negotiate TLS 1.2 or prior
104
+ // ... even if ClientHello.legacy_version is 0x0304 or later"). They were
105
+ // refused (protocol_version), or read as TLS 1.3. DTLS counts down:
106
+ // 0xfefd is 1.2, and anything newer than it offers 1.2 too.
96
107
  if (!('supported_versions' in out)) {
97
- out.supported_versions = [];
98
- }
99
-
100
- if (out.supported_versions.indexOf(out.version) < 0) {
101
- out.supported_versions.push(out.version);
108
+ let v = out.version;
109
+ if (kind === 'client' && typeof v === 'number') {
110
+ if (out.dtls) { if (v <= 0xfefd) v = 0xfefd; }
111
+ else if (v >= 0x0303) v = 0x0303;
112
+ }
113
+ out.supported_versions = (typeof v === 'number') ? [v] : [];
102
114
  }
103
115
 
104
116
  return out;
@@ -178,13 +190,15 @@ function build_tls_message(params) {
178
190
  * @param {number} [negotiatedVersion] — optional; 0x0303 for TLS 1.2, 0x0304 for TLS 1.3.
179
191
  * Needed to disambiguate NewSessionTicket wire format.
180
192
  */
181
- function parse_tls_message(data, negotiatedVersion) {
193
+ // opts.dtls: whether the transport is DTLS (the session knows; a hello's
194
+ // version does not say it reliably).
195
+ function parse_tls_message(data, negotiatedVersion, opts) {
182
196
  let out = {};
183
197
  let message = wire.parse_message(data);
184
198
 
185
199
  if (message.type == wire.TLS_MESSAGE_TYPE.CLIENT_HELLO || message.type == wire.TLS_MESSAGE_TYPE.SERVER_HELLO) {
186
200
  let kind = (message.type == wire.TLS_MESSAGE_TYPE.CLIENT_HELLO) ? 'client' : 'server';
187
- let hello = wire.parse_hello({ kind: kind, body: message.body });
201
+ let hello = wire.parse_hello({ kind: kind, body: message.body, dtls: opts ? opts.dtls : undefined, lenient: !!(opts && opts.lenient) });
188
202
  out = normalize_hello(hello, kind);
189
203
 
190
204
  } else if (message.type == wire.TLS_MESSAGE_TYPE.SERVER_KEY_EXCHANGE) {
@@ -192,6 +206,12 @@ function parse_tls_message(data, negotiatedVersion) {
192
206
  out.type = 'server_key_exchange';
193
207
 
194
208
  } else if (message.type == wire.TLS_MESSAGE_TYPE.CLIENT_KEY_EXCHANGE) {
209
+ // ECDHE (RFC 8422 §5.7): ClientECDiffieHellmanPublic, the point as
210
+ // opaque <1..2^8-1>. Its length byte was skipped, never read: a wrong
211
+ // one was taken — decode_error.
212
+ if (message.body.length < 2 || message.body[0] !== message.body.length - 1) {
213
+ throw wire.parseError('ClientKeyExchange: point length ' + (message.body.length ? message.body[0] : 'missing') + ' for ' + Math.max(0, message.body.length - 1) + ' bytes');
214
+ }
195
215
  out.body = message.body;
196
216
  out.public_key = message.body.slice(1);
197
217
  out.type = 'client_key_exchange';
@@ -207,7 +227,7 @@ function parse_tls_message(data, negotiatedVersion) {
207
227
  out.type = 'encrypted_extensions';
208
228
 
209
229
  } else if (message.type == wire.TLS_MESSAGE_TYPE.CERTIFICATE) {
210
- out = wire.parse_certificate(message.body);
230
+ out = wire.parse_certificate(message.body, wire.is_tls13_wire(negotiatedVersion));
211
231
  out.type = 'certificate';
212
232
 
213
233
  } else if (message.type == wire.TLS_MESSAGE_TYPE.COMPRESSED_CERTIFICATE) {
@@ -241,7 +261,7 @@ function parse_tls_message(data, negotiatedVersion) {
241
261
  // 1.2 does. Testing only for 0x0303 sent a DTLS 1.2 ticket through the
242
262
  // TLS 1.3 parser — the same class of bug as the Certificate encoding one:
243
263
  // a version comparison that does not know DTLS exists.
244
- if (negotiatedVersion === 0x0303 || negotiatedVersion === 0xFEFD) {
264
+ if (wire.is_tls12_wire(negotiatedVersion)) {
245
265
  out = wire.parse_new_session_ticket_tls12(message.body);
246
266
  } else {
247
267
  out = wire.parse_new_session_ticket(message.body);
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import * as crypto from 'crypto';
7
+ import { parse_cert } from './x509.js';
7
8
 
8
9
  let TLS_VERSION_TLS1_2 = 0x0303;
9
10
  let TLS_VERSION_TLS1_3 = 0x0304;
@@ -34,6 +35,11 @@ const SIG_SCHEMES_MODERN = [
34
35
  0x0807, 0x0808, // ed25519, ed448
35
36
  0x0403, 0x0503, 0x0603, // ecdsa_secp256r1/384r1/521r1
36
37
  0x0804, 0x0805, 0x0806, // rsa_pss_rsae_sha256/384/512
38
+ // rsa_pss_pss_sha256/384/512: an RSASSA-PSS key (id-RSASSA-PSS) signs with
39
+ // these only. Verified all along, never offered — so a client certificate
40
+ // of that kind, held now to what the CertificateRequest offers, could not
41
+ // be used at all.
42
+ 0x0809, 0x080a, 0x080b,
37
43
  ];
38
44
 
39
45
  // ML-DSA (FIPS 204), post-quantum signatures, TLS 1.3 only
@@ -61,22 +67,22 @@ const SIG_SCHEMES_LEGACY_PKCS1 = [
61
67
  0x0401, 0x0501, 0x0601, // rsa_pkcs1_sha256/384/512
62
68
  ];
63
69
 
64
- /**
65
- * SHA-1 schemes. Deliberately last, and deliberately still offered.
70
+ /*
71
+ * SHA-1 (rsa_pkcs1_sha1 0x0201, ecdsa_sha1 0x0203) is neither offered nor
72
+ * used, at any version (RFC 9155): not in a ClientHello or CertificateRequest
73
+ * — "MUST NOT include" —, not signed with, and a peer's handshake signature
74
+ * under it is refused (scheme_info knows no SHA-1 scheme). It was offered
75
+ * last, and used whenever a TLS 1.2 peer asked for it first or offered
76
+ * nothing else: a server signed its ServerKeyExchange with SHA-1 for any
77
+ * client that wanted that (internet.nl: "insufficiently secure hash
78
+ * functions for key exchange"; NCSC-NL 2025-05 §3.3.5).
66
79
  *
67
- * Same offer/use split as the PKCS#1 set above, taken one step further: SHA-1
68
- * is unacceptable for a handshake signature, and scheme_info already refuses
69
- * these at TLS 1.3 so they can never be selected for CertificateVerify. But
70
- * signature_algorithms also describes the signatures we can verify ON
71
- * CERTIFICATES (RFC 8446 §4.2.3), and SHA-1-signed intermediates still exist
72
- * in real chains. Withholding them from the offer tells a peer we cannot
73
- * verify such a chain at all, which is a different — and wrong — statement
74
- * from "we will not sign with SHA-1".
80
+ * It had been kept in the offer for certificates — signature_algorithms also
81
+ * names what we verify on a chain (RFC 8446 §4.2.3). That reason is gone:
82
+ * a certificate below the anchor signed with SHA-1 is refused
83
+ * (session/x509.js, CA_MD_TOO_WEAK, as node:tls refuses it), and an anchor's
84
+ * own signature is never checked.
75
85
  */
76
- const SIG_SCHEMES_LEGACY_SHA1 = [
77
- 0x0201, // rsa_pkcs1_sha1
78
- 0x0203, // ecdsa_sha1
79
- ];
80
86
 
81
87
  /**
82
88
  * Default offer list — the same for every version range, deliberately taking
@@ -95,8 +101,7 @@ const SIG_SCHEMES_LEGACY_SHA1 = [
95
101
  function default_signature_schemes() {
96
102
  return SIG_SCHEMES_MODERN
97
103
  .concat(SIG_SCHEMES_MLDSA)
98
- .concat(SIG_SCHEMES_LEGACY_PKCS1)
99
- .concat(SIG_SCHEMES_LEGACY_SHA1);
104
+ .concat(SIG_SCHEMES_LEGACY_PKCS1);
100
105
  }
101
106
 
102
107
 
@@ -146,12 +151,10 @@ function scheme_info(version, scheme) {
146
151
  // (RFC 8446 §4.4.3); TLS 1.2 still uses them.
147
152
  if (version === TLS_VERSION_TLS1_3) return null;
148
153
 
149
- // TLS 1.2: classic (hash, sig) interpretation
150
- if (h === 0x02 && s === 0x01) return { hash: 'sha1', sig: 'rsa', isPSS: false };
154
+ // TLS 1.2: classic (hash, sig) interpretation. No SHA-1 (RFC 9155).
151
155
  if (h === 0x04 && s === 0x01) return { hash: 'sha256', sig: 'rsa', isPSS: false };
152
156
  if (h === 0x05 && s === 0x01) return { hash: 'sha384', sig: 'rsa', isPSS: false };
153
157
  if (h === 0x06 && s === 0x01) return { hash: 'sha512', sig: 'rsa', isPSS: false };
154
- if (h === 0x02 && s === 0x03) return { hash: 'sha1', sig: 'ecdsa', isPSS: false };
155
158
 
156
159
  return null;
157
160
  }
@@ -259,6 +262,18 @@ function scheme_matches_key(version, scheme, keyObj) {
259
262
  let kt = keyObj.asymmetricKeyType;
260
263
 
261
264
  if (info.sig === 'rsa') {
265
+ // The key large enough for the scheme: PSS with a salt of the hash's
266
+ // length needs a modulus of 2·hLen + 2 bytes (RFC 8017 §9.1.1), PKCS#1
267
+ // v1.5 the DigestInfo plus 11. A 1024-bit key under rsa_pss_*_sha512 made
268
+ // node:crypto throw out of the handshake — the process with it — for a
269
+ // client that offered only that.
270
+ let bits = 0;
271
+ try { bits = (keyObj.asymmetricKeyDetails && keyObj.asymmetricKeyDetails.modulusLength) || 0; } catch (e) { bits = 0; }
272
+ if (bits > 0) {
273
+ let hLen = info.hash === 'sha512' ? 64 : info.hash === 'sha384' ? 48 : info.hash === 'sha256' ? 32 : 20;
274
+ let need = info.isPSS ? 2 * hLen + 2 : hLen + 19 + 11;
275
+ if (Math.ceil(bits / 8) < need) return false;
276
+ }
262
277
  if (info.isPSS) {
263
278
  // 0x0809-0x080b are the *_pss_pss_* family; 0x0804-0x0806 are *_pss_rsae_*.
264
279
  let wantsPssKey = (scheme === 0x0809 || scheme === 0x080a || scheme === 0x080b);
@@ -270,8 +285,13 @@ function scheme_matches_key(version, scheme, keyObj) {
270
285
 
271
286
  if (info.sig === 'ecdsa') {
272
287
  if (kt !== 'ec') return false;
273
- // The curve is part of the scheme, not a free choice: signing P-256 data
274
- // with a P-384 key (or vice versa) is a type confusion the RFC forbids.
288
+ // The curve is part of the scheme in TLS 1.3 (RFC 8446 §4.2.3: "ecdsa_
289
+ // secp384r1_sha384" is P-384 with SHA-384): signing with a P-256 key under
290
+ // it is a confusion the RFC forbids. In TLS 1.2 the same codepoint is
291
+ // only {sha384, ecdsa} (RFC 5246 §7.4.1.4.1), for a key of any curve: a
292
+ // P-256 server asked only for sha384 had no scheme, and sent its
293
+ // ServerKeyExchange unsigned.
294
+ if (version !== TLS_VERSION_TLS1_3) return true;
275
295
  let curve = null;
276
296
  try {
277
297
  curve = keyObj.asymmetricKeyDetails && keyObj.asymmetricKeyDetails.namedCurve;
@@ -360,15 +380,53 @@ const SCHEME_NAMES = {
360
380
  };
361
381
  function scheme_name(scheme) { return SCHEME_NAMES[scheme] || null; }
362
382
 
383
+ /**
384
+ * Whether a certificate lets its key sign (RFC 5280 §4.2.1.3): without a
385
+ * keyUsage extension, yes; with one, only if digitalSignature (bit 0) is set.
386
+ * Every TLS 1.3 certificate signs its CertificateVerify, and under TLS 1.2
387
+ * every suite we offer is (EC)DHE, where the key signs the
388
+ * ServerKeyExchange or the client's CertificateVerify — static RSA, the one
389
+ * use of keyEncipherment, is not offered. So a certificate whose keyUsage
390
+ * says otherwise cannot be the one that authenticates this handshake.
391
+ *
392
+ * The certificate is read by cryptographic-formats (session/x509.js's parse_cert,
393
+ * once per DER object — the chain check reads the same one). Unreadable:
394
+ * null (the chain check owns that failure). An all-zero keyUsage does not
395
+ * read either — cryptographic-formats refuses it, as OpenSSL marks such a
396
+ * certificate invalid.
397
+ */
398
+ function cert_allows_signing(der) {
399
+ let cert = parse_cert(der instanceof Uint8Array ? der : new Uint8Array(der));
400
+ if (!cert) return null;
401
+ return cert.ku ? cert.ku.digitalSignature : true;
402
+ }
403
+
404
+ /**
405
+ * The TLS 1.2 named group of an ECDSA key, or null for any other key
406
+ * (RFC 8422 §5.1.1: a client's supported_groups also names the curves it
407
+ * accepts in an ECDSA certificate). TLS 1.3 binds the curve in the scheme
408
+ * instead (scheme_matches_key).
409
+ */
410
+ function ec_key_group(keyObj) {
411
+ let curve = null;
412
+ try {
413
+ if (keyObj.asymmetricKeyType !== 'ec') return null;
414
+ curve = keyObj.asymmetricKeyDetails && keyObj.asymmetricKeyDetails.namedCurve;
415
+ } catch (e) { return null; }
416
+ return curve === 'prime256v1' ? 23 : curve === 'secp384r1' ? 24 : curve === 'secp521r1' ? 25 : -1;
417
+ }
418
+
363
419
  export {
420
+ scheme_matches_key,
364
421
  scheme_name,
422
+ cert_allows_signing,
423
+ ec_key_group,
365
424
  scheme_info,
366
425
  pick_scheme,
367
426
  sign_with_scheme,
368
427
  verify_with_scheme,
369
428
  SIG_SCHEMES_MODERN,
370
429
  SIG_SCHEMES_LEGACY_PKCS1,
371
- SIG_SCHEMES_LEGACY_SHA1,
372
430
  SIG_SCHEMES_MLDSA,
373
431
  MLDSA_AVAILABLE,
374
432
  default_signature_schemes
@@ -109,11 +109,13 @@ function decrypt_session_blob(blob, ticketKeys) {
109
109
  * User is responsible for secure storage (this contains the master_secret).
110
110
  *
111
111
  * @param {Object} state
112
- * @returns {Uint8Array} — utf-8 encoded JSON
112
+ * @returns {Buffer} — utf-8 encoded JSON. A Buffer, as node:tls's 'session'
113
+ * gives (and what the socket's getSession() returned already): it was
114
+ * copied into a plain Uint8Array, so 'session' handed out one type and
115
+ * getSession() another.
113
116
  */
114
117
  function encode_client_session(state) {
115
- let serialized = JSON.stringify(serialize_state(state));
116
- return new Uint8Array(Buffer.from(serialized, 'utf8'));
118
+ return Buffer.from(JSON.stringify(serialize_state(state)), 'utf8');
117
119
  }
118
120
 
119
121
  /**