lemon-tls 0.2.0 → 0.2.1
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 +1 -1
- package/src/session/ecdh.js +25 -1
- package/src/tls_session.js +92 -14
- package/src/wire.js +32 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lemon-tls",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Zero-dependency TLS 1.3/1.2 implementation for Node.js — full control over cryptographic keys, record layer, and handshake. Drop-in replacement for node:tls with advanced options impossible in OpenSSL.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
package/src/session/ecdh.js
CHANGED
|
@@ -53,9 +53,33 @@ function p256_get_shared_secret(localPrivateRaw, remotePublicRaw) {
|
|
|
53
53
|
return new Uint8Array(ecdh.computeSecret(Buffer.from(remotePublicRaw)));
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Generate a P-384 keypair. Returns { private_key: Uint8Array(48), public_key: Uint8Array(97) }.
|
|
58
|
+
* Public key is uncompressed format (0x04 || x || y).
|
|
59
|
+
*/
|
|
60
|
+
function p384_generate_keypair() {
|
|
61
|
+
let ecdh = crypto.createECDH('secp384r1');
|
|
62
|
+
ecdh.generateKeys();
|
|
63
|
+
return {
|
|
64
|
+
private_key: new Uint8Array(ecdh.getPrivateKey()),
|
|
65
|
+
public_key: new Uint8Array(ecdh.getPublicKey(null, 'uncompressed'))
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Compute P-384 shared secret (raw x-coordinate, 48 bytes).
|
|
71
|
+
*/
|
|
72
|
+
function p384_get_shared_secret(localPrivateRaw, remotePublicRaw) {
|
|
73
|
+
let ecdh = crypto.createECDH('secp384r1');
|
|
74
|
+
ecdh.setPrivateKey(Buffer.from(localPrivateRaw));
|
|
75
|
+
return new Uint8Array(ecdh.computeSecret(Buffer.from(remotePublicRaw)));
|
|
76
|
+
}
|
|
77
|
+
|
|
56
78
|
export {
|
|
57
79
|
x25519_get_public_key,
|
|
58
80
|
x25519_get_shared_secret,
|
|
59
81
|
p256_generate_keypair,
|
|
60
|
-
p256_get_shared_secret
|
|
82
|
+
p256_get_shared_secret,
|
|
83
|
+
p384_generate_keypair,
|
|
84
|
+
p384_get_shared_secret
|
|
61
85
|
};
|
package/src/tls_session.js
CHANGED
|
@@ -28,7 +28,7 @@ import * as wire from './wire.js';
|
|
|
28
28
|
// Extracted modules
|
|
29
29
|
import { pick_scheme, sign_with_scheme } from './session/signing.js';
|
|
30
30
|
import createSecureContext from './secure_context.js';
|
|
31
|
-
import { x25519_get_public_key, x25519_get_shared_secret, p256_generate_keypair, p256_get_shared_secret } from './session/ecdh.js';
|
|
31
|
+
import { x25519_get_public_key, x25519_get_shared_secret, p256_generate_keypair, p256_get_shared_secret, p384_generate_keypair, p384_get_shared_secret } from './session/ecdh.js';
|
|
32
32
|
import { build_tls_message, parse_tls_message } from './session/message.js';
|
|
33
33
|
|
|
34
34
|
|
|
@@ -57,7 +57,7 @@ function TLSSession(options){
|
|
|
57
57
|
|
|
58
58
|
//local stuff...
|
|
59
59
|
local_sni: options.servername || null,
|
|
60
|
-
local_session_id: null,
|
|
60
|
+
local_session_id: 'sessionId' in options ? options.sessionId : null,
|
|
61
61
|
|
|
62
62
|
local_random: null,
|
|
63
63
|
local_extensions: [],
|
|
@@ -267,19 +267,26 @@ function TLSSession(options){
|
|
|
267
267
|
if (!hrrCipher) hrrCipher = 0x1301;
|
|
268
268
|
let hashName = TLS_CIPHER_SUITES[hrrCipher] ? TLS_CIPHER_SUITES[hrrCipher].hash : 'sha256';
|
|
269
269
|
|
|
270
|
-
// Replace transcript: CH1 → message_hash
|
|
270
|
+
// Replace transcript: CH1 → message_hash (RFC 8446 §4.4.1)
|
|
271
|
+
// BUG FIX: The HRR was already pushed to transcript at the top of this block (line 195).
|
|
272
|
+
// We must remove it before hashing, since message_hash = Hash(ClientHello1) only.
|
|
273
|
+
let hrrData = context.transcript.pop(); // remove HRR
|
|
271
274
|
let ch1_hash = getHashFn(hashName)(concatUint8Arrays(context.transcript));
|
|
272
275
|
let message_hash = wire.build_message(wire.TLS_MESSAGE_TYPE.MESSAGE_HASH, ch1_hash);
|
|
273
|
-
context.transcript = [message_hash,
|
|
276
|
+
context.transcript = [message_hash, hrrData]; // message_hash + HRR
|
|
274
277
|
|
|
275
|
-
// Find the requested group from HRR
|
|
278
|
+
// Find the requested group from HRR key_share extension
|
|
279
|
+
// After wire.js fix, key_groups contains [{group: N, key_exchange: empty}] for HRR
|
|
276
280
|
let requestedGroup = null;
|
|
277
|
-
if (message.
|
|
278
|
-
requestedGroup = message.supported_groups[0];
|
|
279
|
-
} else if (message.key_groups && message.key_groups.length > 0) {
|
|
281
|
+
if (message.key_groups && message.key_groups.length > 0) {
|
|
280
282
|
requestedGroup = message.key_groups[0].group;
|
|
283
|
+
} else if (message.supported_groups && message.supported_groups.length > 0) {
|
|
284
|
+
requestedGroup = message.supported_groups[0];
|
|
281
285
|
}
|
|
282
286
|
|
|
287
|
+
// Extract cookie from HRR (if present, must be echoed in CH2)
|
|
288
|
+
let hrrCookie = message.cookie || null;
|
|
289
|
+
|
|
283
290
|
if (requestedGroup) {
|
|
284
291
|
// Generate key for the requested group
|
|
285
292
|
let newKeyGroup = null;
|
|
@@ -292,21 +299,52 @@ function TLSSession(options){
|
|
|
292
299
|
let kp = p256_generate_keypair();
|
|
293
300
|
newKeyGroup = { group: requestedGroup, public_key: kp.public_key, private_key: kp.private_key };
|
|
294
301
|
context.local_key_groups[requestedGroup] = { public_key: kp.public_key, private_key: kp.private_key };
|
|
302
|
+
} else if (requestedGroup === 0x0018) {
|
|
303
|
+
let kp = p384_generate_keypair();
|
|
304
|
+
newKeyGroup = { group: requestedGroup, public_key: kp.public_key, private_key: kp.private_key };
|
|
305
|
+
context.local_key_groups[requestedGroup] = { public_key: kp.public_key, private_key: kp.private_key };
|
|
295
306
|
}
|
|
296
307
|
|
|
297
308
|
if (newKeyGroup) {
|
|
298
|
-
// Build and send new ClientHello (CH2) with
|
|
309
|
+
// Build and send new ClientHello (CH2) with:
|
|
310
|
+
// - key_share for requested group
|
|
311
|
+
// - cookie (if HRR included one)
|
|
312
|
+
// - ALPN (same as CH1)
|
|
313
|
+
// - custom extensions (QUIC transport params etc.)
|
|
314
|
+
// - same cipher_suites, session_id, random as CH1
|
|
299
315
|
let extensions = [
|
|
300
316
|
{ type: 'SUPPORTED_VERSIONS', value: context.local_supported_versions },
|
|
301
317
|
{ type: 'SUPPORTED_GROUPS', value: context.local_supported_groups },
|
|
302
318
|
{ type: 'KEY_SHARE', value: [{ group: requestedGroup, key_exchange: newKeyGroup.public_key }] },
|
|
303
|
-
{ type: 'SIGNATURE_ALGORITHMS', value:
|
|
304
|
-
|
|
319
|
+
{ type: 'SIGNATURE_ALGORITHMS', value: [
|
|
320
|
+
// Must match CH1 exactly (RFC 8446 §4.1.2)
|
|
321
|
+
0x0804, 0x0805, 0x0806,
|
|
322
|
+
0x0403, 0x0503, 0x0603,
|
|
323
|
+
0x0807, 0x0808,
|
|
324
|
+
0x0401, 0x0501, 0x0601
|
|
325
|
+
] },
|
|
305
326
|
{ type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) },
|
|
306
|
-
{ type: 23, data: new Uint8Array(0) },
|
|
327
|
+
{ type: 23, data: new Uint8Array(0) }, // extended_master_secret
|
|
307
328
|
];
|
|
329
|
+
|
|
330
|
+
// SNI (must be first)
|
|
308
331
|
if (context.local_sni) extensions.unshift({ type: 'SERVER_NAME', value: context.local_sni });
|
|
309
332
|
|
|
333
|
+
// ALPN (same as CH1 — required for QUIC/h3)
|
|
334
|
+
if (context.local_supported_alpns && context.local_supported_alpns.length > 0) {
|
|
335
|
+
extensions.push({ type: 'ALPN', value: context.local_supported_alpns });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Cookie from HRR (RFC 8446 §4.2.2 — MUST echo if present)
|
|
339
|
+
if (hrrCookie) {
|
|
340
|
+
extensions.push({ type: 'COOKIE', value: hrrCookie });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Custom extensions (e.g. QUIC transport params 0x39)
|
|
344
|
+
for (let ci in context.local_extensions) {
|
|
345
|
+
extensions.push(context.local_extensions[ci]);
|
|
346
|
+
}
|
|
347
|
+
|
|
310
348
|
let ch2 = build_tls_message({
|
|
311
349
|
type: 'client_hello',
|
|
312
350
|
version: 0x0303,
|
|
@@ -780,6 +818,9 @@ function TLSSession(options){
|
|
|
780
818
|
if(context.remote_app_traffic_secret==null && options.remote_app_traffic_secret!==null){
|
|
781
819
|
context.remote_app_traffic_secret=options.remote_app_traffic_secret;
|
|
782
820
|
has_changed=true;
|
|
821
|
+
if(context.local_app_traffic_secret!==null){
|
|
822
|
+
ev.emit('appSecrets', context.local_app_traffic_secret, context.remote_app_traffic_secret);
|
|
823
|
+
}
|
|
783
824
|
}
|
|
784
825
|
}
|
|
785
826
|
|
|
@@ -787,6 +828,9 @@ function TLSSession(options){
|
|
|
787
828
|
if(context.local_app_traffic_secret==null && options.local_app_traffic_secret!==null){
|
|
788
829
|
context.local_app_traffic_secret=options.local_app_traffic_secret;
|
|
789
830
|
has_changed=true;
|
|
831
|
+
if(context.remote_app_traffic_secret!==null){
|
|
832
|
+
ev.emit('appSecrets', context.local_app_traffic_secret, context.remote_app_traffic_secret);
|
|
833
|
+
}
|
|
790
834
|
}
|
|
791
835
|
}
|
|
792
836
|
|
|
@@ -948,6 +992,20 @@ function TLSSession(options){
|
|
|
948
992
|
}
|
|
949
993
|
];
|
|
950
994
|
|
|
995
|
+
} else if (context.selected_group === 0x0018) {
|
|
996
|
+
|
|
997
|
+
let kp = p384_generate_keypair();
|
|
998
|
+
let private_key = kp.private_key;
|
|
999
|
+
let public_key = kp.public_key;
|
|
1000
|
+
|
|
1001
|
+
params_to_set['add_local_key_groups']=[
|
|
1002
|
+
{
|
|
1003
|
+
group: context.selected_group,
|
|
1004
|
+
private_key: private_key,
|
|
1005
|
+
public_key: public_key
|
|
1006
|
+
}
|
|
1007
|
+
];
|
|
1008
|
+
|
|
951
1009
|
}
|
|
952
1010
|
|
|
953
1011
|
|
|
@@ -974,6 +1032,12 @@ function TLSSession(options){
|
|
|
974
1032
|
|
|
975
1033
|
params_to_set['ecdhe_shared_secret']=ecdhe_shared_secret;
|
|
976
1034
|
|
|
1035
|
+
} else if (context.selected_group === 0x0018) { // secp384r1 (P-384)
|
|
1036
|
+
|
|
1037
|
+
let ecdhe_shared_secret = p384_get_shared_secret(local_private_key, remote_public_key);
|
|
1038
|
+
|
|
1039
|
+
params_to_set['ecdhe_shared_secret']=ecdhe_shared_secret;
|
|
1040
|
+
|
|
977
1041
|
}
|
|
978
1042
|
|
|
979
1043
|
}
|
|
@@ -1003,6 +1067,7 @@ function TLSSession(options){
|
|
|
1003
1067
|
cipher_suite: context.selected_cipher_suite,
|
|
1004
1068
|
selected_version: wire.TLS_VERSION.TLS1_3,
|
|
1005
1069
|
selected_group: context.selected_group,
|
|
1070
|
+
session_id: context.remote_session_id,
|
|
1006
1071
|
});
|
|
1007
1072
|
let hrr_data = wire.build_message(wire.TLS_MESSAGE_TYPE.SERVER_HELLO, hrr_body);
|
|
1008
1073
|
context.transcript.push(hrr_data);
|
|
@@ -1027,7 +1092,10 @@ function TLSSession(options){
|
|
|
1027
1092
|
if(context.selected_version!==null && context.selected_cipher_suite!==null && context.selected_session_id!==null){
|
|
1028
1093
|
if(context.selected_version === wire.TLS_VERSION.TLS1_3){
|
|
1029
1094
|
if(context.selected_group in context.local_key_groups==true && context.local_key_groups[context.selected_group].public_key!==null){
|
|
1030
|
-
|
|
1095
|
+
// After HRR, don't send ServerHello until CH2 provides the requested key_share
|
|
1096
|
+
if (!context.helloRetried || (context.selected_group in context.remote_key_groups)) {
|
|
1097
|
+
can_send_hello=true;
|
|
1098
|
+
}
|
|
1031
1099
|
}
|
|
1032
1100
|
}else if(context.selected_version === wire.TLS_VERSION.TLS1_2){
|
|
1033
1101
|
can_send_hello=true;
|
|
@@ -1926,6 +1994,16 @@ function TLSSession(options){
|
|
|
1926
1994
|
extensions.unshift({ type: 'SERVER_NAME', value: context.local_sni });
|
|
1927
1995
|
}
|
|
1928
1996
|
|
|
1997
|
+
// Add ALPN if provided (e.g. 'h3' for QUIC)
|
|
1998
|
+
if (context.local_supported_alpns && context.local_supported_alpns.length > 0) {
|
|
1999
|
+
extensions.push({ type: 'ALPN', value: context.local_supported_alpns });
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
// Add custom extensions (e.g. QUIC transport params 0x39)
|
|
2003
|
+
for (let i in context.local_extensions) {
|
|
2004
|
+
extensions.push(context.local_extensions[i]);
|
|
2005
|
+
}
|
|
2006
|
+
|
|
1929
2007
|
// PSK resumption: check if session/psk was provided
|
|
1930
2008
|
let pskData = options.session || options.psk || null;
|
|
1931
2009
|
let message_data;
|
|
@@ -2144,7 +2222,7 @@ function TLSSession(options){
|
|
|
2144
2222
|
cipher: context.selected_cipher_suite,
|
|
2145
2223
|
cipherName: cipherInfo ? cipherInfo.name : null,
|
|
2146
2224
|
group: context.selected_group,
|
|
2147
|
-
groupName: context.selected_group === 0x001d ? 'X25519' : context.selected_group === 0x0017 ? 'P-256' : null,
|
|
2225
|
+
groupName: context.selected_group === 0x001d ? 'X25519' : context.selected_group === 0x0017 ? 'P-256' : context.selected_group === 0x0018 ? 'P-384' : null,
|
|
2148
2226
|
signatureAlgorithm: context.selected_signature_algorithm,
|
|
2149
2227
|
alpn: context.selected_alpn,
|
|
2150
2228
|
sni: context.selected_sni || context.local_sni,
|
package/src/wire.js
CHANGED
|
@@ -220,6 +220,7 @@ exts.SIGNATURE_ALGORITHMS = { encode: null, decode: null };
|
|
|
220
220
|
exts.PSK_KEY_EXCHANGE_MODES = { encode: null, decode: null };
|
|
221
221
|
exts.KEY_SHARE = { encode: null, decode: null };
|
|
222
222
|
exts.ALPN = { encode: null, decode: null };
|
|
223
|
+
exts.COOKIE = { encode: null, decode: null };
|
|
223
224
|
exts.RENEGOTIATION_INFO = { encode: null, decode: null };
|
|
224
225
|
|
|
225
226
|
/* ------------------------------ SERVER_NAME (0) ------------------------------ */
|
|
@@ -527,6 +528,13 @@ exts.KEY_SHARE.encode = function (value) {
|
|
|
527
528
|
};
|
|
528
529
|
|
|
529
530
|
exts.KEY_SHARE.decode = function (data) {
|
|
531
|
+
// HelloRetryRequest form: just NamedGroup (2 bytes, no key_exchange)
|
|
532
|
+
if (data.length === 2) {
|
|
533
|
+
let g, off = 0;
|
|
534
|
+
[g, off] = r_u16(data, off);
|
|
535
|
+
return [{ group: g, key_exchange: new Uint8Array(0) }];
|
|
536
|
+
}
|
|
537
|
+
|
|
530
538
|
// Try ServerHello form: group(2) + len(2) + key
|
|
531
539
|
if (data.length >= 4) {
|
|
532
540
|
let g, off = 0;
|
|
@@ -627,6 +635,19 @@ exts.RENEGOTIATION_INFO.decode = function (data) {
|
|
|
627
635
|
return v; // return raw bytes (Uint8Array)
|
|
628
636
|
};
|
|
629
637
|
|
|
638
|
+
/* -------------------------------- COOKIE (44) -------------------------------- */
|
|
639
|
+
exts.COOKIE.encode = function (value) {
|
|
640
|
+
let v = toU8(value || new Uint8Array(0));
|
|
641
|
+
return veclen(2, v);
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
exts.COOKIE.decode = function (data) {
|
|
645
|
+
let off = 0;
|
|
646
|
+
let v;
|
|
647
|
+
[v, off] = readVec(data, off, 2);
|
|
648
|
+
return v; // Uint8Array — opaque cookie
|
|
649
|
+
};
|
|
650
|
+
|
|
630
651
|
/* ============================= Extensions helpers ============================= */
|
|
631
652
|
function ext_name_by_code(code) {
|
|
632
653
|
// best-effort pretty name
|
|
@@ -1204,21 +1225,23 @@ function parse_certificate_request(body) {
|
|
|
1204
1225
|
|
|
1205
1226
|
/* ============================== TLS 1.3 HelloRetryRequest ============================== */
|
|
1206
1227
|
function build_hello_retry_request(params) {
|
|
1207
|
-
// params: { cipher_suite, selected_version, selected_group, cookie
|
|
1228
|
+
// params: { cipher_suite, selected_version, selected_group, session_id?, cookie?, other_exts? }
|
|
1208
1229
|
let rnd = TLS13_HRR_RANDOM;
|
|
1209
|
-
|
|
1230
|
+
let sid = (params && params.session_id) ? toU8(params.session_id) : new Uint8Array(0);
|
|
1210
1231
|
let legacy_version = TLS_VERSION.TLS1_2;
|
|
1211
1232
|
|
|
1212
1233
|
let extList = [];
|
|
1213
1234
|
// supported_versions (selected)
|
|
1214
1235
|
extList.push({ type: 'SUPPORTED_VERSIONS', value: (params && params.selected_version) || TLS_VERSION.TLS1_3 });
|
|
1215
|
-
// key_share:
|
|
1236
|
+
// key_share: HRR format = just NamedGroup (2 bytes), NOT ServerHello format
|
|
1216
1237
|
if (params && params.selected_group != null) {
|
|
1217
|
-
|
|
1238
|
+
let ks_data = new Uint8Array(2);
|
|
1239
|
+
ks_data[0] = (params.selected_group >> 8) & 0xff;
|
|
1240
|
+
ks_data[1] = params.selected_group & 0xff;
|
|
1241
|
+
extList.push({ type: 0x0033, data: ks_data });
|
|
1218
1242
|
}
|
|
1219
1243
|
// cookie if supplied
|
|
1220
1244
|
if (params && params.cookie) {
|
|
1221
|
-
if (!exts.COOKIE) { exts.COOKIE = { encode: function(v){ return veclen(2, toU8(v||'')); }, decode: function(d){ let off=0,v; [v,off]=readVec(d,0,2); return v; } }; }
|
|
1222
1245
|
extList.push({ type: 'COOKIE', value: params.cookie });
|
|
1223
1246
|
}
|
|
1224
1247
|
// other extensions passthrough
|
|
@@ -1229,12 +1252,13 @@ function build_hello_retry_request(params) {
|
|
|
1229
1252
|
let extsBuf = build_extensions(extList);
|
|
1230
1253
|
let cipher_suite = (params && typeof params.cipher_suite==='number') ? params.cipher_suite : 0x1301;
|
|
1231
1254
|
|
|
1232
|
-
// Wire = legacy_version + random + sid + cipher_suite + compression(0) + extensions
|
|
1233
|
-
const out = new Uint8Array(2 + 32 + 1 +
|
|
1255
|
+
// Wire = legacy_version + random + sid_len + sid + cipher_suite + compression(0) + extensions
|
|
1256
|
+
const out = new Uint8Array(2 + 32 + 1 + sid.length + 2 + 1 + extsBuf.length);
|
|
1234
1257
|
let off = 0;
|
|
1235
1258
|
off = w_u16(out, off, legacy_version);
|
|
1236
1259
|
off = w_bytes(out, off, rnd);
|
|
1237
|
-
off = w_u8(out, off,
|
|
1260
|
+
off = w_u8(out, off, sid.length);
|
|
1261
|
+
if (sid.length > 0) off = w_bytes(out, off, sid);
|
|
1238
1262
|
off = w_u16(out, off, cipher_suite);
|
|
1239
1263
|
off = w_u8(out, off, 0);
|
|
1240
1264
|
off = w_bytes(out, off, extsBuf);
|