lemon-tls 0.7.0 → 0.10.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
@@ -373,10 +373,52 @@ function buildAad12(seqNum, recordType, plaintextLen) {
373
373
  }
374
374
 
375
375
  /**
376
- * Encrypt a TLS 1.2 GCM record fragment. Returns explicit_nonce(8) || ciphertext || tag(16).
376
+ * TLS 1.2 ChaCha20-Poly1305 (RFC 7905), shared by the three TLS 1.2 record
377
+ * functions below. Told apart from AES-GCM the way the DTLS 1.2 functions do
378
+ * it: by the write IV the key block gave the suite — 12 bytes for ChaCha20,
379
+ * a 4-byte salt for GCM. No explicit nonce on the wire; the nonce is the IV
380
+ * XOR the sequence number; the AAD is the same as GCM's.
381
+ *
382
+ * The TLS 1.2 functions handled GCM only. A ChaCha20 suite was negotiated —
383
+ * it is in the default offer, and clients without AES hardware prefer it —
384
+ * and then every record failed: the peer's Finished did not decrypt
385
+ * (bad_record_mac), and ours could not have been read either.
386
+ */
387
+ function chacha12Seal(pt, key, iv12, seqNum, recordType) {
388
+ const cipher = crypto.createCipheriv('chacha20-poly1305', key, getChachaNonce12(iv12, seqNum), { authTagLength: 16 });
389
+ cipher.setAAD(buildAad12(seqNum, recordType, pt.length), { plaintextLength: pt.length });
390
+ const ct = cipher.update(pt);
391
+ cipher.final();
392
+ return { ct: ct, tag: cipher.getAuthTag() };
393
+ }
394
+
395
+ function chacha12Open(fragment, key, iv12, seqNum, recordType) {
396
+ if (fragment.length < 16) throw new Error('TLS 1.2 ChaCha20 fragment too short');
397
+ const ct = fragment.subarray(0, fragment.length - 16);
398
+ const tag = fragment.subarray(fragment.length - 16);
399
+ const decipher = crypto.createDecipheriv('chacha20-poly1305', key, getChachaNonce12(iv12, seqNum), { authTagLength: 16 });
400
+ decipher.setAAD(buildAad12(seqNum, recordType, ct.length), { plaintextLength: ct.length });
401
+ decipher.setAuthTag(tag);
402
+ const pt = decipher.update(ct);
403
+ decipher.final();
404
+ return pt;
405
+ }
406
+
407
+ /**
408
+ * Encrypt a TLS 1.2 AEAD record fragment.
409
+ * AES-GCM (4-byte salt): explicit_nonce(8) || ciphertext || tag(16).
410
+ * ChaCha20-Poly1305 (12-byte IV): ciphertext || tag(16).
377
411
  * Perf: computes seq-bytes once for both explicit nonce and AAD (was computed twice).
378
412
  */
379
413
  function encrypt12(pt, key, salt4, seqNum, recordType) {
414
+ if (salt4.length === 12) {
415
+ const sealed = chacha12Seal(pt, key, salt4, seqNum, recordType);
416
+ const out12 = new Uint8Array(sealed.ct.length + 16);
417
+ out12.set(sealed.ct, 0);
418
+ out12.set(sealed.tag, sealed.ct.length);
419
+ return out12;
420
+ }
421
+
380
422
  // Shared big-endian seq encoding (used as both explicit_nonce and AAD[0..8])
381
423
  const hi = (seqNum / 0x100000000) | 0;
382
424
  const lo = seqNum >>> 0;
@@ -437,6 +479,23 @@ function encrypt12(pt, key, salt4, seqNum, recordType) {
437
479
  */
438
480
  function encryptCompleteRecord12(pt, key, salt4, seqNum, recordType, version) {
439
481
  const ptLen = pt.length;
482
+
483
+ // ChaCha20-Poly1305: header || ciphertext || tag — no explicit IV.
484
+ if (salt4.length === 12) {
485
+ const sealed = chacha12Seal(pt, key, salt4, seqNum, recordType);
486
+ const cBodyLen = ptLen + 16;
487
+ const cVer = version || 0x0303;
488
+ const cRec = Buffer.allocUnsafe(5 + cBodyLen);
489
+ cRec[0] = recordType & 0xff;
490
+ cRec[1] = (cVer >>> 8) & 0xff;
491
+ cRec[2] = cVer & 0xff;
492
+ cRec[3] = (cBodyLen >>> 8) & 0xff;
493
+ cRec[4] = cBodyLen & 0xff;
494
+ sealed.ct.copy(cRec, 5);
495
+ sealed.tag.copy(cRec, 5 + ptLen);
496
+ return cRec;
497
+ }
498
+
440
499
  // Record body: 8 explicit_iv + ptLen ciphertext + 16 tag
441
500
  const bodyLen = 8 + ptLen + 16;
442
501
  const ver = version || 0x0303;
@@ -493,11 +552,15 @@ function encryptCompleteRecord12(pt, key, salt4, seqNum, recordType, version) {
493
552
  }
494
553
 
495
554
  /**
496
- * Decrypt a TLS 1.2 GCM record fragment. Input: explicit_nonce(8) || ciphertext || tag(16).
555
+ * Decrypt a TLS 1.2 AEAD record fragment.
556
+ * AES-GCM input: explicit_nonce(8) || ciphertext || tag(16).
557
+ * ChaCha20-Poly1305 input: ciphertext || tag(16).
497
558
  * Perf: subarray (zero-copy views) instead of slice (copies); returns Node Buffer
498
559
  * (which IS a Uint8Array subclass) instead of re-copying to a plain Uint8Array.
499
560
  */
500
561
  function decrypt12(fragment, key, salt4, seqNum, recordType) {
562
+ if (salt4.length === 12) return chacha12Open(fragment, key, salt4, seqNum, recordType);
563
+
501
564
  if (fragment.length < 24) throw new Error('TLS 1.2 fragment too short');
502
565
 
503
566
  // Zero-copy views over the input
@@ -602,6 +665,21 @@ function readU48(buf, off) {
602
665
  }
603
666
 
604
667
  /** AES-ECB encrypt a single 16-byte block (for DTLS 1.3 record number encryption). */
668
+ // One AES-ECB cipher per sn_key, reused for every record's number mask
669
+ // (RFC 9147 §4.2.3) instead of creating one per record. ECB keeps no state
670
+ // between blocks, so update() on 16 bytes always returns exactly that block's
671
+ // encryption. Keyed by the key object, which lives as long as its epoch's keys.
672
+ var _snEcbCache = new WeakMap();
673
+ function _snKeyEcb(key) {
674
+ var c = _snEcbCache.get(key);
675
+ if (!c) {
676
+ c = crypto.createCipheriv(key.length === 32 ? 'aes-256-ecb' : 'aes-128-ecb', key, null);
677
+ c.setAutoPadding(false);
678
+ _snEcbCache.set(key, c);
679
+ }
680
+ return c;
681
+ }
682
+
605
683
  function aesEcbEncrypt(key, block) {
606
684
  let algo = key.length === 32 ? 'aes-256-ecb' : 'aes-128-ecb';
607
685
  let cipher = crypto.createCipheriv(algo, key, null);
@@ -722,8 +800,8 @@ function decryptDtls12(fragment, key, ivSalt, epoch, seq, type) {
722
800
  // ChaCha20-Poly1305: no explicit nonce; nonce = iv(12) XOR (epoch||seq).
723
801
  if (ivSalt.length === 12) {
724
802
  if (fragment.length < 16) throw new Error('DTLS 1.2 ChaCha fragment too short');
725
- let ct = fragment.slice(0, fragment.length - 16);
726
- let tag = fragment.slice(fragment.length - 16);
803
+ let ct = fragment.subarray(0, fragment.length - 16); // views: only read
804
+ let tag = fragment.subarray(fragment.length - 16);
727
805
  let seq64 = epoch * 0x1000000000000 + seq;
728
806
  let nonce = getChachaNonce12(ivSalt, seq64);
729
807
  let aad = buildDtlsAad12(epoch, seq, type, ct.length);
@@ -732,14 +810,16 @@ function decryptDtls12(fragment, key, ivSalt, epoch, seq, type) {
732
810
  decipher.setAuthTag(tag);
733
811
  let pt = decipher.update(ct);
734
812
  decipher.final();
735
- return new Uint8Array(pt);
813
+ // A view, not a copy: pt is a fresh Buffer that owns its whole
814
+ // ArrayBuffer and nothing else holds it. Same Uint8Array type as before.
815
+ return new Uint8Array(pt.buffer, pt.byteOffset, pt.byteLength);
736
816
  }
737
817
 
738
818
  // AES-GCM: strip the 8-byte explicit nonce.
739
819
  if (fragment.length < 24) throw new Error('DTLS 1.2 fragment too short');
740
- let explicit = fragment.slice(0, 8);
741
- let tag = fragment.slice(fragment.length - 16);
742
- let ct = fragment.slice(8, fragment.length - 16);
820
+ let explicit = fragment.subarray(0, 8); // views: only read
821
+ let tag = fragment.subarray(fragment.length - 16);
822
+ let ct = fragment.subarray(8, fragment.length - 16);
743
823
  let nonce = getNonce12(ivSalt, explicit);
744
824
  let aad = buildDtlsAad12(epoch, seq, type, ct.length);
745
825
  let algo = key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm';
@@ -748,13 +828,44 @@ function decryptDtls12(fragment, key, ivSalt, epoch, seq, type) {
748
828
  decipher.setAuthTag(tag);
749
829
  let pt = decipher.update(ct);
750
830
  decipher.final();
751
- return new Uint8Array(pt);
831
+ // A view, not a copy — see the ChaCha branch above.
832
+ return new Uint8Array(pt.buffer, pt.byteOffset, pt.byteLength);
752
833
  }
753
834
 
754
835
  /** Build complete encrypted DTLS 1.2 record (header + encrypted payload). */
755
836
  function buildEncryptedDtls12(type, epoch, seq, plaintext, keys) {
756
- let encrypted = encryptDtls12(plaintext, keys.key, keys.iv, epoch, seq, type);
757
- return buildDtlsPlaintext(type, epoch, seq, encrypted);
837
+ // Same bytes as buildDtlsPlaintext(type, epoch, seq, encryptDtls12(...)),
838
+ // written straight into the record instead of through two more buffers.
839
+ let key = keys.key, ivSalt = keys.iv;
840
+ let chacha = ivSalt.length === 12;
841
+ let explicitLen = chacha ? 0 : 8;
842
+ let fragLen = explicitLen + plaintext.length + 16;
843
+ let out = new Uint8Array(13 + fragLen);
844
+ let off = 0;
845
+ off = w_u8(out, off, type);
846
+ off = w_u16(out, off, 0xFEFD);
847
+ off = w_u16(out, off, epoch);
848
+ off = w_u48(out, off, seq);
849
+ off = w_u16(out, off, fragLen);
850
+ let aad = buildDtlsAad12(epoch, seq, type, plaintext.length);
851
+ let cipher;
852
+ if (chacha) {
853
+ let nonce = getChachaNonce12(ivSalt, epoch * 0x1000000000000 + seq);
854
+ cipher = crypto.createCipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
855
+ cipher.setAAD(aad, { plaintextLength: plaintext.length });
856
+ } else {
857
+ // AES-GCM: explicit nonce (= record seq) prepended.
858
+ let explicit = seqToBytes(seq);
859
+ out.set(explicit, off); off += 8;
860
+ cipher = crypto.createCipheriv(key.length === 16 ? 'aes-128-gcm' : 'aes-256-gcm', key, getNonce12(ivSalt, explicit));
861
+ cipher.setAAD(aad);
862
+ }
863
+ let ct = cipher.update(plaintext);
864
+ out.set(ct, off); off += ct.length;
865
+ let fin = cipher.final();
866
+ if (fin.length) { out.set(fin, off); off += fin.length; }
867
+ out.set(cipher.getAuthTag(), off);
868
+ return out;
758
869
  }
759
870
 
760
871
 
@@ -832,7 +943,7 @@ function maskRecordNumber(snKey, rnBytes, ciphertext, algo) {
832
943
  let c = crypto.createCipheriv('chacha20', snKey, iv);
833
944
  mask = new Uint8Array(c.update(Buffer.alloc(16)));
834
945
  } else {
835
- mask = aesEcbEncrypt(snKey, ciphertext.subarray(0, 16));
946
+ mask = _snKeyEcb(snKey).update(ciphertext.subarray(0, 16));
836
947
  }
837
948
 
838
949
  let out = new Uint8Array(rnBytes.length);
@@ -860,42 +971,43 @@ function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys) {
860
971
  let info = buildUnifiedHdr(epoch, true, true, false);
861
972
 
862
973
  // Plaintext record number
863
- let rn = new Uint8Array(2);
864
- rn[0] = (seq >>> 8) & 0xFF;
865
- rn[1] = seq & 0xFF;
974
+ let rn0 = (seq >>> 8) & 0xFF, rn1 = seq & 0xFF;
866
975
 
867
- // Inner plaintext: content + content_type
868
- let inner = new Uint8Array(plaintext.length + 1);
869
- inner.set(plaintext, 0);
870
- inner[plaintext.length] = innerType;
871
-
872
- let encLen = inner.length + 16;
976
+ // Inner plaintext = content || content_type. Fed to the cipher in two
977
+ // pieces rather than first copied into one buffer.
978
+ let innerLen = plaintext.length + 1;
979
+ let encLen = innerLen + 16;
873
980
 
874
981
  // AAD = header with PLAINTEXT record number (before RN encryption)
875
982
  let aad = new Uint8Array(5);
876
983
  aad[0] = info;
877
- aad[1] = rn[0];
878
- aad[2] = rn[1];
984
+ aad[1] = rn0;
985
+ aad[2] = rn1;
879
986
  aad[3] = (encLen >>> 8) & 0xFF;
880
987
  aad[4] = encLen & 0xFF;
881
988
 
882
989
  // Nonce (reuse TLS 1.3 nonce construction)
883
990
  let nonce = getNonce(keys.iv, seq);
884
991
 
885
- // AEAD encrypt
992
+ // AEAD encrypt, written straight into the record: info(1) + encrypted
993
+ // rn(2) + length(2) + ciphertext. (It used to pass through three buffers:
994
+ // inner plaintext, ciphertext+tag, then the record.)
886
995
  let cipher = crypto.createCipheriv(algo, keys.key, nonce,
887
996
  isChaCha ? { authTagLength: 16 } : undefined);
888
- cipher.setAAD(aad, isChaCha ? { plaintextLength: inner.length } : undefined);
889
- let ct = cipher.update(inner);
890
- cipher.final();
891
- let tag = cipher.getAuthTag();
892
-
893
- let ciphertext = new Uint8Array(ct.length + tag.length);
894
- ciphertext.set(new Uint8Array(ct), 0);
895
- ciphertext.set(new Uint8Array(tag), ct.length);
997
+ cipher.setAAD(aad, isChaCha ? { plaintextLength: innerLen } : undefined);
998
+ let record = new Uint8Array(5 + encLen);
999
+ let off = 5;
1000
+ let ct1 = cipher.update(plaintext);
1001
+ record.set(ct1, off); off += ct1.length;
1002
+ let ct2 = cipher.update(_contentTypeByte(innerType));
1003
+ record.set(ct2, off); off += ct2.length;
1004
+ let ct3 = cipher.final();
1005
+ if (ct3.length) { record.set(ct3, off); off += ct3.length; }
1006
+ record.set(cipher.getAuthTag(), off);
1007
+ let ciphertext = record.subarray(5);
896
1008
 
897
1009
  // Encrypt record number
898
- let encRn = maskRecordNumber(keys.snKey, rn, ciphertext, algo);
1010
+ let encRn = maskRecordNumber(keys.snKey, [rn0, rn1], ciphertext, algo);
899
1011
 
900
1012
  // ── DIAGNOSTIC (LEMON_DEBUG=1 / WEBRTC_DEBUG=1) ──────────────────────
901
1013
  // Everything a peer needs to PARSE this record, printed as we build it.
@@ -905,34 +1017,39 @@ function buildEncryptedDtls13(innerType, plaintext, seq, epoch, keys) {
905
1017
  // present), the sequence number before and after masking, and the
906
1018
  // declared length against the actual ciphertext size. If two records
907
1019
  // 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')) {
1020
+ // up here. (Flag read once at load: process.env is slow to read per record.)
1021
+ if (_REC_DEBUG) {
1022
+ try {
912
1023
  var _h = function (b) { return ('0' + (b & 0xFF).toString(16)).slice(-2); };
913
1024
  console.log('[lemon-rec] TX info=0x' + _h(info) +
914
1025
  ' [C=' + ((info >> 4) & 1) + ' S=' + ((info >> 3) & 1) +
915
1026
  ' L=' + ((info >> 2) & 1) + ' epoch=' + (info & 0x03) + ']' +
916
- ' seqPlain=' + _h(rn[0]) + _h(rn[1]) +
1027
+ ' seqPlain=' + _h(rn0) + _h(rn1) +
917
1028
  ' seqMasked=' + _h(encRn[0]) + _h(encRn[1]) +
918
1029
  ' declaredLen=' + ciphertext.length +
919
- ' aadLen=' + (aad ? aad.length : 'null') +
1030
+ ' aadLen=' + aad.length +
920
1031
  ' snKey=' + (keys.snKey ? 'set' : 'MISSING'));
921
- }
922
- } catch (_e) {}
1032
+ } catch (_e) {}
1033
+ }
923
1034
  // ─────────────────────────────────────────────────────────────────────
924
1035
 
925
- // Assemble: info(1) + encrypted_rn(2) + length(2) + ciphertext
926
- let record = new Uint8Array(5 + ciphertext.length);
927
1036
  record[0] = info;
928
1037
  record[1] = encRn[0];
929
1038
  record[2] = encRn[1];
930
1039
  record[3] = (ciphertext.length >>> 8) & 0xFF;
931
1040
  record[4] = ciphertext.length & 0xFF;
932
- record.set(ciphertext, 5);
933
1041
  return record;
934
1042
  }
935
1043
 
1044
+ var _REC_DEBUG = typeof process !== 'undefined' && process.env &&
1045
+ (process.env.LEMON_DEBUG === '1' || process.env.WEBRTC_DEBUG === '1');
1046
+
1047
+ // The one-byte content type appended to a DTLS 1.3 inner plaintext.
1048
+ var _CT_BYTES = [];
1049
+ function _contentTypeByte(t) {
1050
+ return _CT_BYTES[t] || (_CT_BYTES[t] = new Uint8Array([t]));
1051
+ }
1052
+
936
1053
  /**
937
1054
  * Reconstruct a full DTLS 1.3 record sequence number from its truncated wire
938
1055
  * encoding (RFC 9147 §4.2.2).
@@ -1027,7 +1144,9 @@ function decryptEncryptedDtls13(data, keys, fullEpoch) {
1027
1144
  }
1028
1145
 
1029
1146
  if (off + ctLen > data.length) return null;
1030
- let ciphertext = data.slice(off, off + ctLen);
1147
+ // A view: only read here (record-number mask, AEAD input); the plaintext
1148
+ // comes out of the decipher as a fresh buffer.
1149
+ let ciphertext = data.subarray(off, off + ctLen);
1031
1150
 
1032
1151
  // Resolve the AEAD before unmasking: the record-number mask is built with
1033
1152
  // the same primitive as the AEAD (AES-ECB vs ChaCha20), so `algo` is needed
@@ -1102,7 +1221,8 @@ function decryptEncryptedDtls13(data, keys, fullEpoch) {
1102
1221
  seq = acceptedSeq;
1103
1222
 
1104
1223
  {
1105
- let inner = parseInnerPlaintext(new Uint8Array(pt));
1224
+ // A view, not a copy: pt is the decipher's fresh Buffer, owned by no one else.
1225
+ let inner = parseInnerPlaintext(new Uint8Array(pt.buffer, pt.byteOffset, pt.byteLength));
1106
1226
 
1107
1227
  // RFC 9147 §4.5.1 anti-replay: only AFTER the tag verifies do we consult
1108
1228
  // and update the sliding window. Checking before deprotection would let a
@@ -1111,23 +1231,34 @@ function decryptEncryptedDtls13(data, keys, fullEpoch) {
1111
1231
  // next to maxReadSeq, because both are per-epoch receive state.
1112
1232
  // replayWindow: bitmask of the 64 seqs at or below maxReadSeq that have
1113
1233
  // been accepted. Bit 0 = maxReadSeq, bit k = maxReadSeq - k.
1234
+ // The 64-bit window is kept as two 32-bit numbers (replayLo = bits
1235
+ // 0..31, replayHi = 32..63): same window, no BigInt allocations per
1236
+ // record.
1114
1237
  if (keys.maxReadSeq === undefined || keys.maxReadSeq < 0) {
1115
1238
  // First accepted record in this epoch: seed the window with this seq as
1116
1239
  // the high-water mark (bit 0 set), so an immediate replay of it is
1117
1240
  // caught below rather than sailing through an unset window.
1118
1241
  keys.maxReadSeq = seq;
1119
- keys.replayWindow = 1n;
1242
+ keys.replayLo = 1; keys.replayHi = 0;
1120
1243
  } else if (seq > keys.maxReadSeq) {
1121
1244
  // Advance: shift the window up by the gap, then set bit 0.
1122
- let shift = BigInt(seq - keys.maxReadSeq);
1123
- keys.replayWindow = shift >= 64n ? 1n : ((keys.replayWindow << shift) | 1n) & ((1n << 64n) - 1n);
1245
+ let shift = seq - keys.maxReadSeq;
1246
+ let lo = keys.replayLo, hi = keys.replayHi;
1247
+ if (shift >= 64) { lo = 0; hi = 0; }
1248
+ else if (shift >= 32) { hi = (lo << (shift - 32)) >>> 0; lo = 0; }
1249
+ else { hi = ((hi << shift) | (lo >>> (32 - shift))) >>> 0; lo = (lo << shift) >>> 0; }
1250
+ keys.replayLo = (lo | 1) >>> 0; keys.replayHi = hi;
1124
1251
  keys.maxReadSeq = seq;
1125
1252
  } else {
1126
- let back = BigInt(keys.maxReadSeq - seq);
1127
- if (back >= 64n) return null; // too old — outside window
1128
- let bit = 1n << back;
1129
- if (keys.replayWindow & bit) return null; // already seen — replay
1130
- keys.replayWindow |= bit;
1253
+ let back = keys.maxReadSeq - seq;
1254
+ if (back >= 64) return null; // too old — outside window
1255
+ if (back < 32) {
1256
+ if ((keys.replayLo >>> back) & 1) return null; // already seen — replay
1257
+ keys.replayLo = (keys.replayLo | (1 << back)) >>> 0;
1258
+ } else {
1259
+ if ((keys.replayHi >>> (back - 32)) & 1) return null;
1260
+ keys.replayHi = (keys.replayHi | (1 << (back - 32))) >>> 0;
1261
+ }
1131
1262
  }
1132
1263
 
1133
1264
  return {
@@ -1225,19 +1356,23 @@ function parseDtlsDatagram(data, keysByEpoch) {
1225
1356
  let length = readU16(data, off + 11);
1226
1357
  if (off + 13 + length > data.length) break;
1227
1358
 
1228
- let payload = data.slice(off + 13, off + 13 + length);
1359
+ let payload = null;
1229
1360
  let encrypted = false;
1230
1361
 
1231
- // DTLS 1.2: decrypt if epoch > 0 and keys available
1362
+ // DTLS 1.2: decrypt if epoch > 0 and keys available. The ciphertext is
1363
+ // only read (the plaintext comes out of the decipher as a fresh
1364
+ // buffer), so it is passed as a view; a record kept as it is — not
1365
+ // decrypted — gets its own copy, as before.
1232
1366
  if (epoch > 0 && keysByEpoch && keysByEpoch[epoch]) {
1233
1367
  let keys = keysByEpoch[epoch];
1234
1368
  try {
1235
- payload = decryptDtls12(payload, keys.key, keys.iv, epoch, seq, type);
1369
+ payload = decryptDtls12(data.subarray(off + 13, off + 13 + length), keys.key, keys.iv, epoch, seq, type);
1236
1370
  encrypted = true;
1237
1371
  } catch(e) {
1238
1372
  // Decryption failed — return raw
1239
1373
  }
1240
1374
  }
1375
+ if (payload === null) payload = data.slice(off + 13, off + 13 + length);
1241
1376
 
1242
1377
  records.push({
1243
1378
  type: type,
@@ -60,17 +60,28 @@ function makeX509FromDerOrPem(buf) {
60
60
  }
61
61
 
62
62
  function makePrivateKeyFromDerOrPem(buf, passphrase) {
63
+ // node:crypto rejects an explicit `passphrase: null` with ERR_INVALID_ARG_VALUE
64
+ // — for an unencrypted key the property must be ABSENT, not present-and-null.
65
+ // Callers that forward an optional option straight through would otherwise
66
+ // break every unencrypted key, which is the overwhelmingly common case.
67
+ let pass = (passphrase === null || passphrase === undefined) ? undefined : passphrase;
68
+
63
69
  if (isPEM(buf)) {
64
- return crypto.createPrivateKey({ key: buf, format: 'pem', passphrase: passphrase });
70
+ let opts = { key: buf, format: 'pem' };
71
+ if (pass !== undefined) opts.passphrase = pass;
72
+ return crypto.createPrivateKey(opts);
65
73
  } else {
66
74
  let der = Buffer.from(buf), keyObj = null;
67
75
  try {
68
- keyObj = crypto.createPrivateKey({ key: der, format: 'der', type: 'pkcs8', passphrase: passphrase });
76
+ keyObj = crypto.createPrivateKey(Object.assign({ key: der, format: 'der', type: 'pkcs8' },
77
+ pass !== undefined ? { passphrase: pass } : {}));
69
78
  } catch (e1) {
70
79
  try {
71
- keyObj = crypto.createPrivateKey({ key: der, format: 'der', type: 'pkcs1', passphrase: passphrase });
80
+ keyObj = crypto.createPrivateKey(Object.assign({ key: der, format: 'der', type: 'pkcs1' },
81
+ pass !== undefined ? { passphrase: pass } : {}));
72
82
  } catch (e2) {
73
- keyObj = crypto.createPrivateKey({ key: der, format: 'der', type: 'sec1', passphrase: passphrase });
83
+ keyObj = crypto.createPrivateKey(Object.assign({ key: der, format: 'der', type: 'sec1' },
84
+ pass !== undefined ? { passphrase: pass } : {}));
74
85
  }
75
86
  }
76
87
  return keyObj;
@@ -106,6 +117,40 @@ function dedupeDerArray(arr) {
106
117
  function createSecureContext(options) {
107
118
  if (!options) options = {};
108
119
 
120
+ // Several key/certificate pairs — node:tls's arrays: `key` an array of
121
+ // keys (or { pem, passphrase }), `cert` an array of chains, one per key —
122
+ // for a server that holds certificates of more than one kind: an ML-DSA one
123
+ // for clients that ask for post-quantum authentication, an ECDSA or RSA one
124
+ // for everyone else. Each key is paired with the chain whose leaf carries
125
+ // its public key, in whatever order they were given; the first key's pair
126
+ // is the default, the others `alternates`. Which one a handshake presents
127
+ // is the client's signature_algorithms' to decide (tls_session).
128
+ if (Array.isArray(options.key) && options.key.length > 1) {
129
+ let certs = Array.isArray(options.cert) ? options.cert : [options.cert];
130
+ let contexts = options.key.map(function (k, i) {
131
+ let keyIn = (k && typeof k === 'object' && !Buffer.isBuffer(k) && !(k instanceof Uint8Array) && k.pem) ? k.pem : k;
132
+ let pass = (k && typeof k === 'object' && k.passphrase != null) ? k.passphrase : options.passphrase;
133
+ let pub = crypto.createPublicKey(makePrivateKeyFromDerOrPem(readMaybeFile(keyIn), pass)).export({ type: 'spki', format: 'der' });
134
+ let cert = null;
135
+ for (let j = 0; j < certs.length && cert === null; j++) {
136
+ let raw = readMaybeFile(certs[j]);
137
+ let first = isPEM(raw) ? splitPEMBlocks(raw.toString('utf8')).filter(function (b) { return b.type.indexOf('CERTIFICATE') !== -1; })[0] : { der: new Uint8Array(raw) };
138
+ if (!first) continue;
139
+ let leafPub = makeX509FromDerOrPem(first.der).publicKey.export({ type: 'spki', format: 'der' });
140
+ if (Buffer.compare(Buffer.from(leafPub), Buffer.from(pub)) === 0) cert = certs[j];
141
+ }
142
+ if (cert === null) throw new Error('createSecureContext: no certificate in `cert` for key ' + i);
143
+ return createSecureContext(Object.assign({}, options, { key: keyIn, passphrase: pass, cert: cert }));
144
+ });
145
+ let primary = contexts[0];
146
+ primary.alternates = contexts.slice(1).map(function (c) {
147
+ return { certificateChain: c.certificateChain, privateKey: c.privateKey, keyObj: c.keyObj, certObjs: c.certObjs };
148
+ });
149
+ return primary;
150
+ }
151
+ if (Array.isArray(options.key)) options = Object.assign({}, options, { key: options.key[0] && options.key[0].pem ? options.key[0].pem : options.key[0] });
152
+ if (Array.isArray(options.cert) && options.cert.length === 1) options = Object.assign({}, options, { cert: options.cert[0] });
153
+
109
154
  // --- תעודות (כולל שרשרת בתוך cert אם קיימת) ---
110
155
  let certBlocksDer = [];
111
156
  let certObjs = [];
@@ -141,6 +186,20 @@ function createSecureContext(options) {
141
186
  if (!privateKey) throw new Error('createSecureContext: missing private key.');
142
187
  }
143
188
 
189
+ // The key must be the certificate's — node:tls throws on this too
190
+ // (KEY_VALUES_MISMATCH). Unchecked, a renewal that paired the new key with
191
+ // the old certificate went in without a word, and every handshake after it
192
+ // failed at the client's signature check.
193
+ if (keyObj && certObjs.length > 0) {
194
+ let fromKey = crypto.createPublicKey(keyObj).export({ type: 'spki', format: 'der' });
195
+ let fromCert = certObjs[0].publicKey.export({ type: 'spki', format: 'der' });
196
+ if (Buffer.compare(Buffer.from(fromKey), Buffer.from(fromCert)) !== 0) {
197
+ let e = new Error('createSecureContext: the private key does not match the certificate');
198
+ e.code = 'ERR_OSSL_X509_KEY_VALUES_MISMATCH';
199
+ throw e;
200
+ }
201
+ }
202
+
144
203
  // --- CA (Trust store) ---
145
204
  let ca = normalizeCA(options.ca);
146
205
 
@@ -0,0 +1,157 @@
1
+ /**
2
+ * TLS Certificate Compression (RFC 8879), TLS 1.3 and DTLS 1.3.
3
+ *
4
+ * A certificate chain is most of what a server sends in its handshake —
5
+ * 3-5 KB for a public CA's chain, against a few hundred bytes for the rest.
6
+ * Compressed it is often half that or less: the certificates repeat names,
7
+ * extensions and OIDs. Over QUIC it decides how many round trips the
8
+ * handshake takes: before the client's address is validated a server may
9
+ * send three times what it received (RFC 9000 §8.1), ~3.6 KB for a 1200-byte
10
+ * Initial, and a chain that does not fit waits a round trip for the
11
+ * client's next packet. Browsers offer it (Chrome: brotli; Firefox: zlib,
12
+ * brotli, zstd).
13
+ *
14
+ * The client lists the algorithms it can decompress (compress_certificate,
15
+ * extension 27); the server, holding one it can compress with, sends
16
+ * CompressedCertificate (handshake type 25) in place of Certificate. The
17
+ * same works the other way for a client certificate, when the server's
18
+ * CertificateRequest carries the extension.
19
+ */
20
+
21
+ import zlib from 'node:zlib';
22
+ import crypto from 'node:crypto';
23
+
24
+ function brotliOptions(b) {
25
+ return { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11, [zlib.constants.BROTLI_PARAM_SIZE_HINT]: b.length } };
26
+ }
27
+
28
+ // RFC 8879 §3, and the IANA registry: zlib 1, brotli 2, zstd 3.
29
+ const ALGORITHMS = {
30
+ 1: { name: 'zlib',
31
+ compress: (b) => zlib.deflateSync(b, { level: 9 }),
32
+ compressAsync: (b, cb) => zlib.deflate(b, { level: 9 }, cb),
33
+ decompress: (b, max) => zlib.inflateSync(b, { maxOutputLength: max }) },
34
+ 2: { name: 'brotli',
35
+ compress: (b) => zlib.brotliCompressSync(b, brotliOptions(b)),
36
+ compressAsync: (b, cb) => zlib.brotliCompress(b, brotliOptions(b), cb),
37
+ decompress: (b, max) => zlib.brotliDecompressSync(b, { maxOutputLength: max }) },
38
+ };
39
+ // zstd where node:zlib has it (Node 22.15+, 23.8+).
40
+ if (typeof zlib.zstdCompressSync === 'function' && typeof zlib.zstdDecompressSync === 'function') {
41
+ ALGORITHMS[3] = { name: 'zstd',
42
+ compress: (b) => zlib.zstdCompressSync(b),
43
+ compressAsync: (b, cb) => zlib.zstdCompress(b, cb),
44
+ decompress: (b, max) => zlib.zstdDecompressSync(b, { maxOutputLength: max }) };
45
+ }
46
+
47
+ const BY_NAME = {};
48
+ for (const code in ALGORITHMS) BY_NAME[ALGORITHMS[code].name] = Number(code);
49
+
50
+ /** The algorithms this build can use, in its preference (best ratio first). */
51
+ function supported_cert_compression() {
52
+ return [2, 3, 1].filter((c) => !!ALGORITHMS[c]);
53
+ }
54
+
55
+ /**
56
+ * An option naming algorithms — ['brotli', 'zlib'], codes, or true (all) /
57
+ * false (none) — as codes this build supports. Throws on a name it does not
58
+ * know; one it knows but cannot run here (zstd on an older Node) is left out.
59
+ */
60
+ function parse_cert_compression(value, what) {
61
+ if (value === undefined || value === null) return null;
62
+ if (value === false) return [];
63
+ if (value === true) return supported_cert_compression();
64
+ if (!Array.isArray(value)) throw new TypeError((what || 'certificateCompression') + ' must be an array of algorithms (brotli, zlib, zstd), true or false');
65
+ const out = [];
66
+ for (const v of value) {
67
+ let code = typeof v === 'number' ? v : BY_NAME[String(v).toLowerCase()];
68
+ if (code === undefined && String(v).toLowerCase() === 'zstd') continue; // not in this Node
69
+ if (code === undefined || (code !== 1 && code !== 2 && code !== 3)) throw new TypeError((what || 'certificateCompression') + ': unknown algorithm ' + JSON.stringify(v));
70
+ if (ALGORITHMS[code] && out.indexOf(code) < 0) out.push(code);
71
+ }
72
+ return out;
73
+ }
74
+
75
+ // A chain is compressed once per algorithm: the server sends the same bytes
76
+ // on every handshake, and brotli at quality 11 is not cheap. Keyed by a hash
77
+ // of the uncompressed message; bounded.
78
+ const cache = new Map();
79
+ const CACHE_MAX = 64;
80
+
81
+ function cache_key(body, alg) {
82
+ return alg + ':' + crypto.createHash('sha256').update(body).digest('hex');
83
+ }
84
+ function cache_put(key, out) {
85
+ cache.set(key, out);
86
+ if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
87
+ }
88
+
89
+ /** `body` (an encoded Certificate message body) compressed with `alg`. */
90
+ function compress_certificate_body(body, alg) {
91
+ const a = ALGORITHMS[alg];
92
+ if (!a) return null;
93
+ const key = cache_key(body, alg);
94
+ let out = cache.get(key);
95
+ if (out) return out;
96
+ out = new Uint8Array(a.compress(Buffer.from(body)));
97
+ cache_put(key, out);
98
+ return out;
99
+ }
100
+
101
+ // The same, ahead of the first handshake and off the event loop (zlib's
102
+ // thread pool): what that handshake would have done synchronously — brotli
103
+ // at quality 11 is 4 ms for a 400-byte chain and 18 ms for a 9 KB one, the
104
+ // first time a server sends it, while that client waits — is in the cache
105
+ // when it comes. A chain already there, or on its way, is not done twice.
106
+ // callback() when every algorithm's is in; a failure leaves that one to
107
+ // the handshake, as before.
108
+ const pending = new Map(); // key → callbacks waiting on it
109
+ function precompress_certificate_body(body, algs, callback) {
110
+ let left = 1;
111
+ const done = () => { if (--left === 0 && callback) callback(); };
112
+ for (const alg of algs) {
113
+ const a = ALGORITHMS[alg];
114
+ if (!a || !a.compressAsync) continue;
115
+ const key = cache_key(body, alg);
116
+ if (cache.has(key)) continue;
117
+ left++;
118
+ if (pending.has(key)) { pending.get(key).push(done); continue; }
119
+ pending.set(key, [done]);
120
+ a.compressAsync(Buffer.from(body), (err, out) => {
121
+ if (!err && !cache.has(key)) cache_put(key, new Uint8Array(out));
122
+ const waiters = pending.get(key) || []; pending.delete(key);
123
+ for (const w of waiters) w();
124
+ });
125
+ }
126
+ done();
127
+ }
128
+
129
+ /**
130
+ * A CompressedCertificate's contents back to the Certificate message body,
131
+ * or null: an algorithm not ours, data that does not decompress, or a
132
+ * length other than the one declared (RFC 8879 §4: bad_certificate). Never
133
+ * more than the declared length is produced — nor more than `limit`.
134
+ */
135
+ function decompress_certificate_body(alg, data, uncompressedLength, limit) {
136
+ const a = ALGORITHMS[alg];
137
+ if (!a) return null;
138
+ if (!(uncompressedLength > 0) || (limit && uncompressedLength > limit)) return null;
139
+ let out;
140
+ try { out = a.decompress(Buffer.from(data), uncompressedLength); }
141
+ catch (e) { return null; }
142
+ if (out.length !== uncompressedLength) return null;
143
+ return new Uint8Array(out);
144
+ }
145
+
146
+ function cert_compression_name(alg) {
147
+ return ALGORITHMS[alg] ? ALGORITHMS[alg].name : String(alg);
148
+ }
149
+
150
+ export {
151
+ supported_cert_compression,
152
+ parse_cert_compression,
153
+ compress_certificate_body,
154
+ precompress_certificate_body,
155
+ decompress_certificate_body,
156
+ cert_compression_name,
157
+ };