lemon-tls 0.5.5 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -7
- package/src/compat.js +10 -0
- package/src/crypto.js +160 -51
- package/src/dtls_session.js +636 -73
- package/src/dtls_socket.js +7 -0
- package/src/record.js +226 -43
- package/src/session/ecdh.js +124 -0
- package/src/session/message.js +40 -1
- package/src/session/signing.js +240 -47
- package/src/session/ticket.js +6 -3
- package/src/tls_session.js +2000 -454
- package/src/tls_socket.js +378 -40
- package/src/utils.js +7 -1
- package/src/wire.js +344 -18
package/src/dtls_socket.js
CHANGED
|
@@ -52,6 +52,7 @@ function DTLSSocket(udpSocket, options) {
|
|
|
52
52
|
mtu: options.mtu,
|
|
53
53
|
ticketKeys: options.ticketKeys,
|
|
54
54
|
useCookies: options.useCookies,
|
|
55
|
+
srtpProfiles: options.srtpProfiles,
|
|
55
56
|
});
|
|
56
57
|
|
|
57
58
|
// Wire session → UDP
|
|
@@ -136,6 +137,11 @@ function DTLSSocket(udpSocket, options) {
|
|
|
136
137
|
return session.exportKeyingMaterial(length, label, context_value);
|
|
137
138
|
};
|
|
138
139
|
|
|
140
|
+
/** Negotiated DTLS-SRTP protection profile (RFC 5764), or null. */
|
|
141
|
+
self.getSelectedSrtpProfile = function() {
|
|
142
|
+
return session.getSelectedSrtpProfile();
|
|
143
|
+
};
|
|
144
|
+
|
|
139
145
|
/** Peer hello extensions / single extension by numeric type. */
|
|
140
146
|
self.getRemoteExtensions = function() { return session.getRemoteExtensions(); };
|
|
141
147
|
self.getRemoteExtension = function(type) { return session.getRemoteExtension(type); };
|
|
@@ -190,6 +196,7 @@ function createDTLSServer(options, connectionListener) {
|
|
|
190
196
|
mtu: options.mtu,
|
|
191
197
|
ticketKeys: options.ticketKeys,
|
|
192
198
|
useCookies: options.useCookies,
|
|
199
|
+
srtpProfiles: options.srtpProfiles,
|
|
193
200
|
});
|
|
194
201
|
|
|
195
202
|
connections[key] = socket;
|
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 =====================
|
|
@@ -33,12 +34,12 @@ function getAeadAlgo(cipherSuite) {
|
|
|
33
34
|
// ===================== TLS 1.3 primitives =====================
|
|
34
35
|
|
|
35
36
|
/** Derive write key + IV from a TLS 1.3 traffic secret. */
|
|
36
|
-
function deriveKeys(trafficSecret, cipherSuite) {
|
|
37
|
+
function deriveKeys(trafficSecret, cipherSuite, labelPrefix) {
|
|
37
38
|
const empty = new Uint8Array(0);
|
|
38
39
|
let cs = TLS_CIPHER_SUITES[cipherSuite];
|
|
39
40
|
return {
|
|
40
|
-
key: hkdf_expand_label(cs.hash, trafficSecret, 'key', empty, cs.keylen),
|
|
41
|
-
iv: hkdf_expand_label(cs.hash, trafficSecret, 'iv', empty, 12)
|
|
41
|
+
key: hkdf_expand_label(cs.hash, trafficSecret, 'key', empty, cs.keylen, labelPrefix),
|
|
42
|
+
iv: hkdf_expand_label(cs.hash, trafficSecret, 'iv', empty, 12, labelPrefix)
|
|
42
43
|
};
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -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
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
-
//
|
|
944
|
-
|
|
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
|
|
@@ -956,7 +1053,9 @@ function decryptEncryptedDtls13(data, keys) {
|
|
|
956
1053
|
let seq = reconstructDtlsSeq(truncated, hdr.seqLen2 ? 16 : 8,
|
|
957
1054
|
(keys.maxReadSeq === undefined ? -1 : keys.maxReadSeq));
|
|
958
1055
|
|
|
959
|
-
// Rebuild AAD with plaintext RN
|
|
1056
|
+
// Rebuild AAD with plaintext RN. AAD depends only on the wire bytes (the
|
|
1057
|
+
// decrypted record number + length), NOT on the reconstructed full seq —
|
|
1058
|
+
// so it's computed once and reused across seq candidates below.
|
|
960
1059
|
let hdrLen = 1 + rnLen + (hdr.hasLength ? 2 : 0);
|
|
961
1060
|
let aad = new Uint8Array(hdrLen);
|
|
962
1061
|
aad[0] = data[0];
|
|
@@ -966,39 +1065,80 @@ function decryptEncryptedDtls13(data, keys) {
|
|
|
966
1065
|
aad[1 + rnLen + 1] = ctLen & 0xFF;
|
|
967
1066
|
}
|
|
968
1067
|
|
|
969
|
-
let nonce = getNonce(keys.iv, seq);
|
|
970
|
-
let algo = keys.algo || (keys.key.length === 32 ? 'aes-256-gcm' : 'aes-128-gcm');
|
|
971
|
-
let isChaCha = algo === 'chacha20-poly1305';
|
|
972
|
-
|
|
973
|
-
if (ciphertext.length < 16) return null;
|
|
974
1068
|
let ct = ciphertext.subarray(0, ciphertext.length - 16);
|
|
975
1069
|
let tag = ciphertext.subarray(ciphertext.length - 16);
|
|
976
1070
|
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1071
|
+
// The wire carries only the low bits of the record seq. reconstructDtlsSeq
|
|
1072
|
+
// returns the most-likely full value, but under large gaps or reordering
|
|
1073
|
+
// the true seq can be one window away in either direction. Only the NONCE
|
|
1074
|
+
// depends on the full seq (AAD does not), so try the neighbouring
|
|
1075
|
+
// candidates on AEAD failure before giving up — this is what lets
|
|
1076
|
+
// DTLS-Replay-TLS13 / -LargeGaps / -NonMonotonic deprotect instead of
|
|
1077
|
+
// failing as "bad record MAC". Candidates are deduped and non-negative.
|
|
1078
|
+
let window = hdr.seqLen2 ? 65536 : 256;
|
|
1079
|
+
let candidates = [seq];
|
|
1080
|
+
if (seq - window >= 0) candidates.push(seq - window);
|
|
1081
|
+
candidates.push(seq + window);
|
|
1082
|
+
|
|
1083
|
+
let pt = null, acceptedSeq = -1;
|
|
1084
|
+
for (let ci = 0; ci < candidates.length; ci++) {
|
|
1085
|
+
let cand = candidates[ci];
|
|
1086
|
+
let nonce = getNonce(keys.iv, cand);
|
|
1087
|
+
try {
|
|
1088
|
+
let decipher = crypto.createDecipheriv(algo, keys.key, nonce,
|
|
1089
|
+
isChaCha ? { authTagLength: 16 } : undefined);
|
|
1090
|
+
decipher.setAAD(aad, isChaCha ? { plaintextLength: ct.length } : undefined);
|
|
1091
|
+
decipher.setAuthTag(tag);
|
|
1092
|
+
let out = decipher.update(ct);
|
|
1093
|
+
decipher.final();
|
|
1094
|
+
pt = out; acceptedSeq = cand;
|
|
1095
|
+
break;
|
|
1096
|
+
} catch (e) {
|
|
1097
|
+
// try next candidate
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
if (pt === null) return null;
|
|
1102
|
+
seq = acceptedSeq;
|
|
984
1103
|
|
|
1104
|
+
{
|
|
985
1105
|
let inner = parseInnerPlaintext(new Uint8Array(pt));
|
|
986
1106
|
|
|
987
|
-
//
|
|
988
|
-
//
|
|
989
|
-
|
|
1107
|
+
// RFC 9147 §4.5.1 anti-replay: only AFTER the tag verifies do we consult
|
|
1108
|
+
// and update the sliding window. Checking before deprotection would let a
|
|
1109
|
+
// forged record number poison the window; checking after means only
|
|
1110
|
+
// genuine records move it. The window lives on the per-epoch key object,
|
|
1111
|
+
// next to maxReadSeq, because both are per-epoch receive state.
|
|
1112
|
+
// replayWindow: bitmask of the 64 seqs at or below maxReadSeq that have
|
|
1113
|
+
// been accepted. Bit 0 = maxReadSeq, bit k = maxReadSeq - k.
|
|
1114
|
+
if (keys.maxReadSeq === undefined || keys.maxReadSeq < 0) {
|
|
1115
|
+
// First accepted record in this epoch: seed the window with this seq as
|
|
1116
|
+
// the high-water mark (bit 0 set), so an immediate replay of it is
|
|
1117
|
+
// caught below rather than sailing through an unset window.
|
|
1118
|
+
keys.maxReadSeq = seq;
|
|
1119
|
+
keys.replayWindow = 1n;
|
|
1120
|
+
} else if (seq > keys.maxReadSeq) {
|
|
1121
|
+
// 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);
|
|
990
1124
|
keys.maxReadSeq = seq;
|
|
1125
|
+
} 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;
|
|
991
1131
|
}
|
|
992
1132
|
|
|
993
1133
|
return {
|
|
994
|
-
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,
|
|
995
1137
|
seq: seq,
|
|
996
1138
|
type: inner.type,
|
|
997
1139
|
content: inner.content,
|
|
998
1140
|
total_length: off + ctLen,
|
|
999
1141
|
};
|
|
1000
|
-
} catch (e) {
|
|
1001
|
-
return null;
|
|
1002
1142
|
}
|
|
1003
1143
|
}
|
|
1004
1144
|
|
|
@@ -1021,22 +1161,48 @@ function parseDtlsDatagram(data, keysByEpoch) {
|
|
|
1021
1161
|
|
|
1022
1162
|
if (isUnifiedHdr(first)) {
|
|
1023
1163
|
let hdr = parseUnifiedHdr(first);
|
|
1024
|
-
|
|
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];
|
|
1025
1167
|
|
|
1026
1168
|
if (!keys) {
|
|
1027
|
-
//
|
|
1169
|
+
// No keys for this epoch YET. Do not discard the record: a DTLS peer
|
|
1170
|
+
// legitimately packs its whole flight into one datagram, so the very
|
|
1171
|
+
// record that lets us derive these keys (the ServerHello) can sit
|
|
1172
|
+
// ahead of records encrypted under them. Throwing the rest away meant
|
|
1173
|
+
// the entire encrypted flight vanished and the handshake stalled until
|
|
1174
|
+
// the peer retransmitted — or forever, if it does not.
|
|
1175
|
+
//
|
|
1176
|
+
// Keep the raw bytes and mark the record deferred; the caller retries
|
|
1177
|
+
// it after processing each earlier record, once the keys exist. This
|
|
1178
|
+
// generalises the CCS-only re-decrypt that DTLS 1.2 already had.
|
|
1028
1179
|
let rnLen = hdr.seqLen2 ? 2 : 1;
|
|
1029
1180
|
let skip = 1 + rnLen;
|
|
1181
|
+
let total;
|
|
1030
1182
|
if (hdr.hasLength && off + skip + 2 <= data.length) {
|
|
1031
|
-
skip
|
|
1183
|
+
total = skip + 2 + readU16(data, off + skip);
|
|
1032
1184
|
} else {
|
|
1033
|
-
|
|
1185
|
+
total = data.length - off;
|
|
1034
1186
|
}
|
|
1035
|
-
off
|
|
1187
|
+
if (total <= 0 || off + total > data.length) total = data.length - off;
|
|
1188
|
+
|
|
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.
|
|
1192
|
+
type: null,
|
|
1193
|
+
epoch: hdr.epoch,
|
|
1194
|
+
epochBits: hdr.epoch,
|
|
1195
|
+
seq: null,
|
|
1196
|
+
content: null,
|
|
1197
|
+
encrypted: false,
|
|
1198
|
+
deferred: true,
|
|
1199
|
+
raw: data.subarray(off, off + total),
|
|
1200
|
+
});
|
|
1201
|
+
off += total;
|
|
1036
1202
|
continue;
|
|
1037
1203
|
}
|
|
1038
1204
|
|
|
1039
|
-
let result = decryptEncryptedDtls13(data.subarray(off), keys);
|
|
1205
|
+
let result = decryptEncryptedDtls13(data.subarray(off), keys, fullEpoch);
|
|
1040
1206
|
if (result) {
|
|
1041
1207
|
records.push({
|
|
1042
1208
|
type: result.type,
|
|
@@ -1101,13 +1267,25 @@ function parseDtlsDatagram(data, keysByEpoch) {
|
|
|
1101
1267
|
|
|
1102
1268
|
/** Build ACK payload. acks: [{ epoch, seq }, ...] */
|
|
1103
1269
|
function buildDtlsAck(acks) {
|
|
1104
|
-
|
|
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;
|
|
1105
1283
|
let out = new Uint8Array(2 + bodyLen);
|
|
1106
1284
|
let off = 0;
|
|
1107
1285
|
off = w_u16(out, off, bodyLen);
|
|
1108
1286
|
for (let i = 0; i < acks.length; i++) {
|
|
1109
|
-
off =
|
|
1110
|
-
off =
|
|
1287
|
+
off = w_u64(out, off, acks[i].epoch);
|
|
1288
|
+
off = w_u64(out, off, acks[i].seq);
|
|
1111
1289
|
}
|
|
1112
1290
|
return out;
|
|
1113
1291
|
}
|
|
@@ -1118,9 +1296,10 @@ function parseDtlsAck(data) {
|
|
|
1118
1296
|
let off = 2;
|
|
1119
1297
|
let end = off + bodyLen;
|
|
1120
1298
|
let acks = [];
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
let
|
|
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;
|
|
1124
1303
|
acks.push({ epoch, seq });
|
|
1125
1304
|
}
|
|
1126
1305
|
return acks;
|
|
@@ -1133,6 +1312,9 @@ export {
|
|
|
1133
1312
|
// AEAD
|
|
1134
1313
|
getAeadAlgo,
|
|
1135
1314
|
|
|
1315
|
+
// DTLS 1.3 epoch resolution
|
|
1316
|
+
resolveReadEpoch,
|
|
1317
|
+
|
|
1136
1318
|
// TLS 1.3
|
|
1137
1319
|
deriveKeys,
|
|
1138
1320
|
getNonce,
|
|
@@ -1174,6 +1356,7 @@ export {
|
|
|
1174
1356
|
// DTLS 1.3 unified header
|
|
1175
1357
|
buildUnifiedHdr,
|
|
1176
1358
|
parseUnifiedHdr,
|
|
1359
|
+
reconstructDtlsSeq,
|
|
1177
1360
|
maskRecordNumber,
|
|
1178
1361
|
|
|
1179
1362
|
// DTLS 1.3 encrypted records
|
|
@@ -1186,4 +1369,4 @@ export {
|
|
|
1186
1369
|
// DTLS 1.3 ACK
|
|
1187
1370
|
buildDtlsAck,
|
|
1188
1371
|
parseDtlsAck,
|
|
1189
|
-
};
|
|
1372
|
+
};
|
package/src/session/ecdh.js
CHANGED
|
@@ -90,7 +90,131 @@ function p384_get_shared_secret(localPrivateRaw, remotePublicRaw) {
|
|
|
90
90
|
return new Uint8Array(ecdh.computeSecret(Buffer.from(remotePublicRaw)));
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Named groups this module actually implements, in preference order.
|
|
95
|
+
*
|
|
96
|
+
* SINGLE SOURCE OF TRUTH: consumers (TLSSession defaults, TLSSocket options,
|
|
97
|
+
* DTLSSession) must derive their advertised supported_groups from this list
|
|
98
|
+
* rather than hardcoding their own. Previously three call sites carried three
|
|
99
|
+
* different hardcoded lists — DTLS advertised P-384 while TLS did not, even
|
|
100
|
+
* though the code below has supported it all along, so a peer offering only
|
|
101
|
+
* P-384 got "no curve supported by both" from a stack that could do P-384.
|
|
102
|
+
*
|
|
103
|
+
* Adding a curve should be a change to THIS file only.
|
|
104
|
+
*/
|
|
105
|
+
const NAMED_GROUP = {
|
|
106
|
+
X25519: 0x001d,
|
|
107
|
+
SECP256R1: 0x0017,
|
|
108
|
+
SECP384R1: 0x0018,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const SUPPORTED_GROUPS = [
|
|
112
|
+
NAMED_GROUP.X25519, // preferred: fastest, no point validation pitfalls
|
|
113
|
+
NAMED_GROUP.SECP256R1,
|
|
114
|
+
NAMED_GROUP.SECP384R1,
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
function is_supported_group(group) {
|
|
118
|
+
return SUPPORTED_GROUPS.indexOf(group) >= 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Generate a keypair for any supported group.
|
|
123
|
+
* Returns { private_key, public_key } or null for an unsupported group.
|
|
124
|
+
*/
|
|
125
|
+
function generate_keypair(group) {
|
|
126
|
+
if (group === NAMED_GROUP.X25519) {
|
|
127
|
+
let priv = new Uint8Array(crypto.randomBytes(32));
|
|
128
|
+
return { private_key: priv, public_key: x25519_get_public_key(priv) };
|
|
129
|
+
}
|
|
130
|
+
if (group === NAMED_GROUP.SECP256R1) return p256_generate_keypair();
|
|
131
|
+
if (group === NAMED_GROUP.SECP384R1) return p384_generate_keypair();
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Validate a peer's key_exchange value for `group` and normalize it to the
|
|
137
|
+
* form the primitives below expect.
|
|
138
|
+
*
|
|
139
|
+
* Point-format knowledge belongs HERE, next to the curve implementations —
|
|
140
|
+
* not in the handshake state machine, which only needs to know "valid or not"
|
|
141
|
+
* so it can decide whether to abort. Throws on anything malformed; the caller
|
|
142
|
+
* converts that into an illegal_parameter alert.
|
|
143
|
+
*/
|
|
144
|
+
function normalize_peer_public_key(group, pub) {
|
|
145
|
+
if (!pub || pub.length === 0) throw new Error('empty key_exchange');
|
|
146
|
+
|
|
147
|
+
if (group === NAMED_GROUP.X25519) {
|
|
148
|
+
if (pub.length !== 32) {
|
|
149
|
+
throw new Error('X25519 key_exchange must be 32 bytes, got ' + pub.length);
|
|
150
|
+
}
|
|
151
|
+
return pub;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let coordLen = (group === NAMED_GROUP.SECP256R1) ? 32
|
|
155
|
+
: (group === NAMED_GROUP.SECP384R1) ? 48 : 0;
|
|
156
|
+
if (coordLen === 0) throw new Error('unsupported group 0x' + (group >>> 0).toString(16));
|
|
157
|
+
|
|
158
|
+
// RFC 8446 §4.2.8.2: for the secp*r1 groups the key_exchange is an
|
|
159
|
+
// UNCOMPRESSED point — 0x04 || X || Y — and nothing else. The length is
|
|
160
|
+
// therefore exact.
|
|
161
|
+
//
|
|
162
|
+
// This used to also accept a bare X || Y (no 0x04) and re-prefix it, as a
|
|
163
|
+
// tolerance for TLS 1.2 peers that strip the prefix. That tolerance is a
|
|
164
|
+
// security hole, not a kindness: truncating a valid 65-byte P-256 point by
|
|
165
|
+
// one byte yields exactly 64 bytes, which the tolerance then reinterpreted
|
|
166
|
+
// as a DIFFERENT, prefix-less point and happily fed to the ECDH. The same
|
|
167
|
+
// arithmetic applies to P-384 (97 → 96). Malformed input must be rejected,
|
|
168
|
+
// not silently reinterpreted.
|
|
169
|
+
let expected = 1 + 2 * coordLen;
|
|
170
|
+
|
|
171
|
+
if (pub.length !== expected) {
|
|
172
|
+
// Name the two shapes we know we are refusing, so the failure is
|
|
173
|
+
// diagnosable rather than just "bad length".
|
|
174
|
+
if (pub.length === 2 * coordLen) {
|
|
175
|
+
throw new Error('key_exchange is missing the 0x04 uncompressed-point prefix (got ' +
|
|
176
|
+
pub.length + ' bytes, expected ' + expected + ')');
|
|
177
|
+
}
|
|
178
|
+
if (pub.length === 1 + coordLen && (pub[0] === 0x02 || pub[0] === 0x03)) {
|
|
179
|
+
throw new Error('compressed points are not permitted; key_exchange must be uncompressed (RFC 8446 §4.2.8.2)');
|
|
180
|
+
}
|
|
181
|
+
throw new Error('key_exchange must be a ' + expected + '-byte uncompressed point, got ' + pub.length);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (pub[0] !== 0x04) {
|
|
185
|
+
throw new Error('key_exchange must start with the 0x04 uncompressed-point marker, got 0x' +
|
|
186
|
+
pub[0].toString(16));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Whether the point actually lies on the curve is checked by the ECDH
|
|
190
|
+
// primitive below (node throws ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY), which
|
|
191
|
+
// happens before any secret is produced — so an off-curve point can never
|
|
192
|
+
// yield key material.
|
|
193
|
+
return pub;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Compute the ECDHE shared secret for any supported group.
|
|
198
|
+
* Validates the peer's public key first; throws on invalid input (including
|
|
199
|
+
* the RFC 7748 §6.1 all-zero X25519 result and node's own on-curve checks).
|
|
200
|
+
*/
|
|
201
|
+
function get_shared_secret(group, localPrivateRaw, remotePublicRaw) {
|
|
202
|
+
let pub = normalize_peer_public_key(group, remotePublicRaw);
|
|
203
|
+
|
|
204
|
+
if (group === NAMED_GROUP.X25519) return x25519_get_shared_secret(localPrivateRaw, pub);
|
|
205
|
+
if (group === NAMED_GROUP.SECP256R1) return p256_get_shared_secret(localPrivateRaw, pub);
|
|
206
|
+
if (group === NAMED_GROUP.SECP384R1) return p384_get_shared_secret(localPrivateRaw, pub);
|
|
207
|
+
|
|
208
|
+
throw new Error('unsupported group 0x' + (group >>> 0).toString(16));
|
|
209
|
+
}
|
|
210
|
+
|
|
93
211
|
export {
|
|
212
|
+
NAMED_GROUP,
|
|
213
|
+
SUPPORTED_GROUPS,
|
|
214
|
+
is_supported_group,
|
|
215
|
+
generate_keypair,
|
|
216
|
+
normalize_peer_public_key,
|
|
217
|
+
get_shared_secret,
|
|
94
218
|
x25519_get_public_key,
|
|
95
219
|
x25519_get_shared_secret,
|
|
96
220
|
p256_generate_keypair,
|
package/src/session/message.js
CHANGED
|
@@ -193,6 +193,10 @@ function parse_tls_message(data, negotiatedVersion) {
|
|
|
193
193
|
out.type = 'certificate';
|
|
194
194
|
|
|
195
195
|
} else if (message.type == wire.TLS_MESSAGE_TYPE.CERTIFICATE_VERIFY) {
|
|
196
|
+
// Decode scheme + signature so the session can actually VERIFY it.
|
|
197
|
+
// Previously only the raw body was handed up and no caller ever checked
|
|
198
|
+
// the signature — every peer signature was accepted unconditionally.
|
|
199
|
+
out = wire.parse_certificate_verify(message.body);
|
|
196
200
|
out.type = 'certificate_verify';
|
|
197
201
|
out.body = message.body;
|
|
198
202
|
|
|
@@ -204,7 +208,11 @@ function parse_tls_message(data, negotiatedVersion) {
|
|
|
204
208
|
// TLS 1.2 and 1.3 have different wire formats for this message.
|
|
205
209
|
// TLS 1.3: ticket_lifetime | ticket_age_add | ticket_nonce | ticket | extensions
|
|
206
210
|
// TLS 1.2: ticket_lifetime_hint | ticket (RFC 5077)
|
|
207
|
-
|
|
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) {
|
|
208
216
|
out = wire.parse_new_session_ticket_tls12(message.body);
|
|
209
217
|
} else {
|
|
210
218
|
out = wire.parse_new_session_ticket(message.body);
|
|
@@ -218,6 +226,37 @@ function parse_tls_message(data, negotiatedVersion) {
|
|
|
218
226
|
} else if (message.type == wire.TLS_MESSAGE_TYPE.CERTIFICATE_REQUEST) {
|
|
219
227
|
out = wire.parse_certificate_request(message.body);
|
|
220
228
|
out.type = 'certificate_request';
|
|
229
|
+
|
|
230
|
+
// Surface the requested signature algorithms as a plain field.
|
|
231
|
+
//
|
|
232
|
+
// In TLS 1.3 they arrive inside the extensions vector (RFC 8446 §4.3.2);
|
|
233
|
+
// in TLS 1.2 the decoder already returns them as `signature_algorithms`
|
|
234
|
+
// (RFC 5246 §7.4.4). The session asks for one name, so normalise here —
|
|
235
|
+
// exactly what normalize_hello does for the hello messages. Without this
|
|
236
|
+
// the 1.3 list was silently undefined and the client fell back to signing
|
|
237
|
+
// with its OWN preference, ignoring what the server actually asked for;
|
|
238
|
+
// a server that requested a specific algorithm then rejects the signature
|
|
239
|
+
// as an unsupported one.
|
|
240
|
+
if (out.signature_algorithms === undefined && Array.isArray(out.extensions)) {
|
|
241
|
+
for (let i = 0; i < out.extensions.length; i++) {
|
|
242
|
+
let e = out.extensions[i];
|
|
243
|
+
if (e && e.type === wire.TLS_EXT.SIGNATURE_ALGORITHMS && Array.isArray(e.value)) {
|
|
244
|
+
out.signature_algorithms = e.value;
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
} else {
|
|
251
|
+
// No silent fall-through. Returning a typeless object here made every
|
|
252
|
+
// unrecognized handshake type vanish: the session's dispatch chain matched
|
|
253
|
+
// nothing and simply returned, so a peer could stream unknown (or
|
|
254
|
+
// version-illegal, e.g. HelloRequest in TLS 1.3) messages indefinitely with
|
|
255
|
+
// no alert and no state change. Name the message instead and let the
|
|
256
|
+
// session decide — which it can only do if it is told.
|
|
257
|
+
out.type = 'unknown_handshake';
|
|
258
|
+
out.handshake_type = message.type;
|
|
259
|
+
out.body = message.body;
|
|
221
260
|
}
|
|
222
261
|
|
|
223
262
|
return out;
|