tttls1.3 0.2.19 → 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.
@@ -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