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/dtls_session.js
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { EventEmitter } from 'node:events';
|
|
30
|
+
import crypto from 'node:crypto';
|
|
30
31
|
|
|
31
32
|
import TLSSession from './tls_session.js';
|
|
32
33
|
import createSecureContext from './secure_context.js';
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
build_message,
|
|
39
40
|
build_hello_verify_request,
|
|
40
41
|
parse_hello_verify_request,
|
|
42
|
+
parse_hello,
|
|
41
43
|
} from './wire.js';
|
|
42
44
|
|
|
43
45
|
import {
|
|
@@ -59,6 +61,8 @@ import {
|
|
|
59
61
|
parseDtlsAck,
|
|
60
62
|
} from './record.js';
|
|
61
63
|
|
|
64
|
+
import { timingSafeEqualU8 } from './utils.js';
|
|
65
|
+
|
|
62
66
|
|
|
63
67
|
// ============================================================
|
|
64
68
|
// Helpers
|
|
@@ -130,6 +134,15 @@ function DTLSSession(options) {
|
|
|
130
134
|
isServer: isServer,
|
|
131
135
|
mtu: options.mtu || 1200,
|
|
132
136
|
|
|
137
|
+
// Upper bound on a single reassembled handshake message. The DTLS handshake
|
|
138
|
+
// header carries a 24-bit length (up to 16MB), which is attacker-controlled
|
|
139
|
+
// on the very first fragment. Without a cap, a peer could send one fragment
|
|
140
|
+
// claiming length 0xFFFFFF for several msg_seqs and force large allocations
|
|
141
|
+
// before anything is authenticated. 256KB comfortably fits a real
|
|
142
|
+
// certificate-chain flight while bounding the damage. Configurable via
|
|
143
|
+
// options.maxHandshakeMessageSize.
|
|
144
|
+
maxHandshakeMessageSize: options.maxHandshakeMessageSize || (256 * 1024),
|
|
145
|
+
|
|
133
146
|
// Version (determined after negotiation)
|
|
134
147
|
selectedVersion: null, // DTLS version (0xFEFC or 0xFEFD)
|
|
135
148
|
|
|
@@ -146,12 +159,17 @@ function DTLSSession(options) {
|
|
|
146
159
|
cipherSuite: null,
|
|
147
160
|
hashName: null,
|
|
148
161
|
|
|
149
|
-
// Fragment reassembly: msg_seq → { totalLength,
|
|
162
|
+
// Fragment reassembly: msg_seq → { totalLength, type, chunks:[{off,end,body}], covered }
|
|
150
163
|
fragments: {},
|
|
151
164
|
|
|
152
165
|
// Incoming handshake msg_seq tracking
|
|
153
166
|
nextReadMsgSeq: 0,
|
|
154
167
|
|
|
168
|
+
// Out-of-order handshake buffering (RFC 6347 §4.2.4 / RFC 9147 §5.2):
|
|
169
|
+
// complete messages that arrive ahead of nextReadMsgSeq are held here,
|
|
170
|
+
// keyed by msg_seq, and drained in order once the gap is filled.
|
|
171
|
+
pendingMessages: {}, // msg_seq → { type, body }
|
|
172
|
+
|
|
155
173
|
// Flight tracking for retransmission
|
|
156
174
|
currentFlight: [], // array of { epoch, data: Uint8Array (complete datagram) }
|
|
157
175
|
retransmitTimer: null,
|
|
@@ -425,9 +443,18 @@ function DTLSSession(options) {
|
|
|
425
443
|
* Returns array of DTLS handshake message fragments (each with reconstruction header).
|
|
426
444
|
*/
|
|
427
445
|
function fragmentMessage(dtlsMsg, tlsMsg, msgSeq) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
446
|
+
// Record-layer overhead we must leave room for inside one datagram/MTU:
|
|
447
|
+
// DTLS 1.2: 13-byte classic record header + 16-byte AEAD tag (+8 explicit
|
|
448
|
+
// nonce for GCM, but that lives inside the encrypted payload the
|
|
449
|
+
// record layer builds, so we keep a small safety margin).
|
|
450
|
+
// DTLS 1.3: 5-byte unified header (2-byte seq + length) + 16-byte AEAD tag.
|
|
451
|
+
// Using the correct, smaller 1.3 overhead avoids over-fragmenting.
|
|
452
|
+
let isDtls13 = ctx.selectedVersion === DTLS_VERSION.DTLS1_3;
|
|
453
|
+
let recordOverhead = isDtls13 ? (5 + 16) : (13 + 16 + 8);
|
|
454
|
+
let maxFragment = ctx.mtu - recordOverhead;
|
|
455
|
+
|
|
456
|
+
// The DTLS handshake message header is 12 bytes; the body is what we chunk.
|
|
457
|
+
const HS_HDR = 12;
|
|
431
458
|
if (dtlsMsg.length <= maxFragment) return [dtlsMsg];
|
|
432
459
|
|
|
433
460
|
// Need to fragment: split the TLS body into chunks
|
|
@@ -436,8 +463,11 @@ function DTLSSession(options) {
|
|
|
436
463
|
let fragments = [];
|
|
437
464
|
let offset = 0;
|
|
438
465
|
|
|
466
|
+
// Each fragment carries its own 12-byte handshake header, so the payload
|
|
467
|
+
// budget per fragment is maxFragment - HS_HDR.
|
|
468
|
+
let maxChunk = Math.max(1, maxFragment - HS_HDR);
|
|
439
469
|
while (offset < totalLength) {
|
|
440
|
-
let chunkLen = Math.min(
|
|
470
|
+
let chunkLen = Math.min(maxChunk, totalLength - offset);
|
|
441
471
|
let frag = build_dtls_handshake(tlsMsg, msgSeq, offset, chunkLen);
|
|
442
472
|
fragments.push(frag);
|
|
443
473
|
offset += chunkLen;
|
|
@@ -483,7 +513,18 @@ function DTLSSession(options) {
|
|
|
483
513
|
|
|
484
514
|
let records = parseDtlsDatagram(data, keysByEpoch);
|
|
485
515
|
|
|
516
|
+
// RFC 9147 §7: collect the record numbers of handshake records we received
|
|
517
|
+
// in this datagram so we can ACK them once, after processing. Only for
|
|
518
|
+
// DTLS 1.3, and only while we're still handshaking (post-handshake ACKs of
|
|
519
|
+
// NewSessionTicket etc. are optional and skipped here for simplicity).
|
|
520
|
+
let ackable = [];
|
|
521
|
+
|
|
486
522
|
for (let i = 0; i < records.length; i++) {
|
|
523
|
+
if (records[i].type === CT.HANDSHAKE &&
|
|
524
|
+
ctx.selectedVersion === DTLS_VERSION.DTLS1_3) {
|
|
525
|
+
ackable.push({ epoch: records[i].epoch, seq: records[i].seq });
|
|
526
|
+
}
|
|
527
|
+
|
|
487
528
|
processRecord(records[i]);
|
|
488
529
|
|
|
489
530
|
// After CCS, keys are newly available — re-decrypt remaining epoch>0 records
|
|
@@ -500,6 +541,14 @@ function DTLSSession(options) {
|
|
|
500
541
|
}
|
|
501
542
|
}
|
|
502
543
|
}
|
|
544
|
+
|
|
545
|
+
// Send a single ACK covering all handshake records received in this datagram.
|
|
546
|
+
// We ACK from the highest epoch for which we already have write keys (so the
|
|
547
|
+
// ACK itself is protected once handshake keys exist; epoch 0 before that).
|
|
548
|
+
if (ackable.length > 0) {
|
|
549
|
+
let ackEpoch = ctx.keys[3] ? 3 : (ctx.keys[2] ? 2 : 0);
|
|
550
|
+
sendAck(ackEpoch, ackable);
|
|
551
|
+
}
|
|
503
552
|
}
|
|
504
553
|
|
|
505
554
|
function processRecord(record) {
|
|
@@ -569,33 +618,68 @@ function DTLSSession(options) {
|
|
|
569
618
|
}
|
|
570
619
|
|
|
571
620
|
/**
|
|
572
|
-
* Reassemble a fragmented handshake message.
|
|
621
|
+
* Reassemble a (possibly fragmented) handshake message.
|
|
622
|
+
*
|
|
623
|
+
* RFC 6347 §4.2.3 / RFC 9147 §5.5: fragments may overlap and arrive out of
|
|
624
|
+
* order, because a retransmission can re-fragment the same message with
|
|
625
|
+
* different boundaries (e.g. after a PMTU change). We therefore copy each
|
|
626
|
+
* fragment's bytes into a full-length buffer and track coverage as a set of
|
|
627
|
+
* merged byte-ranges, rather than requiring each fragment to start exactly
|
|
628
|
+
* where the previous one ended.
|
|
573
629
|
*/
|
|
574
630
|
function reassembleFragment(parsed) {
|
|
575
631
|
let key = parsed.msg_seq;
|
|
576
|
-
|
|
577
|
-
|
|
632
|
+
|
|
633
|
+
// Ignore fragments for messages we've already delivered.
|
|
634
|
+
if (key < ctx.nextReadMsgSeq) return;
|
|
635
|
+
|
|
636
|
+
// DoS guard: the 24-bit `length` is attacker-controlled on the first
|
|
637
|
+
// fragment. Reject anything claiming more than our cap BEFORE allocating,
|
|
638
|
+
// so a peer can't force huge buffers with a single unauthenticated packet.
|
|
639
|
+
if (parsed.length > ctx.maxHandshakeMessageSize) {
|
|
640
|
+
ev.emit('error', new Error('DTLS handshake message exceeds maxHandshakeMessageSize (' +
|
|
641
|
+
parsed.length + ' > ' + ctx.maxHandshakeMessageSize + ')'));
|
|
642
|
+
return;
|
|
578
643
|
}
|
|
644
|
+
|
|
579
645
|
let frag = ctx.fragments[key];
|
|
580
|
-
frag
|
|
646
|
+
if (!frag) {
|
|
647
|
+
frag = ctx.fragments[key] = {
|
|
648
|
+
totalLength: parsed.length,
|
|
649
|
+
type: parsed.type,
|
|
650
|
+
buf: new Uint8Array(parsed.length),
|
|
651
|
+
ranges: [], // sorted, merged [start, end) covered ranges
|
|
652
|
+
};
|
|
653
|
+
}
|
|
581
654
|
|
|
582
|
-
|
|
583
|
-
let
|
|
584
|
-
let filled = 0;
|
|
585
|
-
let offsets = Object.keys(frag.parts).map(Number).sort((a, b) => a - b);
|
|
655
|
+
let start = parsed.frag_offset;
|
|
656
|
+
let end = parsed.frag_offset + parsed.frag_length;
|
|
586
657
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if (offsets[i] !== filled) return; // gap — not complete yet
|
|
590
|
-
assembled.set(part, offsets[i]);
|
|
591
|
-
filled = offsets[i] + part.length;
|
|
592
|
-
}
|
|
658
|
+
// Bounds guard against a malformed fragment claiming to exceed totalLength.
|
|
659
|
+
if (end > frag.totalLength) return;
|
|
593
660
|
|
|
594
|
-
|
|
661
|
+
// Copy the fragment bytes in (overlaps simply overwrite with identical data).
|
|
662
|
+
frag.buf.set(parsed.body.subarray(0, parsed.frag_length), start);
|
|
595
663
|
|
|
596
|
-
//
|
|
597
|
-
|
|
598
|
-
|
|
664
|
+
// Merge [start, end) into the covered-range set.
|
|
665
|
+
frag.ranges.push([start, end]);
|
|
666
|
+
frag.ranges.sort((a, b) => a[0] - b[0]);
|
|
667
|
+
let merged = [];
|
|
668
|
+
for (let r of frag.ranges) {
|
|
669
|
+
if (merged.length && r[0] <= merged[merged.length - 1][1]) {
|
|
670
|
+
// Overlapping/adjacent → extend the previous range.
|
|
671
|
+
if (r[1] > merged[merged.length - 1][1]) merged[merged.length - 1][1] = r[1];
|
|
672
|
+
} else {
|
|
673
|
+
merged.push([r[0], r[1]]);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
frag.ranges = merged;
|
|
677
|
+
|
|
678
|
+
// Complete when a single range covers [0, totalLength).
|
|
679
|
+
if (merged.length === 1 && merged[0][0] === 0 && merged[0][1] === frag.totalLength) {
|
|
680
|
+
delete ctx.fragments[key];
|
|
681
|
+
deliverHandshakeMessage(frag.type, frag.buf, key);
|
|
682
|
+
}
|
|
599
683
|
}
|
|
600
684
|
|
|
601
685
|
/**
|
|
@@ -603,18 +687,80 @@ function DTLSSession(options) {
|
|
|
603
687
|
* Strips DTLS reconstruction → builds TLS format.
|
|
604
688
|
*/
|
|
605
689
|
function deliverHandshakeMessage(type, body, msgSeq) {
|
|
606
|
-
// Skip if we've already processed this msg_seq
|
|
690
|
+
// Skip if we've already processed this msg_seq (duplicate / retransmit).
|
|
607
691
|
if (msgSeq < ctx.nextReadMsgSeq) return;
|
|
608
|
-
ctx.nextReadMsgSeq = msgSeq + 1;
|
|
609
|
-
|
|
610
692
|
|
|
611
693
|
// Check for HelloVerifyRequest (DTLS 1.2 server→client, type=3)
|
|
612
694
|
if (type === 3 && !isServer) {
|
|
695
|
+
ctx.nextReadMsgSeq = msgSeq + 1;
|
|
613
696
|
let hvr = parse_hello_verify_request(body);
|
|
614
697
|
triggerClientHelloWithCookie(hvr.cookie);
|
|
615
698
|
return;
|
|
616
699
|
}
|
|
617
700
|
|
|
701
|
+
// ---- Server-side HelloVerifyRequest cookie exchange (RFC 6347 §4.2.1) ----
|
|
702
|
+
// For DTLS 1.2 with cookies enabled, the first ClientHello carries no cookie
|
|
703
|
+
// (or an empty one). We answer with a HelloVerifyRequest and do NOT process
|
|
704
|
+
// the ClientHello — that proves return-routability (the client must be at the
|
|
705
|
+
// address it claims before we allocate any handshake state). Only a second
|
|
706
|
+
// ClientHello echoing the correct cookie is processed.
|
|
707
|
+
//
|
|
708
|
+
// Note: msg_seq is NOT advanced for a rejected/HVR-triggering ClientHello, so
|
|
709
|
+
// the client's retried CH (which reuses msg_seq per RFC 6347) is still accepted.
|
|
710
|
+
if (type === 1 && isServer && options.useCookies === true &&
|
|
711
|
+
ctx.selectedVersion !== DTLS_VERSION.DTLS1_3) {
|
|
712
|
+
|
|
713
|
+
let ch = null;
|
|
714
|
+
try { ch = parse_hello({ kind: 'client', body: body }); } catch (e) { ch = null; }
|
|
715
|
+
let incomingCookie = (ch && ch.dtls_cookie) ? ch.dtls_cookie : new Uint8Array(0);
|
|
716
|
+
|
|
717
|
+
if (incomingCookie.length === 0) {
|
|
718
|
+
// First ClientHello → send HVR with a fresh cookie. Don't process the CH,
|
|
719
|
+
// don't advance msg_seq (the retry reuses the same msg_seq).
|
|
720
|
+
sendHelloVerifyRequest();
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Second ClientHello: the cookie MUST match what we issued.
|
|
725
|
+
if (!ctx.hvrCookie || !timingSafeEqualU8(incomingCookie, ctx.hvrCookie)) {
|
|
726
|
+
// Cookie mismatch → silently drop (RFC 6347: server discards). Don't
|
|
727
|
+
// advance msg_seq; a correct retry can still arrive.
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
// Cookie verified → fall through and process the ClientHello normally.
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// ---- In-order delivery (RFC 6347 §4.2.4 / RFC 9147 §5.2) ----
|
|
734
|
+
// DTLS handshake messages must be handed to the TLS state machine strictly
|
|
735
|
+
// in msg_seq order. If this message is ahead of what we expect, buffer it
|
|
736
|
+
// and wait for the gap to fill. If it's the next expected one, feed it, then
|
|
737
|
+
// drain any consecutive messages that were buffered earlier.
|
|
738
|
+
if (msgSeq > ctx.nextReadMsgSeq) {
|
|
739
|
+
// Out of order — hold it (ignore a duplicate already buffered).
|
|
740
|
+
if (!(msgSeq in ctx.pendingMessages)) {
|
|
741
|
+
ctx.pendingMessages[msgSeq] = { type: type, body: body };
|
|
742
|
+
}
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// msgSeq === nextReadMsgSeq → feed it and advance.
|
|
747
|
+
feedHandshakeToTls(type, body, msgSeq);
|
|
748
|
+
ctx.nextReadMsgSeq = msgSeq + 1;
|
|
749
|
+
|
|
750
|
+
// Drain consecutively-numbered buffered messages.
|
|
751
|
+
while (ctx.nextReadMsgSeq in ctx.pendingMessages) {
|
|
752
|
+
let next = ctx.pendingMessages[ctx.nextReadMsgSeq];
|
|
753
|
+
delete ctx.pendingMessages[ctx.nextReadMsgSeq];
|
|
754
|
+
feedHandshakeToTls(next.type, next.body, ctx.nextReadMsgSeq);
|
|
755
|
+
ctx.nextReadMsgSeq++;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Feed one in-order handshake message to the TLSSession, converting from DTLS
|
|
761
|
+
* to TLS wire format and driving version detection.
|
|
762
|
+
*/
|
|
763
|
+
function feedHandshakeToTls(type, body, msgSeq) {
|
|
618
764
|
// Build TLS-format message: type(1) + length(3) + body
|
|
619
765
|
let tlsMsg = build_message(type, body);
|
|
620
766
|
|
|
@@ -633,6 +779,23 @@ function DTLSSession(options) {
|
|
|
633
779
|
}
|
|
634
780
|
}
|
|
635
781
|
|
|
782
|
+
/**
|
|
783
|
+
* Server: build and send a HelloVerifyRequest with a fresh cookie (DTLS 1.2).
|
|
784
|
+
* The cookie is a CSPRNG value stored on the context; a matching cookie must
|
|
785
|
+
* come back in the client's second ClientHello (verified in
|
|
786
|
+
* deliverHandshakeMessage) before we process the handshake.
|
|
787
|
+
*/
|
|
788
|
+
function sendHelloVerifyRequest() {
|
|
789
|
+
let cookie = new Uint8Array(crypto.randomBytes(32));
|
|
790
|
+
ctx.hvrCookie = cookie;
|
|
791
|
+
ctx.hvrDone = true;
|
|
792
|
+
|
|
793
|
+
// HVR always uses msg_seq 0 and is sent in the clear (epoch 0).
|
|
794
|
+
let hvrMsg = build_dtls_handshake(build_message(3, build_hello_verify_request({ cookie: cookie })), 0);
|
|
795
|
+
let record = buildDtlsPlaintext(CT.HANDSHAKE, 0, nextWriteSeq(0), hvrMsg);
|
|
796
|
+
ev.emit('packet', record);
|
|
797
|
+
}
|
|
798
|
+
|
|
636
799
|
/**
|
|
637
800
|
* Trigger a new ClientHello with cookie (DTLS 1.2 HVR response).
|
|
638
801
|
*/
|
|
@@ -650,6 +813,8 @@ function DTLSSession(options) {
|
|
|
650
813
|
tls.context.message_sent_seq = 0;
|
|
651
814
|
ctx.nextReadMsgSeq = 0;
|
|
652
815
|
ctx.currentFlight = [];
|
|
816
|
+
ctx.fragments = {}; // discard any partial reassembly from CH1 flight
|
|
817
|
+
ctx.pendingMessages = {}; // discard any out-of-order buffer from CH1 flight
|
|
653
818
|
transcriptMsgSeqs = [];
|
|
654
819
|
|
|
655
820
|
// Push CH2 to transcript (transcriptHook will store TLS format for now,
|
|
@@ -734,6 +899,16 @@ function DTLSSession(options) {
|
|
|
734
899
|
// Handshake completion
|
|
735
900
|
// ============================================================
|
|
736
901
|
|
|
902
|
+
// Forward 'clienthello' so server-side callers can inspect the peer's
|
|
903
|
+
// offered extensions (tls.getRemoteExtension / message.extensions) and
|
|
904
|
+
// set answering extensions via set_context({ local_extensions }) BEFORE
|
|
905
|
+
// the ServerHello is built — the emit is synchronous within ClientHello
|
|
906
|
+
// processing, ahead of hello construction. This is how DTLS-SRTP's
|
|
907
|
+
// use_srtp answer (RFC 5764 §4.1) gets negotiated generically.
|
|
908
|
+
tls.on('clienthello', function(data, message) {
|
|
909
|
+
ev.emit('clienthello', data, message);
|
|
910
|
+
});
|
|
911
|
+
|
|
737
912
|
tls.on('secureConnect', function() {
|
|
738
913
|
ctx.state = 'connected';
|
|
739
914
|
cancelRetransmit();
|
|
@@ -786,23 +961,12 @@ function DTLSSession(options) {
|
|
|
786
961
|
// ============================================================
|
|
787
962
|
|
|
788
963
|
tls.on('hello', function() {
|
|
789
|
-
// Version is detected lazily in handshakeSecrets and message handlers
|
|
790
|
-
|
|
791
|
-
// DTLS 1.2
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
for (let i = 0; i < 32; i++) cookie[i] = Math.floor(Math.random() * 256);
|
|
796
|
-
ctx.hvrCookie = cookie;
|
|
797
|
-
|
|
798
|
-
let hvrBody = build_hello_verify_request({ cookie: cookie });
|
|
799
|
-
let hvrMsg = build_dtls_handshake(
|
|
800
|
-
build_message(3, hvrBody),
|
|
801
|
-
0
|
|
802
|
-
);
|
|
803
|
-
let record = buildDtlsPlaintext(CT.HANDSHAKE, 0, nextWriteSeq(0), hvrMsg);
|
|
804
|
-
ev.emit('packet', record);
|
|
805
|
-
}
|
|
964
|
+
// Version is detected lazily in handshakeSecrets and message handlers.
|
|
965
|
+
//
|
|
966
|
+
// Note: DTLS 1.2 HelloVerifyRequest is handled earlier, in
|
|
967
|
+
// deliverHandshakeMessage — the first (cookieless) ClientHello never reaches
|
|
968
|
+
// TLSSession, so by the time 'hello' fires here the cookie has already been
|
|
969
|
+
// verified. Nothing to do for HVR at this point.
|
|
806
970
|
});
|
|
807
971
|
|
|
808
972
|
|
|
@@ -830,6 +994,21 @@ function DTLSSession(options) {
|
|
|
830
994
|
/** Access to internal TLSSession (for advanced use). */
|
|
831
995
|
get tls() { return tls; },
|
|
832
996
|
|
|
997
|
+
/**
|
|
998
|
+
* Export keying material (RFC 5705 / RFC 8446 §7.5).
|
|
999
|
+
* DTLS-SRTP: exportKeyingMaterial(len, 'EXTRACTOR-dtls_srtp') — no context.
|
|
1000
|
+
* Returns Buffer, or null before secrets are available.
|
|
1001
|
+
*/
|
|
1002
|
+
exportKeyingMaterial: function(length, label, context_value) {
|
|
1003
|
+
return tls.exportKeyingMaterial(length, label, context_value);
|
|
1004
|
+
},
|
|
1005
|
+
|
|
1006
|
+
/** Peer's hello extensions: [{ type, name, data, value }]. */
|
|
1007
|
+
getRemoteExtensions: function() { return tls.getRemoteExtensions(); },
|
|
1008
|
+
|
|
1009
|
+
/** One peer extension by numeric type (e.g. 14 = use_srtp), or null. */
|
|
1010
|
+
getRemoteExtension: function(type) { return tls.getRemoteExtension(type); },
|
|
1011
|
+
|
|
833
1012
|
/** Current DTLS state. */
|
|
834
1013
|
get state() { return ctx.state; },
|
|
835
1014
|
|
package/src/dtls_socket.js
CHANGED
|
@@ -81,6 +81,13 @@ function DTLSSocket(udpSocket, options) {
|
|
|
81
81
|
ev.emit('session', ticket);
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
// Server-side callers use this to inspect the peer's offered extensions
|
|
85
|
+
// (session.getRemoteExtension) and answer via set_context BEFORE the
|
|
86
|
+
// ServerHello is built — e.g. DTLS-SRTP's use_srtp (RFC 5764).
|
|
87
|
+
session.on('clienthello', function(data, message) {
|
|
88
|
+
ev.emit('clienthello', data, message);
|
|
89
|
+
});
|
|
90
|
+
|
|
84
91
|
/**
|
|
85
92
|
* Feed an incoming UDP datagram (filtered by remote address/port).
|
|
86
93
|
* Called by the UDP socket's message handler or by DTLSServer.
|
|
@@ -122,6 +129,21 @@ function DTLSSocket(udpSocket, options) {
|
|
|
122
129
|
self.getALPN = function() { return session.getALPN(); };
|
|
123
130
|
self.getPeerCertificate = function() { return session.getPeerCertificate(); };
|
|
124
131
|
|
|
132
|
+
/** RFC 5705 / RFC 8446 §7.5 exporter (Node tls.TLSSocket parity).
|
|
133
|
+
* DTLS-SRTP: exportKeyingMaterial(len, 'EXTRACTOR-dtls_srtp') — no context.
|
|
134
|
+
* Returns Buffer, or null before secrets are available. */
|
|
135
|
+
self.exportKeyingMaterial = function(length, label, context_value) {
|
|
136
|
+
return session.exportKeyingMaterial(length, label, context_value);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** Peer hello extensions / single extension by numeric type. */
|
|
140
|
+
self.getRemoteExtensions = function() { return session.getRemoteExtensions(); };
|
|
141
|
+
self.getRemoteExtension = function(type) { return session.getRemoteExtension(type); };
|
|
142
|
+
|
|
143
|
+
/** Pass config through to the underlying session (e.g. local_extensions
|
|
144
|
+
* from a 'clienthello' handler). */
|
|
145
|
+
self.set_context = function(opts) { session.set_context(opts); };
|
|
146
|
+
|
|
125
147
|
/** Access to internal DTLSSession (for advanced use). */
|
|
126
148
|
self.session = session;
|
|
127
149
|
|
package/src/record.js
CHANGED
|
@@ -297,6 +297,37 @@ function getNonce12(salt4, explicit8) {
|
|
|
297
297
|
return out;
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
/**
|
|
301
|
+
* TLS/DTLS 1.2 ChaCha20-Poly1305 nonce (RFC 7905 §2).
|
|
302
|
+
*
|
|
303
|
+
* Unlike AES-GCM, ChaCha20-Poly1305 in TLS 1.2 has NO explicit per-record nonce
|
|
304
|
+
* on the wire. The 12-byte nonce is the record's 64-bit sequence number, left-
|
|
305
|
+
* padded to 12 bytes, XORed with the 12-byte client/server write IV derived from
|
|
306
|
+
* the key block. This is the same construction TLS 1.3 uses — the difference from
|
|
307
|
+
* GCM (salt(4) || explicit(8)) is why the two must be handled separately.
|
|
308
|
+
*
|
|
309
|
+
* For DTLS the "sequence number" input is the 16-bit epoch concatenated with the
|
|
310
|
+
* 48-bit record sequence number, i.e. the same 64-bit value used in the AAD, so
|
|
311
|
+
* callers pass the full record seq and (for DTLS) fold the epoch into it.
|
|
312
|
+
*/
|
|
313
|
+
function getChachaNonce12(iv12, seq) {
|
|
314
|
+
const nonce = new Uint8Array(12);
|
|
315
|
+
nonce.set(iv12, 0);
|
|
316
|
+
// XOR the 64-bit seq into the LAST 8 bytes (big-endian), leaving the first 4
|
|
317
|
+
// bytes of the IV untouched — identical layout to TLS 1.3's getNonce.
|
|
318
|
+
const hi = (seq / 0x100000000) | 0;
|
|
319
|
+
const lo = seq >>> 0;
|
|
320
|
+
nonce[4] ^= (hi >>> 24) & 0xff;
|
|
321
|
+
nonce[5] ^= (hi >>> 16) & 0xff;
|
|
322
|
+
nonce[6] ^= (hi >>> 8) & 0xff;
|
|
323
|
+
nonce[7] ^= hi & 0xff;
|
|
324
|
+
nonce[8] ^= (lo >>> 24) & 0xff;
|
|
325
|
+
nonce[9] ^= (lo >>> 16) & 0xff;
|
|
326
|
+
nonce[10] ^= (lo >>> 8) & 0xff;
|
|
327
|
+
nonce[11] ^= lo & 0xff;
|
|
328
|
+
return nonce;
|
|
329
|
+
}
|
|
330
|
+
|
|
300
331
|
/**
|
|
301
332
|
* Encode sequence number as 8-byte big-endian.
|
|
302
333
|
* Perf: uses Number arithmetic (seq < 2^53 is always safe for TLS).
|
|
@@ -634,11 +665,36 @@ function buildDtlsAad12(epoch, seq, type, plaintextLen) {
|
|
|
634
665
|
return aad;
|
|
635
666
|
}
|
|
636
667
|
|
|
637
|
-
/** Encrypt DTLS 1.2 record payload. Returns: explicit_nonce(8) || ciphertext || tag(16).
|
|
668
|
+
/** Encrypt DTLS 1.2 record payload. Returns: [explicit_nonce(8) ||] ciphertext || tag(16).
|
|
669
|
+
*
|
|
670
|
+
* AES-GCM: prepends an 8-byte explicit nonce (the record seq); nonce = salt(4) || seq(8).
|
|
671
|
+
* ChaCha20-Poly1305 (RFC 7905): NO explicit nonce on the wire; nonce = iv(12) XOR
|
|
672
|
+
* (epoch(16) || seq(48)). Detected by ivSalt length (12 = ChaCha, 4 = GCM).
|
|
673
|
+
*/
|
|
638
674
|
function encryptDtls12(pt, key, ivSalt, epoch, seq, type) {
|
|
675
|
+
let aad = buildDtlsAad12(epoch, seq, type, pt.length);
|
|
676
|
+
|
|
677
|
+
// ChaCha20-Poly1305: full 12-byte IV, no explicit nonce (RFC 7905).
|
|
678
|
+
if (ivSalt.length === 12) {
|
|
679
|
+
// DTLS folds the 16-bit epoch above the 48-bit seq to form the 64-bit
|
|
680
|
+
// sequence input, matching the AAD's epoch||seq (RFC 6347 / RFC 9147).
|
|
681
|
+
let seq64 = epoch * 0x1000000000000 + seq;
|
|
682
|
+
let nonce = getChachaNonce12(ivSalt, seq64);
|
|
683
|
+
let cipher = crypto.createCipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
|
|
684
|
+
cipher.setAAD(aad, { plaintextLength: pt.length });
|
|
685
|
+
let ct = cipher.update(pt);
|
|
686
|
+
cipher.final();
|
|
687
|
+
let tag = cipher.getAuthTag();
|
|
688
|
+
// No explicit nonce prefix for ChaCha.
|
|
689
|
+
let out = new Uint8Array(ct.length + tag.length);
|
|
690
|
+
out.set(new Uint8Array(ct), 0);
|
|
691
|
+
out.set(new Uint8Array(tag), ct.length);
|
|
692
|
+
return out;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// AES-GCM: explicit nonce (= record seq) prepended.
|
|
639
696
|
let explicit = seqToBytes(seq);
|
|
640
697
|
let nonce = getNonce12(ivSalt, explicit);
|
|
641
|
-
let aad = buildDtlsAad12(epoch, seq, type, pt.length);
|
|
642
698
|
let algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
|
|
643
699
|
let cipher = crypto.createCipheriv(algo, key, nonce);
|
|
644
700
|
cipher.setAAD(aad);
|
|
@@ -652,8 +708,28 @@ function encryptDtls12(pt, key, ivSalt, epoch, seq, type) {
|
|
|
652
708
|
return out;
|
|
653
709
|
}
|
|
654
710
|
|
|
655
|
-
/** Decrypt DTLS 1.2 record fragment.
|
|
711
|
+
/** Decrypt DTLS 1.2 record fragment.
|
|
712
|
+
* AES-GCM input: explicit_nonce(8) || ciphertext || tag(16).
|
|
713
|
+
* ChaCha20 input: ciphertext || tag(16) (no explicit nonce — RFC 7905).
|
|
714
|
+
*/
|
|
656
715
|
function decryptDtls12(fragment, key, ivSalt, epoch, seq, type) {
|
|
716
|
+
// ChaCha20-Poly1305: no explicit nonce; nonce = iv(12) XOR (epoch||seq).
|
|
717
|
+
if (ivSalt.length === 12) {
|
|
718
|
+
if (fragment.length < 16) throw new Error('DTLS 1.2 ChaCha fragment too short');
|
|
719
|
+
let ct = fragment.slice(0, fragment.length - 16);
|
|
720
|
+
let tag = fragment.slice(fragment.length - 16);
|
|
721
|
+
let seq64 = epoch * 0x1000000000000 + seq;
|
|
722
|
+
let nonce = getChachaNonce12(ivSalt, seq64);
|
|
723
|
+
let aad = buildDtlsAad12(epoch, seq, type, ct.length);
|
|
724
|
+
let decipher = crypto.createDecipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
|
|
725
|
+
decipher.setAAD(aad, { plaintextLength: ct.length });
|
|
726
|
+
decipher.setAuthTag(tag);
|
|
727
|
+
let pt = decipher.update(ct);
|
|
728
|
+
decipher.final();
|
|
729
|
+
return new Uint8Array(pt);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// AES-GCM: strip the 8-byte explicit nonce.
|
|
657
733
|
if (fragment.length < 24) throw new Error('DTLS 1.2 fragment too short');
|
|
658
734
|
let explicit = fragment.slice(0, 8);
|
|
659
735
|
let tag = fragment.slice(fragment.length - 16);
|
|
@@ -801,10 +877,42 @@ function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys) {
|
|
|
801
877
|
return record;
|
|
802
878
|
}
|
|
803
879
|
|
|
880
|
+
/**
|
|
881
|
+
* Reconstruct a full DTLS 1.3 record sequence number from its truncated wire
|
|
882
|
+
* encoding (RFC 9147 §4.2.2).
|
|
883
|
+
*
|
|
884
|
+
* @param truncated the low `bits` bits of the seq as sent on the wire
|
|
885
|
+
* @param bits 8 or 16 (how many low bits were transmitted)
|
|
886
|
+
* @param maxSeq highest full seq already accepted in this epoch (-1 if none)
|
|
887
|
+
*
|
|
888
|
+
* Returns the full seq: the value congruent to `truncated` (mod 2^bits) that is
|
|
889
|
+
* closest to maxSeq+1. This tolerates reordering within half a window in each
|
|
890
|
+
* direction, which is what the sliding receive window expects.
|
|
891
|
+
*/
|
|
892
|
+
function reconstructDtlsSeq(truncated, bits, maxSeq) {
|
|
893
|
+
let window = Math.pow(2, bits); // 256 or 65536
|
|
894
|
+
let expected = maxSeq + 1; // where we expect the next record
|
|
895
|
+
let high = Math.floor(expected / window) * window;
|
|
896
|
+
let candidate = high + truncated;
|
|
897
|
+
|
|
898
|
+
// Pick whichever of {candidate-window, candidate, candidate+window} is nearest
|
|
899
|
+
// to `expected`, without going negative.
|
|
900
|
+
let best = candidate;
|
|
901
|
+
let bestDist = Math.abs(candidate - expected);
|
|
902
|
+
for (let delta of [-window, window]) {
|
|
903
|
+
let alt = candidate + delta;
|
|
904
|
+
if (alt < 0) continue;
|
|
905
|
+
let dist = Math.abs(alt - expected);
|
|
906
|
+
if (dist < bestDist) { best = alt; bestDist = dist; }
|
|
907
|
+
}
|
|
908
|
+
return best;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
|
|
804
912
|
/**
|
|
805
913
|
* Decrypt a DTLS 1.3 encrypted record.
|
|
806
914
|
* data: full record bytes (starting with unified header byte)
|
|
807
|
-
* keys: { key, iv, snKey, algo? }
|
|
915
|
+
* keys: { key, iv, snKey, algo?, maxReadSeq? }
|
|
808
916
|
* Returns { epoch, seq, type, content } or null on failure.
|
|
809
917
|
*/
|
|
810
918
|
function decryptEncryptedDtls13(data, keys) {
|
|
@@ -834,7 +942,19 @@ function decryptEncryptedDtls13(data, keys) {
|
|
|
834
942
|
|
|
835
943
|
// Decrypt record number
|
|
836
944
|
let rn = maskRecordNumber(keys.snKey, encRn, ciphertext);
|
|
837
|
-
let
|
|
945
|
+
let truncated = hdr.seqLen2 ? ((rn[0] << 8) | rn[1]) : rn[0];
|
|
946
|
+
|
|
947
|
+
// RFC 9147 §4.2.2: the wire carries only the low 8 or 16 bits of the record
|
|
948
|
+
// sequence number. Reconstruct the full value by choosing the candidate
|
|
949
|
+
// congruent to `truncated` (mod window) that is closest to the highest seq
|
|
950
|
+
// we've already accepted in this epoch. Without this, seq silently wraps to 0
|
|
951
|
+
// after 2^16 records and every nonce/AAD past that point is wrong.
|
|
952
|
+
//
|
|
953
|
+
// keys.maxReadSeq holds the highest full seq accepted so far (per-epoch key
|
|
954
|
+
// object). It starts undefined → treated as -1 so the first record maps to
|
|
955
|
+
// its own truncated value.
|
|
956
|
+
let seq = reconstructDtlsSeq(truncated, hdr.seqLen2 ? 16 : 8,
|
|
957
|
+
(keys.maxReadSeq === undefined ? -1 : keys.maxReadSeq));
|
|
838
958
|
|
|
839
959
|
// Rebuild AAD with plaintext RN
|
|
840
960
|
let hdrLen = 1 + rnLen + (hdr.hasLength ? 2 : 0);
|
|
@@ -863,6 +983,13 @@ function decryptEncryptedDtls13(data, keys) {
|
|
|
863
983
|
decipher.final();
|
|
864
984
|
|
|
865
985
|
let inner = parseInnerPlaintext(new Uint8Array(pt));
|
|
986
|
+
|
|
987
|
+
// Advance the per-epoch high-water mark only after the tag verified, so a
|
|
988
|
+
// forged/replayed record number can't poison future reconstruction.
|
|
989
|
+
if (keys.maxReadSeq === undefined || seq > keys.maxReadSeq) {
|
|
990
|
+
keys.maxReadSeq = seq;
|
|
991
|
+
}
|
|
992
|
+
|
|
866
993
|
return {
|
|
867
994
|
epoch: hdr.epoch,
|
|
868
995
|
seq: seq,
|
|
@@ -1018,6 +1145,7 @@ export {
|
|
|
1018
1145
|
|
|
1019
1146
|
// TLS 1.2
|
|
1020
1147
|
getNonce12,
|
|
1148
|
+
getChachaNonce12,
|
|
1021
1149
|
seqToBytes,
|
|
1022
1150
|
buildAad12,
|
|
1023
1151
|
encrypt12,
|
package/src/session/ecdh.js
CHANGED
|
@@ -28,7 +28,22 @@ function x25519_get_shared_secret(localPrivateRaw, remotePublicRaw) {
|
|
|
28
28
|
let pubDer = Buffer.concat([X25519_SPKI_PREFIX, Buffer.from(remotePublicRaw)]);
|
|
29
29
|
let privObj = crypto.createPrivateKey({ key: privDer, format: 'der', type: 'pkcs8' });
|
|
30
30
|
let pubObj = crypto.createPublicKey({ key: pubDer, format: 'der', type: 'spki' });
|
|
31
|
-
|
|
31
|
+
let secret = new Uint8Array(crypto.diffieHellman({ privateKey: privObj, publicKey: pubObj }));
|
|
32
|
+
|
|
33
|
+
// RFC 7748 §6.1: implementations MUST reject an all-zero shared secret. A peer
|
|
34
|
+
// that sends a low-order/small-order public key can force the X25519 result to
|
|
35
|
+
// be all zeros — a value it knows in advance — which would let it choose the
|
|
36
|
+
// key material. The `break` here is safe (does not leak a secret): the peer's
|
|
37
|
+
// public key is public, so an early exit reveals nothing exploitable.
|
|
38
|
+
let allZero = true;
|
|
39
|
+
for (let i = 0; i < secret.length; i++) {
|
|
40
|
+
if (secret[i] !== 0) { allZero = false; break; }
|
|
41
|
+
}
|
|
42
|
+
if (allZero) {
|
|
43
|
+
throw new Error('X25519: all-zero shared secret — invalid peer public key (RFC 7748 §6.1)');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return secret;
|
|
32
47
|
}
|
|
33
48
|
|
|
34
49
|
/**
|