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.
- checksums.yaml +4 -4
- data/Gemfile +2 -0
- data/README.md +4 -1
- data/example/helper.rb +5 -2
- data/example/https_client_using_ech.rb +32 -0
- data/example/https_client_using_grease_ech.rb +26 -0
- data/example/https_client_using_grease_psk.rb +66 -0
- data/example/https_client_using_hrr_and_ech.rb +32 -0
- data/lib/tttls1.3/client.rb +534 -24
- data/lib/tttls1.3/connection.rb +3 -0
- data/lib/tttls1.3/cryptograph/aead.rb +1 -1
- data/lib/tttls1.3/error.rb +1 -1
- data/lib/tttls1.3/hpke.rb +91 -0
- data/lib/tttls1.3/key_schedule.rb +71 -3
- data/lib/tttls1.3/message/alert.rb +2 -1
- data/lib/tttls1.3/message/client_hello.rb +2 -1
- data/lib/tttls1.3/message/encrypted_extensions.rb +2 -1
- data/lib/tttls1.3/message/extension/alpn.rb +4 -5
- data/lib/tttls1.3/message/extension/compress_certificate.rb +1 -1
- data/lib/tttls1.3/message/extension/ech.rb +241 -0
- data/lib/tttls1.3/message/extension/server_name.rb +1 -1
- data/lib/tttls1.3/message/extensions.rb +20 -7
- data/lib/tttls1.3/message/record.rb +1 -1
- data/lib/tttls1.3/message/server_hello.rb +3 -5
- data/lib/tttls1.3/message.rb +3 -1
- data/lib/tttls1.3/named_group.rb +1 -1
- data/lib/tttls1.3/server.rb +1 -1
- data/lib/tttls1.3/utils.rb +8 -0
- data/lib/tttls1.3/version.rb +1 -1
- data/lib/tttls1.3.rb +4 -0
- data/spec/client_spec.rb +40 -0
- data/spec/ech_spec.rb +81 -0
- data/spec/spec_helper.rb +41 -5
- data/tttls1.3.gemspec +2 -0
- metadata +38 -2
data/lib/tttls1.3/client.rb
CHANGED
@@ -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://
|
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(
|
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
|
-
|
279
|
-
|
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
|
-
|
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://
|
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
|
-
|
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:
|
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
|
-
|
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://
|
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://
|
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
|
-
|
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
|