tttls1.3 0.2.18 → 0.3.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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +8 -5
  3. data/Gemfile +2 -0
  4. data/README.md +6 -3
  5. data/example/helper.rb +5 -2
  6. data/example/https_client_using_0rtt.rb +1 -1
  7. data/example/https_client_using_ech.rb +32 -0
  8. data/example/https_client_using_grease_ech.rb +26 -0
  9. data/example/https_client_using_grease_psk.rb +66 -0
  10. data/example/https_client_using_hrr_and_ech.rb +32 -0
  11. data/example/https_client_using_hrr_and_ticket.rb +1 -1
  12. data/example/https_client_using_ticket.rb +1 -1
  13. data/interop/client_spec.rb +3 -2
  14. data/interop/server_spec.rb +1 -3
  15. data/interop/{helper.rb → spec_helper.rb} +12 -5
  16. data/lib/tttls1.3/client.rb +553 -32
  17. data/lib/tttls1.3/connection.rb +9 -8
  18. data/lib/tttls1.3/cryptograph/aead.rb +1 -1
  19. data/lib/tttls1.3/error.rb +1 -1
  20. data/lib/tttls1.3/hpke.rb +91 -0
  21. data/lib/tttls1.3/key_schedule.rb +111 -8
  22. data/lib/tttls1.3/message/alert.rb +2 -1
  23. data/lib/tttls1.3/message/client_hello.rb +2 -1
  24. data/lib/tttls1.3/message/encrypted_extensions.rb +2 -1
  25. data/lib/tttls1.3/message/extension/alpn.rb +4 -5
  26. data/lib/tttls1.3/message/extension/compress_certificate.rb +1 -1
  27. data/lib/tttls1.3/message/extension/ech.rb +241 -0
  28. data/lib/tttls1.3/message/extension/key_share.rb +2 -4
  29. data/lib/tttls1.3/message/extension/server_name.rb +1 -1
  30. data/lib/tttls1.3/message/extensions.rb +20 -7
  31. data/lib/tttls1.3/message/record.rb +1 -1
  32. data/lib/tttls1.3/message/server_hello.rb +3 -5
  33. data/lib/tttls1.3/message.rb +3 -1
  34. data/lib/tttls1.3/named_group.rb +1 -1
  35. data/lib/tttls1.3/server.rb +2 -2
  36. data/lib/tttls1.3/utils.rb +8 -0
  37. data/lib/tttls1.3/version.rb +1 -1
  38. data/lib/tttls1.3.rb +4 -0
  39. data/spec/client_spec.rb +40 -0
  40. data/spec/connection_spec.rb +22 -7
  41. data/spec/ech_spec.rb +81 -0
  42. data/spec/extensions_spec.rb +1 -2
  43. data/spec/key_schedule_spec.rb +2 -2
  44. data/spec/server_spec.rb +22 -7
  45. data/spec/spec_helper.rb +41 -5
  46. data/tttls1.3.gemspec +2 -0
  47. metadata +39 -3
@@ -58,7 +58,9 @@ module TTTLS13
58
58
  alpn: nil,
59
59
  process_new_session_ticket: nil,
60
60
  ticket: nil,
61
+ # @deprecated Please use `resumption_secret` instead
61
62
  resumption_master_secret: nil,
63
+ resumption_secret: nil,
62
64
  psk_cipher_suite: nil,
63
65
  ticket_nonce: nil,
64
66
  ticket_age_add: nil,
@@ -67,14 +69,29 @@ module TTTLS13
67
69
  check_certificate_status: false,
68
70
  process_certificate_status: nil,
69
71
  compress_certificate_algorithms: DEFALUT_CH_COMPRESS_CERTIFICATE_ALGORITHMS,
72
+ ech_config: nil,
73
+ ech_hpke_cipher_suites: nil,
70
74
  compatibility_mode: true,
71
75
  sslkeylogfile: nil,
72
76
  loglevel: Logger::WARN
73
77
  }.freeze
74
78
  private_constant :DEFAULT_CLIENT_SETTINGS
75
79
 
80
+ STANDARD_CLIENT_ECH_HPKE_SYMMETRIC_CIPHER_SUITES = [
81
+ HpkeSymmetricCipherSuite.new(
82
+ HpkeSymmetricCipherSuite::HpkeKdfId.new(
83
+ Hpke::KdfId::HKDF_SHA256
84
+ ),
85
+ HpkeSymmetricCipherSuite::HpkeAeadId.new(
86
+ Hpke::AeadId::AES_128_GCM
87
+ )
88
+ )
89
+ ].freeze
76
90
  # rubocop: disable Metrics/ClassLength
77
91
  class Client < Connection
92
+ HpkeSymmetricCipherSuit = \
93
+ ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite
94
+
78
95
  # @param socket [Socket]
79
96
  # @param hostname [String]
80
97
  # @param settings [Hash]
@@ -84,10 +101,21 @@ module TTTLS13
84
101
  @endpoint = :client
85
102
  @hostname = hostname
86
103
  @settings = DEFAULT_CLIENT_SETTINGS.merge(settings)
104
+ # NOTE: backward compatibility
105
+ if @settings[:resumption_secret].nil? &&
106
+ !@settings[:resumption_master_secret].nil?
107
+ @settings[:resumption_secret] =
108
+ @settings.delete(:resumption_master_secret) \
109
+ end
110
+ raise Error::ConfigError if @settings[:resumption_secret] !=
111
+ @settings[:resumption_master_secret]
112
+
87
113
  logger.level = @settings[:loglevel]
88
114
 
89
115
  @early_data = ''
90
116
  @succeed_early_data = false
117
+ @retry_configs = []
118
+ @rejected_ech = false
91
119
  raise Error::ConfigError unless valid_settings?
92
120
  end
93
121
 
@@ -123,7 +151,7 @@ module TTTLS13
123
151
  # after here v
124
152
  # CONNECTED
125
153
  #
126
- # https://tools.ietf.org/html/rfc8446#appendix-A.1
154
+ # https://datatracker.ietf.org/doc/html/rfc8446#appendix-A.1
127
155
  #
128
156
  # rubocop: disable Metrics/AbcSize
129
157
  # rubocop: disable Metrics/BlockLength
@@ -137,7 +165,7 @@ module TTTLS13
137
165
  priv_keys = {} # Hash of NamedGroup => OpenSSL::PKey::$Object
138
166
  if use_psk?
139
167
  psk = gen_psk_from_nst(
140
- @settings[:resumption_master_secret],
168
+ @settings[:resumption_secret],
141
169
  @settings[:ticket_nonce],
142
170
  CipherSuite.digest(@settings[:psk_cipher_suite])
143
171
  )
@@ -153,6 +181,9 @@ module TTTLS13
153
181
  hs_rcipher = nil # TTTLS13::Cryptograph::$Object
154
182
  e_wcipher = nil # TTTLS13::Cryptograph::$Object
155
183
  sslkeylogfile = nil # TTTLS13::SslKeyLogFile::Writer
184
+ ch1_outer = nil # TTTLS13::Message::ClientHello for rejected ECH
185
+ ch_outer = nil # TTTLS13::Message::ClientHello for rejected ECH
186
+ ech_state = nil # TTTLS13::Client::EchState for ECH with HRR
156
187
  unless @settings[:sslkeylogfile].nil?
157
188
  begin
158
189
  sslkeylogfile = SslKeyLogFile::Writer.new(@settings[:sslkeylogfile])
@@ -170,7 +201,10 @@ module TTTLS13
170
201
 
171
202
  extensions, priv_keys = gen_ch_extensions
172
203
  binder_key = (use_psk? ? key_schedule.binder_key_res : nil)
173
- ch = send_client_hello(extensions, binder_key)
204
+ ch, inner, ech_state = send_client_hello(extensions, binder_key)
205
+ ch_outer = ch
206
+ # use ClientHelloInner messages for the transcript hash
207
+ ch = inner.nil? ? ch : inner
174
208
  transcript[CH] = [ch, ch.serialize]
175
209
  send_ccs if @settings[:compatibility_mode]
176
210
  if use_early_data?
@@ -235,6 +269,8 @@ module TTTLS13
235
269
 
236
270
  ch1, = transcript[CH1] = transcript.delete(CH)
237
271
  hrr, = transcript[HRR] = transcript.delete(SH)
272
+ ch1_outer = ch_outer
273
+ ch_outer = nil
238
274
 
239
275
  # validate cookie
240
276
  diff_sets = sh.extensions.keys - ch1.extensions.keys
@@ -242,7 +278,7 @@ module TTTLS13
242
278
  unless (diff_sets - [Message::ExtensionType::COOKIE]).empty?
243
279
 
244
280
  # validate key_share
245
- # TODO: pre_shared_key
281
+ # TODO: validate pre_shared_key
246
282
  ngl = ch1.extensions[Message::ExtensionType::SUPPORTED_GROUPS]
247
283
  .named_group_list
248
284
  kse = ch1.extensions[Message::ExtensionType::KEY_SHARE]
@@ -256,7 +292,16 @@ module TTTLS13
256
292
  extensions, pk = gen_newch_extensions(ch1, hrr)
257
293
  priv_keys = pk.merge(priv_keys)
258
294
  binder_key = (use_psk? ? key_schedule.binder_key_res : nil)
259
- ch = send_new_client_hello(ch1, hrr, extensions, binder_key)
295
+ ch, inner = send_new_client_hello(
296
+ ch1,
297
+ hrr,
298
+ extensions,
299
+ binder_key,
300
+ ech_state
301
+ )
302
+ # use ClientHelloInner messages for the transcript hash
303
+ ch_outer = ch
304
+ ch = inner.nil? ? ch : inner
260
305
  transcript[CH] = [ch, ch.serialize]
261
306
 
262
307
  @state = ClientState::WAIT_SH
@@ -264,8 +309,11 @@ module TTTLS13
264
309
  end
265
310
 
266
311
  # generate shared secret
267
- psk = nil unless sh.extensions
268
- .include?(Message::ExtensionType::PRE_SHARED_KEY)
312
+ if sh.extensions.include?(Message::ExtensionType::PRE_SHARED_KEY)
313
+ # TODO: validate pre_shared_key
314
+ else
315
+ psk = nil
316
+ end
269
317
  ch_ks = ch.extensions[Message::ExtensionType::KEY_SHARE]
270
318
  .key_share_entry.map(&:group)
271
319
  sh_ks = sh.extensions[Message::ExtensionType::KEY_SHARE]
@@ -285,6 +333,27 @@ module TTTLS13
285
333
  cipher_suite: @cipher_suite,
286
334
  transcript: transcript
287
335
  )
336
+
337
+ # rejected ECH
338
+ # NOTE: It can compute (hrr_)accept_ech until client selects the
339
+ # cipher_suite.
340
+ if !sh.hrr? && use_ech?
341
+ if !transcript.include?(HRR) && !key_schedule.accept_ech?
342
+ # 1sh SH
343
+ transcript[CH] = [ch_outer, ch_outer.serialize]
344
+ @rejected_ech = true
345
+ elsif transcript.include?(HRR) &&
346
+ key_schedule.hrr_accept_ech? != key_schedule.accept_ech?
347
+ # 2nd SH
348
+ terminate(:illegal_parameter)
349
+ elsif transcript.include?(HRR) && !key_schedule.hrr_accept_ech?
350
+ # 2nd SH
351
+ transcript[CH1] = [ch1_outer, ch1_outer.serialize]
352
+ transcript[CH] = [ch_outer, ch_outer.serialize]
353
+ @rejected_ech = true
354
+ end
355
+ end
356
+
288
357
  @alert_wcipher = hs_wcipher = gen_cipher(
289
358
  @cipher_suite,
290
359
  key_schedule.client_handshake_write_key,
@@ -321,6 +390,12 @@ module TTTLS13
321
390
  @alpn = ee.extensions[
322
391
  Message::ExtensionType::APPLICATION_LAYER_PROTOCOL_NEGOTIATION
323
392
  ]&.protocol_name_list&.first
393
+ @retry_configs = ee.extensions[
394
+ Message::ExtensionType::ENCRYPTED_CLIENT_HELLO
395
+ ]&.retry_configs
396
+ terminate(:unsupported_extension) \
397
+ if !rejected_ech? && !@retry_configs.nil?
398
+
324
399
  @state = ClientState::WAIT_CERT_CR
325
400
  @state = ClientState::WAIT_FINISHED unless psk.nil?
326
401
  when ClientState::WAIT_CERT_CR
@@ -423,12 +498,14 @@ module TTTLS13
423
498
  transcript[CH].first.random,
424
499
  key_schedule.server_application_traffic_secret
425
500
  )
426
- @exporter_master_secret = key_schedule.exporter_master_secret
427
- @resumption_master_secret = key_schedule.resumption_master_secret
501
+ @exporter_secret = key_schedule.exporter_secret
502
+ @resumption_secret = key_schedule.resumption_secret
428
503
  @state = ClientState::CONNECTED
429
504
  when ClientState::CONNECTED
430
505
  logger.debug('ClientState::CONNECTED')
431
506
 
507
+ send_alert(:ech_required) \
508
+ if use_ech? && (!@retry_configs.nil? && !@retry_configs.empty?)
432
509
  break
433
510
  end
434
511
  end
@@ -440,6 +517,20 @@ module TTTLS13
440
517
  # rubocop: enable Metrics/MethodLength
441
518
  # rubocop: enable Metrics/PerceivedComplexity
442
519
 
520
+ # @param binary [String]
521
+ def write(binary)
522
+ # the client can regard ECH as securely disabled by the server, and it
523
+ # SHOULD retry the handshake with a new transport connection and ECH
524
+ # disabled.
525
+ if !@retry_configs.nil? && !@retry_configs.empty?
526
+ msg = 'SHOULD retry the handshake with a new transport connection'
527
+ logger.warn(msg)
528
+ return
529
+ end
530
+
531
+ super(binary)
532
+ end
533
+
443
534
  # @param binary [String]
444
535
  #
445
536
  # @raise [TTTLS13::Error::ConfigError]
@@ -449,11 +540,23 @@ module TTTLS13
449
540
  @early_data = binary
450
541
  end
451
542
 
543
+ # @return [Array of ECHConfig]
544
+ def retry_configs
545
+ @retry_configs.filter do |c|
546
+ SUPPORTED_ECHCONFIG_VERSIONS.include?(c.version)
547
+ end
548
+ end
549
+
452
550
  # @return [Boolean]
453
551
  def succeed_early_data?
454
552
  @succeed_early_data
455
553
  end
456
554
 
555
+ # @return [Boolean]
556
+ def rejected_ech?
557
+ @rejected_ech
558
+ end
559
+
457
560
  # @param res [OpenSSL::OCSP::Response]
458
561
  # @param cert [OpenSSL::X509::Certificate]
459
562
  # @param chain [Array of OpenSSL::X509::Certificate, nil]
@@ -535,6 +638,9 @@ module TTTLS13
535
638
  return false if @settings[:check_certificate_status] &&
536
639
  @settings[:process_certificate_status].nil?
537
640
 
641
+ ehcs = @settings[:ech_hpke_cipher_suites] || []
642
+ return false if !@settings[:ech_config].nil? && ehcs.empty?
643
+
538
644
  true
539
645
  end
540
646
  # rubocop: enable Metrics/AbcSize
@@ -544,7 +650,7 @@ module TTTLS13
544
650
  # @return [Boolean]
545
651
  def use_psk?
546
652
  !@settings[:ticket].nil? &&
547
- !@settings[:resumption_master_secret].nil? &&
653
+ !@settings[:resumption_secret].nil? &&
548
654
  !@settings[:psk_cipher_suite].nil? &&
549
655
  !@settings[:ticket_nonce].nil? &&
550
656
  !@settings[:ticket_age_add].nil? &&
@@ -556,6 +662,12 @@ module TTTLS13
556
662
  !(@early_data.nil? || @early_data.empty?)
557
663
  end
558
664
 
665
+ # @return [Boolean]
666
+ def use_ech?
667
+ !@settings[:ech_hpke_cipher_suites].nil? &&
668
+ !@settings[:ech_hpke_cipher_suites].empty?
669
+ end
670
+
559
671
  # @param cipher [TTTLS13::Cryptograph::Aead]
560
672
  def send_early_data(cipher)
561
673
  ap = Message::ApplicationData.new(@early_data)
@@ -568,14 +680,14 @@ module TTTLS13
568
680
  send_record(ap_record)
569
681
  end
570
682
 
571
- # @param resumption_master_secret [String]
683
+ # @param resumption_secret [String]
572
684
  # @param ticket_nonce [String]
573
685
  # @param digest [String] name of digest algorithm
574
686
  #
575
687
  # @return [String]
576
- def gen_psk_from_nst(resumption_master_secret, ticket_nonce, digest)
688
+ def gen_psk_from_nst(resumption_secret, ticket_nonce, digest)
577
689
  hash_len = OpenSSL::Digest.new(digest).digest_length
578
- KeySchedule.hkdf_expand_label(resumption_master_secret, 'resumption',
690
+ KeySchedule.hkdf_expand_label(resumption_secret, 'resumption',
579
691
  ticket_nonce, hash_len, digest)
580
692
  end
581
693
 
@@ -654,36 +766,60 @@ module TTTLS13
654
766
  # @param extensions [TTTLS13::Message::Extensions]
655
767
  # @param binder_key [String, nil]
656
768
  #
657
- # @return [TTTLS13::Message::ClientHello]
769
+ # @return [TTTLS13::Message::ClientHello] outer
770
+ # @return [TTTLS13::Message::ClientHello] inner
771
+ # @return [TTTLS13::Client::EchState]
772
+ # rubocop: disable Metrics/MethodLength
658
773
  def send_client_hello(extensions, binder_key = nil)
659
774
  ch = Message::ClientHello.new(
660
775
  cipher_suites: CipherSuites.new(@settings[:cipher_suites]),
661
776
  extensions: extensions
662
777
  )
663
778
 
779
+ # encrypted_client_hello
780
+ inner = nil # TTTLS13::Message::ClientHello
781
+ if use_ech?
782
+ inner = ch
783
+ inner_ech = Message::Extension::ECHClientHello.new_inner
784
+ inner.extensions[Message::ExtensionType::ENCRYPTED_CLIENT_HELLO] \
785
+ = inner_ech
786
+ ch, inner, ech_state = offer_ech(inner, @settings[:ech_config])
787
+ end
788
+
789
+ # psk_key_exchange_modes
790
+ # In order to use PSKs, clients MUST also send a
791
+ # "psk_key_exchange_modes" extension.
792
+ #
793
+ # https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.9
664
794
  if use_psk?
665
- # pre_shared_key && psk_key_exchange_modes
666
- #
667
- # In order to use PSKs, clients MUST also send a
668
- # "psk_key_exchange_modes" extension.
669
- #
670
- # https://tools.ietf.org/html/rfc8446#section-4.2.9
671
795
  pkem = Message::Extension::PskKeyExchangeModes.new(
672
796
  [Message::Extension::PskKeyExchangeMode::PSK_DHE_KE]
673
797
  )
674
798
  ch.extensions[Message::ExtensionType::PSK_KEY_EXCHANGE_MODES] = pkem
675
- # at the end, sign PSK binder
799
+ end
800
+
801
+ # pre_shared_key
802
+ # at the end, sign PSK binder
803
+ if use_psk?
676
804
  sign_psk_binder(
677
805
  ch: ch,
678
806
  binder_key: binder_key
679
807
  )
808
+
809
+ if use_ech?
810
+ sign_grease_psk_binder(
811
+ ch_outer: ch,
812
+ inner_pks: inner.extensions[Message::ExtensionType::PRE_SHARED_KEY]
813
+ )
814
+ end
680
815
  end
681
816
 
682
817
  send_handshakes(Message::ContentType::HANDSHAKE, [ch],
683
818
  Cryptograph::Passer.new)
684
819
 
685
- ch
820
+ [ch, inner, ech_state]
686
821
  end
822
+ # rubocop: enable Metrics/MethodLength
687
823
 
688
824
  # @param ch1 [TTTLS13::Message::ClientHello]
689
825
  # @param hrr [TTTLS13::Message::ServerHello]
@@ -698,10 +834,10 @@ module TTTLS13
698
834
  # partial ClientHello up to and including the
699
835
  # PreSharedKeyExtension.identities field.
700
836
  #
701
- # https://tools.ietf.org/html/rfc8446#section-4.2.11.2
837
+ # https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.11.2
702
838
  digest = CipherSuite.digest(@settings[:psk_cipher_suite])
703
839
  hash_len = OpenSSL::Digest.new(digest).digest_length
704
- dummy_binders = ["\x00" * hash_len]
840
+ placeholder_binders = [hash_len.zeros]
705
841
  psk = Message::Extension::PreSharedKey.new(
706
842
  msg_type: Message::HandshakeType::CLIENT_HELLO,
707
843
  offered_psks: Message::Extension::OfferedPsks.new(
@@ -709,7 +845,7 @@ module TTTLS13
709
845
  identity: @settings[:ticket],
710
846
  obfuscated_ticket_age: calc_obfuscated_ticket_age
711
847
  )],
712
- binders: dummy_binders
848
+ binders: placeholder_binders
713
849
  )
714
850
  )
715
851
  ch.extensions[Message::ExtensionType::PRE_SHARED_KEY] = psk
@@ -723,6 +859,325 @@ module TTTLS13
723
859
  )
724
860
  end
725
861
 
862
+ # @param ch1 [TTTLS13::Message::ClientHello]
863
+ # @param hrr [TTTLS13::Message::ServerHello]
864
+ # @param ch_outer [TTTLS13::Message::ClientHello]
865
+ # @param inner_psk [Message::Extension::PreSharedKey]
866
+ # @param binder_key [String]
867
+ #
868
+ # @return [String]
869
+ def sign_grease_psk_binder(ch1: nil,
870
+ hrr: nil,
871
+ ch_outer:,
872
+ inner_psk:,
873
+ binder_key:)
874
+ digest = CipherSuite.digest(@settings[:psk_cipher_suite])
875
+ hash_len = OpenSSL::Digest.new(digest).digest_length
876
+ placeholder_binders = [hash_len.zeros]
877
+ # For each PSK identity advertised in the ClientHelloInner, the client
878
+ # generates a random PSK identity with the same length. It also generates
879
+ # a random, 32-bit, unsigned integer to use as the obfuscated_ticket_age.
880
+ # Likewise, for each inner PSK binder, the client generates a random
881
+ # string of the same length.
882
+ #
883
+ # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.1.2-2
884
+ identity = inner_psk.offered_psks
885
+ .identities
886
+ .first
887
+ .identity
888
+ .length
889
+ .then { |len| OpenSSL::Random.random_bytes(len) }
890
+ ota = OpenSSL::Random.random_bytes(4)
891
+ psk = Message::Extension::PreSharedKey.new(
892
+ msg_type: Message::HandshakeType::CLIENT_HELLO,
893
+ offered_psks: Message::Extension::OfferedPsks.new(
894
+ identities: [Message::Extension::PskIdentity.new(
895
+ identity: identity,
896
+ obfuscated_ticket_age: ota
897
+ )],
898
+ binders: placeholder_binders
899
+ )
900
+ )
901
+ ch_outer.extensions[Message::ExtensionType::PRE_SHARED_KEY] = psk
902
+
903
+ psk.offered_psks.binders[0] = do_sign_psk_binder(
904
+ ch1: ch1,
905
+ hrr: hrr,
906
+ ch: ch_outer,
907
+ binder_key: binder_key,
908
+ digest: digest
909
+ )
910
+ end
911
+
912
+ # @param inner [TTTLS13::Message::ClientHello]
913
+ # @param ech_config [ECHConfig]
914
+ #
915
+ # @return [TTTLS13::Message::ClientHello]
916
+ # @return [TTTLS13::Message::ClientHello]
917
+ # @return [TTTLS13::Client::EchState]
918
+ # rubocop: disable Metrics/AbcSize
919
+ # rubocop: disable Metrics/MethodLength
920
+ def offer_ech(inner, ech_config)
921
+ return [new_greased_ch(inner, new_grease_ech), nil, nil] \
922
+ if ech_config.nil? ||
923
+ !SUPPORTED_ECHCONFIG_VERSIONS.include?(ech_config.version)
924
+
925
+ # Encrypted ClientHello Configuration
926
+ public_name = ech_config.echconfig_contents.public_name
927
+ key_config = ech_config.echconfig_contents.key_config
928
+ public_key = key_config.public_key.opaque
929
+ kem_id = key_config&.kem_id&.uint16
930
+ config_id = key_config.config_id
931
+ cipher_suite = select_ech_hpke_cipher_suite(key_config)
932
+ overhead_len = Hpke.aead_id2overhead_len(cipher_suite&.aead_id&.uint16)
933
+ aead_cipher = Hpke.aead_id2aead_cipher(cipher_suite&.aead_id&.uint16)
934
+ kdf_hash = Hpke.kdf_id2kdf_hash(cipher_suite&.kdf_id&.uint16)
935
+ return [new_greased_ch(inner, new_grease_ech), nil, nil] \
936
+ if [kem_id, overhead_len, aead_cipher, kdf_hash].any?(&:nil?)
937
+
938
+ kem_curve_name, kem_hash = Hpke.kem_id2dhkem(kem_id)
939
+ dhkem = Hpke.kem_curve_name2dhkem(kem_curve_name)
940
+ pkr = dhkem&.new(kem_hash)&.deserialize_public_key(public_key)
941
+ return [new_greased_ch(inner, new_grease_ech), nil, nil] if pkr.nil?
942
+
943
+ hpke = HPKE.new(kem_curve_name, kem_hash, kdf_hash, aead_cipher)
944
+ base_s = hpke.setup_base_s(pkr, "tls ech\x00" + ech_config.encode)
945
+ enc = base_s[:enc]
946
+ ctx = base_s[:context_s]
947
+ mnl = ech_config.echconfig_contents.maximum_name_length
948
+ encoded = encode_ch_inner(inner, mnl)
949
+
950
+ # Encoding the ClientHelloInner
951
+ aad = new_ch_outer_aad(
952
+ inner,
953
+ cipher_suite,
954
+ config_id,
955
+ enc,
956
+ encoded.length + overhead_len,
957
+ public_name
958
+ )
959
+ # Authenticating the ClientHelloOuter
960
+ # which does not include the Handshake structure's four byte header.
961
+ outer = new_ch_outer(
962
+ aad,
963
+ cipher_suite,
964
+ config_id,
965
+ enc,
966
+ ctx.seal(aad.serialize[4..], encoded)
967
+ )
968
+
969
+ ech_state = EchState.new(mnl, config_id, cipher_suite, public_name, ctx)
970
+ [outer, inner, ech_state]
971
+ end
972
+ # rubocop: enable Metrics/AbcSize
973
+ # rubocop: enable Metrics/MethodLength
974
+
975
+ # @param inner [TTTLS13::Message::ClientHello]
976
+ # @param ech_state [TTTLS13::Client::EchState]
977
+ #
978
+ # @return [TTTLS13::Message::ClientHello]
979
+ # @return [TTTLS13::Message::ClientHello]
980
+ def offer_new_ech(inner, ech_state)
981
+ encoded = encode_ch_inner(inner, ech_state.maximum_name_length)
982
+ overhead_len \
983
+ = Hpke.aead_id2overhead_len(ech_state.cipher_suite.aead_id.uint16)
984
+
985
+ # It encrypts EncodedClientHelloInner as described in Section 6.1.1, using
986
+ # the second partial ClientHelloOuterAAD, to obtain a second
987
+ # ClientHelloOuter. It reuses the original HPKE encryption context
988
+ # computed in Section 6.1 and uses the empty string for enc.
989
+ #
990
+ # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.1.5-4.4.1
991
+ aad = new_ch_outer_aad(
992
+ inner,
993
+ ech_state.cipher_suite,
994
+ ech_state.config_id,
995
+ '',
996
+ encoded.length + overhead_len,
997
+ ech_state.public_name
998
+ )
999
+ # Authenticating the ClientHelloOuter
1000
+ # which does not include the Handshake structure's four byte header.
1001
+ outer = new_ch_outer(
1002
+ aad,
1003
+ ech_state.cipher_suite,
1004
+ ech_state.config_id,
1005
+ '',
1006
+ ech_state.ctx.seal(aad.serialize[4..], encoded)
1007
+ )
1008
+
1009
+ [outer, inner]
1010
+ end
1011
+
1012
+ # @param inner [TTTLS13::Message::ClientHello]
1013
+ # @param maximum_name_length [Integer]
1014
+ #
1015
+ # @return [String] EncodedClientHelloInner
1016
+ def encode_ch_inner(inner, maximum_name_length)
1017
+ # TODO: ech_outer_extensions
1018
+ encoded = Message::ClientHello.new(
1019
+ legacy_version: inner.legacy_version,
1020
+ random: inner.random,
1021
+ legacy_session_id: '',
1022
+ cipher_suites: inner.cipher_suites,
1023
+ legacy_compression_methods: inner.legacy_compression_methods,
1024
+ extensions: inner.extensions
1025
+ )
1026
+ server_name_length = \
1027
+ inner.extensions[Message::ExtensionType::SERVER_NAME].server_name.length
1028
+
1029
+ # which does not include the Handshake structure's four byte header.
1030
+ padding_encoded_ch_inner(
1031
+ encoded.serialize[4..],
1032
+ server_name_length,
1033
+ maximum_name_length
1034
+ )
1035
+ end
1036
+
1037
+ # @param s [String]
1038
+ # @param server_name_length [Integer]
1039
+ # @param maximum_name_length [Integer]
1040
+ #
1041
+ # @return [String]
1042
+ def padding_encoded_ch_inner(s, server_name_length, maximum_name_length)
1043
+ padding_len =
1044
+ if server_name_length.positive?
1045
+ [maximum_name_length - server_name_length, 0].max
1046
+ else
1047
+ 9 + maximum_name_length
1048
+ end
1049
+
1050
+ padding_len = 31 - ((s.length + padding_len - 1) % 32)
1051
+ s + padding_len.zeros
1052
+ end
1053
+
1054
+ # @param inner [TTTLS13::Message::ClientHello]
1055
+ # @param cipher_suite [HpkeSymmetricCipherSuite]
1056
+ # @param config_id [Integer]
1057
+ # @param enc [String]
1058
+ # @param payload_len [Integer]
1059
+ # @param server_name [String]
1060
+ #
1061
+ # @return [TTTLS13::Message::ClientHello]
1062
+ # rubocop: disable Metrics/ParameterLists
1063
+ def new_ch_outer_aad(inner,
1064
+ cipher_suite,
1065
+ config_id,
1066
+ enc,
1067
+ payload_len,
1068
+ server_name)
1069
+ aad_ech = Message::Extension::ECHClientHello.new_outer(
1070
+ cipher_suite: cipher_suite,
1071
+ config_id: config_id,
1072
+ enc: enc,
1073
+ payload: payload_len.zeros
1074
+ )
1075
+ Message::ClientHello.new(
1076
+ legacy_version: inner.legacy_version,
1077
+ legacy_session_id: inner.legacy_session_id,
1078
+ cipher_suites: inner.cipher_suites,
1079
+ legacy_compression_methods: inner.legacy_compression_methods,
1080
+ extensions: inner.extensions.merge(
1081
+ Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => aad_ech,
1082
+ Message::ExtensionType::SERVER_NAME => \
1083
+ Message::Extension::ServerName.new(server_name)
1084
+ )
1085
+ )
1086
+ end
1087
+ # rubocop: enable Metrics/ParameterLists
1088
+
1089
+ # @param aad [TTTLS13::Message::ClientHello]
1090
+ # @param cipher_suite [HpkeSymmetricCipherSuite]
1091
+ # @param config_id [Integer]
1092
+ # @param enc [String]
1093
+ # @param payload [String]
1094
+ #
1095
+ # @return [TTTLS13::Message::ClientHello]
1096
+ def new_ch_outer(aad, cipher_suite, config_id, enc, payload)
1097
+ outer_ech = Message::Extension::ECHClientHello.new_outer(
1098
+ cipher_suite: cipher_suite,
1099
+ config_id: config_id,
1100
+ enc: enc,
1101
+ payload: payload
1102
+ )
1103
+ Message::ClientHello.new(
1104
+ legacy_version: aad.legacy_version,
1105
+ random: aad.random,
1106
+ legacy_session_id: aad.legacy_session_id,
1107
+ cipher_suites: aad.cipher_suites,
1108
+ legacy_compression_methods: aad.legacy_compression_methods,
1109
+ extensions: aad.extensions.merge(
1110
+ Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => outer_ech
1111
+ )
1112
+ )
1113
+ end
1114
+
1115
+ # @param conf [HpkeKeyConfig]
1116
+ #
1117
+ # @return [HpkeSymmetricCipherSuite, nil]
1118
+ def select_ech_hpke_cipher_suite(conf)
1119
+ @settings[:ech_hpke_cipher_suites].find do |cs|
1120
+ conf.cipher_suites.include?(cs)
1121
+ end
1122
+ end
1123
+
1124
+ # @return [Message::Extension::ECHClientHello]
1125
+ def new_grease_ech
1126
+ # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#name-compliance-requirements
1127
+ cipher_suite = HpkeSymmetricCipherSuite.new(
1128
+ HpkeSymmetricCipherSuite::HpkeKdfId.new(
1129
+ TTTLS13::Hpke::KdfId::HKDF_SHA256
1130
+ ),
1131
+ HpkeSymmetricCipherSuite::HpkeAeadId.new(
1132
+ TTTLS13::Hpke::AeadId::AES_128_GCM
1133
+ )
1134
+ )
1135
+ # Set the enc field to a randomly-generated valid encapsulated public key
1136
+ # output by the HPKE KEM.
1137
+ #
1138
+ # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.2-2.3.1
1139
+ public_key = OpenSSL::PKey.read(
1140
+ OpenSSL::PKey.generate_key('X25519').public_to_pem
1141
+ )
1142
+ hpke = HPKE.new(:x25519, :sha256, :sha256, :aes_128_gcm)
1143
+ enc = hpke.setup_base_s(public_key, '')[:enc]
1144
+ # Set the payload field to a randomly-generated string of L+C bytes, where
1145
+ # C is the ciphertext expansion of the selected AEAD scheme and L is the
1146
+ # size of the EncodedClientHelloInner the client would compute when
1147
+ # offering ECH, padded according to Section 6.1.3.
1148
+ #
1149
+ # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.2-2.4.1
1150
+ payload_len = placeholder_encoded_ch_inner_len \
1151
+ + Hpke.aead_id2overhead_len(Hpke::AeadId::AES_128_GCM)
1152
+
1153
+ Message::Extension::ECHClientHello.new_outer(
1154
+ cipher_suite: cipher_suite,
1155
+ config_id: Convert.bin2i(OpenSSL::Random.random_bytes(1)),
1156
+ enc: enc,
1157
+ payload: OpenSSL::Random.random_bytes(payload_len)
1158
+ )
1159
+ end
1160
+
1161
+ # @return [Integer]
1162
+ def placeholder_encoded_ch_inner_len
1163
+ 448
1164
+ end
1165
+
1166
+ # @param inner [TTTLS13::Message::ClientHello]
1167
+ # @param ech [Message::Extension::ECHClientHello]
1168
+ def new_greased_ch(inner, ech)
1169
+ Message::ClientHello.new(
1170
+ legacy_version: inner.legacy_version,
1171
+ random: inner.random,
1172
+ legacy_session_id: inner.legacy_session_id,
1173
+ cipher_suites: inner.cipher_suites,
1174
+ legacy_compression_methods: inner.legacy_compression_methods,
1175
+ extensions: inner.extensions.merge(
1176
+ Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => ech
1177
+ )
1178
+ )
1179
+ end
1180
+
726
1181
  # @return [Integer]
727
1182
  def calc_obfuscated_ticket_age
728
1183
  # the "ticket_lifetime" field in the NewSessionTicket message is
@@ -743,7 +1198,7 @@ module TTTLS13
743
1198
  group = hrr.extensions[Message::ExtensionType::KEY_SHARE]
744
1199
  .key_share_entry.first.group
745
1200
  key_share, priv_keys \
746
- = Message::Extension::KeyShare.gen_ch_key_share([group])
1201
+ = Message::Extension::KeyShare.gen_ch_key_share([group])
747
1202
  exs << key_share
748
1203
  end
749
1204
 
@@ -754,7 +1209,7 @@ module TTTLS13
754
1209
  # MUST copy the contents of the extension received in the
755
1210
  # HelloRetryRequest into a "cookie" extension in the new ClientHello.
756
1211
  #
757
- # https://tools.ietf.org/html/rfc8446#section-4.2.2
1212
+ # https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.2
758
1213
  exs << hrr.extensions[Message::ExtensionType::COOKIE] \
759
1214
  if hrr.extensions.include?(Message::ExtensionType::COOKIE)
760
1215
 
@@ -766,15 +1221,23 @@ module TTTLS13
766
1221
  end
767
1222
 
768
1223
  # NOTE:
769
- # https://tools.ietf.org/html/rfc8446#section-4.1.2
1224
+ # https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2
770
1225
  #
771
1226
  # @param ch1 [TTTLS13::Message::ClientHello]
772
1227
  # @param hrr [TTTLS13::Message::ServerHello]
773
1228
  # @param extensions [TTTLS13::Message::Extensions]
774
1229
  # @param binder_key [String, nil]
1230
+ # @param ech_state [TTTLS13::Client::EchState]
775
1231
  #
776
- # @return [TTTLS13::Message::ClientHello]
777
- def send_new_client_hello(ch1, hrr, extensions, binder_key = nil)
1232
+ # @return [TTTLS13::Message::ClientHello] outer
1233
+ # @return [TTTLS13::Message::ClientHello] inner
1234
+ # rubocop: disable Metrics/AbcSize
1235
+ # rubocop: disable Metrics/MethodLength
1236
+ def send_new_client_hello(ch1,
1237
+ hrr,
1238
+ extensions,
1239
+ binder_key = nil,
1240
+ ech_state = nil)
778
1241
  ch = Message::ClientHello.new(
779
1242
  legacy_version: ch1.legacy_version,
780
1243
  random: ch1.random,
@@ -784,19 +1247,52 @@ module TTTLS13
784
1247
  extensions: extensions
785
1248
  )
786
1249
 
1250
+ # encrypted_client_hello
1251
+ if use_ech? && ech_state.nil?
1252
+ # If sending a second ClientHello in response to a HelloRetryRequest,
1253
+ # the client copies the entire "encrypted_client_hello" extension from
1254
+ # the first ClientHello.
1255
+ #
1256
+ # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.2-3
1257
+ inner = ch.clone
1258
+ ch.extensions[Message::ExtensionType::ENCRYPTED_CLIENT_HELLO] \
1259
+ = ch1.extensions[Message::ExtensionType::ENCRYPTED_CLIENT_HELLO]
1260
+ elsif use_ech?
1261
+ ch, inner = offer_new_ech(ch, ech_state)
1262
+ end
1263
+
787
1264
  # pre_shared_key
788
1265
  #
789
1266
  # Updating the "pre_shared_key" extension if present by recomputing
790
1267
  # the "obfuscated_ticket_age" and binder values.
791
1268
  if ch1.extensions.include?(Message::ExtensionType::PRE_SHARED_KEY)
792
1269
  sign_psk_binder(ch1: ch1, hrr: hrr, ch: ch, binder_key: binder_key)
1270
+
1271
+ if use_ech?
1272
+ # it MUST also copy the "psk_key_exchange_modes" from the
1273
+ # ClientHelloInner into the ClientHelloOuter.
1274
+ ch.extensions[Message::ExtensionType::PSK_KEY_EXCHANGE_MODES] \
1275
+ = inner.extensions[Message::ExtensionType::PSK_KEY_EXCHANGE_MODES]
1276
+ # it MUST also include the "early_data" extension in ClientHelloOuter.
1277
+ ch.extensions[Message::ExtensionType::EARLY_DATA] \
1278
+ = inner.extensions[Message::ExtensionType::EARLY_DATA]
1279
+ sign_grease_psk_binder(
1280
+ ch1: ch1,
1281
+ hrr: hrr,
1282
+ ch_outer: ch,
1283
+ inner_psk: inner.extensions[Message::ExtensionType::PRE_SHARED_KEY],
1284
+ binder_key: binder_key
1285
+ )
1286
+ end
793
1287
  end
794
1288
 
795
1289
  send_handshakes(Message::ContentType::HANDSHAKE, [ch],
796
1290
  Cryptograph::Passer.new)
797
1291
 
798
- ch
1292
+ [ch, inner]
799
1293
  end
1294
+ # rubocop: enable Metrics/AbcSize
1295
+ # rubocop: enable Metrics/MethodLength
800
1296
 
801
1297
  # @raise [TTTLS13::Error::ErrorAlerts]
802
1298
  #
@@ -949,10 +1445,35 @@ module TTTLS13
949
1445
  def process_new_session_ticket(nst)
950
1446
  super(nst)
951
1447
 
952
- rms = @resumption_master_secret
1448
+ rms = @resumption_secret
953
1449
  cs = @cipher_suite
954
1450
  @settings[:process_new_session_ticket]&.call(nst, rms, cs)
955
1451
  end
1452
+
1453
+ class EchState
1454
+ attr_accessor :maximum_name_length
1455
+ attr_accessor :config_id
1456
+ attr_accessor :cipher_suite
1457
+ attr_accessor :public_name
1458
+ attr_accessor :ctx
1459
+
1460
+ # @param maximum_name_length [Integer]
1461
+ # @param config_id [Integer]
1462
+ # @param cipher_suite [HpkeSymmetricCipherSuite]
1463
+ # @param public_name [String]
1464
+ # @param ctx [[HPKE::ContextS]
1465
+ def initialize(maximum_name_length,
1466
+ config_id,
1467
+ cipher_suite,
1468
+ public_name,
1469
+ ctx)
1470
+ @maximum_name_length = maximum_name_length
1471
+ @config_id = config_id
1472
+ @cipher_suite = cipher_suite
1473
+ @public_name = public_name
1474
+ @ctx = ctx
1475
+ end
1476
+ end
956
1477
  end
957
1478
  # rubocop: enable Metrics/ClassLength
958
1479
  end