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.
- package/README.md +168 -41
- package/index.cjs +9 -7
- package/index.d.ts +626 -30
- package/package.json +45 -7
- package/src/ca_store.js +31 -0
- package/src/compat.js +76 -116
- package/src/crypto.js +21 -114
- package/src/dtls_session.js +1284 -390
- package/src/dtls_socket.js +318 -96
- package/src/node_options.js +154 -17
- package/src/record.js +355 -191
- package/src/secure_context.js +97 -39
- package/src/session/ecdh.js +140 -122
- package/src/session/ech.js +92 -32
- package/src/session/hpke.js +12 -44
- package/src/session/message.js +29 -9
- package/src/session/signing.js +80 -22
- package/src/session/ticket.js +5 -3
- package/src/session/x509.js +385 -0
- package/src/tls_session.js +2221 -902
- package/src/tls_socket.js +451 -452
- package/src/wire.js +271 -158
- package/types/crypto.d.ts +40 -0
- package/types/record.d.ts +40 -0
- package/types/session.d.ts +3 -0
- package/types/wire.d.ts +62 -0
package/src/session/ech.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|
|
648
|
-
*
|
|
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
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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,
|
package/src/session/hpke.js
CHANGED
|
@@ -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 {
|
|
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
|
|
69
|
-
|
|
70
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
119
|
-
shared:
|
|
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:
|
|
122
|
-
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
|
|
package/src/session/message.js
CHANGED
|
@@ -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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
|
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);
|
package/src/session/signing.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
|
274
|
-
//
|
|
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
|
package/src/session/ticket.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
116
|
-
return new Uint8Array(Buffer.from(serialized, 'utf8'));
|
|
118
|
+
return Buffer.from(JSON.stringify(serialize_state(state)), 'utf8');
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
/**
|