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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lemon-tls",
3
- "version": "0.2.0",
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",
@@ -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
  };
@@ -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, data]; // message_hash + HRR
276
+ context.transcript = [message_hash, hrrData]; // message_hash + HRR
274
277
 
275
- // Find the requested group from HRR extensions
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.supported_groups && message.supported_groups.length > 0) {
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 key_share for requested group
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: context.local_supported_signature_algorithms.length > 0 ?
304
- context.local_supported_signature_algorithms : [0x0804, 0x0805, 0x0806, 0x0403, 0x0503, 0x0603, 0x0401, 0x0501, 0x0601] },
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
- can_send_hello=true;
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?: Uint8Array|string, other_exts?: list }
1228
+ // params: { cipher_suite, selected_version, selected_group, session_id?, cookie?, other_exts? }
1208
1229
  let rnd = TLS13_HRR_RANDOM;
1209
- const sid = new Uint8Array(0);
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: only selected_group (no key)
1236
+ // key_share: HRR format = just NamedGroup (2 bytes), NOT ServerHello format
1216
1237
  if (params && params.selected_group != null) {
1217
- extList.push({ type: 'KEY_SHARE', value: { selected_group: params.selected_group, key_exchange: new Uint8Array(0) } });
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 + 0 + 2 + 1 + extsBuf.length);
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, 0);
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);