lemon-tls 0.3.0 → 0.4.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,7 +1,7 @@
1
1
  {
2
2
  "name": "lemon-tls",
3
- "version": "0.3.0",
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.",
3
+ "version": "0.4.1",
4
+ "description": "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",
7
7
  "types": "index.d.ts",
@@ -44,6 +44,7 @@ function DTLSSocket(udpSocket, options) {
44
44
  cert: options.cert,
45
45
  key: options.key,
46
46
  rejectUnauthorized: options.rejectUnauthorized,
47
+ requestCert: options.requestCert,
47
48
  ca: options.ca,
48
49
  alpnProtocols: options.alpnProtocols,
49
50
  minVersion: options.minVersion,
@@ -159,6 +160,7 @@ function createDTLSServer(options, connectionListener) {
159
160
  remotePort: rinfo.port,
160
161
  cert: options.cert,
161
162
  key: options.key,
163
+ requestCert: options.requestCert,
162
164
  SNICallback: options.SNICallback,
163
165
  alpnProtocols: options.alpnProtocols,
164
166
  minVersion: options.minVersion,
@@ -239,6 +241,7 @@ function connectDTLS(options, callback) {
239
241
  remotePort: port,
240
242
  servername: options.servername || host,
241
243
  rejectUnauthorized: options.rejectUnauthorized,
244
+ requestCert: options.requestCert,
242
245
  ca: options.ca,
243
246
  alpnProtocols: options.alpnProtocols,
244
247
  minVersion: options.minVersion,
@@ -29,7 +29,7 @@ function normalize_hello(hello) {
29
29
  } else if (name === 'KEY_SHARE') {
30
30
  if (!('key_groups' in out)) out.key_groups = [];
31
31
  if (!('supported_groups' in out)) out.supported_groups = [];
32
- for (let i2 in value) {
32
+ for (let i2 = 0; i2 < value.length; i2++) {
33
33
  if (out.supported_groups.indexOf(value[i2].group) < 0) {
34
34
  out.supported_groups.push(value[i2].group);
35
35
  }
@@ -504,20 +504,24 @@ function TLSSession(options){
504
504
  // - ALPN (same as CH1)
505
505
  // - custom extensions (QUIC transport params etc.)
506
506
  // - same cipher_suites, session_id, random as CH1
507
+ // CH2 MUST match CH1 exactly except key_share/cookie (RFC 8446
508
+ // §4.1.2) — so mirror CH1's conditional composition: same sigalg
509
+ // source (CH1 wrote its final list back to the context), and the
510
+ // same tls13Only rules for the legacy extensions and psk modes.
511
+ let tls13Only2 = context.local_supported_versions.length > 0 &&
512
+ context.local_supported_versions.every(v => v === 0x0304);
507
513
  let extensions = [
508
514
  { type: 'SUPPORTED_VERSIONS', value: context.local_supported_versions },
509
515
  { type: 'SUPPORTED_GROUPS', value: context.local_supported_groups },
510
516
  { type: 'KEY_SHARE', value: [{ group: requestedGroup, key_exchange: newKeyGroup.public_key }] },
511
- { type: 'SIGNATURE_ALGORITHMS', value: [
512
- // Must match CH1 exactly (RFC 8446 §4.1.2)
513
- 0x0804, 0x0805, 0x0806,
514
- 0x0403, 0x0503, 0x0603,
515
- 0x0807, 0x0808,
516
- 0x0401, 0x0501, 0x0601
517
- ] },
518
- { type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) },
519
- { type: 'EXTENDED_MASTER_SECRET', value: null },
517
+ { type: 'SIGNATURE_ALGORITHMS', value: context.local_supported_signature_algorithms },
520
518
  ];
519
+ if (!tls13Only2) {
520
+ extensions.push({ type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) });
521
+ extensions.push({ type: 'EXTENDED_MASTER_SECRET', value: null });
522
+ } else {
523
+ extensions.push({ type: 'PSK_KEY_EXCHANGE_MODES', value: [1] });
524
+ }
521
525
 
522
526
  // SNI (must be first)
523
527
  if (context.local_sni) extensions.unshift({ type: 'SERVER_NAME', value: context.local_sni });
@@ -533,7 +537,7 @@ function TLSSession(options){
533
537
  }
534
538
 
535
539
  // Custom extensions (e.g. QUIC transport params 0x39)
536
- for (let ci in context.local_extensions) {
540
+ for (let ci = 0; ci < context.local_extensions.length; ci++) {
537
541
  extensions.push(context.local_extensions[ci]);
538
542
  }
539
543
 
@@ -543,6 +547,11 @@ function TLSSession(options){
543
547
  random: context.local_random,
544
548
  session_id: context.local_session_id,
545
549
  cookie: context.dtls_cookie,
550
+ // BUGFIX: `cipher_suites` (plural) is what wire.js's client_hello
551
+ // builder actually reads; the singular key was silently ignored and
552
+ // wire.js substituted its own hardcoded fallback list — the
553
+ // configured cipher suites never reached the wire. Both names passed.
554
+ cipher_suites: context.local_supported_cipher_suites,
546
555
  cipher_suite: context.local_supported_cipher_suites,
547
556
  extensions: extensions,
548
557
  });
@@ -999,7 +1008,7 @@ function TLSSession(options){
999
1008
 
1000
1009
 
1001
1010
  if('add_local_key_groups' in options){
1002
- for(let i in options['add_local_key_groups']){
1011
+ for(let i = 0; i < options['add_local_key_groups'].length; i++){
1003
1012
 
1004
1013
  let group=options['add_local_key_groups'][i].group;
1005
1014
  if(group in context.local_key_groups==false){
@@ -1029,7 +1038,7 @@ function TLSSession(options){
1029
1038
 
1030
1039
 
1031
1040
  if('add_remote_key_groups' in options){
1032
- for(let i in options['add_remote_key_groups']){
1041
+ for(let i = 0; i < options['add_remote_key_groups'].length; i++){
1033
1042
 
1034
1043
  let group=options['add_remote_key_groups'][i].group;
1035
1044
  if(group in context.remote_key_groups==false){
@@ -1779,7 +1788,7 @@ function TLSSession(options){
1779
1788
  }
1780
1789
 
1781
1790
 
1782
- for(let i in context.local_extensions){
1791
+ for(let i = 0; i < context.local_extensions.length; i++){
1783
1792
  extensions.push(context.local_extensions[i]);
1784
1793
  }
1785
1794
 
@@ -2105,6 +2114,41 @@ function TLSSession(options){
2105
2114
  }
2106
2115
  }
2107
2116
 
2117
+ // TLS 1.2 / DTLS 1.2 server: send CertificateRequest if mutual auth was
2118
+ // requested via { requestCert: true }. Per RFC 5246 §7.4.4 this message
2119
+ // goes between ServerKeyExchange and ServerHelloDone. The TLS 1.3 path
2120
+ // (line ~1805) sends CertificateRequest between EncryptedExtensions and
2121
+ // Certificate and is handled separately — version branching matters
2122
+ // because the wire formats differ (RFC 5246 §7.4.4 vs RFC 8446 §4.3.2).
2123
+ //
2124
+ // Without this block, a 1.2 server that sets requestCert never actually
2125
+ // requests the client's certificate, the client never sends one (TLS
2126
+ // clients only send a cert in response to CertificateRequest), and any
2127
+ // application-layer fingerprint check on the server fails with "peer
2128
+ // presented no certificate" — most notably breaking WebRTC, which
2129
+ // mandates mutual authentication (RFC 8827 §6.5) and pins to DTLS 1.2.
2130
+ if (context.isServer == true && context.requestCert == true &&
2131
+ !context.certificateRequestSent &&
2132
+ context.key_exchange_sent == true && !context.hello_done_sent &&
2133
+ (context.selected_version === wire.TLS_VERSION.TLS1_2 ||
2134
+ context.selected_version === wire.DTLS_VERSION.DTLS1_2)) {
2135
+
2136
+ let cr_body = wire.build_certificate_request({
2137
+ version: wire.TLS_VERSION.TLS1_2,
2138
+ // rsa_sign(1), ecdsa_sign(64) — accept both. WebRTC uses ECDSA,
2139
+ // most other 1.2 deployments use RSA. The client picks whichever
2140
+ // matches its certificate.
2141
+ certificate_types: [1, 64],
2142
+ signature_algorithms: context.local_supported_signature_algorithms || [],
2143
+ certificate_authorities: [], // empty: accept any CA
2144
+ });
2145
+ let cr_data = wire.build_message(wire.TLS_MESSAGE_TYPE.CERTIFICATE_REQUEST, cr_body);
2146
+ pushTranscript(cr_data);
2147
+ context.certificateRequestSent = true;
2148
+ ev.emit('message', 0, context.message_sent_seq, 'certificate_request', cr_data);
2149
+ context.message_sent_seq++;
2150
+ }
2151
+
2108
2152
  //server hello done - 1.2 only...
2109
2153
  if(context.isServer==true && (context.selected_version === wire.TLS_VERSION.TLS1_2 || context.selected_version === wire.DTLS_VERSION.DTLS1_2)){
2110
2154
  if(context.hello_done_sent==false && context.key_exchange_sent==true){
@@ -2655,9 +2699,22 @@ function TLSSession(options){
2655
2699
  context.local_session_id=new Uint8Array(crypto.randomBytes(32));
2656
2700
  }
2657
2701
 
2702
+ // TLS 1.3-only mode (QUIC and any client that offers no other version):
2703
+ // the ClientHello must not carry TLS 1.2 baggage. QUIC mandates TLS 1.3
2704
+ // (RFC 9001), and strict 1.3-only stacks reject hellos with legacy
2705
+ // artifacts. Detected from the configured versions, so plain TLS over
2706
+ // TCP (which offers 1.3+1.2) keeps the full compatibility behavior.
2707
+ let tls13Only = context.local_supported_versions.length > 0 &&
2708
+ context.local_supported_versions.every(v => v === 0x0304);
2709
+
2658
2710
  // Support both TLS 1.3 and 1.2 (server picks the best)
2659
2711
  if(context.local_supported_cipher_suites.length<=0){
2660
- context.local_supported_cipher_suites=[
2712
+ context.local_supported_cipher_suites = tls13Only ? [
2713
+ // TLS 1.3 only — no ECDHE 1.2 suites in a QUIC hello
2714
+ 0x1301, // TLS_AES_128_GCM_SHA256
2715
+ 0x1302, // TLS_AES_256_GCM_SHA384
2716
+ 0x1303, // TLS_CHACHA20_POLY1305_SHA256
2717
+ ] : [
2661
2718
  // TLS 1.3
2662
2719
  0x1301, // TLS_AES_128_GCM_SHA256
2663
2720
  0x1302, // TLS_AES_256_GCM_SHA384
@@ -2687,6 +2744,23 @@ function TLSSession(options){
2687
2744
  private_key: private_key
2688
2745
  };
2689
2746
 
2747
+ // Honor the configured signature algorithms; the hardcoded list is only
2748
+ // the default when none were set (previously the configured list was
2749
+ // ignored here — while the HRR CH2 hardcoded its own, so CH1 and CH2
2750
+ // could differ, violating RFC 8446 §4.1.2's exact-match requirement).
2751
+ let ch_sigalgs = (context.local_supported_signature_algorithms &&
2752
+ context.local_supported_signature_algorithms.length > 0)
2753
+ ? context.local_supported_signature_algorithms
2754
+ : [
2755
+ // TLS 1.3 (PSS + ECDSA + EdDSA)
2756
+ 0x0804, 0x0805, 0x0806,
2757
+ 0x0403, 0x0503, 0x0603,
2758
+ 0x0807, 0x0808,
2759
+ // TLS 1.2 (PKCS1)
2760
+ 0x0401, 0x0501, 0x0601
2761
+ ];
2762
+ context.local_supported_signature_algorithms = ch_sigalgs; // for CH2 reuse
2763
+
2690
2764
  let extensions = [
2691
2765
  {
2692
2766
  type: 'SUPPORTED_VERSIONS',
@@ -2705,19 +2779,20 @@ function TLSSession(options){
2705
2779
  },
2706
2780
  {
2707
2781
  type: 'SIGNATURE_ALGORITHMS',
2708
- value: [
2709
- // TLS 1.3 (PSS + ECDSA)
2710
- 0x0804, 0x0805, 0x0806,
2711
- 0x0403, 0x0503, 0x0603,
2712
- 0x0807, 0x0808,
2713
- // TLS 1.2 (PKCS1)
2714
- 0x0401, 0x0501, 0x0601
2715
- ]
2716
- },
2717
- // TLS 1.2 compatibility
2718
- { type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) },
2719
- { type: 'EXTENDED_MASTER_SECRET', value: null }
2782
+ value: ch_sigalgs
2783
+ }
2720
2784
  ];
2785
+ if (!tls13Only) {
2786
+ // TLS 1.2 compatibility — meaningless (and suspicious to strict
2787
+ // stacks) in a 1.3-only/QUIC hello
2788
+ extensions.push({ type: 'RENEGOTIATION_INFO', value: new Uint8Array(0) });
2789
+ extensions.push({ type: 'EXTENDED_MASTER_SECRET', value: null });
2790
+ } else {
2791
+ // psk_key_exchange_modes: every mainstream 1.3 client sends it, and a
2792
+ // server MUST NOT send NewSessionTicket to a client that omitted it
2793
+ // (RFC 8446 §4.2.9) — required for future 0-RTT/resumption anyway.
2794
+ extensions.push({ type: 'PSK_KEY_EXCHANGE_MODES', value: [1] });
2795
+ }
2721
2796
 
2722
2797
  // Add SNI if servername was provided
2723
2798
  if (context.local_sni) {
@@ -2730,7 +2805,7 @@ function TLSSession(options){
2730
2805
  }
2731
2806
 
2732
2807
  // Add custom extensions (e.g. QUIC transport params 0x39)
2733
- for (let i in context.local_extensions) {
2808
+ for (let i = 0; i < context.local_extensions.length; i++) {
2734
2809
  extensions.push(context.local_extensions[i]);
2735
2810
  }
2736
2811
 
@@ -2787,6 +2862,11 @@ function TLSSession(options){
2787
2862
  random: context.local_random,
2788
2863
  session_id: context.local_session_id,
2789
2864
  cookie: context.dtls_cookie,
2865
+ // BUGFIX: `cipher_suites` (plural) is what wire.js's client_hello
2866
+ // builder actually reads; the singular key was silently ignored and
2867
+ // wire.js substituted its own hardcoded fallback list — the
2868
+ // configured cipher suites never reached the wire. Both names passed.
2869
+ cipher_suites: context.local_supported_cipher_suites,
2790
2870
  cipher_suite: context.local_supported_cipher_suites,
2791
2871
  extensions: extensions
2792
2872
  };
@@ -2842,6 +2922,11 @@ function TLSSession(options){
2842
2922
  random: context.local_random,
2843
2923
  session_id: sid,
2844
2924
  cookie: context.dtls_cookie,
2925
+ // BUGFIX: `cipher_suites` (plural) is what wire.js's client_hello
2926
+ // builder actually reads; the singular key was silently ignored and
2927
+ // wire.js substituted its own hardcoded fallback list — the
2928
+ // configured cipher suites never reached the wire. Both names passed.
2929
+ cipher_suites: context.local_supported_cipher_suites,
2845
2930
  cipher_suite: context.local_supported_cipher_suites,
2846
2931
  extensions: extensions
2847
2932
  };
@@ -2852,7 +2937,9 @@ function TLSSession(options){
2852
2937
  // Skip for DTLS (DTLS clients/servers often don't implement RFC 5077 fully,
2853
2938
  // and adding it caused interop issues with openssl s_server -dtls1_2).
2854
2939
  let isDtls = context.local_supported_versions && context.local_supported_versions.some(v => (v & 0xFF00) === 0xFE00);
2855
- if (!isDtls && context.sessionTickets) {
2940
+ if (!isDtls && !tls13Only && context.sessionTickets) {
2941
+ // RFC 5077 SessionTicket is a TLS 1.2 mechanism; 1.3 resumption uses
2942
+ // NewSessionTicket/PSK. Never advertise it in a 1.3-only/QUIC hello.
2856
2943
  extensions.push({ type: 'SESSION_TICKET', value: new Uint8Array(0) });
2857
2944
  }
2858
2945
 
@@ -2863,6 +2950,11 @@ function TLSSession(options){
2863
2950
  random: context.local_random,
2864
2951
  session_id: context.local_session_id,
2865
2952
  cookie: context.dtls_cookie,
2953
+ // BUGFIX: `cipher_suites` (plural) is what wire.js's client_hello
2954
+ // builder actually reads; the singular key was silently ignored and
2955
+ // wire.js substituted its own hardcoded fallback list — the
2956
+ // configured cipher suites never reached the wire. Both names passed.
2957
+ cipher_suites: context.local_supported_cipher_suites,
2866
2958
  cipher_suite: context.local_supported_cipher_suites,
2867
2959
  extensions: extensions
2868
2960
  };
@@ -3073,5 +3165,4 @@ function TLSSession(options){
3073
3165
  return this;
3074
3166
  }
3075
3167
 
3076
- export default TLSSession;
3077
-
3168
+ export default TLSSession;