tttls1.3 0.2.19 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -69,14 +69,29 @@ module TTTLS13
69
69
  check_certificate_status: false,
70
70
  process_certificate_status: nil,
71
71
  compress_certificate_algorithms: DEFALUT_CH_COMPRESS_CERTIFICATE_ALGORITHMS,
72
+ ech_config: nil,
73
+ ech_hpke_cipher_suites: nil,
72
74
  compatibility_mode: true,
73
75
  sslkeylogfile: nil,
74
76
  loglevel: Logger::WARN
75
77
  }.freeze
76
78
  private_constant :DEFAULT_CLIENT_SETTINGS
77
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
78
90
  # rubocop: disable Metrics/ClassLength
79
91
  class Client < Connection
92
+ HpkeSymmetricCipherSuit = \
93
+ ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite
94
+
80
95
  # @param socket [Socket]
81
96
  # @param hostname [String]
82
97
  # @param settings [Hash]
@@ -99,6 +114,8 @@ module TTTLS13
99
114
 
100
115
  @early_data = ''
101
116
  @succeed_early_data = false
117
+ @retry_configs = []
118
+ @rejected_ech = false
102
119
  raise Error::ConfigError unless valid_settings?
103
120
  end
104
121
 
@@ -134,7 +151,7 @@ module TTTLS13
134
151
  # after here v
135
152
  # CONNECTED
136
153
  #
137
- # https://tools.ietf.org/html/rfc8446#appendix-A.1
154
+ # https://datatracker.ietf.org/doc/html/rfc8446#appendix-A.1
138
155
  #
139
156
  # rubocop: disable Metrics/AbcSize
140
157
  # rubocop: disable Metrics/BlockLength
@@ -164,6 +181,9 @@ module TTTLS13
164
181
  hs_rcipher = nil # TTTLS13::Cryptograph::$Object
165
182
  e_wcipher = nil # TTTLS13::Cryptograph::$Object
166
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
167
187
  unless @settings[:sslkeylogfile].nil?
168
188
  begin
169
189
  sslkeylogfile = SslKeyLogFile::Writer.new(@settings[:sslkeylogfile])
@@ -181,7 +201,10 @@ module TTTLS13
181
201
 
182
202
  extensions, priv_keys = gen_ch_extensions
183
203
  binder_key = (use_psk? ? key_schedule.binder_key_res : nil)
184
- 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
185
208
  transcript[CH] = [ch, ch.serialize]
186
209
  send_ccs if @settings[:compatibility_mode]
187
210
  if use_early_data?
@@ -246,6 +269,8 @@ module TTTLS13
246
269
 
247
270
  ch1, = transcript[CH1] = transcript.delete(CH)
248
271
  hrr, = transcript[HRR] = transcript.delete(SH)
272
+ ch1_outer = ch_outer
273
+ ch_outer = nil
249
274
 
250
275
  # validate cookie
251
276
  diff_sets = sh.extensions.keys - ch1.extensions.keys
@@ -253,7 +278,7 @@ module TTTLS13
253
278
  unless (diff_sets - [Message::ExtensionType::COOKIE]).empty?
254
279
 
255
280
  # validate key_share
256
- # TODO: pre_shared_key
281
+ # TODO: validate pre_shared_key
257
282
  ngl = ch1.extensions[Message::ExtensionType::SUPPORTED_GROUPS]
258
283
  .named_group_list
259
284
  kse = ch1.extensions[Message::ExtensionType::KEY_SHARE]
@@ -267,7 +292,16 @@ module TTTLS13
267
292
  extensions, pk = gen_newch_extensions(ch1, hrr)
268
293
  priv_keys = pk.merge(priv_keys)
269
294
  binder_key = (use_psk? ? key_schedule.binder_key_res : nil)
270
- 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
271
305
  transcript[CH] = [ch, ch.serialize]
272
306
 
273
307
  @state = ClientState::WAIT_SH
@@ -275,8 +309,11 @@ module TTTLS13
275
309
  end
276
310
 
277
311
  # generate shared secret
278
- psk = nil unless sh.extensions
279
- .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
280
317
  ch_ks = ch.extensions[Message::ExtensionType::KEY_SHARE]
281
318
  .key_share_entry.map(&:group)
282
319
  sh_ks = sh.extensions[Message::ExtensionType::KEY_SHARE]
@@ -296,6 +333,27 @@ module TTTLS13
296
333
  cipher_suite: @cipher_suite,
297
334
  transcript: transcript
298
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
+
299
357
  @alert_wcipher = hs_wcipher = gen_cipher(
300
358
  @cipher_suite,
301
359
  key_schedule.client_handshake_write_key,
@@ -332,6 +390,12 @@ module TTTLS13
332
390
  @alpn = ee.extensions[
333
391
  Message::ExtensionType::APPLICATION_LAYER_PROTOCOL_NEGOTIATION
334
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
+
335
399
  @state = ClientState::WAIT_CERT_CR
336
400
  @state = ClientState::WAIT_FINISHED unless psk.nil?
337
401
  when ClientState::WAIT_CERT_CR
@@ -440,6 +504,8 @@ module TTTLS13
440
504
  when ClientState::CONNECTED
441
505
  logger.debug('ClientState::CONNECTED')
442
506
 
507
+ send_alert(:ech_required) \
508
+ if use_ech? && (!@retry_configs.nil? && !@retry_configs.empty?)
443
509
  break
444
510
  end
445
511
  end
@@ -451,6 +517,20 @@ module TTTLS13
451
517
  # rubocop: enable Metrics/MethodLength
452
518
  # rubocop: enable Metrics/PerceivedComplexity
453
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
+
454
534
  # @param binary [String]
455
535
  #
456
536
  # @raise [TTTLS13::Error::ConfigError]
@@ -460,11 +540,23 @@ module TTTLS13
460
540
  @early_data = binary
461
541
  end
462
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
+
463
550
  # @return [Boolean]
464
551
  def succeed_early_data?
465
552
  @succeed_early_data
466
553
  end
467
554
 
555
+ # @return [Boolean]
556
+ def rejected_ech?
557
+ @rejected_ech
558
+ end
559
+
468
560
  # @param res [OpenSSL::OCSP::Response]
469
561
  # @param cert [OpenSSL::X509::Certificate]
470
562
  # @param chain [Array of OpenSSL::X509::Certificate, nil]
@@ -546,6 +638,9 @@ module TTTLS13
546
638
  return false if @settings[:check_certificate_status] &&
547
639
  @settings[:process_certificate_status].nil?
548
640
 
641
+ ehcs = @settings[:ech_hpke_cipher_suites] || []
642
+ return false if !@settings[:ech_config].nil? && ehcs.empty?
643
+
549
644
  true
550
645
  end
551
646
  # rubocop: enable Metrics/AbcSize
@@ -567,6 +662,12 @@ module TTTLS13
567
662
  !(@early_data.nil? || @early_data.empty?)
568
663
  end
569
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
+
570
671
  # @param cipher [TTTLS13::Cryptograph::Aead]
571
672
  def send_early_data(cipher)
572
673
  ap = Message::ApplicationData.new(@early_data)
@@ -665,36 +766,60 @@ module TTTLS13
665
766
  # @param extensions [TTTLS13::Message::Extensions]
666
767
  # @param binder_key [String, nil]
667
768
  #
668
- # @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
669
773
  def send_client_hello(extensions, binder_key = nil)
670
774
  ch = Message::ClientHello.new(
671
775
  cipher_suites: CipherSuites.new(@settings[:cipher_suites]),
672
776
  extensions: extensions
673
777
  )
674
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
675
794
  if use_psk?
676
- # pre_shared_key && psk_key_exchange_modes
677
- #
678
- # In order to use PSKs, clients MUST also send a
679
- # "psk_key_exchange_modes" extension.
680
- #
681
- # https://tools.ietf.org/html/rfc8446#section-4.2.9
682
795
  pkem = Message::Extension::PskKeyExchangeModes.new(
683
796
  [Message::Extension::PskKeyExchangeMode::PSK_DHE_KE]
684
797
  )
685
798
  ch.extensions[Message::ExtensionType::PSK_KEY_EXCHANGE_MODES] = pkem
686
- # at the end, sign PSK binder
799
+ end
800
+
801
+ # pre_shared_key
802
+ # at the end, sign PSK binder
803
+ if use_psk?
687
804
  sign_psk_binder(
688
805
  ch: ch,
689
806
  binder_key: binder_key
690
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
691
815
  end
692
816
 
693
817
  send_handshakes(Message::ContentType::HANDSHAKE, [ch],
694
818
  Cryptograph::Passer.new)
695
819
 
696
- ch
820
+ [ch, inner, ech_state]
697
821
  end
822
+ # rubocop: enable Metrics/MethodLength
698
823
 
699
824
  # @param ch1 [TTTLS13::Message::ClientHello]
700
825
  # @param hrr [TTTLS13::Message::ServerHello]
@@ -709,10 +834,10 @@ module TTTLS13
709
834
  # partial ClientHello up to and including the
710
835
  # PreSharedKeyExtension.identities field.
711
836
  #
712
- # https://tools.ietf.org/html/rfc8446#section-4.2.11.2
837
+ # https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.11.2
713
838
  digest = CipherSuite.digest(@settings[:psk_cipher_suite])
714
839
  hash_len = OpenSSL::Digest.new(digest).digest_length
715
- dummy_binders = ["\x00" * hash_len]
840
+ placeholder_binders = [hash_len.zeros]
716
841
  psk = Message::Extension::PreSharedKey.new(
717
842
  msg_type: Message::HandshakeType::CLIENT_HELLO,
718
843
  offered_psks: Message::Extension::OfferedPsks.new(
@@ -720,7 +845,7 @@ module TTTLS13
720
845
  identity: @settings[:ticket],
721
846
  obfuscated_ticket_age: calc_obfuscated_ticket_age
722
847
  )],
723
- binders: dummy_binders
848
+ binders: placeholder_binders
724
849
  )
725
850
  )
726
851
  ch.extensions[Message::ExtensionType::PRE_SHARED_KEY] = psk
@@ -734,6 +859,325 @@ module TTTLS13
734
859
  )
735
860
  end
736
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
+
737
1181
  # @return [Integer]
738
1182
  def calc_obfuscated_ticket_age
739
1183
  # the "ticket_lifetime" field in the NewSessionTicket message is
@@ -754,7 +1198,7 @@ module TTTLS13
754
1198
  group = hrr.extensions[Message::ExtensionType::KEY_SHARE]
755
1199
  .key_share_entry.first.group
756
1200
  key_share, priv_keys \
757
- = Message::Extension::KeyShare.gen_ch_key_share([group])
1201
+ = Message::Extension::KeyShare.gen_ch_key_share([group])
758
1202
  exs << key_share
759
1203
  end
760
1204
 
@@ -765,7 +1209,7 @@ module TTTLS13
765
1209
  # MUST copy the contents of the extension received in the
766
1210
  # HelloRetryRequest into a "cookie" extension in the new ClientHello.
767
1211
  #
768
- # https://tools.ietf.org/html/rfc8446#section-4.2.2
1212
+ # https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.2
769
1213
  exs << hrr.extensions[Message::ExtensionType::COOKIE] \
770
1214
  if hrr.extensions.include?(Message::ExtensionType::COOKIE)
771
1215
 
@@ -777,15 +1221,23 @@ module TTTLS13
777
1221
  end
778
1222
 
779
1223
  # NOTE:
780
- # https://tools.ietf.org/html/rfc8446#section-4.1.2
1224
+ # https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2
781
1225
  #
782
1226
  # @param ch1 [TTTLS13::Message::ClientHello]
783
1227
  # @param hrr [TTTLS13::Message::ServerHello]
784
1228
  # @param extensions [TTTLS13::Message::Extensions]
785
1229
  # @param binder_key [String, nil]
1230
+ # @param ech_state [TTTLS13::Client::EchState]
786
1231
  #
787
- # @return [TTTLS13::Message::ClientHello]
788
- 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)
789
1241
  ch = Message::ClientHello.new(
790
1242
  legacy_version: ch1.legacy_version,
791
1243
  random: ch1.random,
@@ -795,19 +1247,52 @@ module TTTLS13
795
1247
  extensions: extensions
796
1248
  )
797
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
+
798
1264
  # pre_shared_key
799
1265
  #
800
1266
  # Updating the "pre_shared_key" extension if present by recomputing
801
1267
  # the "obfuscated_ticket_age" and binder values.
802
1268
  if ch1.extensions.include?(Message::ExtensionType::PRE_SHARED_KEY)
803
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
804
1287
  end
805
1288
 
806
1289
  send_handshakes(Message::ContentType::HANDSHAKE, [ch],
807
1290
  Cryptograph::Passer.new)
808
1291
 
809
- ch
1292
+ [ch, inner]
810
1293
  end
1294
+ # rubocop: enable Metrics/AbcSize
1295
+ # rubocop: enable Metrics/MethodLength
811
1296
 
812
1297
  # @raise [TTTLS13::Error::ErrorAlerts]
813
1298
  #
@@ -964,6 +1449,31 @@ module TTTLS13
964
1449
  cs = @cipher_suite
965
1450
  @settings[:process_new_session_ticket]&.call(nst, rms, cs)
966
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
967
1477
  end
968
1478
  # rubocop: enable Metrics/ClassLength
969
1479
  end