tttls1.3 0.2.18 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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