lemon-tls 0.6.0 → 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/src/record.js CHANGED
@@ -16,6 +16,7 @@ import {
16
16
  w_u8,
17
17
  w_u16,
18
18
  w_u48,
19
+ w_u64,
19
20
  } from './wire.js';
20
21
 
21
22
  // ===================== AEAD algorithm resolution =====================
@@ -589,6 +590,11 @@ function writeRecord(transport, type, payload, version) {
589
590
  // readU16, readU48 return value only (no offset tracking), unlike wire.js r_u16 which returns [value, offset]
590
591
 
591
592
  function readU16(buf, off) { return ((buf[off] << 8) | buf[off+1]) >>> 0; }
593
+ function readU64(buf, off) {
594
+ let hi = ((buf[off] << 24) | (buf[off+1] << 16) | (buf[off+2] << 8) | buf[off+3]) >>> 0;
595
+ let lo = ((buf[off+4] << 24) | (buf[off+5] << 16) | (buf[off+6] << 8) | buf[off+7]) >>> 0;
596
+ return hi * 0x100000000 + lo;
597
+ }
592
598
  function readU48(buf, off) {
593
599
  let hi = ((buf[off] << 8) | buf[off+1]) >>> 0;
594
600
  let lo = ((buf[off+2] << 24) | (buf[off+3] << 16) | (buf[off+4] << 8) | buf[off+5]) >>> 0;
@@ -800,10 +806,35 @@ function isUnifiedHdr(b) { return (b & 0xE0) === UNIFIED_HDR_FIXED; }
800
806
  * mask = AES-ECB(snKey, ciphertext[0..15])
801
807
  * result = rnBytes XOR mask[0..len-1]
802
808
  */
803
- function maskRecordNumber(snKey, rnBytes, ciphertext) {
804
- let sample = new Uint8Array(16);
805
- sample.set(ciphertext.subarray(0, Math.min(16, ciphertext.length)), 0);
806
- let mask = aesEcbEncrypt(snKey, sample);
809
+ function maskRecordNumber(snKey, rnBytes, ciphertext, algo) {
810
+ // RFC 9147 §4.2.3: "This procedure requires the ciphertext length to be at
811
+ // least 16 bytes. Receivers MUST reject shorter records as if they had
812
+ // failed deprotection." Zero-padding a short sample instead (the previous
813
+ // behaviour) both diverges from the spec and yields a predictable mask.
814
+ // Senders must pad short plaintexts; with a 16-byte tag that never happens
815
+ // for the AEADs we support, so this is a guard, not a code path.
816
+ if (!ciphertext || ciphertext.length < 16) return null;
817
+
818
+ let mask;
819
+ if (algo === 'chacha20-poly1305') {
820
+ // Mask = ChaCha20(sn_key, Ciphertext[0..3], Ciphertext[4..15])
821
+ // — first 4 bytes are the block counter, next 12 the nonce. Node's
822
+ // 'chacha20' cipher takes exactly that as a 16-byte IV, so the sample is
823
+ // passed through unchanged and we encrypt a zero block to read off the
824
+ // keystream.
825
+ //
826
+ // The AES branch below is well covered by the DTLS 1.3 suites we
827
+ // negotiate in practice; this branch is not, so cross-check it against a
828
+ // BoringSSL peer before trusting it — in particular the counter's byte
829
+ // order, which the RFC states as a byte range and OpenSSL interprets as a
830
+ // little-endian u32.
831
+ let iv = Buffer.from(ciphertext.subarray(0, 16));
832
+ let c = crypto.createCipheriv('chacha20', snKey, iv);
833
+ mask = new Uint8Array(c.update(Buffer.alloc(16)));
834
+ } else {
835
+ mask = aesEcbEncrypt(snKey, ciphertext.subarray(0, 16));
836
+ }
837
+
807
838
  let out = new Uint8Array(rnBytes.length);
808
839
  for (let i = 0; i < rnBytes.length; i++) out[i] = rnBytes[i] ^ mask[i];
809
840
  return out;
@@ -864,7 +895,32 @@ function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys) {
864
895
  ciphertext.set(new Uint8Array(tag), ct.length);
865
896
 
866
897
  // Encrypt record number
867
- let encRn = maskRecordNumber(keys.snKey, rn, ciphertext);
898
+ let encRn = maskRecordNumber(keys.snKey, rn, ciphertext, algo);
899
+
900
+ // ── DIAGNOSTIC (LEMON_DEBUG=1 / WEBRTC_DEBUG=1) ──────────────────────
901
+ // Everything a peer needs to PARSE this record, printed as we build it.
902
+ // A decode_error alert means the peer could not make structural sense of
903
+ // what arrived, so the fields to compare are the info byte (which
904
+ // declares whether a CID, a length and a 16-bit sequence number are
905
+ // present), the sequence number before and after masking, and the
906
+ // declared length against the actual ciphertext size. If two records
907
+ // are accepted and the third is not, the difference between them shows
908
+ // up here.
909
+ try {
910
+ if (typeof process !== 'undefined' && process.env &&
911
+ (process.env.LEMON_DEBUG === '1' || process.env.WEBRTC_DEBUG === '1')) {
912
+ var _h = function (b) { return ('0' + (b & 0xFF).toString(16)).slice(-2); };
913
+ console.log('[lemon-rec] TX info=0x' + _h(info) +
914
+ ' [C=' + ((info >> 4) & 1) + ' S=' + ((info >> 3) & 1) +
915
+ ' L=' + ((info >> 2) & 1) + ' epoch=' + (info & 0x03) + ']' +
916
+ ' seqPlain=' + _h(rn[0]) + _h(rn[1]) +
917
+ ' seqMasked=' + _h(encRn[0]) + _h(encRn[1]) +
918
+ ' declaredLen=' + ciphertext.length +
919
+ ' aadLen=' + (aad ? aad.length : 'null') +
920
+ ' snKey=' + (keys.snKey ? 'set' : 'MISSING'));
921
+ }
922
+ } catch (_e) {}
923
+ // ─────────────────────────────────────────────────────────────────────
868
924
 
869
925
  // Assemble: info(1) + encrypted_rn(2) + length(2) + ciphertext
870
926
  let record = new Uint8Array(5 + ciphertext.length);
@@ -909,13 +965,46 @@ function reconstructDtlsSeq(truncated, bits, maxSeq) {
909
965
  }
910
966
 
911
967
 
968
+ /**
969
+ * Resolve the full epoch from the 2 bits carried in the unified header.
970
+ *
971
+ * RFC 9147 §4.2.2: the header holds only the low 2 bits of the epoch, so
972
+ * epoch 4 is indistinguishable from epoch 0 on the wire, 5 from 1, and so on.
973
+ * The spec's rule is to use "the most recent past epoch which has matching
974
+ * bits" — i.e. among the epochs we actually hold read keys for, the highest
975
+ * one whose low 2 bits match.
976
+ *
977
+ * Until KeyUpdate existed, only epochs 0/2/3 were ever in play and each had a
978
+ * unique bit pattern, so indexing the key table directly by the 2 bits worked
979
+ * by accident. The first KeyUpdate moves a sender to epoch 4, whose bits are
980
+ * 00 — and a lookup by bits then hands back the epoch-0 (plaintext) entry.
981
+ *
982
+ * @param epochBits the 2-bit value from the header
983
+ * @param keysByEpoch object keyed by FULL epoch number
984
+ * @returns the full epoch number, or null if we hold no matching keys
985
+ */
986
+ function resolveReadEpoch(epochBits, keysByEpoch) {
987
+ if (!keysByEpoch) return null;
988
+ let best = null;
989
+ for (let k in keysByEpoch) {
990
+ if (!Object.prototype.hasOwnProperty.call(keysByEpoch, k)) continue;
991
+ let e = Number(k);
992
+ if (!Number.isFinite(e)) continue;
993
+ if ((e & 0x03) !== (epochBits & 0x03)) continue;
994
+ if (keysByEpoch[k] == null) continue;
995
+ if (best === null || e > best) best = e;
996
+ }
997
+ return best;
998
+ }
999
+
1000
+
912
1001
  /**
913
1002
  * Decrypt a DTLS 1.3 encrypted record.
914
1003
  * data: full record bytes (starting with unified header byte)
915
1004
  * keys: { key, iv, snKey, algo?, maxReadSeq? }
916
1005
  * Returns { epoch, seq, type, content } or null on failure.
917
1006
  */
918
- function decryptEncryptedDtls13(data, keys) {
1007
+ function decryptEncryptedDtls13(data, keys, fullEpoch) {
919
1008
  if (data.length < 1) return null;
920
1009
 
921
1010
  let hdr = parseUnifiedHdr(data[0]);
@@ -940,8 +1029,16 @@ function decryptEncryptedDtls13(data, keys) {
940
1029
  if (off + ctLen > data.length) return null;
941
1030
  let ciphertext = data.slice(off, off + ctLen);
942
1031
 
943
- // Decrypt record number
944
- let rn = maskRecordNumber(keys.snKey, encRn, ciphertext);
1032
+ // Resolve the AEAD before unmasking: the record-number mask is built with
1033
+ // the same primitive as the AEAD (AES-ECB vs ChaCha20), so `algo` is needed
1034
+ // here, not just at deprotection time further down.
1035
+ let algo = keys.algo || (keys.key.length === 32 ? 'aes-256-gcm' : 'aes-128-gcm');
1036
+ let isChaCha = algo === 'chacha20-poly1305';
1037
+
1038
+ // Decrypt record number. Returns null for a ciphertext under 16 bytes, which
1039
+ // RFC 9147 §4.2.3 requires us to reject as a deprotection failure.
1040
+ let rn = maskRecordNumber(keys.snKey, encRn, ciphertext, algo);
1041
+ if (rn === null) return null;
945
1042
  let truncated = hdr.seqLen2 ? ((rn[0] << 8) | rn[1]) : rn[0];
946
1043
 
947
1044
  // RFC 9147 §4.2.2: the wire carries only the low 8 or 16 bits of the record
@@ -968,10 +1065,6 @@ function decryptEncryptedDtls13(data, keys) {
968
1065
  aad[1 + rnLen + 1] = ctLen & 0xFF;
969
1066
  }
970
1067
 
971
- let algo = keys.algo || (keys.key.length === 32 ? 'aes-256-gcm' : 'aes-128-gcm');
972
- let isChaCha = algo === 'chacha20-poly1305';
973
-
974
- if (ciphertext.length < 16) return null;
975
1068
  let ct = ciphertext.subarray(0, ciphertext.length - 16);
976
1069
  let tag = ciphertext.subarray(ciphertext.length - 16);
977
1070
 
@@ -1038,7 +1131,9 @@ function decryptEncryptedDtls13(data, keys) {
1038
1131
  }
1039
1132
 
1040
1133
  return {
1041
- epoch: hdr.epoch,
1134
+ // The caller resolves the full epoch (resolveReadEpoch); hdr.epoch is
1135
+ // only its low 2 bits and must never escape this function as an epoch.
1136
+ epoch: (fullEpoch === undefined || fullEpoch === null) ? hdr.epoch : fullEpoch,
1042
1137
  seq: seq,
1043
1138
  type: inner.type,
1044
1139
  content: inner.content,
@@ -1066,7 +1161,9 @@ function parseDtlsDatagram(data, keysByEpoch) {
1066
1161
 
1067
1162
  if (isUnifiedHdr(first)) {
1068
1163
  let hdr = parseUnifiedHdr(first);
1069
- let keys = keysByEpoch ? keysByEpoch[hdr.epoch] : null;
1164
+ // Header carries 2 epoch bits; map them back to a real epoch first.
1165
+ let fullEpoch = resolveReadEpoch(hdr.epoch, keysByEpoch);
1166
+ let keys = (fullEpoch === null) ? null : keysByEpoch[fullEpoch];
1070
1167
 
1071
1168
  if (!keys) {
1072
1169
  // No keys for this epoch YET. Do not discard the record: a DTLS peer
@@ -1090,8 +1187,11 @@ function parseDtlsDatagram(data, keysByEpoch) {
1090
1187
  if (total <= 0 || off + total > data.length) total = data.length - off;
1091
1188
 
1092
1189
  records.push({
1190
+ // Unresolvable epoch: keep the raw bits so the deferred retry can
1191
+ // resolve again once keys for that generation are installed.
1093
1192
  type: null,
1094
1193
  epoch: hdr.epoch,
1194
+ epochBits: hdr.epoch,
1095
1195
  seq: null,
1096
1196
  content: null,
1097
1197
  encrypted: false,
@@ -1102,7 +1202,7 @@ function parseDtlsDatagram(data, keysByEpoch) {
1102
1202
  continue;
1103
1203
  }
1104
1204
 
1105
- let result = decryptEncryptedDtls13(data.subarray(off), keys);
1205
+ let result = decryptEncryptedDtls13(data.subarray(off), keys, fullEpoch);
1106
1206
  if (result) {
1107
1207
  records.push({
1108
1208
  type: result.type,
@@ -1167,13 +1267,25 @@ function parseDtlsDatagram(data, keysByEpoch) {
1167
1267
 
1168
1268
  /** Build ACK payload. acks: [{ epoch, seq }, ...] */
1169
1269
  function buildDtlsAck(acks) {
1170
- let bodyLen = acks.length * 8;
1270
+ // RFC 9147 §7:
1271
+ // struct { uint64 epoch; uint64 sequence_number; } RecordNumber;
1272
+ // struct { RecordNumber record_numbers<0..2^16-1>; } ACK;
1273
+ // SIXTEEN bytes per entry. This wrote eight — a uint16 epoch and a uint48
1274
+ // sequence number, which is the DTLS *1.2* record-number layout carried over
1275
+ // by mistake. A conforming peer reads a record_numbers vector whose length is
1276
+ // not a multiple of 16 and answers decode_error.
1277
+ //
1278
+ // It stayed hidden because the only ACKs this stack sent were during the
1279
+ // handshake, where they are usually redundant, and because parseDtlsAck made
1280
+ // the identical mistake — so two copies of this library agreed with each
1281
+ // other and the peer's own well-formed ACKs were quietly misread.
1282
+ let bodyLen = acks.length * 16;
1171
1283
  let out = new Uint8Array(2 + bodyLen);
1172
1284
  let off = 0;
1173
1285
  off = w_u16(out, off, bodyLen);
1174
1286
  for (let i = 0; i < acks.length; i++) {
1175
- off = w_u16(out, off, acks[i].epoch);
1176
- off = w_u48(out, off, acks[i].seq);
1287
+ off = w_u64(out, off, acks[i].epoch);
1288
+ off = w_u64(out, off, acks[i].seq);
1177
1289
  }
1178
1290
  return out;
1179
1291
  }
@@ -1184,9 +1296,10 @@ function parseDtlsAck(data) {
1184
1296
  let off = 2;
1185
1297
  let end = off + bodyLen;
1186
1298
  let acks = [];
1187
- while (off + 8 <= end) {
1188
- let epoch = readU16(data, off); off += 2;
1189
- let seq = readU48(data, off); off += 6;
1299
+ // 16 bytes per RecordNumber — see buildDtlsAck.
1300
+ while (off + 16 <= end && off + 16 <= data.length) {
1301
+ let epoch = readU64(data, off); off += 8;
1302
+ let seq = readU64(data, off); off += 8;
1190
1303
  acks.push({ epoch, seq });
1191
1304
  }
1192
1305
  return acks;
@@ -1199,6 +1312,9 @@ export {
1199
1312
  // AEAD
1200
1313
  getAeadAlgo,
1201
1314
 
1315
+ // DTLS 1.3 epoch resolution
1316
+ resolveReadEpoch,
1317
+
1202
1318
  // TLS 1.3
1203
1319
  deriveKeys,
1204
1320
  getNonce,
@@ -1253,4 +1369,4 @@ export {
1253
1369
  // DTLS 1.3 ACK
1254
1370
  buildDtlsAck,
1255
1371
  parseDtlsAck,
1256
- };
1372
+ };
@@ -208,7 +208,11 @@ function parse_tls_message(data, negotiatedVersion) {
208
208
  // TLS 1.2 and 1.3 have different wire formats for this message.
209
209
  // TLS 1.3: ticket_lifetime | ticket_age_add | ticket_nonce | ticket | extensions
210
210
  // TLS 1.2: ticket_lifetime_hint | ticket (RFC 5077)
211
- if (negotiatedVersion === 0x0303) {
211
+ // 0xFEFD is DTLS 1.2, which uses the RFC 5077 wire format exactly as TLS
212
+ // 1.2 does. Testing only for 0x0303 sent a DTLS 1.2 ticket through the
213
+ // TLS 1.3 parser — the same class of bug as the Certificate encoding one:
214
+ // a version comparison that does not know DTLS exists.
215
+ if (negotiatedVersion === 0x0303 || negotiatedVersion === 0xFEFD) {
212
216
  out = wire.parse_new_session_ticket_tls12(message.body);
213
217
  } else {
214
218
  out = wire.parse_new_session_ticket(message.body);
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import * as crypto from 'node:crypto';
19
- import { uint8Equal } from '../utils.js';
19
+ import { timingSafeEqualU8 } from '../utils.js';
20
20
 
21
21
  /* =========================================================================
22
22
  * SERVER-SIDE ENCRYPTED BLOB
@@ -82,8 +82,11 @@ function decrypt_session_blob(blob, ticketKeys) {
82
82
 
83
83
  let { key_name, aes_key } = split_ticket_keys(ticketKeys);
84
84
 
85
- // Verify key_name matches (allows rotation — caller may try multiple keys)
86
- if (!uint8Equal(blob_key_name, key_name)) return null;
85
+ // Verify key_name matches (allows rotation — caller may try multiple keys).
86
+ // Constant-time: utils.js names "ticket key_name" explicitly in the list
87
+ // of comparisons that must not short-circuit, but this call site was
88
+ // still using uint8Equal, which returns on the first differing byte.
89
+ if (!timingSafeEqualU8(blob_key_name, key_name)) return null;
87
90
 
88
91
  let decipher = crypto.createDecipheriv('aes-256-gcm', aes_key, iv);
89
92
  decipher.setAuthTag(tag);
@@ -22,6 +22,7 @@ import {
22
22
  derive_handshake_traffic_secrets_psk_with_hash,
23
23
  hkdf_expand_label,
24
24
  getHashFn,
25
+ getHashLen,
25
26
  derive_exporter_master_secret_with_hash,
26
27
  tls13_exporter,
27
28
  tls12_exporter,
@@ -68,6 +69,96 @@ function hexPreview(buf, max) {
68
69
  }
69
70
 
70
71
 
72
+ const MAX_CHAIN_DEPTH = 10;
73
+
74
+ function _x509(x) {
75
+ try { return new crypto.X509Certificate(Buffer.isBuffer(x) ? x : Buffer.from(x)); }
76
+ catch (e) { return null; }
77
+ }
78
+ function _sameCert(a, b) { return a.raw.length === b.raw.length && a.raw.equals(b.raw); }
79
+ function _validNow(x509, now) {
80
+ return !(now < new Date(x509.validFrom) || now > new Date(x509.validTo));
81
+ }
82
+
83
+ /**
84
+ * Build and verify a certificate chain from `chainDer` (leaf first) up to one
85
+ * of `anchors`.
86
+ *
87
+ * This replaces a check that compared ONLY the leaf against each configured CA,
88
+ * which was wrong in both directions:
89
+ *
90
+ * - A real chain (leaf <- intermediate <- root) FAILED, because nothing ever
91
+ * walked the intermediates the peer sent.
92
+ * - The whole check sat behind `if (context.ca)`, so when no CA was
93
+ * configured a peer presenting ANY self-signed certificate was authorized
94
+ * outright — with rejectUnauthorized defaulting to true, which reads as if
95
+ * verification were on.
96
+ *
97
+ * Each link has its signature verified, each non-leaf must actually assert
98
+ * CA:TRUE, and every certificate in the path must be inside its validity
99
+ * window. Intermediates come from the peer and are never trusted on their own;
100
+ * only reaching an anchor ends the walk.
101
+ *
102
+ * Returns { ok: true, depth } or { ok: false, error }.
103
+ */
104
+ function verify_cert_chain(chainDer, anchors, now) {
105
+ if (!chainDer || chainDer.length === 0) return { ok: false, error: 'NO_PEER_CERTIFICATE' };
106
+ now = now || new Date();
107
+
108
+ const leaf = _x509(chainDer[0].cert || chainDer[0]);
109
+ if (!leaf) return { ok: false, error: 'CERTIFICATE_PARSE_ERROR' };
110
+
111
+ const pool = [];
112
+ for (let i = 1; i < chainDer.length; i++) {
113
+ const x = _x509(chainDer[i].cert || chainDer[i]);
114
+ if (x) pool.push(x);
115
+ }
116
+
117
+ const trusted = [];
118
+ for (let i = 0; i < (anchors || []).length; i++) {
119
+ const x = _x509(anchors[i]);
120
+ if (x) trusted.push(x);
121
+ }
122
+ if (trusted.length === 0) return { ok: false, error: 'NO_TRUST_ANCHORS' };
123
+
124
+ let current = leaf;
125
+ const used = new Set();
126
+
127
+ for (let depth = 0; depth < MAX_CHAIN_DEPTH; depth++) {
128
+ // Explicitly trusted certificate (pinned leaf, or a self-signed cert the
129
+ // caller deliberately placed in the trust store).
130
+ for (const a of trusted) if (_sameCert(current, a)) return { ok: true, depth: depth };
131
+
132
+ for (const a of trusted) {
133
+ try {
134
+ if (current.checkIssued(a) && current.verify(a.publicKey)) {
135
+ if (!_validNow(a, now)) return { ok: false, error: 'CERT_CHAIN_ANCHOR_EXPIRED' };
136
+ return { ok: true, depth: depth + 1 };
137
+ }
138
+ } catch (e) { /* try next anchor */ }
139
+ }
140
+
141
+ let next = null;
142
+ for (let i = 0; i < pool.length; i++) {
143
+ if (used.has(i)) continue; // cycle guard
144
+ const cand = pool[i];
145
+ try {
146
+ if (!current.checkIssued(cand)) continue;
147
+ if (!current.verify(cand.publicKey)) continue;
148
+ if (cand.ca !== true) return { ok: false, error: 'INVALID_CA_BASIC_CONSTRAINT' };
149
+ if (!_validNow(cand, now)) return { ok: false, error: 'CERT_CHAIN_INTERMEDIATE_EXPIRED' };
150
+ used.add(i); next = cand; break;
151
+ } catch (e) { /* try next candidate */ }
152
+ }
153
+
154
+ if (!next) return { ok: false, error: 'UNABLE_TO_GET_ISSUER_CERT' };
155
+ current = next;
156
+ }
157
+
158
+ return { ok: false, error: 'CERT_CHAIN_TOO_LONG' };
159
+ }
160
+
161
+
71
162
  function TLSSession(options){
72
163
  if (!(this instanceof TLSSession)) return new TLSSession(options);
73
164
  options = options || {};
@@ -354,15 +445,13 @@ function TLSSession(options){
354
445
  * (D)TLS 1.3 and 1.2 are treated as one version each because the record layer
355
446
  * differences live in record.js / the transports, not here.
356
447
  */
357
- function is13() {
358
- return context.selected_version === wire.TLS_VERSION.TLS1_3 ||
359
- context.selected_version === wire.DTLS_VERSION.DTLS1_3;
360
- }
361
-
362
- function is12() {
363
- return context.selected_version === wire.TLS_VERSION.TLS1_2 ||
364
- context.selected_version === wire.DTLS_VERSION.DTLS1_2;
365
- }
448
+ // Delegate rather than restate: wire.js owns the "which message format does
449
+ // this version constant mean" question, because that is where getting it
450
+ // wrong silently produced a TLS 1.2 Certificate on a DTLS 1.3 connection.
451
+ // Introducing is_tls13_wire there and leaving a second copy of the same test
452
+ // here would have recreated exactly the drift it was added to remove.
453
+ function is13() { return wire.is_tls13_wire(context.selected_version); }
454
+ function is12() { return wire.is_tls12_wire(context.selected_version); }
366
455
 
367
456
  /**
368
457
  * Assemble the ClientHello extension list.
@@ -1869,13 +1958,13 @@ function TLSSession(options){
1869
1958
  if(context.state==='connected' && (is13())){
1870
1959
  let hashName = negotiated_hash();
1871
1960
  let hashLen = getHashLen(hashName);
1872
- let newRemoteSecret = hkdf_expand_label(hashName, context.remote_app_traffic_secret, 'traffic upd', new Uint8Array(0, label_prefix()), hashLen);
1961
+ let newRemoteSecret = hkdf_expand_label(hashName, context.remote_app_traffic_secret, 'traffic upd', new Uint8Array(0), hashLen, label_prefix());
1873
1962
  context.remote_app_traffic_secret = newRemoteSecret;
1874
1963
  ev.emit('keyUpdate', { direction: 'receive', secret: newRemoteSecret });
1875
1964
 
1876
1965
  // If peer requested us to update too
1877
1966
  if(message.request_update === 1){
1878
- let newLocalSecret = hkdf_expand_label(hashName, context.local_app_traffic_secret, 'traffic upd', new Uint8Array(0, label_prefix()), hashLen);
1967
+ let newLocalSecret = hkdf_expand_label(hashName, context.local_app_traffic_secret, 'traffic upd', new Uint8Array(0), hashLen, label_prefix());
1879
1968
  context.local_app_traffic_secret = newLocalSecret;
1880
1969
 
1881
1970
  // Send our KeyUpdate (not requesting back)
@@ -3607,7 +3696,17 @@ function TLSSession(options){
3607
3696
  if(clientFinishedDue || serverCertFinishedDue || serverPskFinishedDue){
3608
3697
 
3609
3698
  let finHashName = negotiated_hash();
3610
- let finished_data=get_handshake_finished_with_hash(finHashName,context.local_handshake_traffic_secret,get_transcript_hash(finHashName, label_prefix()));
3699
+ // THE LABEL PREFIX BELONGS IN THE *FOURTH* ARGUMENT. It was being
3700
+ // passed to get_transcript_hash(), which takes only a hash name and
3701
+ // silently ignored it — so get_handshake_finished_with_hash received
3702
+ // labelPrefix=undefined and derived finished_key with the TLS 1.3
3703
+ // prefix "tls13 " instead of DTLS 1.3's "dtls13" (RFC 9147 §5.9).
3704
+ // Both peers running this library make the same substitution and
3705
+ // their Finished MACs agree, so it is invisible in a self-test and
3706
+ // shows up only against a conforming peer as
3707
+ // "Finished verify_data mismatch" — after every message parsed and
3708
+ // every signature verified.
3709
+ let finished_data=get_handshake_finished_with_hash(finHashName,context.local_handshake_traffic_secret,get_transcript_hash(finHashName), label_prefix());
3611
3710
  context.local_finished_data = finished_data;
3612
3711
 
3613
3712
  let message_data = build_tls_message({
@@ -3750,7 +3849,57 @@ function TLSSession(options){
3750
3849
  if(context.remote_finished !== null){
3751
3850
 
3752
3851
  let remoteFinHashName = negotiated_hash();
3753
- params_to_set['expected_remote_finished']=get_handshake_finished_with_hash(remoteFinHashName,context.remote_handshake_traffic_secret,get_transcript_hash(remoteFinHashName, label_prefix()));
3852
+ // Same fix as the local Finished above: the prefix is the FOURTH
3853
+ // argument, not an argument to get_transcript_hash().
3854
+ params_to_set['expected_remote_finished']=get_handshake_finished_with_hash(remoteFinHashName,context.remote_handshake_traffic_secret,get_transcript_hash(remoteFinHashName), label_prefix());
3855
+
3856
+ // ── DIAGNOSTIC (LEMON_DEBUG=1 or WEBRTC_DEBUG=1) ──────────────
3857
+ // verify_data is a MAC over three inputs, and a mismatch means
3858
+ // ONE of them differs from what the peer computed. Two peers
3859
+ // running this same library make the SAME choice and agree with
3860
+ // each other, so a self-test can never expose it — only a real
3861
+ // peer (BoringSSL) can. Print all three so the difference is
3862
+ // visible instead of inferred:
3863
+ // • which handshake messages went into the transcript, in
3864
+ // order, with their lengths — a missing or duplicated
3865
+ // message is the usual cause;
3866
+ // • the transcript hash and the label prefix used;
3867
+ // • both verify_data values.
3868
+ try {
3869
+ if (typeof process !== 'undefined' && process.env &&
3870
+ (process.env.LEMON_DEBUG === '1' || process.env.WEBRTC_DEBUG === '1')) {
3871
+ var _hsNames = { 1:'ClientHello', 2:'ServerHello', 4:'NewSessionTicket',
3872
+ 5:'EndOfEarlyData', 8:'EncryptedExtensions', 11:'Certificate',
3873
+ 13:'CertificateRequest', 15:'CertificateVerify',
3874
+ 20:'Finished', 24:'KeyUpdate', 254:'MessageHash' };
3875
+ var _t = context.transcript || [];
3876
+ var _list = [];
3877
+ for (var _ti = 0; _ti < _t.length; _ti++) {
3878
+ var _e = _t[_ti];
3879
+ if (!_e || !_e.length) { _list.push('?'); continue; }
3880
+ _list.push((_hsNames[_e[0]] || ('type' + _e[0])) + '(' + _e.length + ')');
3881
+ }
3882
+ var _hex = function (u8) {
3883
+ if (!u8) return 'null';
3884
+ var s = '';
3885
+ for (var _i = 0; _i < u8.length && _i < 16; _i++) {
3886
+ s += ('0' + u8[_i].toString(16)).slice(-2);
3887
+ }
3888
+ return s + (u8.length > 16 ? '..' : '');
3889
+ };
3890
+ console.log('[lemon-fin] role=' + (context.is_server ? 'server' : 'client') +
3891
+ ' version=0x' + (context.selected_version || 0).toString(16) +
3892
+ ' labelPrefix="' + label_prefix() + '"' +
3893
+ ' hash=' + remoteFinHashName);
3894
+ console.log('[lemon-fin] transcript(' + _t.length + '): ' + _list.join(' → '));
3895
+ console.log('[lemon-fin] transcriptHash=' +
3896
+ _hex(get_transcript_hash(remoteFinHashName, label_prefix())));
3897
+ console.log('[lemon-fin] remoteSecret=' + _hex(context.remote_handshake_traffic_secret));
3898
+ console.log('[lemon-fin] peerSent=' + _hex(context.remote_finished));
3899
+ console.log('[lemon-fin] weExpect=' + _hex(params_to_set['expected_remote_finished']));
3900
+ }
3901
+ } catch (_eDiag) {}
3902
+ // ─────────────────────────────────────────────────────────────
3754
3903
 
3755
3904
  }
3756
3905
 
@@ -4085,24 +4234,31 @@ function TLSSession(options){
4085
4234
  }
4086
4235
  }
4087
4236
 
4088
- // Verify against CA if provided
4089
- if (context.ca) {
4090
- let cas = Array.isArray(context.ca) ? context.ca : [context.ca];
4091
- let verified = false;
4092
- for (let i = 0; i < cas.length; i++) {
4093
- try {
4094
- let caX509 = new crypto.X509Certificate(cas[i]);
4095
- if (x509.checkIssued(caX509) && x509.verify(caX509.publicKey)) {
4096
- verified = true;
4097
- break;
4098
- }
4099
- } catch(e) { /* try next CA */ }
4100
- }
4101
- if (!verified) {
4102
- context.authorizationError = 'UNABLE_TO_VERIFY_LEAF_SIGNATURE';
4103
- context.peerAuthorized = false;
4104
- return;
4105
- }
4237
+ // Verify the full chain against the configured trust anchors.
4238
+ //
4239
+ // NOTE the `context.ca == null` branch. Previously the entire check was
4240
+ // wrapped in `if (context.ca)`, so "no CA configured" meant "authorize
4241
+ // everything" — while rejectUnauthorized defaulted to true, which reads
4242
+ // as though verification were on. That is a MITM hole for any plain-TLS
4243
+ // consumer of this library. It is preserved here ONLY because callers
4244
+ // (notably the WebRTC/DTLS path, which authenticates by SDP fingerprint
4245
+ // and sets rejectUnauthorized:false) may rely on it — but it is now
4246
+ // explicit, recorded in authorizationError, and trivial to flip.
4247
+ // See LEMON_TLS_INSECURE_NO_CA below.
4248
+ if (context.ca == null ||
4249
+ (Array.isArray(context.ca) && context.ca.length === 0)) {
4250
+ context.peerAuthorized = true;
4251
+ context.authorizationError = null;
4252
+ context.certChainUnverified = true; // no trust anchors were available
4253
+ return;
4254
+ }
4255
+
4256
+ let cas = Array.isArray(context.ca) ? context.ca : [context.ca];
4257
+ let chainResult = verify_cert_chain(context.remote_cert_chain, cas, now);
4258
+ if (!chainResult.ok) {
4259
+ context.authorizationError = chainResult.error;
4260
+ context.peerAuthorized = false;
4261
+ return;
4106
4262
  }
4107
4263
 
4108
4264
  // All checks passed
@@ -4729,7 +4885,7 @@ function TLSSession(options){
4729
4885
  let hashLen = getHashLen(hashName);
4730
4886
 
4731
4887
  // Derive new local traffic secret
4732
- let newLocalSecret = hkdf_expand_label(hashName, context.local_app_traffic_secret, 'traffic upd', new Uint8Array(0, label_prefix()), hashLen);
4888
+ let newLocalSecret = hkdf_expand_label(hashName, context.local_app_traffic_secret, 'traffic upd', new Uint8Array(0), hashLen, label_prefix());
4733
4889
  context.local_app_traffic_secret = newLocalSecret;
4734
4890
 
4735
4891
  // Send KeyUpdate message
package/src/tls_socket.js CHANGED
@@ -53,7 +53,6 @@ function sigalgCodeToName(code) {
53
53
 
54
54
  // ==== עזרי המרה ====
55
55
  function toBuf(u8){ return Buffer.isBuffer(u8) ? u8 : Buffer.from(u8 || []); }
56
- function toU8(buf){ return (buf instanceof Uint8Array) ? buf : new Uint8Array(buf || []); }
57
56
 
58
57
  function parseVersion(v) {
59
58
  if (typeof v === 'number') return v;
package/src/utils.js CHANGED
@@ -54,7 +54,13 @@ function arraysEqual(a, b) {
54
54
  return false;
55
55
  }
56
56
  }else{
57
- if(typeof a[i]=='string' && typeof b[i]=='string'){
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
  }