lemon-tls 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +253 -20
- package/index.d.ts +94 -6
- package/index.js +126 -0
- package/package.json +1 -1
- package/src/compat.js +316 -45
- package/src/crypto.js +19 -0
- package/src/dtls_session.js +62 -6
- package/src/dtls_socket.js +32 -0
- package/src/node_options.js +138 -0
- package/src/record.js +72 -5
- package/src/secure_context.js +49 -4
- package/src/session/cert_compression.js +157 -0
- package/src/session/ecdh.js +237 -10
- package/src/session/ech.js +715 -0
- package/src/session/hpke.js +414 -0
- package/src/session/message.js +39 -4
- package/src/session/signing.js +56 -3
- package/src/session/ticket.js +38 -25
- package/src/tls_session.js +2825 -195
- package/src/tls_socket.js +839 -148
- package/src/utils.js +68 -1
- package/src/wire.js +148 -9
package/src/dtls_session.js
CHANGED
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
} from './record.js';
|
|
68
68
|
|
|
69
69
|
import { timingSafeEqualU8 } from './utils.js';
|
|
70
|
-
import { SUPPORTED_GROUPS as ECDH_SUPPORTED_GROUPS } from './session/ecdh.js';
|
|
70
|
+
import { SUPPORTED_GROUPS as ECDH_SUPPORTED_GROUPS, SERVER_DEFAULT_GROUPS } from './session/ecdh.js';
|
|
71
71
|
import { default_signature_schemes } from './session/signing.js';
|
|
72
72
|
|
|
73
73
|
|
|
@@ -194,6 +194,11 @@ function DTLSSession(options) {
|
|
|
194
194
|
servername: options.servername,
|
|
195
195
|
SNICallback: options.SNICallback,
|
|
196
196
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
197
|
+
checkServerIdentity: options.checkServerIdentity,
|
|
198
|
+
// DTLS peers in WebRTC present self-signed certificates by design and are
|
|
199
|
+
// authenticated by the SDP fingerprint, not by a CA chain. Falling back to
|
|
200
|
+
// the platform trust store here would mark every such peer unauthorized.
|
|
201
|
+
useDefaultCA: options.useDefaultCA === true,
|
|
197
202
|
ca: options.ca,
|
|
198
203
|
noTickets: options.noTickets,
|
|
199
204
|
requestCert: options.requestCert,
|
|
@@ -331,6 +336,21 @@ function DTLSSession(options) {
|
|
|
331
336
|
if (options.groups) tlsSetup.local_supported_groups = options.groups;
|
|
332
337
|
if (options.alpnProtocols) tlsSetup.local_supported_alpns = options.alpnProtocols;
|
|
333
338
|
|
|
339
|
+
// Default key-exchange groups, BOTH roles. With DTLS 1.3 offered: the
|
|
340
|
+
// hybrid post-quantum group first (X25519MLKEM768, when node:crypto has
|
|
341
|
+
// ML-KEM — see ecdh.js), then the classical ones. In WebRTC either peer can
|
|
342
|
+
// end up the DTLS client, and harvest-now-decrypt-later applies to media
|
|
343
|
+
// too (Chrome rolls PQ DTLS out from 149). A client leading with the hybrid
|
|
344
|
+
// group sends a classical share beside it (tls_session), so a classical
|
|
345
|
+
// server costs nothing extra. DTLS 1.2 only: classical — a KEM does not fit
|
|
346
|
+
// its key exchange, and the server skips KEM groups there anyway.
|
|
347
|
+
if (!tlsSetup.local_supported_groups) {
|
|
348
|
+
// (ECDH_SUPPORTED_GROUPS leads with the hybrid too now; tls_session leaves
|
|
349
|
+
// a KEM group out of a hello that does not offer 1.3.)
|
|
350
|
+
tlsSetup.local_supported_groups = (versions.indexOf(DTLS_VERSION.DTLS1_3) >= 0
|
|
351
|
+
? SERVER_DEFAULT_GROUPS : ECDH_SUPPORTED_GROUPS).slice();
|
|
352
|
+
}
|
|
353
|
+
|
|
334
354
|
// Server defaults — TLSSession client auto-populates these, but server expects them set externally
|
|
335
355
|
if (isServer) {
|
|
336
356
|
if (!tlsSetup.local_supported_cipher_suites) {
|
|
@@ -340,16 +360,20 @@ function DTLSSession(options) {
|
|
|
340
360
|
versions.indexOf(DTLS_VERSION.DTLS1_2) >= 0
|
|
341
361
|
);
|
|
342
362
|
}
|
|
343
|
-
|
|
344
|
-
tlsSetup.local_supported_groups = ECDH_SUPPORTED_GROUPS.slice(); // whatever ecdh.js implements
|
|
345
|
-
}
|
|
363
|
+
// (key-exchange groups: set above, for both roles)
|
|
346
364
|
// signing.js owns the scheme list; DTLS spans 1.2+1.3 so include PKCS#1.
|
|
347
365
|
tlsSetup.local_supported_signature_algorithms = default_signature_schemes();
|
|
348
366
|
}
|
|
349
367
|
|
|
350
|
-
// Server cert/key
|
|
368
|
+
// Server cert/key.
|
|
369
|
+
//
|
|
370
|
+
// The whole options object goes in, not just key+cert: createSecureContext
|
|
371
|
+
// also reads passphrase, ca, ciphers, sigalgs, ecdhCurve, honorCipherOrder,
|
|
372
|
+
// pfx and ocsp, and picking out two fields silently dropped all of them —
|
|
373
|
+
// an encrypted private key could never be decrypted here no matter what the
|
|
374
|
+
// caller passed. Same defect the TLS paths carried.
|
|
351
375
|
if (options.cert && options.key) {
|
|
352
|
-
let sctx = createSecureContext(
|
|
376
|
+
let sctx = createSecureContext(options);
|
|
353
377
|
tlsSetup.local_cert_chain = sctx.certificateChain;
|
|
354
378
|
tlsSetup.cert_private_key = sctx.privateKey;
|
|
355
379
|
}
|
|
@@ -1020,6 +1044,20 @@ function DTLSSession(options) {
|
|
|
1020
1044
|
}
|
|
1021
1045
|
|
|
1022
1046
|
function processRecord(record) {
|
|
1047
|
+
// A record in an epoch above 0 that did not decrypt is not the peer's
|
|
1048
|
+
// (forged, corrupted, or a late packet from a previous session on the
|
|
1049
|
+
// same address) — RFC 6347 §4.1.2.7 / RFC 9147 §4.5.2: discard it
|
|
1050
|
+
// silently. Its content is still ciphertext: as an ALERT its second byte
|
|
1051
|
+
// is random, which emitted 'error' and failed the whole connection; as
|
|
1052
|
+
// APPLICATION_DATA it went up to SCTP as data.
|
|
1053
|
+
if (record.epoch > 0 && !record.encrypted) return;
|
|
1054
|
+
// Once connected, everything the peer sends is protected. A plaintext
|
|
1055
|
+
// (epoch 0) alert or application data at that point is unauthenticated —
|
|
1056
|
+
// anyone able to send a UDP packet from the peer's address could close
|
|
1057
|
+
// the connection with a one-record close_notify, or inject data.
|
|
1058
|
+
if (ctx.state === 'connected' && !record.encrypted &&
|
|
1059
|
+
(record.type === CT.ALERT || record.type === CT.APPLICATION_DATA)) return;
|
|
1060
|
+
|
|
1023
1061
|
if (record.type === CT.HANDSHAKE) {
|
|
1024
1062
|
// An epoch>0 record that could not be decrypted (keys not yet
|
|
1025
1063
|
// installed, AEAD failure) must NOT be fed to the handshake
|
|
@@ -1841,6 +1879,24 @@ function DTLSSession(options) {
|
|
|
1841
1879
|
|
|
1842
1880
|
/** Peer certificate. */
|
|
1843
1881
|
getPeerCertificate: function() { return tls.getPeerCertificate(); },
|
|
1882
|
+
|
|
1883
|
+
/** TLS fingerprints (see TLSSession): JA4, JA5 of the ClientHello, JA4S
|
|
1884
|
+
* of the ServerHello. The transport character resolves to 'd' on its
|
|
1885
|
+
* own — the inner TLSSession reads the 0xFE high byte off the hello's
|
|
1886
|
+
* legacy_version — so no `protocol` override is needed for plain DTLS. */
|
|
1887
|
+
getJA4: function(opts) { return tls.getJA4(opts); },
|
|
1888
|
+
getJA5: function(opts) { return tls.getJA5(opts); },
|
|
1889
|
+
getJA4S: function(opts) { return tls.getJA4S(opts); },
|
|
1890
|
+
|
|
1891
|
+
/** Set the key and certificate for this connection (Node parity). */
|
|
1892
|
+
setKeyCert: function(ctxOrOpts) { return tls.setKeyCert(ctxOrOpts); },
|
|
1893
|
+
|
|
1894
|
+
/** Finished verify_data, ours and the peer's (RFC 5929 channel binding). */
|
|
1895
|
+
getFinished: function() { return tls.getFinished(); },
|
|
1896
|
+
getPeerFinished: function() { return tls.getPeerFinished(); },
|
|
1897
|
+
|
|
1898
|
+
/** Whether this connection resumed an earlier session. */
|
|
1899
|
+
isSessionReused: function() { return !!tls.isResumed; },
|
|
1844
1900
|
};
|
|
1845
1901
|
|
|
1846
1902
|
for (let k in api) {
|
package/src/dtls_socket.js
CHANGED
|
@@ -44,12 +44,14 @@ function DTLSSocket(udpSocket, options) {
|
|
|
44
44
|
cert: options.cert,
|
|
45
45
|
key: options.key,
|
|
46
46
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
47
|
+
checkServerIdentity: options.checkServerIdentity,
|
|
47
48
|
requestCert: options.requestCert,
|
|
48
49
|
ca: options.ca,
|
|
49
50
|
alpnProtocols: options.alpnProtocols,
|
|
50
51
|
minVersion: options.minVersion,
|
|
51
52
|
maxVersion: options.maxVersion,
|
|
52
53
|
mtu: options.mtu,
|
|
54
|
+
groups: options.groups,
|
|
53
55
|
ticketKeys: options.ticketKeys,
|
|
54
56
|
useCookies: options.useCookies,
|
|
55
57
|
srtpProfiles: options.srtpProfiles,
|
|
@@ -130,6 +132,33 @@ function DTLSSocket(udpSocket, options) {
|
|
|
130
132
|
self.getALPN = function() { return session.getALPN(); };
|
|
131
133
|
self.getPeerCertificate = function() { return session.getPeerCertificate(); };
|
|
132
134
|
|
|
135
|
+
/** TLS fingerprints, { hash, raw } or null (see TLSSession): JA4 and JA5
|
|
136
|
+
* of the ClientHello, JA4S of the ServerHello — in either role. */
|
|
137
|
+
self.getJA4 = function(opts) { return session.getJA4(opts); };
|
|
138
|
+
self.getJA5 = function(opts) { return session.getJA5(opts); };
|
|
139
|
+
self.getJA4S = function(opts) { return session.getJA4S(opts); };
|
|
140
|
+
|
|
141
|
+
/** Set the key and certificate for this connection (Node parity). */
|
|
142
|
+
self.setKeyCert = function(ctxOrOpts) {
|
|
143
|
+
if (session.setKeyCert) session.setKeyCert(ctxOrOpts);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/** Finished verify_data — the tls-unique channel binding of RFC 5929.
|
|
147
|
+
* DTLS-SRTP callers use this alongside exportKeyingMaterial(). */
|
|
148
|
+
self.getFinished = function() {
|
|
149
|
+
let v = session.getFinished ? session.getFinished() : null;
|
|
150
|
+
return v == null ? undefined : v;
|
|
151
|
+
};
|
|
152
|
+
self.getPeerFinished = function() {
|
|
153
|
+
let v = session.getPeerFinished ? session.getPeerFinished() : null;
|
|
154
|
+
return v == null ? undefined : v;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** Whether this connection resumed an earlier session. */
|
|
158
|
+
self.isSessionReused = function() {
|
|
159
|
+
return session.isSessionReused ? session.isSessionReused() : false;
|
|
160
|
+
};
|
|
161
|
+
|
|
133
162
|
/** RFC 5705 / RFC 8446 §7.5 exporter (Node tls.TLSSocket parity).
|
|
134
163
|
* DTLS-SRTP: exportKeyingMaterial(len, 'EXTRACTOR-dtls_srtp') — no context.
|
|
135
164
|
* Returns Buffer, or null before secrets are available. */
|
|
@@ -194,6 +223,7 @@ function createDTLSServer(options, connectionListener) {
|
|
|
194
223
|
minVersion: options.minVersion,
|
|
195
224
|
maxVersion: options.maxVersion,
|
|
196
225
|
mtu: options.mtu,
|
|
226
|
+
groups: options.groups,
|
|
197
227
|
ticketKeys: options.ticketKeys,
|
|
198
228
|
useCookies: options.useCookies,
|
|
199
229
|
srtpProfiles: options.srtpProfiles,
|
|
@@ -270,12 +300,14 @@ function connectDTLS(options, callback) {
|
|
|
270
300
|
remotePort: port,
|
|
271
301
|
servername: options.servername || host,
|
|
272
302
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
303
|
+
checkServerIdentity: options.checkServerIdentity,
|
|
273
304
|
requestCert: options.requestCert,
|
|
274
305
|
ca: options.ca,
|
|
275
306
|
alpnProtocols: options.alpnProtocols,
|
|
276
307
|
minVersion: options.minVersion,
|
|
277
308
|
maxVersion: options.maxVersion,
|
|
278
309
|
mtu: options.mtu,
|
|
310
|
+
groups: options.groups,
|
|
279
311
|
});
|
|
280
312
|
|
|
281
313
|
if (callback) socket.on('connect', callback);
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// node:tls option strings → the codes lemon-tls works with
|
|
3
|
+
// ============================================================
|
|
4
|
+
//
|
|
5
|
+
// node:tls takes `ciphers`, `ecdhCurve` and `sigalgs` as OpenSSL strings,
|
|
6
|
+
// colon-separated; lemon-tls works with code points (allowedCipherSuites,
|
|
7
|
+
// groups, signatureAlgorithms). compat.js promised Node's API and never read
|
|
8
|
+
// the three — a server given `ciphers` or `ecdhCurve` ran with the defaults
|
|
9
|
+
// and said nothing. These turn them into the lists, once, where an option is
|
|
10
|
+
// taken (compat.js, and QUICO for both TCP and QUIC), and refuse what cannot
|
|
11
|
+
// be honoured, as Node does ("no cipher match", "Failed to set ECDH curve").
|
|
12
|
+
//
|
|
13
|
+
// What is understood:
|
|
14
|
+
// ciphers suite names — OpenSSL's for TLS 1.2 (ECDHE-RSA-AES128-GCM-SHA256)
|
|
15
|
+
// and the standard ones (TLS_AES_128_GCM_SHA256, or for 1.2
|
|
16
|
+
// TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256), in the order given;
|
|
17
|
+
// `!name` / `-name` take one out; DEFAULT, HIGH and ALL stand for
|
|
18
|
+
// every suite this library implements — it implements no weak
|
|
19
|
+
// ones. Other OpenSSL keywords (aNULL, MD5, …) select nothing
|
|
20
|
+
// here and are skipped: what remains must name something.
|
|
21
|
+
// ecdhCurve 'auto' (the default list) or names: X25519, P-256
|
|
22
|
+
// (prime256v1, secp256r1), P-384 (secp384r1), and post-quantum:
|
|
23
|
+
// X25519MLKEM768, SecP256r1MLKEM768, SecP384r1MLKEM1024,
|
|
24
|
+
// MLKEM512, MLKEM768, MLKEM1024 (OpenSSL's names; case ignored).
|
|
25
|
+
// sigalgs scheme names (ecdsa_secp256r1_sha256, rsa_pss_rsae_sha256,
|
|
26
|
+
// ed25519, …) or OpenSSL's SIG+HASH form (ECDSA+SHA256,
|
|
27
|
+
// RSA-PSS+SHA256, RSA+SHA256).
|
|
28
|
+
|
|
29
|
+
import { TLS_CIPHER_SUITES, DEFAULT_CIPHER_SUITES_TLS13, DEFAULT_CIPHER_SUITES_TLS12 } from './crypto.js';
|
|
30
|
+
import { SERVER_DEFAULT_GROUPS } from './session/ecdh.js';
|
|
31
|
+
import { MLDSA_AVAILABLE } from './session/signing.js';
|
|
32
|
+
|
|
33
|
+
function splitList(s) {
|
|
34
|
+
return String(s).split(/[:,\s]+/).filter(function (x) { return x !== ''; });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
var cipherByName = null;
|
|
38
|
+
function cipherTable() {
|
|
39
|
+
if (cipherByName) return cipherByName;
|
|
40
|
+
cipherByName = {};
|
|
41
|
+
for (var code in TLS_CIPHER_SUITES) {
|
|
42
|
+
var info = TLS_CIPHER_SUITES[code];
|
|
43
|
+
if (info.name) cipherByName[info.name.toUpperCase()] = Number(code);
|
|
44
|
+
if (info.standardName) cipherByName[info.standardName.toUpperCase()] = Number(code);
|
|
45
|
+
}
|
|
46
|
+
return cipherByName;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `ciphers` → [code, …] in the order given. Throws when nothing is left. */
|
|
50
|
+
function parseCiphers(str) {
|
|
51
|
+
if (str === undefined || str === null) return null;
|
|
52
|
+
if (typeof str !== 'string') throw new TypeError('ciphers must be a string, got ' + typeof str);
|
|
53
|
+
var table = cipherTable(), all = DEFAULT_CIPHER_SUITES_TLS13.concat(DEFAULT_CIPHER_SUITES_TLS12);
|
|
54
|
+
var out = [], removed = [];
|
|
55
|
+
var tokens = splitList(str);
|
|
56
|
+
for (var i = 0; i < tokens.length; i++) {
|
|
57
|
+
var t = tokens[i], neg = t[0] === '!' || t[0] === '-';
|
|
58
|
+
if (neg || t[0] === '+') t = t.slice(1);
|
|
59
|
+
var u = t.toUpperCase(), codes;
|
|
60
|
+
if (u === 'DEFAULT' || u === 'HIGH' || u === 'ALL') codes = all;
|
|
61
|
+
else if (u in table) codes = [table[u]];
|
|
62
|
+
else continue; // a keyword this library has nothing for
|
|
63
|
+
for (var j = 0; j < codes.length; j++) {
|
|
64
|
+
if (neg) { removed.push(codes[j]); var k = out.indexOf(codes[j]); if (k >= 0) out.splice(k, 1); }
|
|
65
|
+
else if (out.indexOf(codes[j]) < 0 && removed.indexOf(codes[j]) < 0) out.push(codes[j]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (out.length === 0) throw new Error('ciphers: no cipher match in "' + str + '"');
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
var GROUPS = {
|
|
73
|
+
'X25519': 0x001d,
|
|
74
|
+
'P-256': 0x0017, 'PRIME256V1': 0x0017, 'SECP256R1': 0x0017,
|
|
75
|
+
'P-384': 0x0018, 'SECP384R1': 0x0018,
|
|
76
|
+
'X25519MLKEM768': 0x11ec,
|
|
77
|
+
'SECP256R1MLKEM768': 0x11eb, 'SECP384R1MLKEM1024': 0x11ed,
|
|
78
|
+
'MLKEM512': 0x0200, 'MLKEM768': 0x0201, 'MLKEM1024': 0x0202,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** `ecdhCurve` → [group, …]; 'auto' → null, the role's own default list
|
|
82
|
+
* (a server's has the hybrid first, a client's does not offer it unasked).
|
|
83
|
+
* Throws on a name it does not implement. */
|
|
84
|
+
function parseEcdhCurve(str) {
|
|
85
|
+
if (str === undefined || str === null) return null;
|
|
86
|
+
if (typeof str !== 'string') throw new TypeError('ecdhCurve must be a string, got ' + typeof str);
|
|
87
|
+
if (str.trim().toLowerCase() === 'auto') return null;
|
|
88
|
+
var out = [], tokens = splitList(str);
|
|
89
|
+
for (var i = 0; i < tokens.length; i++) {
|
|
90
|
+
var g = GROUPS[tokens[i].toUpperCase()];
|
|
91
|
+
if (g === undefined) throw new Error('ecdhCurve: unsupported curve "' + tokens[i] + '"');
|
|
92
|
+
// The hybrid group only where node:crypto has ML-KEM (session/ecdh.js).
|
|
93
|
+
if (SERVER_DEFAULT_GROUPS.indexOf(g) < 0) throw new Error('ecdhCurve: "' + tokens[i] + '" is not available in this Node.js');
|
|
94
|
+
if (out.indexOf(g) < 0) out.push(g);
|
|
95
|
+
}
|
|
96
|
+
if (out.length === 0) throw new Error('ecdhCurve: no curve in "' + str + '"');
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
var SCHEMES = {
|
|
101
|
+
'RSA_PKCS1_SHA256': 0x0401, 'RSA_PKCS1_SHA384': 0x0501, 'RSA_PKCS1_SHA512': 0x0601,
|
|
102
|
+
'ECDSA_SECP256R1_SHA256': 0x0403, 'ECDSA_SECP384R1_SHA384': 0x0503, 'ECDSA_SECP521R1_SHA512': 0x0603,
|
|
103
|
+
'RSA_PSS_RSAE_SHA256': 0x0804, 'RSA_PSS_RSAE_SHA384': 0x0805, 'RSA_PSS_RSAE_SHA512': 0x0806,
|
|
104
|
+
'ED25519': 0x0807, 'ED448': 0x0808,
|
|
105
|
+
'RSA_PSS_PSS_SHA256': 0x0809, 'RSA_PSS_PSS_SHA384': 0x080a, 'RSA_PSS_PSS_SHA512': 0x080b,
|
|
106
|
+
// ML-DSA (FIPS 204), as OpenSSL names it.
|
|
107
|
+
'MLDSA44': 0x0904, 'MLDSA65': 0x0905, 'MLDSA87': 0x0906,
|
|
108
|
+
// OpenSSL's SIG+HASH spelling.
|
|
109
|
+
'ECDSA+SHA256': 0x0403, 'ECDSA+SHA384': 0x0503, 'ECDSA+SHA512': 0x0603,
|
|
110
|
+
'RSA-PSS+SHA256': 0x0804, 'RSA-PSS+SHA384': 0x0805, 'RSA-PSS+SHA512': 0x0806,
|
|
111
|
+
'RSA+SHA256': 0x0401, 'RSA+SHA384': 0x0501, 'RSA+SHA512': 0x0601,
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** `sigalgs` → [scheme, …]. Throws on a name it does not know. */
|
|
115
|
+
function parseSigalgs(str) {
|
|
116
|
+
if (str === undefined || str === null) return null;
|
|
117
|
+
if (typeof str !== 'string') throw new TypeError('sigalgs must be a string, got ' + typeof str);
|
|
118
|
+
var out = [], tokens = splitList(str);
|
|
119
|
+
for (var i = 0; i < tokens.length; i++) {
|
|
120
|
+
var s = SCHEMES[tokens[i].toUpperCase()];
|
|
121
|
+
if (s === undefined) throw new Error('sigalgs: unsupported signature algorithm "' + tokens[i] + '"');
|
|
122
|
+
// ML-DSA only where node:crypto has it (session/signing.js).
|
|
123
|
+
if (s >= 0x0904 && s <= 0x0906 && !MLDSA_AVAILABLE) throw new Error('sigalgs: "' + tokens[i] + '" is not available in this Node.js');
|
|
124
|
+
if (out.indexOf(s) < 0) out.push(s);
|
|
125
|
+
}
|
|
126
|
+
if (out.length === 0) throw new Error('sigalgs: none in "' + str + '"');
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** 'TLSv1.2' / 'TLSv1.3' → 0x0303 / 0x0304, as node:tls names them. */
|
|
131
|
+
function parseTlsVersion(v, name) {
|
|
132
|
+
if (v === undefined || v === null) return null;
|
|
133
|
+
if (v === 'TLSv1.3') return 0x0304;
|
|
134
|
+
if (v === 'TLSv1.2') return 0x0303;
|
|
135
|
+
throw new Error(name + ': "' + v + '" is not supported (TLSv1.2 or TLSv1.3)');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export { parseCiphers, parseEcdhCurve, parseSigalgs, parseTlsVersion };
|
package/src/record.js
CHANGED
|
@@ -373,10 +373,52 @@ function buildAad12(seqNum, recordType, plaintextLen) {
|
|
|
373
373
|
}
|
|
374
374
|
|
|
375
375
|
/**
|
|
376
|
-
*
|
|
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
|
|
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
|
|
@@ -732,7 +795,9 @@ function decryptDtls12(fragment, key, ivSalt, epoch, seq, type) {
|
|
|
732
795
|
decipher.setAuthTag(tag);
|
|
733
796
|
let pt = decipher.update(ct);
|
|
734
797
|
decipher.final();
|
|
735
|
-
|
|
798
|
+
// A view, not a copy: pt is a fresh Buffer that owns its whole
|
|
799
|
+
// ArrayBuffer and nothing else holds it. Same Uint8Array type as before.
|
|
800
|
+
return new Uint8Array(pt.buffer, pt.byteOffset, pt.byteLength);
|
|
736
801
|
}
|
|
737
802
|
|
|
738
803
|
// AES-GCM: strip the 8-byte explicit nonce.
|
|
@@ -748,7 +813,8 @@ function decryptDtls12(fragment, key, ivSalt, epoch, seq, type) {
|
|
|
748
813
|
decipher.setAuthTag(tag);
|
|
749
814
|
let pt = decipher.update(ct);
|
|
750
815
|
decipher.final();
|
|
751
|
-
|
|
816
|
+
// A view, not a copy — see the ChaCha branch above.
|
|
817
|
+
return new Uint8Array(pt.buffer, pt.byteOffset, pt.byteLength);
|
|
752
818
|
}
|
|
753
819
|
|
|
754
820
|
/** Build complete encrypted DTLS 1.2 record (header + encrypted payload). */
|
|
@@ -1102,7 +1168,8 @@ function decryptEncryptedDtls13(data, keys, fullEpoch) {
|
|
|
1102
1168
|
seq = acceptedSeq;
|
|
1103
1169
|
|
|
1104
1170
|
{
|
|
1105
|
-
|
|
1171
|
+
// A view, not a copy: pt is the decipher's fresh Buffer, owned by no one else.
|
|
1172
|
+
let inner = parseInnerPlaintext(new Uint8Array(pt.buffer, pt.byteOffset, pt.byteLength));
|
|
1106
1173
|
|
|
1107
1174
|
// RFC 9147 §4.5.1 anti-replay: only AFTER the tag verifies do we consult
|
|
1108
1175
|
// and update the sliding window. Checking before deprotection would let a
|
package/src/secure_context.js
CHANGED
|
@@ -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
|
-
|
|
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'
|
|
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'
|
|
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'
|
|
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 = [];
|