lemon-tls 0.4.0 → 0.5.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 +104 -2
- package/index.js +10 -0
- package/package.json +1 -1
- package/src/compat.js +30 -17
- package/src/crypto.js +92 -2
- package/src/dtls_session.js +222 -43
- package/src/dtls_socket.js +22 -0
- package/src/record.js +133 -5
- package/src/session/ecdh.js +16 -1
- package/src/tls_session.js +269 -39
- package/src/tls_socket.js +44 -1
- package/src/utils.js +28 -1
package/src/tls_session.js
CHANGED
|
@@ -21,12 +21,16 @@ import {
|
|
|
21
21
|
derive_handshake_traffic_secrets_psk_with_hash,
|
|
22
22
|
hkdf_expand_label,
|
|
23
23
|
getHashFn,
|
|
24
|
+
derive_exporter_master_secret_with_hash,
|
|
25
|
+
tls13_exporter,
|
|
26
|
+
tls12_exporter,
|
|
24
27
|
} from './crypto.js';
|
|
25
28
|
|
|
26
29
|
import {
|
|
27
30
|
concatUint8Arrays,
|
|
28
31
|
arraysEqual,
|
|
29
|
-
uint8Equal
|
|
32
|
+
uint8Equal,
|
|
33
|
+
timingSafeEqualU8
|
|
30
34
|
} from './utils.js';
|
|
31
35
|
|
|
32
36
|
import * as wire from './wire.js';
|
|
@@ -38,6 +42,15 @@ import { x25519_get_public_key, x25519_get_shared_secret, p256_generate_keypair,
|
|
|
38
42
|
import { build_tls_message, parse_tls_message } from './session/message.js';
|
|
39
43
|
import { encrypt_session_blob, decrypt_session_blob, encode_client_session, decode_client_session } from './session/ticket.js';
|
|
40
44
|
|
|
45
|
+
// RFC 8446 §4.1.3 downgrade-protection sentinels. A TLS 1.3-capable server that
|
|
46
|
+
// negotiates a lower version stamps one of these into the last 8 bytes of
|
|
47
|
+
// ServerHello.random; a TLS 1.3-capable client that ends up on a lower version
|
|
48
|
+
// MUST reject the handshake if it sees the sentinel. This defeats an active
|
|
49
|
+
// attacker forcing a version downgrade (the sentinel is covered by the server's
|
|
50
|
+
// signature / Finished, so it can't be stripped without breaking the handshake).
|
|
51
|
+
const DOWNGRADE_SENTINEL_TLS12 = new Uint8Array([0x44,0x4F,0x57,0x4E,0x47,0x52,0x44,0x01]); // "DOWNGRD\x01"
|
|
52
|
+
const DOWNGRADE_SENTINEL_TLS11 = new Uint8Array([0x44,0x4F,0x57,0x4E,0x47,0x52,0x44,0x00]); // "DOWNGRD\x00" (TLS 1.1 and below)
|
|
53
|
+
|
|
41
54
|
// Debug logging — enabled via LEMON_DEBUG=1 env var
|
|
42
55
|
const LEMON_DEBUG = typeof process !== 'undefined' && process.env && process.env.LEMON_DEBUG === '1';
|
|
43
56
|
function dbg(tag, ...args) { if (LEMON_DEBUG) console.error('[LEMON ' + tag + ']', ...args); }
|
|
@@ -199,6 +212,7 @@ function TLSSession(options){
|
|
|
199
212
|
|
|
200
213
|
// TLS 1.3 resumption
|
|
201
214
|
tls13_master_secret: null,
|
|
215
|
+
exporter_master_secret: null, // RFC 8446 §7.1 "exp master" — for exportKeyingMaterial
|
|
202
216
|
resumption_master_secret: null,
|
|
203
217
|
ticket_nonce_counter: 0,
|
|
204
218
|
session_ticket_sent: false,
|
|
@@ -363,6 +377,15 @@ function TLSSession(options){
|
|
|
363
377
|
// Save raw ClientHello + emit event (server side)
|
|
364
378
|
if (context.isServer && message.type === 'client_hello') {
|
|
365
379
|
context.rawClientHello = data;
|
|
380
|
+
// Make the peer's extensions readable INSIDE the 'clienthello'
|
|
381
|
+
// handler (getRemoteExtension/getRemoteExtensions) — the main
|
|
382
|
+
// set_context that stores them runs after this emit, and the
|
|
383
|
+
// whole point of the event is to let callers inspect the offer
|
|
384
|
+
// and set answering local_extensions before the ServerHello is
|
|
385
|
+
// built (e.g. DTLS-SRTP use_srtp, RFC 5764 §4.1.2). The later
|
|
386
|
+
// set_context stores the identical array; its equality check
|
|
387
|
+
// makes that a no-op.
|
|
388
|
+
context.remote_extensions = message.extensions || [];
|
|
366
389
|
ev.emit('clienthello', data, message);
|
|
367
390
|
}
|
|
368
391
|
|
|
@@ -412,12 +435,10 @@ function TLSSession(options){
|
|
|
412
435
|
dbg('SRV-PSK', 'expected binder:', hexPreview(expectedBinder, 16));
|
|
413
436
|
dbg('SRV-PSK', 'received binder:', hexPreview(pskBinder, 16));
|
|
414
437
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
420
|
-
}
|
|
438
|
+
// Constant-time binder comparison. A byte-by-byte early-exit compare
|
|
439
|
+
// would let an attacker recover a valid binder via timing, one byte at
|
|
440
|
+
// a time. timingSafeEqualU8 always compares the full length.
|
|
441
|
+
let binderOk = timingSafeEqualU8(expectedBinder, pskBinder);
|
|
421
442
|
|
|
422
443
|
dbg('SRV-PSK', binderOk ? '✓ BINDER MATCH — psk_accepted' : '✗ BINDER MISMATCH — full handshake');
|
|
423
444
|
|
|
@@ -504,20 +525,24 @@ function TLSSession(options){
|
|
|
504
525
|
// - ALPN (same as CH1)
|
|
505
526
|
// - custom extensions (QUIC transport params etc.)
|
|
506
527
|
// - same cipher_suites, session_id, random as CH1
|
|
528
|
+
// CH2 MUST match CH1 exactly except key_share/cookie (RFC 8446
|
|
529
|
+
// §4.1.2) — so mirror CH1's conditional composition: same sigalg
|
|
530
|
+
// source (CH1 wrote its final list back to the context), and the
|
|
531
|
+
// same tls13Only rules for the legacy extensions and psk modes.
|
|
532
|
+
let tls13Only2 = context.local_supported_versions.length > 0 &&
|
|
533
|
+
context.local_supported_versions.every(v => v === 0x0304);
|
|
507
534
|
let extensions = [
|
|
508
535
|
{ type: 'SUPPORTED_VERSIONS', value: context.local_supported_versions },
|
|
509
536
|
{ type: 'SUPPORTED_GROUPS', value: context.local_supported_groups },
|
|
510
537
|
{ type: 'KEY_SHARE', value: [{ group: requestedGroup, key_exchange: newKeyGroup.public_key }] },
|
|
511
|
-
{ type: 'SIGNATURE_ALGORITHMS', value:
|
|
512
|
-
// Must match CH1 exactly (RFC 8446 §4.1.2)
|
|
513
|
-
0x0804, 0x0805, 0x0806,
|
|
514
|
-
0x0403, 0x0503, 0x0603,
|
|
515
|
-
0x0807, 0x0808,
|
|
516
|
-
0x0401, 0x0501, 0x0601
|
|
517
|
-
] },
|
|
518
|
-
{ type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) },
|
|
519
|
-
{ type: 'EXTENDED_MASTER_SECRET', value: null },
|
|
538
|
+
{ type: 'SIGNATURE_ALGORITHMS', value: context.local_supported_signature_algorithms },
|
|
520
539
|
];
|
|
540
|
+
if (!tls13Only2) {
|
|
541
|
+
extensions.push({ type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) });
|
|
542
|
+
extensions.push({ type: 'EXTENDED_MASTER_SECRET', value: null });
|
|
543
|
+
} else {
|
|
544
|
+
extensions.push({ type: 'PSK_KEY_EXCHANGE_MODES', value: [1] });
|
|
545
|
+
}
|
|
521
546
|
|
|
522
547
|
// SNI (must be first)
|
|
523
548
|
if (context.local_sni) extensions.unshift({ type: 'SERVER_NAME', value: context.local_sni });
|
|
@@ -543,6 +568,11 @@ function TLSSession(options){
|
|
|
543
568
|
random: context.local_random,
|
|
544
569
|
session_id: context.local_session_id,
|
|
545
570
|
cookie: context.dtls_cookie,
|
|
571
|
+
// BUGFIX: `cipher_suites` (plural) is what wire.js's client_hello
|
|
572
|
+
// builder actually reads; the singular key was silently ignored and
|
|
573
|
+
// wire.js substituted its own hardcoded fallback list — the
|
|
574
|
+
// configured cipher suites never reached the wire. Both names passed.
|
|
575
|
+
cipher_suites: context.local_supported_cipher_suites,
|
|
546
576
|
cipher_suite: context.local_supported_cipher_suites,
|
|
547
577
|
extensions: extensions,
|
|
548
578
|
});
|
|
@@ -572,6 +602,28 @@ function TLSSession(options){
|
|
|
572
602
|
add_remote_key_groups: message.key_groups || []
|
|
573
603
|
});
|
|
574
604
|
|
|
605
|
+
// RFC 8446 §4.1.3: downgrade-protection check (client side).
|
|
606
|
+
// If we offered TLS 1.3 but the server steered us to a lower version, the
|
|
607
|
+
// server's random must NOT carry the downgrade sentinel — if it does, an
|
|
608
|
+
// active attacker forced the downgrade. HRR was already handled and
|
|
609
|
+
// returned above, so `message` here is a genuine ServerHello.
|
|
610
|
+
if (!context.isServer && message.type === 'server_hello' &&
|
|
611
|
+
context.local_supported_versions.indexOf(wire.TLS_VERSION.TLS1_3) >= 0 &&
|
|
612
|
+
context.selected_version !== null &&
|
|
613
|
+
context.selected_version !== wire.TLS_VERSION.TLS1_3 &&
|
|
614
|
+
context.selected_version !== wire.DTLS_VERSION.DTLS1_3 &&
|
|
615
|
+
message.random && message.random.length === 32) {
|
|
616
|
+
|
|
617
|
+
let tail = message.random.subarray(24, 32);
|
|
618
|
+
if (timingSafeEqualU8(tail, DOWNGRADE_SENTINEL_TLS12) ||
|
|
619
|
+
timingSafeEqualU8(tail, DOWNGRADE_SENTINEL_TLS11)) {
|
|
620
|
+
dbg('DOWNGRADE', 'sentinel detected in ServerHello.random — aborting');
|
|
621
|
+
sendAlert(2, wire.TLS_ALERT.ILLEGAL_PARAMETER); // fatal, illegal_parameter (47)
|
|
622
|
+
ev.emit('error', new Error('TLS downgrade attack detected (RFC 8446 §4.1.3 sentinel present)'));
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
575
627
|
// Server: TLS 1.2 resumption detection (only makes sense from ClientHello).
|
|
576
628
|
// This runs BEFORE set_context's reactive loop picks a version, so we mark state
|
|
577
629
|
// but defer the decision. The reactive loop will check tls12_abbreviated once
|
|
@@ -754,6 +806,26 @@ function TLSSession(options){
|
|
|
754
806
|
|
|
755
807
|
pushTranscript(data);
|
|
756
808
|
|
|
809
|
+
// TLS 1.3: most server extensions arrive HERE, not in the
|
|
810
|
+
// ServerHello (RFC 8446 §4.3.1) — including application-protocol
|
|
811
|
+
// ones like use_srtp (RFC 8842 requires it in EncryptedExtensions).
|
|
812
|
+
// Merge them into remote_extensions so getRemoteExtension() sees a
|
|
813
|
+
// version-agnostic union: ServerHello entries first (kept on type
|
|
814
|
+
// collision, which the RFC forbids anyway), then EE entries.
|
|
815
|
+
if (Array.isArray(message.extensions) && message.extensions.length > 0) {
|
|
816
|
+
var _mergedExts = (context.remote_extensions || []).slice();
|
|
817
|
+
for (var _eei = 0; _eei < message.extensions.length; _eei++) {
|
|
818
|
+
var _ee = message.extensions[_eei];
|
|
819
|
+
if (!_ee) continue;
|
|
820
|
+
var _dup = false;
|
|
821
|
+
for (var _mj = 0; _mj < _mergedExts.length; _mj++) {
|
|
822
|
+
if (_mergedExts[_mj] && _mergedExts[_mj].type === _ee.type) { _dup = true; break; }
|
|
823
|
+
}
|
|
824
|
+
if (!_dup) _mergedExts.push(_ee);
|
|
825
|
+
}
|
|
826
|
+
context.remote_extensions = _mergedExts;
|
|
827
|
+
}
|
|
828
|
+
|
|
757
829
|
set_context({
|
|
758
830
|
remote_supported_groups: message.supported_groups || [],
|
|
759
831
|
});
|
|
@@ -1178,6 +1250,12 @@ function TLSSession(options){
|
|
|
1178
1250
|
}
|
|
1179
1251
|
}
|
|
1180
1252
|
|
|
1253
|
+
if('exporter_master_secret' in options){
|
|
1254
|
+
if(context.exporter_master_secret==null && options.exporter_master_secret!==null){
|
|
1255
|
+
context.exporter_master_secret=options.exporter_master_secret;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1181
1259
|
if('tls13_master_secret' in options){
|
|
1182
1260
|
if(context.tls13_master_secret==null && options.tls13_master_secret!==null){
|
|
1183
1261
|
context.tls13_master_secret=options.tls13_master_secret;
|
|
@@ -1577,6 +1655,20 @@ function TLSSession(options){
|
|
|
1577
1655
|
|
|
1578
1656
|
if(context.local_random==null){
|
|
1579
1657
|
context.local_random=new Uint8Array(crypto.randomBytes(32));
|
|
1658
|
+
|
|
1659
|
+
// RFC 8446 §4.1.3: if we support TLS 1.3 but negotiated something
|
|
1660
|
+
// lower, stamp the downgrade sentinel into the last 8 bytes of the
|
|
1661
|
+
// ServerHello random so a 1.3-capable client can detect the downgrade.
|
|
1662
|
+
// Only meaningful for a real ServerHello (not HRR, which reuses the
|
|
1663
|
+
// magic HRR random), and only for the TLS transport (not DTLS here).
|
|
1664
|
+
if (context.local_supported_versions.indexOf(wire.TLS_VERSION.TLS1_3) >= 0) {
|
|
1665
|
+
if (context.selected_version === wire.TLS_VERSION.TLS1_2) {
|
|
1666
|
+
context.local_random.set(DOWNGRADE_SENTINEL_TLS12, 24);
|
|
1667
|
+
} else if (context.selected_version === wire.TLS_VERSION.TLS1_1 ||
|
|
1668
|
+
context.selected_version === wire.TLS_VERSION.TLS1_0) {
|
|
1669
|
+
context.local_random.set(DOWNGRADE_SENTINEL_TLS11, 24);
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1580
1672
|
}
|
|
1581
1673
|
|
|
1582
1674
|
let build_message_params=null;
|
|
@@ -1641,6 +1733,19 @@ function TLSSession(options){
|
|
|
1641
1733
|
ext_list.push({ type: 'SESSION_TICKET', value: new Uint8Array(0) });
|
|
1642
1734
|
}
|
|
1643
1735
|
|
|
1736
|
+
// Custom local extensions (generic pass-through — same mechanism
|
|
1737
|
+
// as the ClientHello and TLS 1.3 EncryptedExtensions paths).
|
|
1738
|
+
// This is where server-side answers to client-offered extensions
|
|
1739
|
+
// (e.g. use_srtp for DTLS-SRTP, RFC 5764 §4.1) go on the wire in
|
|
1740
|
+
// (D)TLS 1.2. Previously local_extensions were silently dropped
|
|
1741
|
+
// for the 1.2 ServerHello, so a server could never answer them.
|
|
1742
|
+
// Responsibility for only answering extensions the client
|
|
1743
|
+
// actually offered stays with the caller (it can inspect the
|
|
1744
|
+
// ClientHello via the 'clienthello' event / getRemoteExtension).
|
|
1745
|
+
for (let lei = 0; lei < context.local_extensions.length; lei++) {
|
|
1746
|
+
ext_list.push(context.local_extensions[lei]);
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1644
1749
|
// Session ID: for abbreviated (resumed) handshake, MUST echo client's session_id
|
|
1645
1750
|
// (RFC 5077 §3.4). For TLS 1.2 full handshake, generate a fresh 32-byte session_id
|
|
1646
1751
|
// (matches OpenSSL/Node.js behavior for middlebox compatibility).
|
|
@@ -2358,10 +2463,18 @@ function TLSSession(options){
|
|
|
2358
2463
|
if((context.isServer==true && context.finished_sent==true && context.remote_finished_ok==false) || (context.isServer==false && context.finished_sent==false && context.remote_finished_ok==true)){
|
|
2359
2464
|
|
|
2360
2465
|
let appHashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
2361
|
-
let
|
|
2466
|
+
let appTranscriptHash = get_transcript_hash(appHashName);
|
|
2467
|
+
let result2 = derive_app_traffic_secrets_with_hash(appHashName, context.base_secret, appTranscriptHash);
|
|
2362
2468
|
|
|
2363
2469
|
// Save master_secret for resumption before clearing
|
|
2364
2470
|
params_to_set['tls13_master_secret'] = result2.master_secret;
|
|
2471
|
+
|
|
2472
|
+
// RFC 8446 §7.1: exporter_master_secret uses the SAME transcript
|
|
2473
|
+
// hash (ClientHello..server Finished) as the app traffic secrets.
|
|
2474
|
+
// Kept for the lifetime of the connection so exportKeyingMaterial
|
|
2475
|
+
// (RFC 8446 §7.5) works after handshake completion.
|
|
2476
|
+
params_to_set['exporter_master_secret'] =
|
|
2477
|
+
derive_exporter_master_secret_with_hash(appHashName, result2.master_secret, appTranscriptHash);
|
|
2365
2478
|
params_to_set['base_secret']=null;
|
|
2366
2479
|
|
|
2367
2480
|
if (context.isServer === true) {
|
|
@@ -2417,7 +2530,9 @@ function TLSSession(options){
|
|
|
2417
2530
|
//compare finished to expected...
|
|
2418
2531
|
if(context.remote_finished_ok==false && context.remote_finished!==null && context.expected_remote_finished!==null){
|
|
2419
2532
|
|
|
2420
|
-
|
|
2533
|
+
// Constant-time Finished comparison — verify_data is a MAC; a timing
|
|
2534
|
+
// oracle here would let a peer forge a valid Finished byte by byte.
|
|
2535
|
+
if(timingSafeEqualU8(context.remote_finished, context.expected_remote_finished)==true){
|
|
2421
2536
|
|
|
2422
2537
|
let message_data = build_tls_message({
|
|
2423
2538
|
type: 'finished',
|
|
@@ -2690,9 +2805,22 @@ function TLSSession(options){
|
|
|
2690
2805
|
context.local_session_id=new Uint8Array(crypto.randomBytes(32));
|
|
2691
2806
|
}
|
|
2692
2807
|
|
|
2808
|
+
// TLS 1.3-only mode (QUIC and any client that offers no other version):
|
|
2809
|
+
// the ClientHello must not carry TLS 1.2 baggage. QUIC mandates TLS 1.3
|
|
2810
|
+
// (RFC 9001), and strict 1.3-only stacks reject hellos with legacy
|
|
2811
|
+
// artifacts. Detected from the configured versions, so plain TLS over
|
|
2812
|
+
// TCP (which offers 1.3+1.2) keeps the full compatibility behavior.
|
|
2813
|
+
let tls13Only = context.local_supported_versions.length > 0 &&
|
|
2814
|
+
context.local_supported_versions.every(v => v === 0x0304);
|
|
2815
|
+
|
|
2693
2816
|
// Support both TLS 1.3 and 1.2 (server picks the best)
|
|
2694
2817
|
if(context.local_supported_cipher_suites.length<=0){
|
|
2695
|
-
context.local_supported_cipher_suites=[
|
|
2818
|
+
context.local_supported_cipher_suites = tls13Only ? [
|
|
2819
|
+
// TLS 1.3 only — no ECDHE 1.2 suites in a QUIC hello
|
|
2820
|
+
0x1301, // TLS_AES_128_GCM_SHA256
|
|
2821
|
+
0x1302, // TLS_AES_256_GCM_SHA384
|
|
2822
|
+
0x1303, // TLS_CHACHA20_POLY1305_SHA256
|
|
2823
|
+
] : [
|
|
2696
2824
|
// TLS 1.3
|
|
2697
2825
|
0x1301, // TLS_AES_128_GCM_SHA256
|
|
2698
2826
|
0x1302, // TLS_AES_256_GCM_SHA384
|
|
@@ -2722,6 +2850,23 @@ function TLSSession(options){
|
|
|
2722
2850
|
private_key: private_key
|
|
2723
2851
|
};
|
|
2724
2852
|
|
|
2853
|
+
// Honor the configured signature algorithms; the hardcoded list is only
|
|
2854
|
+
// the default when none were set (previously the configured list was
|
|
2855
|
+
// ignored here — while the HRR CH2 hardcoded its own, so CH1 and CH2
|
|
2856
|
+
// could differ, violating RFC 8446 §4.1.2's exact-match requirement).
|
|
2857
|
+
let ch_sigalgs = (context.local_supported_signature_algorithms &&
|
|
2858
|
+
context.local_supported_signature_algorithms.length > 0)
|
|
2859
|
+
? context.local_supported_signature_algorithms
|
|
2860
|
+
: [
|
|
2861
|
+
// TLS 1.3 (PSS + ECDSA + EdDSA)
|
|
2862
|
+
0x0804, 0x0805, 0x0806,
|
|
2863
|
+
0x0403, 0x0503, 0x0603,
|
|
2864
|
+
0x0807, 0x0808,
|
|
2865
|
+
// TLS 1.2 (PKCS1)
|
|
2866
|
+
0x0401, 0x0501, 0x0601
|
|
2867
|
+
];
|
|
2868
|
+
context.local_supported_signature_algorithms = ch_sigalgs; // for CH2 reuse
|
|
2869
|
+
|
|
2725
2870
|
let extensions = [
|
|
2726
2871
|
{
|
|
2727
2872
|
type: 'SUPPORTED_VERSIONS',
|
|
@@ -2740,19 +2885,20 @@ function TLSSession(options){
|
|
|
2740
2885
|
},
|
|
2741
2886
|
{
|
|
2742
2887
|
type: 'SIGNATURE_ALGORITHMS',
|
|
2743
|
-
value:
|
|
2744
|
-
|
|
2745
|
-
0x0804, 0x0805, 0x0806,
|
|
2746
|
-
0x0403, 0x0503, 0x0603,
|
|
2747
|
-
0x0807, 0x0808,
|
|
2748
|
-
// TLS 1.2 (PKCS1)
|
|
2749
|
-
0x0401, 0x0501, 0x0601
|
|
2750
|
-
]
|
|
2751
|
-
},
|
|
2752
|
-
// TLS 1.2 compatibility
|
|
2753
|
-
{ type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) },
|
|
2754
|
-
{ type: 'EXTENDED_MASTER_SECRET', value: null }
|
|
2888
|
+
value: ch_sigalgs
|
|
2889
|
+
}
|
|
2755
2890
|
];
|
|
2891
|
+
if (!tls13Only) {
|
|
2892
|
+
// TLS 1.2 compatibility — meaningless (and suspicious to strict
|
|
2893
|
+
// stacks) in a 1.3-only/QUIC hello
|
|
2894
|
+
extensions.push({ type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) });
|
|
2895
|
+
extensions.push({ type: 'EXTENDED_MASTER_SECRET', value: null });
|
|
2896
|
+
} else {
|
|
2897
|
+
// psk_key_exchange_modes: every mainstream 1.3 client sends it, and a
|
|
2898
|
+
// server MUST NOT send NewSessionTicket to a client that omitted it
|
|
2899
|
+
// (RFC 8446 §4.2.9) — required for future 0-RTT/resumption anyway.
|
|
2900
|
+
extensions.push({ type: 'PSK_KEY_EXCHANGE_MODES', value: [1] });
|
|
2901
|
+
}
|
|
2756
2902
|
|
|
2757
2903
|
// Add SNI if servername was provided
|
|
2758
2904
|
if (context.local_sni) {
|
|
@@ -2822,6 +2968,11 @@ function TLSSession(options){
|
|
|
2822
2968
|
random: context.local_random,
|
|
2823
2969
|
session_id: context.local_session_id,
|
|
2824
2970
|
cookie: context.dtls_cookie,
|
|
2971
|
+
// BUGFIX: `cipher_suites` (plural) is what wire.js's client_hello
|
|
2972
|
+
// builder actually reads; the singular key was silently ignored and
|
|
2973
|
+
// wire.js substituted its own hardcoded fallback list — the
|
|
2974
|
+
// configured cipher suites never reached the wire. Both names passed.
|
|
2975
|
+
cipher_suites: context.local_supported_cipher_suites,
|
|
2825
2976
|
cipher_suite: context.local_supported_cipher_suites,
|
|
2826
2977
|
extensions: extensions
|
|
2827
2978
|
};
|
|
@@ -2877,6 +3028,11 @@ function TLSSession(options){
|
|
|
2877
3028
|
random: context.local_random,
|
|
2878
3029
|
session_id: sid,
|
|
2879
3030
|
cookie: context.dtls_cookie,
|
|
3031
|
+
// BUGFIX: `cipher_suites` (plural) is what wire.js's client_hello
|
|
3032
|
+
// builder actually reads; the singular key was silently ignored and
|
|
3033
|
+
// wire.js substituted its own hardcoded fallback list — the
|
|
3034
|
+
// configured cipher suites never reached the wire. Both names passed.
|
|
3035
|
+
cipher_suites: context.local_supported_cipher_suites,
|
|
2880
3036
|
cipher_suite: context.local_supported_cipher_suites,
|
|
2881
3037
|
extensions: extensions
|
|
2882
3038
|
};
|
|
@@ -2887,7 +3043,9 @@ function TLSSession(options){
|
|
|
2887
3043
|
// Skip for DTLS (DTLS clients/servers often don't implement RFC 5077 fully,
|
|
2888
3044
|
// and adding it caused interop issues with openssl s_server -dtls1_2).
|
|
2889
3045
|
let isDtls = context.local_supported_versions && context.local_supported_versions.some(v => (v & 0xFF00) === 0xFE00);
|
|
2890
|
-
if (!isDtls && context.sessionTickets) {
|
|
3046
|
+
if (!isDtls && !tls13Only && context.sessionTickets) {
|
|
3047
|
+
// RFC 5077 SessionTicket is a TLS 1.2 mechanism; 1.3 resumption uses
|
|
3048
|
+
// NewSessionTicket/PSK. Never advertise it in a 1.3-only/QUIC hello.
|
|
2891
3049
|
extensions.push({ type: 'SESSION_TICKET', value: new Uint8Array(0) });
|
|
2892
3050
|
}
|
|
2893
3051
|
|
|
@@ -2898,6 +3056,11 @@ function TLSSession(options){
|
|
|
2898
3056
|
random: context.local_random,
|
|
2899
3057
|
session_id: context.local_session_id,
|
|
2900
3058
|
cookie: context.dtls_cookie,
|
|
3059
|
+
// BUGFIX: `cipher_suites` (plural) is what wire.js's client_hello
|
|
3060
|
+
// builder actually reads; the singular key was silently ignored and
|
|
3061
|
+
// wire.js substituted its own hardcoded fallback list — the
|
|
3062
|
+
// configured cipher suites never reached the wire. Both names passed.
|
|
3063
|
+
cipher_suites: context.local_supported_cipher_suites,
|
|
2901
3064
|
cipher_suite: context.local_supported_cipher_suites,
|
|
2902
3065
|
extensions: extensions
|
|
2903
3066
|
};
|
|
@@ -3013,12 +3176,80 @@ function TLSSession(options){
|
|
|
3013
3176
|
};
|
|
3014
3177
|
},
|
|
3015
3178
|
|
|
3179
|
+
/**
|
|
3180
|
+
* Export keying material (RFC 5705 for TLS 1.2, RFC 8446 §7.5 for
|
|
3181
|
+
* TLS 1.3). Mirrors Node's tls.TLSSocket#exportKeyingMaterial.
|
|
3182
|
+
*
|
|
3183
|
+
* DTLS-SRTP (RFC 5764) usage:
|
|
3184
|
+
* session.exportKeyingMaterial(len, 'EXTRACTOR-dtls_srtp')
|
|
3185
|
+
* with NO context argument. Note the TLS 1.2 subtlety: "no context"
|
|
3186
|
+
* and "empty context" produce different output — omit the argument
|
|
3187
|
+
* (or pass null) for the RFC 5764 form.
|
|
3188
|
+
*
|
|
3189
|
+
* Returns a Buffer of `length` bytes, or null before the secrets
|
|
3190
|
+
* are available (handshake not far enough along).
|
|
3191
|
+
*
|
|
3192
|
+
* NOTE: the previous implementation was TLS 1.3-only AND derived from
|
|
3193
|
+
* the app traffic secret in a single HKDF stage — which is not the
|
|
3194
|
+
* RFC 8446 §7.5 construction and would not interoperate with
|
|
3195
|
+
* OpenSSL/BoringSSL exporters. Both issues are fixed here; if anything
|
|
3196
|
+
* consumed the old output, its derived values will change.
|
|
3197
|
+
*
|
|
3198
|
+
* @param {number} length
|
|
3199
|
+
* @param {string} label
|
|
3200
|
+
* @param {Uint8Array|Buffer|null} [context_value]
|
|
3201
|
+
* @returns {Buffer|null}
|
|
3202
|
+
*/
|
|
3016
3203
|
exportKeyingMaterial: function(length, label, context_value){
|
|
3017
|
-
if (!context.
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3204
|
+
if (!context.selected_cipher_suite || !length || !label) return null;
|
|
3205
|
+
var suite = TLS_CIPHER_SUITES[context.selected_cipher_suite];
|
|
3206
|
+
if (!suite) return null;
|
|
3207
|
+
var is13 = (context.selected_version === wire.TLS_VERSION.TLS1_3 ||
|
|
3208
|
+
context.selected_version === wire.DTLS_VERSION.DTLS1_3);
|
|
3209
|
+
|
|
3210
|
+
if (is13) {
|
|
3211
|
+
// RFC 8446 §7.5 — two-stage derivation from exporter_master_secret.
|
|
3212
|
+
if (!context.exporter_master_secret) return null;
|
|
3213
|
+
return Buffer.from(tls13_exporter(
|
|
3214
|
+
suite.hash, context.exporter_master_secret, label,
|
|
3215
|
+
(context_value != null) ? context_value : null, length
|
|
3216
|
+
));
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
// TLS 1.2 — RFC 5705 over the master secret + hello randoms.
|
|
3220
|
+
// client_random comes first in the seed regardless of our role.
|
|
3221
|
+
if (!context.base_secret || !context.local_random || !context.remote_random) return null;
|
|
3222
|
+
var clientRandom = context.isServer ? context.remote_random : context.local_random;
|
|
3223
|
+
var serverRandom = context.isServer ? context.local_random : context.remote_random;
|
|
3224
|
+
return Buffer.from(tls12_exporter(
|
|
3225
|
+
suite.hash, context.base_secret, label,
|
|
3226
|
+
clientRandom, serverRandom,
|
|
3227
|
+
(context_value != null) ? context_value : null, length
|
|
3228
|
+
));
|
|
3229
|
+
},
|
|
3230
|
+
|
|
3231
|
+
/**
|
|
3232
|
+
* All extensions the peer sent in its hello (ClientHello when we're
|
|
3233
|
+
* the server, ServerHello when we're the client), as parsed by
|
|
3234
|
+
* wire.parse_extensions: [{ type, name, data, value }].
|
|
3235
|
+
* `data` is the raw extension payload (Uint8Array); `value` is the
|
|
3236
|
+
* decoded form for known extensions, null for unknown ones.
|
|
3237
|
+
*/
|
|
3238
|
+
getRemoteExtensions: function(){
|
|
3239
|
+
return context.remote_extensions ? context.remote_extensions.slice() : [];
|
|
3240
|
+
},
|
|
3241
|
+
|
|
3242
|
+
/**
|
|
3243
|
+
* Look up a single peer extension by its numeric type
|
|
3244
|
+
* (e.g. 14 = use_srtp, 0x39 = QUIC transport parameters).
|
|
3245
|
+
* Returns the { type, name, data, value } entry, or null.
|
|
3246
|
+
*/
|
|
3247
|
+
getRemoteExtension: function(type){
|
|
3248
|
+
var list = context.remote_extensions || [];
|
|
3249
|
+
for (var i = 0; i < list.length; i++) {
|
|
3250
|
+
if (list[i] && list[i].type === type) return list[i];
|
|
3251
|
+
}
|
|
3252
|
+
return null;
|
|
3022
3253
|
},
|
|
3023
3254
|
|
|
3024
3255
|
/** Returns the local Finished verify_data (Buffer), or null. */
|
|
@@ -3108,5 +3339,4 @@ function TLSSession(options){
|
|
|
3108
3339
|
return this;
|
|
3109
3340
|
}
|
|
3110
3341
|
|
|
3111
|
-
export default TLSSession;
|
|
3112
|
-
|
|
3342
|
+
export default TLSSession;
|
package/src/tls_socket.js
CHANGED
|
@@ -331,6 +331,18 @@ function TLSSocket(duplex, options){
|
|
|
331
331
|
? context.isTls13
|
|
332
332
|
: session.getVersion() === 0x0304; // early records (pre-secureConnect)
|
|
333
333
|
|
|
334
|
+
// AEAD decryption can throw on any forged/corrupted record (GCM/ChaCha
|
|
335
|
+
// tag mismatch surfaces as an exception from decipher.final()). Left
|
|
336
|
+
// unhandled, a single injected bad segment would throw out of the
|
|
337
|
+
// transport 'data' handler — an unhandled exception that tears down the
|
|
338
|
+
// connection or the process: a trivial remote DoS.
|
|
339
|
+
//
|
|
340
|
+
// RFC 8446 §5.2: in TLS (unlike DTLS, which silently drops), a record
|
|
341
|
+
// that fails to decrypt MUST be treated as a fatal error. So we catch,
|
|
342
|
+
// send a fatal bad_record_mac alert, destroy the connection, and stop.
|
|
343
|
+
// The read sequence number is intentionally NOT advanced on failure —
|
|
344
|
+
// the rejected record never counts.
|
|
345
|
+
try {
|
|
334
346
|
if (!isTls13 && context.remote_ccs_seen === true) {
|
|
335
347
|
// TLS 1.2: decrypt with key_block derived keys
|
|
336
348
|
if (context.app_read_key === null) {
|
|
@@ -394,7 +406,14 @@ function TLSSocket(duplex, options){
|
|
|
394
406
|
}
|
|
395
407
|
context.handshake_read_seq++;
|
|
396
408
|
}
|
|
397
|
-
|
|
409
|
+
} catch (e) {
|
|
410
|
+
// Record failed authentication → fatal bad_record_mac (RFC 8446 §5.2).
|
|
411
|
+
try { session.sendAlert ? session.sendAlert(2, TLS_ALERT.BAD_RECORD_MAC) : null; } catch (_) {}
|
|
412
|
+
self.emit('error', new Error('TLS record decryption failed (bad_record_mac)'));
|
|
413
|
+
self.destroy();
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
398
417
|
|
|
399
418
|
if (out !== null) {
|
|
400
419
|
// Reuse cached isTls13 computed at the top of this function
|
|
@@ -1427,6 +1446,30 @@ function TLSSocket(duplex, options){
|
|
|
1427
1446
|
/** Full negotiation result — all selected parameters in one object. */
|
|
1428
1447
|
self.getNegotiationResult = function(){ return session.getNegotiationResult ? session.getNegotiationResult() : null; };
|
|
1429
1448
|
|
|
1449
|
+
/**
|
|
1450
|
+
* Export keying material (RFC 5705 for TLS 1.2, RFC 8446 §7.5 for
|
|
1451
|
+
* TLS 1.3). Mirrors Node's tls.TLSSocket#exportKeyingMaterial —
|
|
1452
|
+
* same argument order (length, label, context). One deliberate
|
|
1453
|
+
* difference: Node THROWS before the handshake completes; we return
|
|
1454
|
+
* null (matching this library's null-until-ready convention used by
|
|
1455
|
+
* getTrafficSecrets etc.).
|
|
1456
|
+
*/
|
|
1457
|
+
self.exportKeyingMaterial = function(length, label, context_value){
|
|
1458
|
+
return session.exportKeyingMaterial
|
|
1459
|
+
? session.exportKeyingMaterial(length, label, context_value)
|
|
1460
|
+
: null;
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
/** Peer hello extensions: [{ type, name, data, value }]. */
|
|
1464
|
+
self.getRemoteExtensions = function(){
|
|
1465
|
+
return session.getRemoteExtensions ? session.getRemoteExtensions() : [];
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
/** One peer extension by numeric type, or null. */
|
|
1469
|
+
self.getRemoteExtension = function(type){
|
|
1470
|
+
return session.getRemoteExtension ? session.getRemoteExtension(type) : null;
|
|
1471
|
+
};
|
|
1472
|
+
|
|
1430
1473
|
/** Request Key Update — refresh outgoing encryption keys (TLS 1.3 only). */
|
|
1431
1474
|
self.rekeySend = function(){ if (session.requestKeyUpdate) session.requestKeyUpdate(false); };
|
|
1432
1475
|
|
package/src/utils.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
1
2
|
|
|
2
3
|
function concatUint8Arrays(arrays) {
|
|
3
4
|
let totalLength = 0;
|
|
@@ -93,9 +94,35 @@ function uint8Equal(a, b) {
|
|
|
93
94
|
}
|
|
94
95
|
|
|
95
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Constant-time equality for two byte arrays.
|
|
99
|
+
*
|
|
100
|
+
* Unlike uint8Equal (which short-circuits on the first differing byte and
|
|
101
|
+
* therefore leaks — via timing — how many leading bytes matched), this always
|
|
102
|
+
* compares the full length. Use it wherever a mismatch would otherwise be
|
|
103
|
+
* exploitable as an oracle: Finished verify_data, PSK binders, ticket key_name,
|
|
104
|
+
* and any MAC/tag comparison.
|
|
105
|
+
*
|
|
106
|
+
* Returns false immediately (and safely) on length mismatch — that's public
|
|
107
|
+
* information, not a secret, so short-circuiting there is fine.
|
|
108
|
+
*
|
|
109
|
+
* Note: crypto.timingSafeEqual requires two equal-length Buffers. We wrap the
|
|
110
|
+
* inputs as Buffer views over the same memory (no copy) via byteOffset/length.
|
|
111
|
+
*/
|
|
112
|
+
function timingSafeEqualU8(a, b) {
|
|
113
|
+
if (a == null || b == null) return false;
|
|
114
|
+
if (a.length !== b.length) return false;
|
|
115
|
+
if (a.length === 0) return true;
|
|
116
|
+
let ba = Buffer.isBuffer(a) ? a : Buffer.from(a.buffer, a.byteOffset, a.length);
|
|
117
|
+
let bb = Buffer.isBuffer(b) ? b : Buffer.from(b.buffer, b.byteOffset, b.length);
|
|
118
|
+
return crypto.timingSafeEqual(ba, bb);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
96
122
|
export {
|
|
97
123
|
concatUint8Arrays,
|
|
98
124
|
arraybufferEqual,
|
|
99
125
|
arraysEqual,
|
|
100
|
-
uint8Equal
|
|
126
|
+
uint8Equal,
|
|
127
|
+
timingSafeEqualU8
|
|
101
128
|
};
|