lemon-tls 0.4.1 → 0.5.5
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 +5 -3
- package/src/compat.js +30 -17
- package/src/crypto.js +92 -2
- package/src/dtls_session.js +487 -56
- 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 +224 -32
- package/src/tls_socket.js +44 -1
- package/src/utils.js +28 -1
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
|
/**
|
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
|
|
|
@@ -581,6 +602,28 @@ function TLSSession(options){
|
|
|
581
602
|
add_remote_key_groups: message.key_groups || []
|
|
582
603
|
});
|
|
583
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
|
+
|
|
584
627
|
// Server: TLS 1.2 resumption detection (only makes sense from ClientHello).
|
|
585
628
|
// This runs BEFORE set_context's reactive loop picks a version, so we mark state
|
|
586
629
|
// but defer the decision. The reactive loop will check tls12_abbreviated once
|
|
@@ -763,6 +806,26 @@ function TLSSession(options){
|
|
|
763
806
|
|
|
764
807
|
pushTranscript(data);
|
|
765
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
|
+
|
|
766
829
|
set_context({
|
|
767
830
|
remote_supported_groups: message.supported_groups || [],
|
|
768
831
|
});
|
|
@@ -1187,6 +1250,12 @@ function TLSSession(options){
|
|
|
1187
1250
|
}
|
|
1188
1251
|
}
|
|
1189
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
|
+
|
|
1190
1259
|
if('tls13_master_secret' in options){
|
|
1191
1260
|
if(context.tls13_master_secret==null && options.tls13_master_secret!==null){
|
|
1192
1261
|
context.tls13_master_secret=options.tls13_master_secret;
|
|
@@ -1586,6 +1655,20 @@ function TLSSession(options){
|
|
|
1586
1655
|
|
|
1587
1656
|
if(context.local_random==null){
|
|
1588
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
|
+
}
|
|
1589
1672
|
}
|
|
1590
1673
|
|
|
1591
1674
|
let build_message_params=null;
|
|
@@ -1650,6 +1733,19 @@ function TLSSession(options){
|
|
|
1650
1733
|
ext_list.push({ type: 'SESSION_TICKET', value: new Uint8Array(0) });
|
|
1651
1734
|
}
|
|
1652
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
|
+
|
|
1653
1749
|
// Session ID: for abbreviated (resumed) handshake, MUST echo client's session_id
|
|
1654
1750
|
// (RFC 5077 §3.4). For TLS 1.2 full handshake, generate a fresh 32-byte session_id
|
|
1655
1751
|
// (matches OpenSSL/Node.js behavior for middlebox compatibility).
|
|
@@ -2367,10 +2463,18 @@ function TLSSession(options){
|
|
|
2367
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)){
|
|
2368
2464
|
|
|
2369
2465
|
let appHashName = TLS_CIPHER_SUITES[context.selected_cipher_suite].hash;
|
|
2370
|
-
let
|
|
2466
|
+
let appTranscriptHash = get_transcript_hash(appHashName);
|
|
2467
|
+
let result2 = derive_app_traffic_secrets_with_hash(appHashName, context.base_secret, appTranscriptHash);
|
|
2371
2468
|
|
|
2372
2469
|
// Save master_secret for resumption before clearing
|
|
2373
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);
|
|
2374
2478
|
params_to_set['base_secret']=null;
|
|
2375
2479
|
|
|
2376
2480
|
if (context.isServer === true) {
|
|
@@ -2426,7 +2530,9 @@ function TLSSession(options){
|
|
|
2426
2530
|
//compare finished to expected...
|
|
2427
2531
|
if(context.remote_finished_ok==false && context.remote_finished!==null && context.expected_remote_finished!==null){
|
|
2428
2532
|
|
|
2429
|
-
|
|
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){
|
|
2430
2536
|
|
|
2431
2537
|
let message_data = build_tls_message({
|
|
2432
2538
|
type: 'finished',
|
|
@@ -2761,27 +2867,45 @@ function TLSSession(options){
|
|
|
2761
2867
|
];
|
|
2762
2868
|
context.local_supported_signature_algorithms = ch_sigalgs; // for CH2 reuse
|
|
2763
2869
|
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2870
|
+
// supported_versions (43) and key_share (51) are the TLS 1.3
|
|
2871
|
+
// negotiation machinery (RFC 8446 §4.1.2, §4.2.8). Offer them only
|
|
2872
|
+
// when 1.3 is actually in the offered version set — a pure 1.2
|
|
2873
|
+
// hello must look like a 1.2 hello, exactly as OpenSSL/BoringSSL
|
|
2874
|
+
// behave with max_version=1.2. Previously both were emitted
|
|
2875
|
+
// unconditionally, so even a maxVersion:'DTLSv1.2' profile (the
|
|
2876
|
+
// WebRTC default) advertised a single-entry supported_versions and
|
|
2877
|
+
// a dangling key_share: spec-legal (receivers MUST ignore unknown
|
|
2878
|
+
// extensions, RFC 5246 §7.4.1.4) but non-canonical — and fatal to
|
|
2879
|
+
// at least one deployed stack (webrtc-dtls ≤0.7 hard-errors on any
|
|
2880
|
+
// extension outside its known set, then poisons its replay window
|
|
2881
|
+
// so no retransmission can ever recover the handshake).
|
|
2882
|
+
let offers13 = context.local_supported_versions.some(function (v) {
|
|
2883
|
+
return v === wire.TLS_VERSION.TLS1_3 || v === wire.DTLS_VERSION.DTLS1_3;
|
|
2884
|
+
});
|
|
2885
|
+
let extensions = [];
|
|
2886
|
+
if (offers13) {
|
|
2887
|
+
extensions.push({
|
|
2888
|
+
type: 'SUPPORTED_VERSIONS',
|
|
2767
2889
|
value: context.local_supported_versions
|
|
2768
|
-
}
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2890
|
+
});
|
|
2891
|
+
}
|
|
2892
|
+
extensions.push({
|
|
2893
|
+
type: 'SUPPORTED_GROUPS',
|
|
2894
|
+
value: context.local_supported_groups
|
|
2895
|
+
});
|
|
2896
|
+
if (offers13) {
|
|
2897
|
+
extensions.push({
|
|
2898
|
+
type: 'KEY_SHARE',
|
|
2775
2899
|
value: [{
|
|
2776
|
-
group: 0x001d,
|
|
2777
|
-
key_exchange: public_key
|
|
2900
|
+
group: 0x001d,
|
|
2901
|
+
key_exchange: public_key
|
|
2778
2902
|
}]
|
|
2779
|
-
}
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
extensions.push({
|
|
2906
|
+
type: 'SIGNATURE_ALGORITHMS',
|
|
2907
|
+
value: ch_sigalgs
|
|
2908
|
+
});
|
|
2785
2909
|
if (!tls13Only) {
|
|
2786
2910
|
// TLS 1.2 compatibility — meaningless (and suspicious to strict
|
|
2787
2911
|
// stacks) in a 1.3-only/QUIC hello
|
|
@@ -3070,12 +3194,80 @@ function TLSSession(options){
|
|
|
3070
3194
|
};
|
|
3071
3195
|
},
|
|
3072
3196
|
|
|
3197
|
+
/**
|
|
3198
|
+
* Export keying material (RFC 5705 for TLS 1.2, RFC 8446 §7.5 for
|
|
3199
|
+
* TLS 1.3). Mirrors Node's tls.TLSSocket#exportKeyingMaterial.
|
|
3200
|
+
*
|
|
3201
|
+
* DTLS-SRTP (RFC 5764) usage:
|
|
3202
|
+
* session.exportKeyingMaterial(len, 'EXTRACTOR-dtls_srtp')
|
|
3203
|
+
* with NO context argument. Note the TLS 1.2 subtlety: "no context"
|
|
3204
|
+
* and "empty context" produce different output — omit the argument
|
|
3205
|
+
* (or pass null) for the RFC 5764 form.
|
|
3206
|
+
*
|
|
3207
|
+
* Returns a Buffer of `length` bytes, or null before the secrets
|
|
3208
|
+
* are available (handshake not far enough along).
|
|
3209
|
+
*
|
|
3210
|
+
* NOTE: the previous implementation was TLS 1.3-only AND derived from
|
|
3211
|
+
* the app traffic secret in a single HKDF stage — which is not the
|
|
3212
|
+
* RFC 8446 §7.5 construction and would not interoperate with
|
|
3213
|
+
* OpenSSL/BoringSSL exporters. Both issues are fixed here; if anything
|
|
3214
|
+
* consumed the old output, its derived values will change.
|
|
3215
|
+
*
|
|
3216
|
+
* @param {number} length
|
|
3217
|
+
* @param {string} label
|
|
3218
|
+
* @param {Uint8Array|Buffer|null} [context_value]
|
|
3219
|
+
* @returns {Buffer|null}
|
|
3220
|
+
*/
|
|
3073
3221
|
exportKeyingMaterial: function(length, label, context_value){
|
|
3074
|
-
if (!context.
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3222
|
+
if (!context.selected_cipher_suite || !length || !label) return null;
|
|
3223
|
+
var suite = TLS_CIPHER_SUITES[context.selected_cipher_suite];
|
|
3224
|
+
if (!suite) return null;
|
|
3225
|
+
var is13 = (context.selected_version === wire.TLS_VERSION.TLS1_3 ||
|
|
3226
|
+
context.selected_version === wire.DTLS_VERSION.DTLS1_3);
|
|
3227
|
+
|
|
3228
|
+
if (is13) {
|
|
3229
|
+
// RFC 8446 §7.5 — two-stage derivation from exporter_master_secret.
|
|
3230
|
+
if (!context.exporter_master_secret) return null;
|
|
3231
|
+
return Buffer.from(tls13_exporter(
|
|
3232
|
+
suite.hash, context.exporter_master_secret, label,
|
|
3233
|
+
(context_value != null) ? context_value : null, length
|
|
3234
|
+
));
|
|
3235
|
+
}
|
|
3236
|
+
|
|
3237
|
+
// TLS 1.2 — RFC 5705 over the master secret + hello randoms.
|
|
3238
|
+
// client_random comes first in the seed regardless of our role.
|
|
3239
|
+
if (!context.base_secret || !context.local_random || !context.remote_random) return null;
|
|
3240
|
+
var clientRandom = context.isServer ? context.remote_random : context.local_random;
|
|
3241
|
+
var serverRandom = context.isServer ? context.local_random : context.remote_random;
|
|
3242
|
+
return Buffer.from(tls12_exporter(
|
|
3243
|
+
suite.hash, context.base_secret, label,
|
|
3244
|
+
clientRandom, serverRandom,
|
|
3245
|
+
(context_value != null) ? context_value : null, length
|
|
3246
|
+
));
|
|
3247
|
+
},
|
|
3248
|
+
|
|
3249
|
+
/**
|
|
3250
|
+
* All extensions the peer sent in its hello (ClientHello when we're
|
|
3251
|
+
* the server, ServerHello when we're the client), as parsed by
|
|
3252
|
+
* wire.parse_extensions: [{ type, name, data, value }].
|
|
3253
|
+
* `data` is the raw extension payload (Uint8Array); `value` is the
|
|
3254
|
+
* decoded form for known extensions, null for unknown ones.
|
|
3255
|
+
*/
|
|
3256
|
+
getRemoteExtensions: function(){
|
|
3257
|
+
return context.remote_extensions ? context.remote_extensions.slice() : [];
|
|
3258
|
+
},
|
|
3259
|
+
|
|
3260
|
+
/**
|
|
3261
|
+
* Look up a single peer extension by its numeric type
|
|
3262
|
+
* (e.g. 14 = use_srtp, 0x39 = QUIC transport parameters).
|
|
3263
|
+
* Returns the { type, name, data, value } entry, or null.
|
|
3264
|
+
*/
|
|
3265
|
+
getRemoteExtension: function(type){
|
|
3266
|
+
var list = context.remote_extensions || [];
|
|
3267
|
+
for (var i = 0; i < list.length; i++) {
|
|
3268
|
+
if (list[i] && list[i].type === type) return list[i];
|
|
3269
|
+
}
|
|
3270
|
+
return null;
|
|
3079
3271
|
},
|
|
3080
3272
|
|
|
3081
3273
|
/** Returns the local Finished verify_data (Buffer), or null. */
|