tttls1.3 0.2.18 → 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/.github/workflows/ci.yml +8 -5
- data/Gemfile +2 -0
- data/README.md +6 -3
- data/example/helper.rb +5 -2
- data/example/https_client_using_0rtt.rb +1 -1
- 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/example/https_client_using_hrr_and_ticket.rb +1 -1
- data/example/https_client_using_ticket.rb +1 -1
- data/interop/client_spec.rb +3 -2
- data/interop/server_spec.rb +1 -3
- data/interop/{helper.rb → spec_helper.rb} +12 -5
- data/lib/tttls1.3/client.rb +553 -32
- data/lib/tttls1.3/connection.rb +9 -8
- 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 +111 -8
- 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/key_share.rb +2 -4
- 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 +2 -2
- 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/connection_spec.rb +22 -7
- data/spec/ech_spec.rb +81 -0
- data/spec/extensions_spec.rb +1 -2
- data/spec/key_schedule_spec.rb +2 -2
- data/spec/server_spec.rb +22 -7
- data/spec/spec_helper.rb +41 -5
- data/tttls1.3.gemspec +2 -0
- metadata +39 -3
data/lib/tttls1.3/client.rb
CHANGED
@@ -58,7 +58,9 @@ module TTTLS13
|
|
58
58
|
alpn: nil,
|
59
59
|
process_new_session_ticket: nil,
|
60
60
|
ticket: nil,
|
61
|
+
# @deprecated Please use `resumption_secret` instead
|
61
62
|
resumption_master_secret: nil,
|
63
|
+
resumption_secret: nil,
|
62
64
|
psk_cipher_suite: nil,
|
63
65
|
ticket_nonce: nil,
|
64
66
|
ticket_age_add: nil,
|
@@ -67,14 +69,29 @@ module TTTLS13
|
|
67
69
|
check_certificate_status: false,
|
68
70
|
process_certificate_status: nil,
|
69
71
|
compress_certificate_algorithms: DEFALUT_CH_COMPRESS_CERTIFICATE_ALGORITHMS,
|
72
|
+
ech_config: nil,
|
73
|
+
ech_hpke_cipher_suites: nil,
|
70
74
|
compatibility_mode: true,
|
71
75
|
sslkeylogfile: nil,
|
72
76
|
loglevel: Logger::WARN
|
73
77
|
}.freeze
|
74
78
|
private_constant :DEFAULT_CLIENT_SETTINGS
|
75
79
|
|
80
|
+
STANDARD_CLIENT_ECH_HPKE_SYMMETRIC_CIPHER_SUITES = [
|
81
|
+
HpkeSymmetricCipherSuite.new(
|
82
|
+
HpkeSymmetricCipherSuite::HpkeKdfId.new(
|
83
|
+
Hpke::KdfId::HKDF_SHA256
|
84
|
+
),
|
85
|
+
HpkeSymmetricCipherSuite::HpkeAeadId.new(
|
86
|
+
Hpke::AeadId::AES_128_GCM
|
87
|
+
)
|
88
|
+
)
|
89
|
+
].freeze
|
76
90
|
# rubocop: disable Metrics/ClassLength
|
77
91
|
class Client < Connection
|
92
|
+
HpkeSymmetricCipherSuit = \
|
93
|
+
ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite
|
94
|
+
|
78
95
|
# @param socket [Socket]
|
79
96
|
# @param hostname [String]
|
80
97
|
# @param settings [Hash]
|
@@ -84,10 +101,21 @@ module TTTLS13
|
|
84
101
|
@endpoint = :client
|
85
102
|
@hostname = hostname
|
86
103
|
@settings = DEFAULT_CLIENT_SETTINGS.merge(settings)
|
104
|
+
# NOTE: backward compatibility
|
105
|
+
if @settings[:resumption_secret].nil? &&
|
106
|
+
!@settings[:resumption_master_secret].nil?
|
107
|
+
@settings[:resumption_secret] =
|
108
|
+
@settings.delete(:resumption_master_secret) \
|
109
|
+
end
|
110
|
+
raise Error::ConfigError if @settings[:resumption_secret] !=
|
111
|
+
@settings[:resumption_master_secret]
|
112
|
+
|
87
113
|
logger.level = @settings[:loglevel]
|
88
114
|
|
89
115
|
@early_data = ''
|
90
116
|
@succeed_early_data = false
|
117
|
+
@retry_configs = []
|
118
|
+
@rejected_ech = false
|
91
119
|
raise Error::ConfigError unless valid_settings?
|
92
120
|
end
|
93
121
|
|
@@ -123,7 +151,7 @@ module TTTLS13
|
|
123
151
|
# after here v
|
124
152
|
# CONNECTED
|
125
153
|
#
|
126
|
-
# https://
|
154
|
+
# https://datatracker.ietf.org/doc/html/rfc8446#appendix-A.1
|
127
155
|
#
|
128
156
|
# rubocop: disable Metrics/AbcSize
|
129
157
|
# rubocop: disable Metrics/BlockLength
|
@@ -137,7 +165,7 @@ module TTTLS13
|
|
137
165
|
priv_keys = {} # Hash of NamedGroup => OpenSSL::PKey::$Object
|
138
166
|
if use_psk?
|
139
167
|
psk = gen_psk_from_nst(
|
140
|
-
@settings[:
|
168
|
+
@settings[:resumption_secret],
|
141
169
|
@settings[:ticket_nonce],
|
142
170
|
CipherSuite.digest(@settings[:psk_cipher_suite])
|
143
171
|
)
|
@@ -153,6 +181,9 @@ module TTTLS13
|
|
153
181
|
hs_rcipher = nil # TTTLS13::Cryptograph::$Object
|
154
182
|
e_wcipher = nil # TTTLS13::Cryptograph::$Object
|
155
183
|
sslkeylogfile = nil # TTTLS13::SslKeyLogFile::Writer
|
184
|
+
ch1_outer = nil # TTTLS13::Message::ClientHello for rejected ECH
|
185
|
+
ch_outer = nil # TTTLS13::Message::ClientHello for rejected ECH
|
186
|
+
ech_state = nil # TTTLS13::Client::EchState for ECH with HRR
|
156
187
|
unless @settings[:sslkeylogfile].nil?
|
157
188
|
begin
|
158
189
|
sslkeylogfile = SslKeyLogFile::Writer.new(@settings[:sslkeylogfile])
|
@@ -170,7 +201,10 @@ module TTTLS13
|
|
170
201
|
|
171
202
|
extensions, priv_keys = gen_ch_extensions
|
172
203
|
binder_key = (use_psk? ? key_schedule.binder_key_res : nil)
|
173
|
-
ch = send_client_hello(extensions, binder_key)
|
204
|
+
ch, inner, ech_state = send_client_hello(extensions, binder_key)
|
205
|
+
ch_outer = ch
|
206
|
+
# use ClientHelloInner messages for the transcript hash
|
207
|
+
ch = inner.nil? ? ch : inner
|
174
208
|
transcript[CH] = [ch, ch.serialize]
|
175
209
|
send_ccs if @settings[:compatibility_mode]
|
176
210
|
if use_early_data?
|
@@ -235,6 +269,8 @@ module TTTLS13
|
|
235
269
|
|
236
270
|
ch1, = transcript[CH1] = transcript.delete(CH)
|
237
271
|
hrr, = transcript[HRR] = transcript.delete(SH)
|
272
|
+
ch1_outer = ch_outer
|
273
|
+
ch_outer = nil
|
238
274
|
|
239
275
|
# validate cookie
|
240
276
|
diff_sets = sh.extensions.keys - ch1.extensions.keys
|
@@ -242,7 +278,7 @@ module TTTLS13
|
|
242
278
|
unless (diff_sets - [Message::ExtensionType::COOKIE]).empty?
|
243
279
|
|
244
280
|
# validate key_share
|
245
|
-
# TODO: pre_shared_key
|
281
|
+
# TODO: validate pre_shared_key
|
246
282
|
ngl = ch1.extensions[Message::ExtensionType::SUPPORTED_GROUPS]
|
247
283
|
.named_group_list
|
248
284
|
kse = ch1.extensions[Message::ExtensionType::KEY_SHARE]
|
@@ -256,7 +292,16 @@ module TTTLS13
|
|
256
292
|
extensions, pk = gen_newch_extensions(ch1, hrr)
|
257
293
|
priv_keys = pk.merge(priv_keys)
|
258
294
|
binder_key = (use_psk? ? key_schedule.binder_key_res : nil)
|
259
|
-
ch = send_new_client_hello(
|
295
|
+
ch, inner = send_new_client_hello(
|
296
|
+
ch1,
|
297
|
+
hrr,
|
298
|
+
extensions,
|
299
|
+
binder_key,
|
300
|
+
ech_state
|
301
|
+
)
|
302
|
+
# use ClientHelloInner messages for the transcript hash
|
303
|
+
ch_outer = ch
|
304
|
+
ch = inner.nil? ? ch : inner
|
260
305
|
transcript[CH] = [ch, ch.serialize]
|
261
306
|
|
262
307
|
@state = ClientState::WAIT_SH
|
@@ -264,8 +309,11 @@ module TTTLS13
|
|
264
309
|
end
|
265
310
|
|
266
311
|
# generate shared secret
|
267
|
-
|
268
|
-
|
312
|
+
if sh.extensions.include?(Message::ExtensionType::PRE_SHARED_KEY)
|
313
|
+
# TODO: validate pre_shared_key
|
314
|
+
else
|
315
|
+
psk = nil
|
316
|
+
end
|
269
317
|
ch_ks = ch.extensions[Message::ExtensionType::KEY_SHARE]
|
270
318
|
.key_share_entry.map(&:group)
|
271
319
|
sh_ks = sh.extensions[Message::ExtensionType::KEY_SHARE]
|
@@ -285,6 +333,27 @@ module TTTLS13
|
|
285
333
|
cipher_suite: @cipher_suite,
|
286
334
|
transcript: transcript
|
287
335
|
)
|
336
|
+
|
337
|
+
# rejected ECH
|
338
|
+
# NOTE: It can compute (hrr_)accept_ech until client selects the
|
339
|
+
# cipher_suite.
|
340
|
+
if !sh.hrr? && use_ech?
|
341
|
+
if !transcript.include?(HRR) && !key_schedule.accept_ech?
|
342
|
+
# 1sh SH
|
343
|
+
transcript[CH] = [ch_outer, ch_outer.serialize]
|
344
|
+
@rejected_ech = true
|
345
|
+
elsif transcript.include?(HRR) &&
|
346
|
+
key_schedule.hrr_accept_ech? != key_schedule.accept_ech?
|
347
|
+
# 2nd SH
|
348
|
+
terminate(:illegal_parameter)
|
349
|
+
elsif transcript.include?(HRR) && !key_schedule.hrr_accept_ech?
|
350
|
+
# 2nd SH
|
351
|
+
transcript[CH1] = [ch1_outer, ch1_outer.serialize]
|
352
|
+
transcript[CH] = [ch_outer, ch_outer.serialize]
|
353
|
+
@rejected_ech = true
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
288
357
|
@alert_wcipher = hs_wcipher = gen_cipher(
|
289
358
|
@cipher_suite,
|
290
359
|
key_schedule.client_handshake_write_key,
|
@@ -321,6 +390,12 @@ module TTTLS13
|
|
321
390
|
@alpn = ee.extensions[
|
322
391
|
Message::ExtensionType::APPLICATION_LAYER_PROTOCOL_NEGOTIATION
|
323
392
|
]&.protocol_name_list&.first
|
393
|
+
@retry_configs = ee.extensions[
|
394
|
+
Message::ExtensionType::ENCRYPTED_CLIENT_HELLO
|
395
|
+
]&.retry_configs
|
396
|
+
terminate(:unsupported_extension) \
|
397
|
+
if !rejected_ech? && !@retry_configs.nil?
|
398
|
+
|
324
399
|
@state = ClientState::WAIT_CERT_CR
|
325
400
|
@state = ClientState::WAIT_FINISHED unless psk.nil?
|
326
401
|
when ClientState::WAIT_CERT_CR
|
@@ -423,12 +498,14 @@ module TTTLS13
|
|
423
498
|
transcript[CH].first.random,
|
424
499
|
key_schedule.server_application_traffic_secret
|
425
500
|
)
|
426
|
-
@
|
427
|
-
@
|
501
|
+
@exporter_secret = key_schedule.exporter_secret
|
502
|
+
@resumption_secret = key_schedule.resumption_secret
|
428
503
|
@state = ClientState::CONNECTED
|
429
504
|
when ClientState::CONNECTED
|
430
505
|
logger.debug('ClientState::CONNECTED')
|
431
506
|
|
507
|
+
send_alert(:ech_required) \
|
508
|
+
if use_ech? && (!@retry_configs.nil? && !@retry_configs.empty?)
|
432
509
|
break
|
433
510
|
end
|
434
511
|
end
|
@@ -440,6 +517,20 @@ module TTTLS13
|
|
440
517
|
# rubocop: enable Metrics/MethodLength
|
441
518
|
# rubocop: enable Metrics/PerceivedComplexity
|
442
519
|
|
520
|
+
# @param binary [String]
|
521
|
+
def write(binary)
|
522
|
+
# the client can regard ECH as securely disabled by the server, and it
|
523
|
+
# SHOULD retry the handshake with a new transport connection and ECH
|
524
|
+
# disabled.
|
525
|
+
if !@retry_configs.nil? && !@retry_configs.empty?
|
526
|
+
msg = 'SHOULD retry the handshake with a new transport connection'
|
527
|
+
logger.warn(msg)
|
528
|
+
return
|
529
|
+
end
|
530
|
+
|
531
|
+
super(binary)
|
532
|
+
end
|
533
|
+
|
443
534
|
# @param binary [String]
|
444
535
|
#
|
445
536
|
# @raise [TTTLS13::Error::ConfigError]
|
@@ -449,11 +540,23 @@ module TTTLS13
|
|
449
540
|
@early_data = binary
|
450
541
|
end
|
451
542
|
|
543
|
+
# @return [Array of ECHConfig]
|
544
|
+
def retry_configs
|
545
|
+
@retry_configs.filter do |c|
|
546
|
+
SUPPORTED_ECHCONFIG_VERSIONS.include?(c.version)
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
452
550
|
# @return [Boolean]
|
453
551
|
def succeed_early_data?
|
454
552
|
@succeed_early_data
|
455
553
|
end
|
456
554
|
|
555
|
+
# @return [Boolean]
|
556
|
+
def rejected_ech?
|
557
|
+
@rejected_ech
|
558
|
+
end
|
559
|
+
|
457
560
|
# @param res [OpenSSL::OCSP::Response]
|
458
561
|
# @param cert [OpenSSL::X509::Certificate]
|
459
562
|
# @param chain [Array of OpenSSL::X509::Certificate, nil]
|
@@ -535,6 +638,9 @@ module TTTLS13
|
|
535
638
|
return false if @settings[:check_certificate_status] &&
|
536
639
|
@settings[:process_certificate_status].nil?
|
537
640
|
|
641
|
+
ehcs = @settings[:ech_hpke_cipher_suites] || []
|
642
|
+
return false if !@settings[:ech_config].nil? && ehcs.empty?
|
643
|
+
|
538
644
|
true
|
539
645
|
end
|
540
646
|
# rubocop: enable Metrics/AbcSize
|
@@ -544,7 +650,7 @@ module TTTLS13
|
|
544
650
|
# @return [Boolean]
|
545
651
|
def use_psk?
|
546
652
|
!@settings[:ticket].nil? &&
|
547
|
-
!@settings[:
|
653
|
+
!@settings[:resumption_secret].nil? &&
|
548
654
|
!@settings[:psk_cipher_suite].nil? &&
|
549
655
|
!@settings[:ticket_nonce].nil? &&
|
550
656
|
!@settings[:ticket_age_add].nil? &&
|
@@ -556,6 +662,12 @@ module TTTLS13
|
|
556
662
|
!(@early_data.nil? || @early_data.empty?)
|
557
663
|
end
|
558
664
|
|
665
|
+
# @return [Boolean]
|
666
|
+
def use_ech?
|
667
|
+
!@settings[:ech_hpke_cipher_suites].nil? &&
|
668
|
+
!@settings[:ech_hpke_cipher_suites].empty?
|
669
|
+
end
|
670
|
+
|
559
671
|
# @param cipher [TTTLS13::Cryptograph::Aead]
|
560
672
|
def send_early_data(cipher)
|
561
673
|
ap = Message::ApplicationData.new(@early_data)
|
@@ -568,14 +680,14 @@ module TTTLS13
|
|
568
680
|
send_record(ap_record)
|
569
681
|
end
|
570
682
|
|
571
|
-
# @param
|
683
|
+
# @param resumption_secret [String]
|
572
684
|
# @param ticket_nonce [String]
|
573
685
|
# @param digest [String] name of digest algorithm
|
574
686
|
#
|
575
687
|
# @return [String]
|
576
|
-
def gen_psk_from_nst(
|
688
|
+
def gen_psk_from_nst(resumption_secret, ticket_nonce, digest)
|
577
689
|
hash_len = OpenSSL::Digest.new(digest).digest_length
|
578
|
-
KeySchedule.hkdf_expand_label(
|
690
|
+
KeySchedule.hkdf_expand_label(resumption_secret, 'resumption',
|
579
691
|
ticket_nonce, hash_len, digest)
|
580
692
|
end
|
581
693
|
|
@@ -654,36 +766,60 @@ module TTTLS13
|
|
654
766
|
# @param extensions [TTTLS13::Message::Extensions]
|
655
767
|
# @param binder_key [String, nil]
|
656
768
|
#
|
657
|
-
# @return [TTTLS13::Message::ClientHello]
|
769
|
+
# @return [TTTLS13::Message::ClientHello] outer
|
770
|
+
# @return [TTTLS13::Message::ClientHello] inner
|
771
|
+
# @return [TTTLS13::Client::EchState]
|
772
|
+
# rubocop: disable Metrics/MethodLength
|
658
773
|
def send_client_hello(extensions, binder_key = nil)
|
659
774
|
ch = Message::ClientHello.new(
|
660
775
|
cipher_suites: CipherSuites.new(@settings[:cipher_suites]),
|
661
776
|
extensions: extensions
|
662
777
|
)
|
663
778
|
|
779
|
+
# encrypted_client_hello
|
780
|
+
inner = nil # TTTLS13::Message::ClientHello
|
781
|
+
if use_ech?
|
782
|
+
inner = ch
|
783
|
+
inner_ech = Message::Extension::ECHClientHello.new_inner
|
784
|
+
inner.extensions[Message::ExtensionType::ENCRYPTED_CLIENT_HELLO] \
|
785
|
+
= inner_ech
|
786
|
+
ch, inner, ech_state = offer_ech(inner, @settings[:ech_config])
|
787
|
+
end
|
788
|
+
|
789
|
+
# psk_key_exchange_modes
|
790
|
+
# In order to use PSKs, clients MUST also send a
|
791
|
+
# "psk_key_exchange_modes" extension.
|
792
|
+
#
|
793
|
+
# https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.9
|
664
794
|
if use_psk?
|
665
|
-
# pre_shared_key && psk_key_exchange_modes
|
666
|
-
#
|
667
|
-
# In order to use PSKs, clients MUST also send a
|
668
|
-
# "psk_key_exchange_modes" extension.
|
669
|
-
#
|
670
|
-
# https://tools.ietf.org/html/rfc8446#section-4.2.9
|
671
795
|
pkem = Message::Extension::PskKeyExchangeModes.new(
|
672
796
|
[Message::Extension::PskKeyExchangeMode::PSK_DHE_KE]
|
673
797
|
)
|
674
798
|
ch.extensions[Message::ExtensionType::PSK_KEY_EXCHANGE_MODES] = pkem
|
675
|
-
|
799
|
+
end
|
800
|
+
|
801
|
+
# pre_shared_key
|
802
|
+
# at the end, sign PSK binder
|
803
|
+
if use_psk?
|
676
804
|
sign_psk_binder(
|
677
805
|
ch: ch,
|
678
806
|
binder_key: binder_key
|
679
807
|
)
|
808
|
+
|
809
|
+
if use_ech?
|
810
|
+
sign_grease_psk_binder(
|
811
|
+
ch_outer: ch,
|
812
|
+
inner_pks: inner.extensions[Message::ExtensionType::PRE_SHARED_KEY]
|
813
|
+
)
|
814
|
+
end
|
680
815
|
end
|
681
816
|
|
682
817
|
send_handshakes(Message::ContentType::HANDSHAKE, [ch],
|
683
818
|
Cryptograph::Passer.new)
|
684
819
|
|
685
|
-
ch
|
820
|
+
[ch, inner, ech_state]
|
686
821
|
end
|
822
|
+
# rubocop: enable Metrics/MethodLength
|
687
823
|
|
688
824
|
# @param ch1 [TTTLS13::Message::ClientHello]
|
689
825
|
# @param hrr [TTTLS13::Message::ServerHello]
|
@@ -698,10 +834,10 @@ module TTTLS13
|
|
698
834
|
# partial ClientHello up to and including the
|
699
835
|
# PreSharedKeyExtension.identities field.
|
700
836
|
#
|
701
|
-
# https://
|
837
|
+
# https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.11.2
|
702
838
|
digest = CipherSuite.digest(@settings[:psk_cipher_suite])
|
703
839
|
hash_len = OpenSSL::Digest.new(digest).digest_length
|
704
|
-
|
840
|
+
placeholder_binders = [hash_len.zeros]
|
705
841
|
psk = Message::Extension::PreSharedKey.new(
|
706
842
|
msg_type: Message::HandshakeType::CLIENT_HELLO,
|
707
843
|
offered_psks: Message::Extension::OfferedPsks.new(
|
@@ -709,7 +845,7 @@ module TTTLS13
|
|
709
845
|
identity: @settings[:ticket],
|
710
846
|
obfuscated_ticket_age: calc_obfuscated_ticket_age
|
711
847
|
)],
|
712
|
-
binders:
|
848
|
+
binders: placeholder_binders
|
713
849
|
)
|
714
850
|
)
|
715
851
|
ch.extensions[Message::ExtensionType::PRE_SHARED_KEY] = psk
|
@@ -723,6 +859,325 @@ module TTTLS13
|
|
723
859
|
)
|
724
860
|
end
|
725
861
|
|
862
|
+
# @param ch1 [TTTLS13::Message::ClientHello]
|
863
|
+
# @param hrr [TTTLS13::Message::ServerHello]
|
864
|
+
# @param ch_outer [TTTLS13::Message::ClientHello]
|
865
|
+
# @param inner_psk [Message::Extension::PreSharedKey]
|
866
|
+
# @param binder_key [String]
|
867
|
+
#
|
868
|
+
# @return [String]
|
869
|
+
def sign_grease_psk_binder(ch1: nil,
|
870
|
+
hrr: nil,
|
871
|
+
ch_outer:,
|
872
|
+
inner_psk:,
|
873
|
+
binder_key:)
|
874
|
+
digest = CipherSuite.digest(@settings[:psk_cipher_suite])
|
875
|
+
hash_len = OpenSSL::Digest.new(digest).digest_length
|
876
|
+
placeholder_binders = [hash_len.zeros]
|
877
|
+
# For each PSK identity advertised in the ClientHelloInner, the client
|
878
|
+
# generates a random PSK identity with the same length. It also generates
|
879
|
+
# a random, 32-bit, unsigned integer to use as the obfuscated_ticket_age.
|
880
|
+
# Likewise, for each inner PSK binder, the client generates a random
|
881
|
+
# string of the same length.
|
882
|
+
#
|
883
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.1.2-2
|
884
|
+
identity = inner_psk.offered_psks
|
885
|
+
.identities
|
886
|
+
.first
|
887
|
+
.identity
|
888
|
+
.length
|
889
|
+
.then { |len| OpenSSL::Random.random_bytes(len) }
|
890
|
+
ota = OpenSSL::Random.random_bytes(4)
|
891
|
+
psk = Message::Extension::PreSharedKey.new(
|
892
|
+
msg_type: Message::HandshakeType::CLIENT_HELLO,
|
893
|
+
offered_psks: Message::Extension::OfferedPsks.new(
|
894
|
+
identities: [Message::Extension::PskIdentity.new(
|
895
|
+
identity: identity,
|
896
|
+
obfuscated_ticket_age: ota
|
897
|
+
)],
|
898
|
+
binders: placeholder_binders
|
899
|
+
)
|
900
|
+
)
|
901
|
+
ch_outer.extensions[Message::ExtensionType::PRE_SHARED_KEY] = psk
|
902
|
+
|
903
|
+
psk.offered_psks.binders[0] = do_sign_psk_binder(
|
904
|
+
ch1: ch1,
|
905
|
+
hrr: hrr,
|
906
|
+
ch: ch_outer,
|
907
|
+
binder_key: binder_key,
|
908
|
+
digest: digest
|
909
|
+
)
|
910
|
+
end
|
911
|
+
|
912
|
+
# @param inner [TTTLS13::Message::ClientHello]
|
913
|
+
# @param ech_config [ECHConfig]
|
914
|
+
#
|
915
|
+
# @return [TTTLS13::Message::ClientHello]
|
916
|
+
# @return [TTTLS13::Message::ClientHello]
|
917
|
+
# @return [TTTLS13::Client::EchState]
|
918
|
+
# rubocop: disable Metrics/AbcSize
|
919
|
+
# rubocop: disable Metrics/MethodLength
|
920
|
+
def offer_ech(inner, ech_config)
|
921
|
+
return [new_greased_ch(inner, new_grease_ech), nil, nil] \
|
922
|
+
if ech_config.nil? ||
|
923
|
+
!SUPPORTED_ECHCONFIG_VERSIONS.include?(ech_config.version)
|
924
|
+
|
925
|
+
# Encrypted ClientHello Configuration
|
926
|
+
public_name = ech_config.echconfig_contents.public_name
|
927
|
+
key_config = ech_config.echconfig_contents.key_config
|
928
|
+
public_key = key_config.public_key.opaque
|
929
|
+
kem_id = key_config&.kem_id&.uint16
|
930
|
+
config_id = key_config.config_id
|
931
|
+
cipher_suite = select_ech_hpke_cipher_suite(key_config)
|
932
|
+
overhead_len = Hpke.aead_id2overhead_len(cipher_suite&.aead_id&.uint16)
|
933
|
+
aead_cipher = Hpke.aead_id2aead_cipher(cipher_suite&.aead_id&.uint16)
|
934
|
+
kdf_hash = Hpke.kdf_id2kdf_hash(cipher_suite&.kdf_id&.uint16)
|
935
|
+
return [new_greased_ch(inner, new_grease_ech), nil, nil] \
|
936
|
+
if [kem_id, overhead_len, aead_cipher, kdf_hash].any?(&:nil?)
|
937
|
+
|
938
|
+
kem_curve_name, kem_hash = Hpke.kem_id2dhkem(kem_id)
|
939
|
+
dhkem = Hpke.kem_curve_name2dhkem(kem_curve_name)
|
940
|
+
pkr = dhkem&.new(kem_hash)&.deserialize_public_key(public_key)
|
941
|
+
return [new_greased_ch(inner, new_grease_ech), nil, nil] if pkr.nil?
|
942
|
+
|
943
|
+
hpke = HPKE.new(kem_curve_name, kem_hash, kdf_hash, aead_cipher)
|
944
|
+
base_s = hpke.setup_base_s(pkr, "tls ech\x00" + ech_config.encode)
|
945
|
+
enc = base_s[:enc]
|
946
|
+
ctx = base_s[:context_s]
|
947
|
+
mnl = ech_config.echconfig_contents.maximum_name_length
|
948
|
+
encoded = encode_ch_inner(inner, mnl)
|
949
|
+
|
950
|
+
# Encoding the ClientHelloInner
|
951
|
+
aad = new_ch_outer_aad(
|
952
|
+
inner,
|
953
|
+
cipher_suite,
|
954
|
+
config_id,
|
955
|
+
enc,
|
956
|
+
encoded.length + overhead_len,
|
957
|
+
public_name
|
958
|
+
)
|
959
|
+
# Authenticating the ClientHelloOuter
|
960
|
+
# which does not include the Handshake structure's four byte header.
|
961
|
+
outer = new_ch_outer(
|
962
|
+
aad,
|
963
|
+
cipher_suite,
|
964
|
+
config_id,
|
965
|
+
enc,
|
966
|
+
ctx.seal(aad.serialize[4..], encoded)
|
967
|
+
)
|
968
|
+
|
969
|
+
ech_state = EchState.new(mnl, config_id, cipher_suite, public_name, ctx)
|
970
|
+
[outer, inner, ech_state]
|
971
|
+
end
|
972
|
+
# rubocop: enable Metrics/AbcSize
|
973
|
+
# rubocop: enable Metrics/MethodLength
|
974
|
+
|
975
|
+
# @param inner [TTTLS13::Message::ClientHello]
|
976
|
+
# @param ech_state [TTTLS13::Client::EchState]
|
977
|
+
#
|
978
|
+
# @return [TTTLS13::Message::ClientHello]
|
979
|
+
# @return [TTTLS13::Message::ClientHello]
|
980
|
+
def offer_new_ech(inner, ech_state)
|
981
|
+
encoded = encode_ch_inner(inner, ech_state.maximum_name_length)
|
982
|
+
overhead_len \
|
983
|
+
= Hpke.aead_id2overhead_len(ech_state.cipher_suite.aead_id.uint16)
|
984
|
+
|
985
|
+
# It encrypts EncodedClientHelloInner as described in Section 6.1.1, using
|
986
|
+
# the second partial ClientHelloOuterAAD, to obtain a second
|
987
|
+
# ClientHelloOuter. It reuses the original HPKE encryption context
|
988
|
+
# computed in Section 6.1 and uses the empty string for enc.
|
989
|
+
#
|
990
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.1.5-4.4.1
|
991
|
+
aad = new_ch_outer_aad(
|
992
|
+
inner,
|
993
|
+
ech_state.cipher_suite,
|
994
|
+
ech_state.config_id,
|
995
|
+
'',
|
996
|
+
encoded.length + overhead_len,
|
997
|
+
ech_state.public_name
|
998
|
+
)
|
999
|
+
# Authenticating the ClientHelloOuter
|
1000
|
+
# which does not include the Handshake structure's four byte header.
|
1001
|
+
outer = new_ch_outer(
|
1002
|
+
aad,
|
1003
|
+
ech_state.cipher_suite,
|
1004
|
+
ech_state.config_id,
|
1005
|
+
'',
|
1006
|
+
ech_state.ctx.seal(aad.serialize[4..], encoded)
|
1007
|
+
)
|
1008
|
+
|
1009
|
+
[outer, inner]
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
# @param inner [TTTLS13::Message::ClientHello]
|
1013
|
+
# @param maximum_name_length [Integer]
|
1014
|
+
#
|
1015
|
+
# @return [String] EncodedClientHelloInner
|
1016
|
+
def encode_ch_inner(inner, maximum_name_length)
|
1017
|
+
# TODO: ech_outer_extensions
|
1018
|
+
encoded = Message::ClientHello.new(
|
1019
|
+
legacy_version: inner.legacy_version,
|
1020
|
+
random: inner.random,
|
1021
|
+
legacy_session_id: '',
|
1022
|
+
cipher_suites: inner.cipher_suites,
|
1023
|
+
legacy_compression_methods: inner.legacy_compression_methods,
|
1024
|
+
extensions: inner.extensions
|
1025
|
+
)
|
1026
|
+
server_name_length = \
|
1027
|
+
inner.extensions[Message::ExtensionType::SERVER_NAME].server_name.length
|
1028
|
+
|
1029
|
+
# which does not include the Handshake structure's four byte header.
|
1030
|
+
padding_encoded_ch_inner(
|
1031
|
+
encoded.serialize[4..],
|
1032
|
+
server_name_length,
|
1033
|
+
maximum_name_length
|
1034
|
+
)
|
1035
|
+
end
|
1036
|
+
|
1037
|
+
# @param s [String]
|
1038
|
+
# @param server_name_length [Integer]
|
1039
|
+
# @param maximum_name_length [Integer]
|
1040
|
+
#
|
1041
|
+
# @return [String]
|
1042
|
+
def padding_encoded_ch_inner(s, server_name_length, maximum_name_length)
|
1043
|
+
padding_len =
|
1044
|
+
if server_name_length.positive?
|
1045
|
+
[maximum_name_length - server_name_length, 0].max
|
1046
|
+
else
|
1047
|
+
9 + maximum_name_length
|
1048
|
+
end
|
1049
|
+
|
1050
|
+
padding_len = 31 - ((s.length + padding_len - 1) % 32)
|
1051
|
+
s + padding_len.zeros
|
1052
|
+
end
|
1053
|
+
|
1054
|
+
# @param inner [TTTLS13::Message::ClientHello]
|
1055
|
+
# @param cipher_suite [HpkeSymmetricCipherSuite]
|
1056
|
+
# @param config_id [Integer]
|
1057
|
+
# @param enc [String]
|
1058
|
+
# @param payload_len [Integer]
|
1059
|
+
# @param server_name [String]
|
1060
|
+
#
|
1061
|
+
# @return [TTTLS13::Message::ClientHello]
|
1062
|
+
# rubocop: disable Metrics/ParameterLists
|
1063
|
+
def new_ch_outer_aad(inner,
|
1064
|
+
cipher_suite,
|
1065
|
+
config_id,
|
1066
|
+
enc,
|
1067
|
+
payload_len,
|
1068
|
+
server_name)
|
1069
|
+
aad_ech = Message::Extension::ECHClientHello.new_outer(
|
1070
|
+
cipher_suite: cipher_suite,
|
1071
|
+
config_id: config_id,
|
1072
|
+
enc: enc,
|
1073
|
+
payload: payload_len.zeros
|
1074
|
+
)
|
1075
|
+
Message::ClientHello.new(
|
1076
|
+
legacy_version: inner.legacy_version,
|
1077
|
+
legacy_session_id: inner.legacy_session_id,
|
1078
|
+
cipher_suites: inner.cipher_suites,
|
1079
|
+
legacy_compression_methods: inner.legacy_compression_methods,
|
1080
|
+
extensions: inner.extensions.merge(
|
1081
|
+
Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => aad_ech,
|
1082
|
+
Message::ExtensionType::SERVER_NAME => \
|
1083
|
+
Message::Extension::ServerName.new(server_name)
|
1084
|
+
)
|
1085
|
+
)
|
1086
|
+
end
|
1087
|
+
# rubocop: enable Metrics/ParameterLists
|
1088
|
+
|
1089
|
+
# @param aad [TTTLS13::Message::ClientHello]
|
1090
|
+
# @param cipher_suite [HpkeSymmetricCipherSuite]
|
1091
|
+
# @param config_id [Integer]
|
1092
|
+
# @param enc [String]
|
1093
|
+
# @param payload [String]
|
1094
|
+
#
|
1095
|
+
# @return [TTTLS13::Message::ClientHello]
|
1096
|
+
def new_ch_outer(aad, cipher_suite, config_id, enc, payload)
|
1097
|
+
outer_ech = Message::Extension::ECHClientHello.new_outer(
|
1098
|
+
cipher_suite: cipher_suite,
|
1099
|
+
config_id: config_id,
|
1100
|
+
enc: enc,
|
1101
|
+
payload: payload
|
1102
|
+
)
|
1103
|
+
Message::ClientHello.new(
|
1104
|
+
legacy_version: aad.legacy_version,
|
1105
|
+
random: aad.random,
|
1106
|
+
legacy_session_id: aad.legacy_session_id,
|
1107
|
+
cipher_suites: aad.cipher_suites,
|
1108
|
+
legacy_compression_methods: aad.legacy_compression_methods,
|
1109
|
+
extensions: aad.extensions.merge(
|
1110
|
+
Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => outer_ech
|
1111
|
+
)
|
1112
|
+
)
|
1113
|
+
end
|
1114
|
+
|
1115
|
+
# @param conf [HpkeKeyConfig]
|
1116
|
+
#
|
1117
|
+
# @return [HpkeSymmetricCipherSuite, nil]
|
1118
|
+
def select_ech_hpke_cipher_suite(conf)
|
1119
|
+
@settings[:ech_hpke_cipher_suites].find do |cs|
|
1120
|
+
conf.cipher_suites.include?(cs)
|
1121
|
+
end
|
1122
|
+
end
|
1123
|
+
|
1124
|
+
# @return [Message::Extension::ECHClientHello]
|
1125
|
+
def new_grease_ech
|
1126
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#name-compliance-requirements
|
1127
|
+
cipher_suite = HpkeSymmetricCipherSuite.new(
|
1128
|
+
HpkeSymmetricCipherSuite::HpkeKdfId.new(
|
1129
|
+
TTTLS13::Hpke::KdfId::HKDF_SHA256
|
1130
|
+
),
|
1131
|
+
HpkeSymmetricCipherSuite::HpkeAeadId.new(
|
1132
|
+
TTTLS13::Hpke::AeadId::AES_128_GCM
|
1133
|
+
)
|
1134
|
+
)
|
1135
|
+
# Set the enc field to a randomly-generated valid encapsulated public key
|
1136
|
+
# output by the HPKE KEM.
|
1137
|
+
#
|
1138
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.2-2.3.1
|
1139
|
+
public_key = OpenSSL::PKey.read(
|
1140
|
+
OpenSSL::PKey.generate_key('X25519').public_to_pem
|
1141
|
+
)
|
1142
|
+
hpke = HPKE.new(:x25519, :sha256, :sha256, :aes_128_gcm)
|
1143
|
+
enc = hpke.setup_base_s(public_key, '')[:enc]
|
1144
|
+
# Set the payload field to a randomly-generated string of L+C bytes, where
|
1145
|
+
# C is the ciphertext expansion of the selected AEAD scheme and L is the
|
1146
|
+
# size of the EncodedClientHelloInner the client would compute when
|
1147
|
+
# offering ECH, padded according to Section 6.1.3.
|
1148
|
+
#
|
1149
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.2-2.4.1
|
1150
|
+
payload_len = placeholder_encoded_ch_inner_len \
|
1151
|
+
+ Hpke.aead_id2overhead_len(Hpke::AeadId::AES_128_GCM)
|
1152
|
+
|
1153
|
+
Message::Extension::ECHClientHello.new_outer(
|
1154
|
+
cipher_suite: cipher_suite,
|
1155
|
+
config_id: Convert.bin2i(OpenSSL::Random.random_bytes(1)),
|
1156
|
+
enc: enc,
|
1157
|
+
payload: OpenSSL::Random.random_bytes(payload_len)
|
1158
|
+
)
|
1159
|
+
end
|
1160
|
+
|
1161
|
+
# @return [Integer]
|
1162
|
+
def placeholder_encoded_ch_inner_len
|
1163
|
+
448
|
1164
|
+
end
|
1165
|
+
|
1166
|
+
# @param inner [TTTLS13::Message::ClientHello]
|
1167
|
+
# @param ech [Message::Extension::ECHClientHello]
|
1168
|
+
def new_greased_ch(inner, ech)
|
1169
|
+
Message::ClientHello.new(
|
1170
|
+
legacy_version: inner.legacy_version,
|
1171
|
+
random: inner.random,
|
1172
|
+
legacy_session_id: inner.legacy_session_id,
|
1173
|
+
cipher_suites: inner.cipher_suites,
|
1174
|
+
legacy_compression_methods: inner.legacy_compression_methods,
|
1175
|
+
extensions: inner.extensions.merge(
|
1176
|
+
Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => ech
|
1177
|
+
)
|
1178
|
+
)
|
1179
|
+
end
|
1180
|
+
|
726
1181
|
# @return [Integer]
|
727
1182
|
def calc_obfuscated_ticket_age
|
728
1183
|
# the "ticket_lifetime" field in the NewSessionTicket message is
|
@@ -743,7 +1198,7 @@ module TTTLS13
|
|
743
1198
|
group = hrr.extensions[Message::ExtensionType::KEY_SHARE]
|
744
1199
|
.key_share_entry.first.group
|
745
1200
|
key_share, priv_keys \
|
746
|
-
|
1201
|
+
= Message::Extension::KeyShare.gen_ch_key_share([group])
|
747
1202
|
exs << key_share
|
748
1203
|
end
|
749
1204
|
|
@@ -754,7 +1209,7 @@ module TTTLS13
|
|
754
1209
|
# MUST copy the contents of the extension received in the
|
755
1210
|
# HelloRetryRequest into a "cookie" extension in the new ClientHello.
|
756
1211
|
#
|
757
|
-
# https://
|
1212
|
+
# https://datatracker.ietf.org/doc/html/rfc8446#section-4.2.2
|
758
1213
|
exs << hrr.extensions[Message::ExtensionType::COOKIE] \
|
759
1214
|
if hrr.extensions.include?(Message::ExtensionType::COOKIE)
|
760
1215
|
|
@@ -766,15 +1221,23 @@ module TTTLS13
|
|
766
1221
|
end
|
767
1222
|
|
768
1223
|
# NOTE:
|
769
|
-
# https://
|
1224
|
+
# https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2
|
770
1225
|
#
|
771
1226
|
# @param ch1 [TTTLS13::Message::ClientHello]
|
772
1227
|
# @param hrr [TTTLS13::Message::ServerHello]
|
773
1228
|
# @param extensions [TTTLS13::Message::Extensions]
|
774
1229
|
# @param binder_key [String, nil]
|
1230
|
+
# @param ech_state [TTTLS13::Client::EchState]
|
775
1231
|
#
|
776
|
-
# @return [TTTLS13::Message::ClientHello]
|
777
|
-
|
1232
|
+
# @return [TTTLS13::Message::ClientHello] outer
|
1233
|
+
# @return [TTTLS13::Message::ClientHello] inner
|
1234
|
+
# rubocop: disable Metrics/AbcSize
|
1235
|
+
# rubocop: disable Metrics/MethodLength
|
1236
|
+
def send_new_client_hello(ch1,
|
1237
|
+
hrr,
|
1238
|
+
extensions,
|
1239
|
+
binder_key = nil,
|
1240
|
+
ech_state = nil)
|
778
1241
|
ch = Message::ClientHello.new(
|
779
1242
|
legacy_version: ch1.legacy_version,
|
780
1243
|
random: ch1.random,
|
@@ -784,19 +1247,52 @@ module TTTLS13
|
|
784
1247
|
extensions: extensions
|
785
1248
|
)
|
786
1249
|
|
1250
|
+
# encrypted_client_hello
|
1251
|
+
if use_ech? && ech_state.nil?
|
1252
|
+
# If sending a second ClientHello in response to a HelloRetryRequest,
|
1253
|
+
# the client copies the entire "encrypted_client_hello" extension from
|
1254
|
+
# the first ClientHello.
|
1255
|
+
#
|
1256
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.2-3
|
1257
|
+
inner = ch.clone
|
1258
|
+
ch.extensions[Message::ExtensionType::ENCRYPTED_CLIENT_HELLO] \
|
1259
|
+
= ch1.extensions[Message::ExtensionType::ENCRYPTED_CLIENT_HELLO]
|
1260
|
+
elsif use_ech?
|
1261
|
+
ch, inner = offer_new_ech(ch, ech_state)
|
1262
|
+
end
|
1263
|
+
|
787
1264
|
# pre_shared_key
|
788
1265
|
#
|
789
1266
|
# Updating the "pre_shared_key" extension if present by recomputing
|
790
1267
|
# the "obfuscated_ticket_age" and binder values.
|
791
1268
|
if ch1.extensions.include?(Message::ExtensionType::PRE_SHARED_KEY)
|
792
1269
|
sign_psk_binder(ch1: ch1, hrr: hrr, ch: ch, binder_key: binder_key)
|
1270
|
+
|
1271
|
+
if use_ech?
|
1272
|
+
# it MUST also copy the "psk_key_exchange_modes" from the
|
1273
|
+
# ClientHelloInner into the ClientHelloOuter.
|
1274
|
+
ch.extensions[Message::ExtensionType::PSK_KEY_EXCHANGE_MODES] \
|
1275
|
+
= inner.extensions[Message::ExtensionType::PSK_KEY_EXCHANGE_MODES]
|
1276
|
+
# it MUST also include the "early_data" extension in ClientHelloOuter.
|
1277
|
+
ch.extensions[Message::ExtensionType::EARLY_DATA] \
|
1278
|
+
= inner.extensions[Message::ExtensionType::EARLY_DATA]
|
1279
|
+
sign_grease_psk_binder(
|
1280
|
+
ch1: ch1,
|
1281
|
+
hrr: hrr,
|
1282
|
+
ch_outer: ch,
|
1283
|
+
inner_psk: inner.extensions[Message::ExtensionType::PRE_SHARED_KEY],
|
1284
|
+
binder_key: binder_key
|
1285
|
+
)
|
1286
|
+
end
|
793
1287
|
end
|
794
1288
|
|
795
1289
|
send_handshakes(Message::ContentType::HANDSHAKE, [ch],
|
796
1290
|
Cryptograph::Passer.new)
|
797
1291
|
|
798
|
-
ch
|
1292
|
+
[ch, inner]
|
799
1293
|
end
|
1294
|
+
# rubocop: enable Metrics/AbcSize
|
1295
|
+
# rubocop: enable Metrics/MethodLength
|
800
1296
|
|
801
1297
|
# @raise [TTTLS13::Error::ErrorAlerts]
|
802
1298
|
#
|
@@ -949,10 +1445,35 @@ module TTTLS13
|
|
949
1445
|
def process_new_session_ticket(nst)
|
950
1446
|
super(nst)
|
951
1447
|
|
952
|
-
rms = @
|
1448
|
+
rms = @resumption_secret
|
953
1449
|
cs = @cipher_suite
|
954
1450
|
@settings[:process_new_session_ticket]&.call(nst, rms, cs)
|
955
1451
|
end
|
1452
|
+
|
1453
|
+
class EchState
|
1454
|
+
attr_accessor :maximum_name_length
|
1455
|
+
attr_accessor :config_id
|
1456
|
+
attr_accessor :cipher_suite
|
1457
|
+
attr_accessor :public_name
|
1458
|
+
attr_accessor :ctx
|
1459
|
+
|
1460
|
+
# @param maximum_name_length [Integer]
|
1461
|
+
# @param config_id [Integer]
|
1462
|
+
# @param cipher_suite [HpkeSymmetricCipherSuite]
|
1463
|
+
# @param public_name [String]
|
1464
|
+
# @param ctx [[HPKE::ContextS]
|
1465
|
+
def initialize(maximum_name_length,
|
1466
|
+
config_id,
|
1467
|
+
cipher_suite,
|
1468
|
+
public_name,
|
1469
|
+
ctx)
|
1470
|
+
@maximum_name_length = maximum_name_length
|
1471
|
+
@config_id = config_id
|
1472
|
+
@cipher_suite = cipher_suite
|
1473
|
+
@public_name = public_name
|
1474
|
+
@ctx = ctx
|
1475
|
+
end
|
1476
|
+
end
|
956
1477
|
end
|
957
1478
|
# rubocop: enable Metrics/ClassLength
|
958
1479
|
end
|