lemon-tls 0.5.5 → 0.7.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/package.json +2 -7
- package/src/compat.js +10 -0
- package/src/crypto.js +160 -51
- package/src/dtls_session.js +636 -73
- package/src/dtls_socket.js +7 -0
- package/src/record.js +226 -43
- package/src/session/ecdh.js +124 -0
- package/src/session/message.js +40 -1
- package/src/session/signing.js +240 -47
- package/src/session/ticket.js +6 -3
- package/src/tls_session.js +2000 -454
- package/src/tls_socket.js +378 -40
- package/src/utils.js +7 -1
- package/src/wire.js +344 -18
package/src/tls_socket.js
CHANGED
|
@@ -3,8 +3,10 @@ import TLSSession from './tls_session.js';
|
|
|
3
3
|
import crypto from 'node:crypto';
|
|
4
4
|
import { Duplex } from 'node:stream';
|
|
5
5
|
|
|
6
|
-
import { TLS_CIPHER_SUITES } from './crypto.js';
|
|
6
|
+
import { TLS_CIPHER_SUITES, default_cipher_suites } from './crypto.js';
|
|
7
7
|
import { TLS_CONTENT_TYPE as CT, TLS_ALERT_LEVEL, TLS_ALERT } from './wire.js';
|
|
8
|
+
import { SUPPORTED_GROUPS as ECDH_SUPPORTED_GROUPS } from './session/ecdh.js';
|
|
9
|
+
import { default_signature_schemes } from './session/signing.js';
|
|
8
10
|
import { decrypt_session_blob } from './session/ticket.js';
|
|
9
11
|
|
|
10
12
|
import {
|
|
@@ -51,7 +53,6 @@ function sigalgCodeToName(code) {
|
|
|
51
53
|
|
|
52
54
|
// ==== עזרי המרה ====
|
|
53
55
|
function toBuf(u8){ return Buffer.isBuffer(u8) ? u8 : Buffer.from(u8 || []); }
|
|
54
|
-
function toU8(buf){ return (buf instanceof Uint8Array) ? buf : new Uint8Array(buf || []); }
|
|
55
56
|
|
|
56
57
|
function parseVersion(v) {
|
|
57
58
|
if (typeof v === 'number') return v;
|
|
@@ -159,8 +160,24 @@ function TLSSocket(duplex, options){
|
|
|
159
160
|
remote_ccs_seen: false,
|
|
160
161
|
local_ccs_sent: false,
|
|
161
162
|
|
|
163
|
+
// Middlebox-compatibility CCS (TLS 1.3, RFC 8446 §D.4). This is a record-
|
|
164
|
+
// layer artifact — a single 0x01 byte sent as a plaintext CCS record so
|
|
165
|
+
// legacy middleboxes see a "cipher change" and let the flow through. It
|
|
166
|
+
// is NEVER a handshake message; TLSSession neither sends nor receives it.
|
|
167
|
+
// That is exactly why it lives here and not in the session: the QUIC
|
|
168
|
+
// consumer drives TLSSession directly, and RFC 9001 §8.4 forbids CCS in
|
|
169
|
+
// QUIC — keeping this in the TCP record layer means QUIC can never emit it.
|
|
170
|
+
tls13_compat_ccs_sent: false,
|
|
171
|
+
|
|
162
172
|
tls12_read_seq: 0,
|
|
163
173
|
|
|
174
|
+
// Record-layer abuse limits. A peer that never advances the handshake but
|
|
175
|
+
// keeps us parsing is a denial of service even though every individual
|
|
176
|
+
// record is well formed, so the limit belongs at the record layer where
|
|
177
|
+
// the records are counted — not in any one message handler.
|
|
178
|
+
consecutive_empty_records: 0,
|
|
179
|
+
consecutive_ccs_records: 0,
|
|
180
|
+
|
|
164
181
|
// Buffers and queues.
|
|
165
182
|
//
|
|
166
183
|
// readBuffer is a growable receive buffer with two offsets:
|
|
@@ -183,6 +200,17 @@ function TLSSocket(duplex, options){
|
|
|
183
200
|
readBuffer: Buffer.allocUnsafe(65536),
|
|
184
201
|
readStart: 0,
|
|
185
202
|
readEnd: 0,
|
|
203
|
+
|
|
204
|
+
// Handshake MESSAGE stream buffer (distinct from readBuffer, which is the
|
|
205
|
+
// record stream). RFC 8446 §5.1: handshake messages may be coalesced into
|
|
206
|
+
// one record or fragmented across records — record boundaries carry no
|
|
207
|
+
// meaning at the message layer. Record payloads are appended here and
|
|
208
|
+
// complete messages are carved out by their own 4-byte type+length
|
|
209
|
+
// header. Without this, a record carrying EE+Cert+CV+Finished (as
|
|
210
|
+
// OpenSSL/BoringSSL routinely pack) fed only its FIRST message to the
|
|
211
|
+
// session and silently dropped the rest.
|
|
212
|
+
hsBuf: null,
|
|
213
|
+
|
|
186
214
|
appWriteQueue: [],
|
|
187
215
|
pendingHandshake: [],
|
|
188
216
|
|
|
@@ -435,7 +463,7 @@ function TLSSocket(duplex, options){
|
|
|
435
463
|
// zero-copy view into the decrypted plaintext
|
|
436
464
|
self.push(out.subarray(0, j));
|
|
437
465
|
} else if (content_type === CT.HANDSHAKE) {
|
|
438
|
-
|
|
466
|
+
feedHandshakeBytes(out.subarray(0, j));
|
|
439
467
|
} else if (content_type === CT.ALERT) {
|
|
440
468
|
// TODO: handle alert
|
|
441
469
|
}
|
|
@@ -444,7 +472,7 @@ function TLSSocket(duplex, options){
|
|
|
444
472
|
if (context.using_app_keys === true) {
|
|
445
473
|
self.push(out);
|
|
446
474
|
} else {
|
|
447
|
-
|
|
475
|
+
feedHandshakeBytes(out);
|
|
448
476
|
}
|
|
449
477
|
}
|
|
450
478
|
}
|
|
@@ -505,6 +533,44 @@ function TLSSocket(duplex, options){
|
|
|
505
533
|
context.readEnd = needed;
|
|
506
534
|
}
|
|
507
535
|
|
|
536
|
+
/**
|
|
537
|
+
* Handshake message reassembly (RFC 8446 §5.1).
|
|
538
|
+
*
|
|
539
|
+
* Sits between the record layer and TLSSession: record payloads of type
|
|
540
|
+
* handshake go in; complete handshake messages come out, one
|
|
541
|
+
* session.message() call each. Handles both directions of mismatch —
|
|
542
|
+
* several messages coalesced into one record, and one message fragmented
|
|
543
|
+
* across records. TLSSession keeps receiving exactly what its contract
|
|
544
|
+
* says: one whole handshake message per call.
|
|
545
|
+
*
|
|
546
|
+
* Note on memory: session.message() stashes views of the fed bytes into
|
|
547
|
+
* its transcript, so every message handed over must be backed by memory
|
|
548
|
+
* we never overwrite. Buffer.from()/Buffer.concat() below always copy out
|
|
549
|
+
* of the (reused) record buffers; the carve-out subarrays then alias the
|
|
550
|
+
* fresh copy, which nothing mutates.
|
|
551
|
+
*/
|
|
552
|
+
function feedHandshakeBytes(bytes){
|
|
553
|
+
if (context.hsBuf === null || context.hsBuf.length === 0) {
|
|
554
|
+
context.hsBuf = Buffer.from(bytes);
|
|
555
|
+
} else {
|
|
556
|
+
context.hsBuf = Buffer.concat([context.hsBuf, Buffer.from(bytes)]);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
while (context.hsBuf.length >= 4) {
|
|
560
|
+
const len = (context.hsBuf[1] << 16) | (context.hsBuf[2] << 8) | context.hsBuf[3];
|
|
561
|
+
if (context.hsBuf.length < 4 + len) break; // partial message — wait for more records
|
|
562
|
+
|
|
563
|
+
const msg = context.hsBuf.subarray(0, 4 + len);
|
|
564
|
+
context.hsBuf = context.hsBuf.subarray(4 + len);
|
|
565
|
+
|
|
566
|
+
session.message(msg);
|
|
567
|
+
|
|
568
|
+
// A message may have fatally ended the session (alert sent,
|
|
569
|
+
// transport torn down). Don't keep feeding a corpse.
|
|
570
|
+
if (context.destroyed) return;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
508
574
|
function parseRecordsAndDispatch(){
|
|
509
575
|
// Hot path — zero-copy parsing:
|
|
510
576
|
// - Work directly on readBuffer with moving readStart offset (no slicing)
|
|
@@ -517,6 +583,24 @@ function TLSSocket(duplex, options){
|
|
|
517
583
|
const end = context.readEnd;
|
|
518
584
|
|
|
519
585
|
while (end - off >= 5) {
|
|
586
|
+
// ── Stop feeding a session that has already failed ──
|
|
587
|
+
// Once the session aborts (fatalAlert -> state 'error') the
|
|
588
|
+
// handshake is over: we have sent our alert and are closing. Any
|
|
589
|
+
// record still in the buffer, or arriving after, belongs to a
|
|
590
|
+
// conversation that no longer exists — and in TLS 1.3 it is
|
|
591
|
+
// encrypted under keys the peer derived from a transcript we
|
|
592
|
+
// rejected, so decrypting it fails and surfaces as a spurious
|
|
593
|
+
// bad_record_mac that masks the real reason for the abort.
|
|
594
|
+
//
|
|
595
|
+
// This lives here, in the one loop every record passes through,
|
|
596
|
+
// rather than as a guard inside each handler: "the session is
|
|
597
|
+
// dead, stop processing" is a property of the transport, not of
|
|
598
|
+
// any particular record type.
|
|
599
|
+
if (context.destroyed || (session && session.getState &&
|
|
600
|
+
(session.getState() === 'error' || session.getState() === 'closed'))) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
520
604
|
const type = rb[off];
|
|
521
605
|
// Version at rb[off+1..off+3] — currently unused (legacy 0x0303 always)
|
|
522
606
|
const len = (rb[off + 3] << 8) | rb[off + 4];
|
|
@@ -528,6 +612,48 @@ function TLSSocket(duplex, options){
|
|
|
528
612
|
const body = rb.subarray(off + 5, off + 5 + len);
|
|
529
613
|
off += 5 + len;
|
|
530
614
|
|
|
615
|
+
// ── Record-layer sanity, applied to every record ──
|
|
616
|
+
// RFC 8446 §5.1: the legacy_record_version is 0x0303 for every
|
|
617
|
+
// record after the first ClientHello, and 0x03xx before it. A
|
|
618
|
+
// record claiming anything else is malformed framing, not a
|
|
619
|
+
// version negotiation signal.
|
|
620
|
+
const recVer = (rb[off - len - 5 + 1] << 8) | rb[off - len - 5 + 2];
|
|
621
|
+
if ((recVer >>> 8) !== 0x03) {
|
|
622
|
+
try { session.sendAlert(2, TLS_ALERT.PROTOCOL_VERSION); } catch (e) {}
|
|
623
|
+
self.emit('error', new Error('Record with illegal legacy_record_version 0x' + recVer.toString(16)));
|
|
624
|
+
markTransportDead();
|
|
625
|
+
if (context.transport && !context.transport.destroyed) context.transport.destroy();
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// A peer may legally send an empty record, and a TLS 1.3 peer may
|
|
630
|
+
// legally send a compat CCS — but only finitely many. Unbounded
|
|
631
|
+
// streams of either make no progress and exist only to burn our
|
|
632
|
+
// CPU (BoGo: TooManyEmptyFragments / TooManyChangeCipherSpec).
|
|
633
|
+
if (len === 0) {
|
|
634
|
+
if (++context.consecutive_empty_records > 32) {
|
|
635
|
+
try { session.sendAlert(2, TLS_ALERT.UNEXPECTED_MESSAGE); } catch (e) {}
|
|
636
|
+
self.emit('error', new Error('Too many consecutive empty records'));
|
|
637
|
+
markTransportDead();
|
|
638
|
+
if (context.transport && !context.transport.destroyed) context.transport.destroy();
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
} else {
|
|
642
|
+
context.consecutive_empty_records = 0;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (type === CT.CHANGE_CIPHER_SPEC) {
|
|
646
|
+
if (++context.consecutive_ccs_records > 8) {
|
|
647
|
+
try { session.sendAlert(2, TLS_ALERT.UNEXPECTED_MESSAGE); } catch (e) {}
|
|
648
|
+
self.emit('error', new Error('Too many consecutive ChangeCipherSpec records'));
|
|
649
|
+
markTransportDead();
|
|
650
|
+
if (context.transport && !context.transport.destroyed) context.transport.destroy();
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
} else {
|
|
654
|
+
context.consecutive_ccs_records = 0;
|
|
655
|
+
}
|
|
656
|
+
|
|
531
657
|
if (type === CT.APPLICATION_DATA) {
|
|
532
658
|
processCiphertext(body, header);
|
|
533
659
|
context.tls12_read_seq++;
|
|
@@ -547,12 +673,35 @@ function TLSSocket(duplex, options){
|
|
|
547
673
|
// those stashed views would point to overwritten bytes — causing
|
|
548
674
|
// bad key derivations and GCM tag failures much later. We must hand
|
|
549
675
|
// the session an owned Buffer so its transcript survives.
|
|
550
|
-
|
|
676
|
+
feedHandshakeBytes(body);
|
|
551
677
|
}
|
|
552
678
|
context.tls12_read_seq++;
|
|
553
679
|
} else if (type === CT.CHANGE_CIPHER_SPEC) {
|
|
554
|
-
|
|
555
|
-
|
|
680
|
+
// Version-dependent meaning:
|
|
681
|
+
// - TLS 1.2: a real protocol event — the peer's next records
|
|
682
|
+
// are encrypted. Flip remote_ccs_seen so type-22 records are
|
|
683
|
+
// routed to the decrypt path.
|
|
684
|
+
// - TLS 1.3: a middlebox-compat artifact (RFC 8446 §D.4)
|
|
685
|
+
// carrying no meaning; cipher state is driven by the key
|
|
686
|
+
// schedule. It MUST be ignored and MUST NOT advance any
|
|
687
|
+
// read sequence.
|
|
688
|
+
//
|
|
689
|
+
// The test is "do we POSITIVELY know this is TLS 1.2", not
|
|
690
|
+
// "is it not 1.3", and that is the correct test on its own
|
|
691
|
+
// merits: a CCS carries no meaning unless we have actually
|
|
692
|
+
// negotiated 1.2, so an unknown version must not be treated as
|
|
693
|
+
// a cipher change. (It also used to be load-bearing for a
|
|
694
|
+
// second reason — the HelloRetryRequest path did not record
|
|
695
|
+
// selected_version, so mid-HRR the version was simply unknown.
|
|
696
|
+
// That gap is fixed at its source now, but the rule stands.)
|
|
697
|
+
const ccsVersion = (context.isTls13 !== undefined)
|
|
698
|
+
? (context.isTls13 ? 0x0304 : 0x0303)
|
|
699
|
+
: session.getVersion();
|
|
700
|
+
if (ccsVersion === 0x0303) {
|
|
701
|
+
context.tls12_read_seq = 0;
|
|
702
|
+
context.remote_ccs_seen = true;
|
|
703
|
+
}
|
|
704
|
+
// TLS 1.3, or version not yet negotiated: swallow silently.
|
|
556
705
|
} else if (type === CT.ALERT) {
|
|
557
706
|
// Alert: 2 bytes — level (1=warning, 2=fatal), description
|
|
558
707
|
if (body.length >= 2) {
|
|
@@ -584,14 +733,71 @@ function TLSSocket(duplex, options){
|
|
|
584
733
|
}
|
|
585
734
|
|
|
586
735
|
|
|
736
|
+
/**
|
|
737
|
+
* The single truth for "may we still write to the wire".
|
|
738
|
+
* `writable` (not just destroyed/writableEnded) matters: after the peer
|
|
739
|
+
* resets the connection, Node flips writable to false before 'close'
|
|
740
|
+
* fires — a write() in that window is the EPIPE the session's reactive
|
|
741
|
+
* loop used to trigger while continuing the handshake against a dead
|
|
742
|
+
* transport.
|
|
743
|
+
*/
|
|
744
|
+
function canWrite() {
|
|
745
|
+
const t = context.transport;
|
|
746
|
+
return !!t && !context.destroyed && !t.destroyed && !t.writableEnded && t.writable !== false;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Transport is gone → make BOTH layers inert:
|
|
751
|
+
* - context.destroyed short-circuits every write path here, and
|
|
752
|
+
* - session.close() flips TLSSession into a terminal state, which stops
|
|
753
|
+
* the reactive loop from producing further handshake messages at the
|
|
754
|
+
* source (its close_notify emission is dropped by canWrite()).
|
|
755
|
+
*/
|
|
756
|
+
/**
|
|
757
|
+
* Emit the TLS 1.3 middlebox-compatibility ChangeCipherSpec (RFC 8446 §D.4).
|
|
758
|
+
*
|
|
759
|
+
* ONE implementation for all three points at which it can legally appear,
|
|
760
|
+
* because "send the compat record, exactly once" is a single rule. It was
|
|
761
|
+
* previously written out three times — client-before-CH2,
|
|
762
|
+
* server-after-SH/HRR, and client-before-its-first-encrypted-record — each
|
|
763
|
+
* repeating the guard, the write and the flag. Three copies of a
|
|
764
|
+
* send-once rule is three chances for them to disagree.
|
|
765
|
+
*
|
|
766
|
+
* The record is meaningless to the protocol: it exists so middleboxes that
|
|
767
|
+
* expect a TLS 1.2-shaped flow do not drop the connection. It is plaintext,
|
|
768
|
+
* a single 0x01 byte, never enters the transcript, and never advances a
|
|
769
|
+
* sequence number.
|
|
770
|
+
*
|
|
771
|
+
* WHERE it goes differs by role, and the two are on opposite sides of the
|
|
772
|
+
* hello:
|
|
773
|
+
* server — immediately AFTER ServerHello / HelloRetryRequest
|
|
774
|
+
* client — immediately BEFORE its second flight (CH2, or failing that
|
|
775
|
+
* the first encrypted record)
|
|
776
|
+
* Callers state their position; this function owns the once-only rule.
|
|
777
|
+
*
|
|
778
|
+
* @returns {boolean} true if a record was actually written
|
|
779
|
+
*/
|
|
780
|
+
function sendCompatCCS() {
|
|
781
|
+
if (context.tls13_compat_ccs_sent) return false;
|
|
782
|
+
context.tls13_compat_ccs_sent = true;
|
|
783
|
+
writeRecord(CT.CHANGE_CIPHER_SPEC, Buffer.from([0x01]));
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function markTransportDead() {
|
|
788
|
+
if (context.destroyed) return;
|
|
789
|
+
context.destroyed = true;
|
|
790
|
+
try { session.close(); } catch (e) {}
|
|
791
|
+
}
|
|
792
|
+
|
|
587
793
|
function bindTransport(){
|
|
588
794
|
if (!context.transport) return;
|
|
589
795
|
context.transport.on('data', function(chunk){
|
|
590
796
|
_appendChunk(chunk);
|
|
591
797
|
parseRecordsAndDispatch();
|
|
592
798
|
});
|
|
593
|
-
context.transport.on('error', function(err){ self.emit('error', err); });
|
|
594
|
-
context.transport.on('close', function(){ self.emit('close'); });
|
|
799
|
+
context.transport.on('error', function(err){ markTransportDead(); self.emit('error', err); });
|
|
800
|
+
context.transport.on('close', function(){ markTransportDead(); self.emit('close'); });
|
|
595
801
|
}
|
|
596
802
|
|
|
597
803
|
session.on('message', function(epoch, seq, type, data){
|
|
@@ -613,16 +819,88 @@ function TLSSocket(duplex, options){
|
|
|
613
819
|
|
|
614
820
|
if (isTls13) {
|
|
615
821
|
// TLS 1.3: wrap alert as APPLICATION_DATA (inner type is ALERT)
|
|
822
|
+
//
|
|
823
|
+
// The app write key may not have been materialised yet: it is
|
|
824
|
+
// derived lazily on first application write, and an abort can
|
|
825
|
+
// happen before any application data flows. Derive it here the
|
|
826
|
+
// same way the data path does, rather than silently falling
|
|
827
|
+
// back to a cleartext alert — a peer that has switched to
|
|
828
|
+
// application keys cannot read a cleartext record and reports a
|
|
829
|
+
// record-header error instead of the reason we sent. This is
|
|
830
|
+
// exactly what turned "certificate_required" into an opaque
|
|
831
|
+
// decrypt failure.
|
|
832
|
+
// Materialise the write key for THIS epoch if it does not exist
|
|
833
|
+
// yet. Both epochs derive lazily on first write, and an abort
|
|
834
|
+
// can precede any write at either level:
|
|
835
|
+
//
|
|
836
|
+
// epoch 2 — abort after our Finished, before app data.
|
|
837
|
+
// epoch 1 — abort while the PEER's flight is still arriving,
|
|
838
|
+
// before we have written anything at all. A client
|
|
839
|
+
// rejecting the server's Certificate/Finished is
|
|
840
|
+
// exactly here, and it was NOT covered: the code
|
|
841
|
+
// fell through to the cleartext path below and sent
|
|
842
|
+
// `15 03 03` where the peer required `17 03 03`.
|
|
843
|
+
// The peer then reports a decrypt failure instead
|
|
844
|
+
// of the reason we gave it — the same symptom the
|
|
845
|
+
// epoch-2 case used to have, one level down.
|
|
846
|
+
if (epoch === 2 && context.app_write_key === null) {
|
|
847
|
+
const ts = session.getTrafficSecrets();
|
|
848
|
+
if (ts && ts.localAppSecret) {
|
|
849
|
+
const d = tls_derive_from_tls_secrets(ts.localAppSecret, ts.cipher);
|
|
850
|
+
context.app_write_key = d.key;
|
|
851
|
+
context.app_write_iv = d.iv;
|
|
852
|
+
}
|
|
853
|
+
} else if (epoch === 1 && !context.handshake_write_key) {
|
|
854
|
+
const ts = session.getTrafficSecrets();
|
|
855
|
+
if (ts && ts.localHandshakeSecret) {
|
|
856
|
+
const d = tls_derive_from_tls_secrets(ts.localHandshakeSecret, ts.cipher);
|
|
857
|
+
context.handshake_write_key = d.key;
|
|
858
|
+
context.handshake_write_iv = d.iv;
|
|
859
|
+
if (context.handshake_write_seq === undefined ||
|
|
860
|
+
context.handshake_write_seq === null) {
|
|
861
|
+
context.handshake_write_seq = 0;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
616
866
|
const writeKey = (epoch === 2) ? context.app_write_key : context.handshake_write_key;
|
|
617
867
|
const writeIv = (epoch === 2) ? context.app_write_iv : context.handshake_write_iv;
|
|
618
868
|
const writeSeq = (epoch === 2) ? context.app_write_seq : context.handshake_write_seq;
|
|
619
869
|
|
|
620
870
|
if (!writeKey) {
|
|
621
|
-
//
|
|
622
|
-
|
|
871
|
+
// We are past epoch 0, so the peer is reading ciphertext.
|
|
872
|
+
// A cleartext alert here is NOT defensive: the peer cannot
|
|
873
|
+
// read it, reports a decrypt failure, and the reason we
|
|
874
|
+
// were conveying is lost behind that. This exact fallback
|
|
875
|
+
// turned three separate correct validations into "bad
|
|
876
|
+
// record MAC" — in DTLS, at epoch 2, and at epoch 1.
|
|
877
|
+
//
|
|
878
|
+
// If we cannot protect the record, say so loudly and send
|
|
879
|
+
// nothing: a missing alert and an unreadable alert look the
|
|
880
|
+
// same to the peer, but only one is diagnosable from our
|
|
881
|
+
// side. Mirrors the DTLS record layer, which likewise
|
|
882
|
+
// refuses to emit an unprotectable record.
|
|
883
|
+
self.emit('error', new Error(
|
|
884
|
+
'TLS 1.3: cannot protect an alert at epoch ' + epoch +
|
|
885
|
+
' — no write key available; alert not sent'));
|
|
623
886
|
return;
|
|
624
887
|
}
|
|
625
888
|
|
|
889
|
+
// Middlebox-compatibility CCS (RFC 8446 §D.4) before our FIRST
|
|
890
|
+
// encrypted record — not merely before the first encrypted
|
|
891
|
+
// HANDSHAKE record.
|
|
892
|
+
//
|
|
893
|
+
// The handshake path already does this a few lines below, but
|
|
894
|
+
// the alert path returns before reaching it. When a client
|
|
895
|
+
// rejects something in the server's flight it never writes a
|
|
896
|
+
// handshake record at all, so its whole output was
|
|
897
|
+
// ClientHello + encrypted alert with no CCS in between, and a
|
|
898
|
+
// peer enforcing compatibility mode rejects the stream before
|
|
899
|
+
// it ever reads the alert. The abort path is still a flight and
|
|
900
|
+
// carries the same obligation. sendCompatCCS owns the
|
|
901
|
+
// once-only rule, so calling it here cannot double-send.
|
|
902
|
+
if (!session.isServer) sendCompatCCS();
|
|
903
|
+
|
|
626
904
|
const algo = context.aeadAlgo || getAeadAlgo(session.getCipher());
|
|
627
905
|
const enc = encrypt_tls_record(CT.ALERT, buf, writeKey, get_nonce(writeIv, writeSeq), algo);
|
|
628
906
|
|
|
@@ -634,7 +912,12 @@ function TLSSocket(duplex, options){
|
|
|
634
912
|
// TLS 1.2: post-CCS alerts are encrypted with app keys (AES-GCM).
|
|
635
913
|
// Outer record type remains ALERT (21), body is encrypted.
|
|
636
914
|
if (!context.app_write_key) {
|
|
637
|
-
|
|
915
|
+
// Same rule as the 1.3 path above: past CCS the peer reads
|
|
916
|
+
// encrypted records, so a cleartext alert is unreadable
|
|
917
|
+
// rather than best-effort.
|
|
918
|
+
self.emit('error', new Error(
|
|
919
|
+
'TLS 1.2: cannot protect an alert after CCS — no ' +
|
|
920
|
+
'application write key; alert not sent'));
|
|
638
921
|
return;
|
|
639
922
|
}
|
|
640
923
|
|
|
@@ -654,12 +937,57 @@ function TLSSocket(duplex, options){
|
|
|
654
937
|
context.pendingHandshake.push({ type: CT.HANDSHAKE, data: buf });
|
|
655
938
|
return;
|
|
656
939
|
}
|
|
940
|
+
// ── Middlebox-compat CCS (RFC 8446 §D.4) ──
|
|
941
|
+
// The two roles place it on OPPOSITE sides of their hello, and the
|
|
942
|
+
// spec is explicit about both:
|
|
943
|
+
//
|
|
944
|
+
// server — "immediately AFTER its first handshake message",
|
|
945
|
+
// i.e. after ServerHello or HelloRetryRequest.
|
|
946
|
+
// client — "immediately BEFORE its second flight", i.e. BEFORE
|
|
947
|
+
// the second ClientHello.
|
|
948
|
+
//
|
|
949
|
+
// Getting the client side backwards is not cosmetic: a peer that
|
|
950
|
+
// requires the compat record reads the next thing on the wire and
|
|
951
|
+
// finds a handshake record where the CCS should be, and reports
|
|
952
|
+
// exactly that ("invalid TLS 1.3 ChangeCipherSpec: 160303...").
|
|
953
|
+
// So the client's CCS is emitted BEFORE writeRecord below, and the
|
|
954
|
+
// server's after.
|
|
955
|
+
//
|
|
956
|
+
// We cannot rely on context.isTls13 yet (it is set at
|
|
957
|
+
// secureConnect), so both gate on the session's negotiated
|
|
958
|
+
// version. For the client that is only known once a
|
|
959
|
+
// HelloRetryRequest has told us — which is precisely the CH2 case
|
|
960
|
+
// this branch exists for. In the no-HRR flow the version is still
|
|
961
|
+
// unknown at CH1 time, so the flag stays false and the CCS goes out
|
|
962
|
+
// just before the first encrypted record instead (epoch-1 path
|
|
963
|
+
// below); that is the same "before its second flight" placement.
|
|
964
|
+
if (!session.isServer && type === 'hello' && session.getVersion() === 0x0304) {
|
|
965
|
+
sendCompatCCS();
|
|
966
|
+
}
|
|
967
|
+
|
|
657
968
|
writeRecord(CT.HANDSHAKE, buf);
|
|
969
|
+
|
|
970
|
+
if (session.isServer &&
|
|
971
|
+
(type === 'hello' || type === 'hello_retry_request') &&
|
|
972
|
+
session.getVersion() === 0x0304) {
|
|
973
|
+
sendCompatCCS();
|
|
974
|
+
}
|
|
658
975
|
return;
|
|
659
976
|
}
|
|
660
977
|
|
|
661
978
|
if (epoch === 1) {
|
|
662
979
|
if (isTls13) {
|
|
980
|
+
// Middlebox-compat CCS (RFC 8446 §D.4): a TLS 1.3 client sends
|
|
981
|
+
// the fake CCS after its ClientHello (or, under HRR, after CH2)
|
|
982
|
+
// and before its first encrypted handshake record — i.e. right
|
|
983
|
+
// here, the moment we're about to emit the first epoch-1 record.
|
|
984
|
+
// Plaintext, one 0x01 byte. Server side handled after the SH.
|
|
985
|
+
// Fallback placement: reached only when the CH2 path above did
|
|
986
|
+
// not fire (no HelloRetryRequest, so the version was still
|
|
987
|
+
// unknown at ClientHello time). Same "before the second
|
|
988
|
+
// flight" position §D.4 describes.
|
|
989
|
+
if (!session.isServer) sendCompatCCS();
|
|
990
|
+
|
|
663
991
|
// TLS 1.3: encrypt handshake messages with handshake traffic secret
|
|
664
992
|
if (context.handshake_write_key === null) {
|
|
665
993
|
const hs = session.getHandshakeSecrets();
|
|
@@ -684,7 +1012,7 @@ function TLSSocket(duplex, options){
|
|
|
684
1012
|
context.rec_version
|
|
685
1013
|
);
|
|
686
1014
|
context.handshake_write_seq++;
|
|
687
|
-
if (
|
|
1015
|
+
if (canWrite()) {
|
|
688
1016
|
context.transport.write(rec);
|
|
689
1017
|
}
|
|
690
1018
|
} catch(e) {
|
|
@@ -714,7 +1042,7 @@ function TLSSocket(duplex, options){
|
|
|
714
1042
|
context.app_write_seq, CT.HANDSHAKE, context.rec_version
|
|
715
1043
|
);
|
|
716
1044
|
context.app_write_seq++;
|
|
717
|
-
if (
|
|
1045
|
+
if (canWrite()) {
|
|
718
1046
|
context.transport.write(rec);
|
|
719
1047
|
}
|
|
720
1048
|
} catch(e) {
|
|
@@ -744,7 +1072,7 @@ function TLSSocket(duplex, options){
|
|
|
744
1072
|
context.rec_version
|
|
745
1073
|
);
|
|
746
1074
|
context.app_write_seq++;
|
|
747
|
-
if (
|
|
1075
|
+
if (canWrite()) {
|
|
748
1076
|
context.transport.write(rec);
|
|
749
1077
|
}
|
|
750
1078
|
} catch(e) {
|
|
@@ -772,35 +1100,17 @@ function TLSSocket(duplex, options){
|
|
|
772
1100
|
if (versions.length === 0) versions.push(0x0303); // fallback
|
|
773
1101
|
|
|
774
1102
|
// Cipher suites based on supported versions
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
ciphers.push(0x1301, 0x1302, 0x1303); // TLS_AES_128_GCM, TLS_AES_256_GCM, TLS_CHACHA20
|
|
778
|
-
}
|
|
779
|
-
if (versions.includes(0x0303)) {
|
|
780
|
-
ciphers.push(
|
|
781
|
-
0xC02F, // ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
|
782
|
-
0xC030, // ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
|
783
|
-
0xC02B, // ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
|
|
784
|
-
0xCCA8 // ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
|
|
785
|
-
);
|
|
786
|
-
}
|
|
1103
|
+
// crypto.js owns the default suite list (see default_cipher_suites).
|
|
1104
|
+
let ciphers = default_cipher_suites(versions.includes(0x0304), versions.includes(0x0303));
|
|
787
1105
|
|
|
788
1106
|
// Signature algorithms: override or default
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
if (versions.includes(0x0304)) {
|
|
794
|
-
sigalgs.push(0x0804, 0x0805, 0x0806); // RSA-PSS
|
|
795
|
-
sigalgs.push(0x0403, 0x0503, 0x0603); // ECDSA
|
|
796
|
-
}
|
|
797
|
-
if (versions.includes(0x0303)) {
|
|
798
|
-
sigalgs.push(0x0401, 0x0501, 0x0601); // RSA-PKCS1
|
|
799
|
-
}
|
|
800
|
-
}
|
|
1107
|
+
// signing.js owns the scheme list (see default_signature_schemes):
|
|
1108
|
+
// the PKCS#1 set is only added when TLS 1.2 is actually in range.
|
|
1109
|
+
let sigalgs = options.signatureAlgorithms
|
|
1110
|
+
|| default_signature_schemes();
|
|
801
1111
|
|
|
802
1112
|
// Groups: override or default
|
|
803
|
-
let groups = options.groups ||
|
|
1113
|
+
let groups = options.groups || ECDH_SUPPORTED_GROUPS.slice(); // whatever ecdh.js implements
|
|
804
1114
|
|
|
805
1115
|
// prioritizeChaCha: move ChaCha20 to front of cipher list
|
|
806
1116
|
if (options.prioritizeChaCha) {
|
|
@@ -833,6 +1143,19 @@ function TLSSocket(duplex, options){
|
|
|
833
1143
|
});
|
|
834
1144
|
|
|
835
1145
|
session.on('secureConnect', function(){
|
|
1146
|
+
// RFC 8446 §5.1: handshake messages must not straddle a key change,
|
|
1147
|
+
// and the flight ending in Finished must end EXACTLY there. Leftover
|
|
1148
|
+
// bytes in the message stream at completion are trailing garbage the
|
|
1149
|
+
// peer packed after its Finished — a framing violation, not padding.
|
|
1150
|
+
if (context.hsBuf !== null && context.hsBuf.length > 0) {
|
|
1151
|
+
context.hsBuf = null;
|
|
1152
|
+
markTransportDead();
|
|
1153
|
+
self.emit('error', new Error('Trailing data after handshake completion (decode_error)'));
|
|
1154
|
+
const t = context.transport;
|
|
1155
|
+
if (t && typeof t.destroy === 'function' && !t.destroyed) t.destroy();
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
836
1159
|
context.using_app_keys=true;
|
|
837
1160
|
context.secureEstablished=true;
|
|
838
1161
|
context.aeadAlgo = getAeadAlgo(session.getCipher());
|
|
@@ -904,6 +1227,21 @@ function TLSSocket(duplex, options){
|
|
|
904
1227
|
self.emit('session', ticketData);
|
|
905
1228
|
});
|
|
906
1229
|
|
|
1230
|
+
// Bridge fatal protocol errors from the session (malformed peer message,
|
|
1231
|
+
// invalid key share, Finished mismatch, ...). The session has already
|
|
1232
|
+
// emitted the fatal alert as an epoch-tagged 'message' — which the handler
|
|
1233
|
+
// above framed and wrote — and flipped itself to a terminal state. Here we
|
|
1234
|
+
// surface it to the owner and tear down the transport. Without this bridge
|
|
1235
|
+
// the session's 'error' emit has no listener, and Node throws it as an
|
|
1236
|
+
// uncaught exception out of the transport's data callback — the very DoS
|
|
1237
|
+
// the validation was added to prevent.
|
|
1238
|
+
session.on('error', function(err) {
|
|
1239
|
+
markTransportDead();
|
|
1240
|
+
self.emit('error', err);
|
|
1241
|
+
const t = context.transport;
|
|
1242
|
+
if (t && typeof t.destroy === 'function' && !t.destroyed) t.destroy();
|
|
1243
|
+
});
|
|
1244
|
+
|
|
907
1245
|
// Forward handshakeMessage event (for debugging/inspection)
|
|
908
1246
|
session.on('handshakeMessage', function(type, raw, parsed) {
|
|
909
1247
|
self.emit('handshakeMessage', type, raw, parsed);
|
package/src/utils.js
CHANGED
|
@@ -54,7 +54,13 @@ function arraysEqual(a, b) {
|
|
|
54
54
|
return false;
|
|
55
55
|
}
|
|
56
56
|
}else{
|
|
57
|
-
|
|
57
|
+
// Either element may be null/undefined here: the branch above requires
|
|
58
|
+
// BOTH to be non-null typed arrays, so a null slips through to this
|
|
59
|
+
// side and the `.constructor` reads below threw a TypeError. Settle
|
|
60
|
+
// those cases by identity before anything is dereferenced.
|
|
61
|
+
if(a[i]==null || b[i]==null){
|
|
62
|
+
if(a[i]!==b[i]) return false;
|
|
63
|
+
}else if(typeof a[i]=='string' && typeof b[i]=='string'){
|
|
58
64
|
if (a[i] !== b[i]){
|
|
59
65
|
return false;
|
|
60
66
|
}
|