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.
- 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
|