echspec 0.0.1

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.
@@ -0,0 +1,85 @@
1
+ module EchSpec
2
+ module Log
3
+ class MessageStack
4
+ def initialize
5
+ @stack = []
6
+ end
7
+
8
+ # @param msg [TTTLS13::Message::$Object]
9
+ def <<(msg)
10
+ @stack << msg
11
+ end
12
+
13
+ def marshal
14
+ arr = []
15
+ arr = @stack.reduce(arr) { |sum, msg| sum << "\"#{MessageStack.msg2name(msg)}\":#{MessageStack.obj2json(msg)}" }
16
+ "{#{arr.reverse.join(',')}}"
17
+ end
18
+
19
+ # rubocop: disable Metrics/CyclomaticComplexity
20
+ # rubocop: disable Metrics/PerceivedComplexity
21
+ def self.msg2name(msg)
22
+ case msg
23
+ in TTTLS13::Message::ClientHello if msg.ch_inner?
24
+ 'ClientHelloInner'
25
+ in TTTLS13::Message::ClientHello
26
+ 'ClientHello'
27
+ in TTTLS13::Message::ServerHello if msg.hrr?
28
+ 'HelloRetryRequest'
29
+ in TTTLS13::Message::ServerHello
30
+ 'ServerHello'
31
+ in TTTLS13::Message::ChangeCipherSpec
32
+ 'ChangeCipherSpec'
33
+ in TTTLS13::Message::EncryptedExtensions
34
+ 'EncryptedExtensions'
35
+ in TTTLS13::Message::Certificate
36
+ 'Certificate'
37
+ in TTTLS13::Message::CertificateVerify
38
+ 'CertificateVerify'
39
+ in TTTLS13::Message::Finished
40
+ 'Finished'
41
+ in TTTLS13::Message::EndOfEarlyData
42
+ 'EndOfEarlyData'
43
+ in TTTLS13::Message::Alert
44
+ 'Alert'
45
+ end
46
+ end
47
+ # rubocop: enable Metrics/CyclomaticComplexity
48
+ # rubocop: enable Metrics/PerceivedComplexity
49
+
50
+ # rubocop: disable Metrics/CyclomaticComplexity
51
+ # rubocop: disable Metrics/PerceivedComplexity
52
+ def self.obj2json(obj)
53
+ case obj
54
+ in OpenSSL::X509::Certificate
55
+ obj.to_pem.gsub("\n", '\n')
56
+ in Numeric | TrueClass | FalseClass
57
+ obj.pretty_print_inspect
58
+ in ''
59
+ '""'
60
+ in String
61
+ "\"0x#{obj.unpack1('H*')}\""
62
+ in NilClass
63
+ 'null'
64
+ in Array
65
+ s = obj.map { |i| obj2json(i) }.join(',')
66
+ "[#{s}]"
67
+ in Hash
68
+ s = obj.map { |k, v| "#{obj2json(k)}:#{obj2json(v)}" }.join(',')
69
+ "{#{s}}"
70
+ in Object if !obj.instance_variables.empty?
71
+ arr = obj.instance_variables.map do |i|
72
+ k = i[1..]
73
+ v = obj2json(obj.instance_variable_get(i))
74
+ "\"#{k}\":#{v}"
75
+ end
76
+ "{#{arr.join(',')}}"
77
+ else
78
+ "\"$#{obj.class.name}\""
79
+ end
80
+ end
81
+ # rubocop: enable Metrics/CyclomaticComplexity
82
+ # rubocop: enable Metrics/PerceivedComplexity
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,26 @@
1
+ module EchSpec
2
+ class Ok
3
+ attr_reader :obj
4
+
5
+ def initialize(obj)
6
+ @obj = obj
7
+ end
8
+
9
+ def deconstruct
10
+ [@obj]
11
+ end
12
+ end
13
+
14
+ class Err
15
+ attr_reader :details, :message_stack
16
+
17
+ def initialize(details, message_stack)
18
+ @details = details
19
+ @message_stack = message_stack
20
+ end
21
+
22
+ def deconstruct
23
+ [@details, @message_stack]
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,222 @@
1
+ module EchSpec
2
+ module Spec
3
+ class Spec5_1_10 < WithSocket
4
+ # Next it makes a copy of the client_hello field and copies the
5
+ # legacy_session_id field from ClientHelloOuter. It then looks for an
6
+ # "ech_outer_extensions" extension. If found, it replaces the extension
7
+ # with the corresponding sequence of extensions in the
8
+ # ClientHelloOuter. The server MUST abort the connection with an
9
+ # "illegal_parameter" alert if any of the following are true:
10
+ #
11
+ # * Any referenced extension is missing in ClientHelloOuter.
12
+ # * Any extension is referenced in OuterExtensions more than once.
13
+ # * "encrypted_client_hello" is referenced in OuterExtensions.
14
+ # * The extensions in ClientHelloOuter corresponding to those in
15
+ # OuterExtensions do not occur in the same order.
16
+ #
17
+ # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-5.1-10
18
+
19
+ # @return [EchSpec::SpecGroup]
20
+ def self.spec_group
21
+ SpecGroup.new(
22
+ '5.1-10',
23
+ [
24
+ SpecCase.new(
25
+ 'MUST abort with an "illegal_parameter" alert, if any referenced extension is missing in ClientHelloOuter.',
26
+ method(:validate_missing_referenced_extensions)
27
+ ),
28
+ SpecCase.new(
29
+ 'MUST abort with an "illegal_parameter" alert, if any extension is referenced in OuterExtensions more than once.',
30
+ method(:validate_duplicated_outer_extensions)
31
+ ),
32
+ SpecCase.new(
33
+ 'MUST abort with an "illegal_parameter" alert, if "encrypted_client_hello" is referenced in OuterExtensions.',
34
+ method(:validate_referenced_encrypted_client_hello)
35
+ ),
36
+ SpecCase.new(
37
+ 'MUST abort with an "illegal_parameter" alert, if the extensions in ClientHelloOuter corresponding to those in OuterExtensions do not occur in the same order.',
38
+ method(:validate_not_same_order_extensions)
39
+ )
40
+ ]
41
+ )
42
+ end
43
+
44
+ # @param hostname [String]
45
+ # @param port [Integer]
46
+ # @param ech_config [ECHConfig]
47
+ #
48
+ # @return [EchSpec::Ok | Err]
49
+ def self.validate_missing_referenced_extensions(hostname, port, ech_config)
50
+ Spec5_1_10.new.validate_invalid_ech_outer_extensions(hostname, port, ech_config, MissingReferencedExtensions)
51
+ end
52
+
53
+ # @param hostname [String]
54
+ # @param port [Integer]
55
+ # @param ech_config [ECHConfig]
56
+ #
57
+ # @return [EchSpec::Ok | Err]
58
+ def self.validate_duplicated_outer_extensions(hostname, port, ech_config)
59
+ Spec5_1_10.new.validate_invalid_ech_outer_extensions(hostname, port, ech_config, DuplicatedOuterExtensions)
60
+ end
61
+
62
+ # @param hostname [String]
63
+ # @param port [Integer]
64
+ # @param ech_config [ECHConfig]
65
+ #
66
+ # @return [EchSpec::Ok | Err]
67
+ def self.validate_referenced_encrypted_client_hello(hostname, port, ech_config)
68
+ Spec5_1_10.new.validate_invalid_ech_outer_extensions(hostname, port, ech_config, ReferencedEncryptedClientHello)
69
+ end
70
+
71
+ # @param hostname [String]
72
+ # @param port [Integer]
73
+ # @param ech_config [ECHConfig]
74
+ #
75
+ # @return [EchSpec::Ok | Err]
76
+ def self.validate_not_same_order_extensions(hostname, port, ech_config)
77
+ Spec5_1_10.new.validate_invalid_ech_outer_extensions(hostname, port, ech_config, NotSameOrderExtensions)
78
+ end
79
+
80
+ # @param hostname [String]
81
+ # @param port [Integer]
82
+ # @param ech_config [ECHConfig]
83
+ # @param super_extensions [TTTLS13::Message::Extension::$Object]
84
+ #
85
+ # @return [EchSpec::Ok | Err]
86
+ def validate_invalid_ech_outer_extensions(hostname, port, ech_config, super_extensions)
87
+ with_socket(hostname, port) do |socket|
88
+ recv = send_invalid_ech_outer_extensions(socket, hostname, ech_config, super_extensions)
89
+ return Err.new('did not send expected alert: illegal_parameter', message_stack) \
90
+ unless Spec.expect_alert(recv, :illegal_parameter)
91
+
92
+ Ok.new(nil)
93
+ end
94
+ end
95
+
96
+ def send_invalid_ech_outer_extensions(socket, hostname, ech_config, super_extensions)
97
+ conn = TLS13Client::Connection.new(socket, :client)
98
+ inner_ech = TTTLS13::Message::Extension::ECHClientHello.new_inner
99
+ exs, = TLS13Client.gen_ch_extensions(hostname)
100
+ exs = super_extensions.new(exs.values)
101
+ inner = TTTLS13::Message::ClientHello.new(
102
+ cipher_suites: TTTLS13::CipherSuites.new(
103
+ [
104
+ TTTLS13::CipherSuite::TLS_AES_256_GCM_SHA384,
105
+ TTTLS13::CipherSuite::TLS_CHACHA20_POLY1305_SHA256,
106
+ TTTLS13::CipherSuite::TLS_AES_128_GCM_SHA256
107
+ ]
108
+ ),
109
+ extensions: exs.merge(
110
+ TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => inner_ech
111
+ )
112
+ )
113
+
114
+ selector = proc { |x| TLS13Client.select_ech_hpke_cipher_suite(x) }
115
+ ch, inner, = TTTLS13::Ech.offer_ech(inner, ech_config, selector)
116
+ conn.send_record(
117
+ TTTLS13::Message::Record.new(
118
+ type: TTTLS13::Message::ContentType::HANDSHAKE,
119
+ messages: [ch],
120
+ cipher: TTTLS13::Cryptograph::Passer.new
121
+ )
122
+ )
123
+ @stack << inner
124
+ @stack << ch
125
+
126
+ recv, = conn.recv_message(TTTLS13::Cryptograph::Passer.new)
127
+ @stack << recv
128
+
129
+ recv
130
+ end
131
+
132
+ class MissingReferencedExtensions < TTTLS13::Message::Extensions
133
+ # @param _ [Array of TTTLS13::Message::ExtensionType]
134
+ #
135
+ # @return [TTTLS13::Message::Extensions] for EncodedClientHelloInner
136
+ def remove_and_replace!(_)
137
+ outer_extensions = [TTTLS13::Message::ExtensionType::KEY_SHARE]
138
+ tmp1 = filter { |k, _| !outer_extensions.include?(k) }
139
+
140
+ clear
141
+ replaced = TTTLS13::Message::Extensions.new
142
+
143
+ tmp1.each_value { |v| self << v; replaced << v }
144
+ # key_share is referenced, but it is missing in ClientHelloOuter.
145
+ replaced << TTTLS13::Message::Extension::ECHOuterExtensions.new(
146
+ [TTTLS13::Message::ExtensionType::KEY_SHARE]
147
+ )
148
+ replaced
149
+ end
150
+ end
151
+
152
+ class DuplicatedOuterExtensions < TTTLS13::Message::Extensions
153
+ # @param _ [Array of TTTLS13::Message::ExtensionType]
154
+ #
155
+ # @return [TTTLS13::Message::Extensions] for EncodedClientHelloInner
156
+ def remove_and_replace!(_)
157
+ outer_extensions = [TTTLS13::Message::ExtensionType::KEY_SHARE]
158
+ tmp1 = filter { |k, _| !outer_extensions.include?(k) }
159
+ tmp2 = filter { |k, _| outer_extensions.include?(k) }
160
+
161
+ clear
162
+ replaced = TTTLS13::Message::Extensions.new
163
+
164
+ tmp1.each_value { |v| self << v; replaced << v }
165
+ tmp2.each_value { |v| self << v }
166
+ # key_share appears twice in OuterExtensions.
167
+ replaced << TTTLS13::Message::Extension::ECHOuterExtensions.new(
168
+ [TTTLS13::Message::ExtensionType::KEY_SHARE] * 2
169
+ )
170
+ replaced
171
+ end
172
+ end
173
+
174
+ class ReferencedEncryptedClientHello < TTTLS13::Message::Extensions
175
+ # @param _ [Array of TTTLS13::Message::ExtensionType]
176
+ #
177
+ # @return [TTTLS13::Message::Extensions] for EncodedClientHelloInner
178
+ def remove_and_replace!(_)
179
+ outer_extensions = [TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO]
180
+ tmp1 = filter { |k, _| !outer_extensions.include?(k) }
181
+ tmp2 = filter { |k, _| outer_extensions.include?(k) }
182
+
183
+ clear
184
+ replaced = TTTLS13::Message::Extensions.new
185
+
186
+ tmp1.each_value { |v| self << v; replaced << v }
187
+ tmp2.each_value { |v| self << v }
188
+ # encrypted_client_hello appears in OuterExtensions.
189
+ replaced << TTTLS13::Message::Extension::ECHOuterExtensions.new(
190
+ [TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO]
191
+ )
192
+ replaced
193
+ end
194
+ end
195
+
196
+ class NotSameOrderExtensions < TTTLS13::Message::Extensions
197
+ # @param _ [Array of TTTLS13::Message::ExtensionType]
198
+ #
199
+ # @return [TTTLS13::Message::Extensions] for EncodedClientHelloInner
200
+ def remove_and_replace!(_)
201
+ outer_extensions = [
202
+ TTTLS13::Message::ExtensionType::KEY_SHARE,
203
+ TTTLS13::Message::ExtensionType::SUPPORTED_VERSIONS
204
+ ]
205
+ tmp1 = filter { |k, _| !outer_extensions.include?(k) }
206
+ tmp2 = filter { |k, _| outer_extensions.include?(k) }
207
+
208
+ clear
209
+ replaced = TTTLS13::Message::Extensions.new
210
+
211
+ tmp1.each_value { |v| self << v; replaced << v }
212
+ tmp2.each_value { |v| self << v }
213
+ # extensions in ClientHelloOuter and OuterExtensions are not in the same order.
214
+ replaced << TTTLS13::Message::Extension::ECHOuterExtensions.new(
215
+ tmp2.keys.reverse
216
+ )
217
+ replaced
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,108 @@
1
+ module EchSpec
2
+ module Spec
3
+ class Spec5_1_9 < WithSocket
4
+ # The client-facing server computes ClientHelloInner by reversing this
5
+ # process. First it parses EncodedClientHelloInner, interpreting all
6
+ # bytes after client_hello as padding. If any padding byte is non-
7
+ # zero, the server MUST abort the connection with an
8
+ # "illegal_parameter" alert.
9
+ #
10
+ # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-5.1-9
11
+
12
+ # @return [EchSpec::SpecGroup]
13
+ def self.spec_group
14
+ SpecGroup.new(
15
+ '5.1-9',
16
+ [
17
+ SpecCase.new(
18
+ 'MUST abort with an "illegal_parameter" alert, if EncodedClientHelloInner is padded with non-zero values.',
19
+ method(:validate_nonzero_padding_encoded_ch_inner)
20
+ )
21
+ ]
22
+ )
23
+ end
24
+
25
+ # @param hostname [String]
26
+ # @param port [Integer]
27
+ # @param ech_config [ECHConfig]
28
+ #
29
+ # @return [EchSpec::Ok | Err]
30
+ def self.validate_nonzero_padding_encoded_ch_inner(hostname, port, ech_config)
31
+ Spec5_1_9.new.do_validate_nonzero_padding_encoded_ch_inner(hostname, port, ech_config)
32
+ end
33
+
34
+ # @param hostname [String]
35
+ # @param port [Integer]
36
+ # @param ech_config [ECHConfig]
37
+ #
38
+ # @return [EchSpec::Ok | Err]
39
+ def do_validate_nonzero_padding_encoded_ch_inner(hostname, port, ech_config)
40
+ with_socket(hostname, port) do |socket|
41
+ recv = send_nonzero_padding_encoded_ch_inner(socket, hostname, ech_config)
42
+ return Err.new('did not send expected alert: illegal_parameter', message_stack) \
43
+ unless Spec.expect_alert(recv, :illegal_parameter)
44
+
45
+ Ok.new(nil)
46
+ end
47
+ end
48
+
49
+ def send_nonzero_padding_encoded_ch_inner(socket, hostname, ech_config)
50
+ conn = TLS13Client::Connection.new(socket, :client)
51
+ inner_ech = TTTLS13::Message::Extension::ECHClientHello.new_inner
52
+ exs, = TLS13Client.gen_ch_extensions(hostname)
53
+ inner = TTTLS13::Message::ClientHello.new(
54
+ cipher_suites: TTTLS13::CipherSuites.new(
55
+ [
56
+ TTTLS13::CipherSuite::TLS_AES_256_GCM_SHA384,
57
+ TTTLS13::CipherSuite::TLS_CHACHA20_POLY1305_SHA256,
58
+ TTTLS13::CipherSuite::TLS_AES_128_GCM_SHA256
59
+ ]
60
+ ),
61
+ extensions: exs.merge(
62
+ TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => inner_ech
63
+ )
64
+ )
65
+
66
+ selector = proc { |x| TLS13Client.select_ech_hpke_cipher_suite(x) }
67
+ ch, inner, = NonzeroPaddingEch.offer_ech(inner, ech_config, selector)
68
+ conn.send_record(
69
+ TTTLS13::Message::Record.new(
70
+ type: TTTLS13::Message::ContentType::HANDSHAKE,
71
+ messages: [ch],
72
+ cipher: TTTLS13::Cryptograph::Passer.new
73
+ )
74
+ )
75
+ @stack << inner
76
+ @stack << ch
77
+
78
+ recv, = conn.recv_message(TTTLS13::Cryptograph::Passer.new)
79
+ @stack << recv
80
+
81
+ recv
82
+ end
83
+
84
+ class NonzeroPaddingEch < TTTLS13::Ech
85
+ NON_ZERO = "\x11".freeze
86
+
87
+ # @param s [String]
88
+ # @param server_name_length [Integer]
89
+ # @param maximum_name_length [Integer]
90
+ #
91
+ # @return [String]
92
+ def self.padding_encoded_ch_inner(s,
93
+ server_name_length,
94
+ maximum_name_length)
95
+ padding_len =
96
+ if server_name_length.positive?
97
+ [maximum_name_length - server_name_length, 0].max
98
+ else
99
+ 9 + maximum_name_length
100
+ end
101
+
102
+ padding_len = 31 - ((s.length + padding_len - 1) % 32)
103
+ s + NON_ZERO * padding_len # padding with non-zero value
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,242 @@
1
+ module EchSpec
2
+ module Spec
3
+ class Spec7_5 < WithSocket
4
+ # If ECHClientHello.type is not a valid ECHClientHelloType, then the
5
+ # server MUST abort with an "illegal_parameter" alert.
6
+ #
7
+ # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-7-5
8
+
9
+ # @return [EchSpec::SpecGroup]
10
+ def self.spec_group
11
+ SpecGroup.new(
12
+ '7-5',
13
+ [
14
+ SpecCase.new(
15
+ 'MUST abort with an "illegal_parameter" alert, if ECHClientHello.type is not a valid ECHClientHelloType in ClientHelloInner.',
16
+ method(:validate_illegal_inner_ech_type)
17
+ ),
18
+ SpecCase.new(
19
+ 'MUST abort with an "illegal_parameter" alert, if ECHClientHello.type is not a valid ECHClientHelloType in ClientHelloOuter.',
20
+ method(:validate_illegal_outer_ech_type)
21
+ )
22
+ ]
23
+ )
24
+ end
25
+
26
+ # @param hostname [String]
27
+ # @param port [Integer]
28
+ # @param ech_config [ECHConfig]
29
+ #
30
+ # @return [EchSpec::Ok | Err]
31
+ def self.validate_illegal_inner_ech_type(hostname, port, ech_config)
32
+ Spec7_5.new.do_validate_illegal_ech_type(
33
+ hostname,
34
+ port,
35
+ ech_config,
36
+ :send_ch_illegal_inner_ech_type
37
+ )
38
+ end
39
+
40
+ # @param hostname [String]
41
+ # @param port [Integer]
42
+ # @param ech_config [ECHConfig]
43
+ #
44
+ # @return [EchSpec::Ok | Err]
45
+ def self.validate_illegal_outer_ech_type(hostname, port, ech_config)
46
+ Spec7_5.new.do_validate_illegal_ech_type(
47
+ hostname,
48
+ port,
49
+ ech_config,
50
+ :send_ch_illegal_outer_ech_type
51
+ )
52
+ end
53
+
54
+ # @param hostname [String]
55
+ # @param port [Integer]
56
+ # @param ech_config [ECHConfig]
57
+ # @param method [Method]
58
+ #
59
+ # @return [EchSpec::Ok | Err]
60
+ def do_validate_illegal_ech_type(hostname, port, ech_config, method)
61
+ with_socket(hostname, port) do |socket|
62
+ recv = send(method, socket, hostname, ech_config)
63
+ return Err.new('did not send expected alert: illegal_parameter', message_stack) \
64
+ unless Spec.expect_alert(recv, :illegal_parameter)
65
+
66
+ Ok.new(nil)
67
+ end
68
+ end
69
+
70
+ # @param socket [TCPSocket]
71
+ # @param hostname [String]
72
+ # @param ech_config [ECHConfig]
73
+ #
74
+ # @return [TTTLS13::Message::Record]
75
+ def send_ch_illegal_inner_ech_type(socket, hostname, ech_config)
76
+ conn = TLS13Client::Connection.new(socket, :client)
77
+ inner_ech = IllegalEchClientHello.new_inner
78
+ exs, = TLS13Client.gen_ch_extensions(hostname)
79
+ inner = TTTLS13::Message::ClientHello.new(
80
+ cipher_suites: TTTLS13::CipherSuites.new(
81
+ [
82
+ TTTLS13::CipherSuite::TLS_AES_256_GCM_SHA384,
83
+ TTTLS13::CipherSuite::TLS_CHACHA20_POLY1305_SHA256,
84
+ TTTLS13::CipherSuite::TLS_AES_128_GCM_SHA256
85
+ ]
86
+ ),
87
+ extensions: exs.merge(
88
+ TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => inner_ech
89
+ )
90
+ )
91
+
92
+ selector = proc { |x| TLS13Client.select_ech_hpke_cipher_suite(x) }
93
+ ch, inner, = TTTLS13::Ech.offer_ech(inner, ech_config, selector)
94
+ conn.send_record(
95
+ TTTLS13::Message::Record.new(
96
+ type: TTTLS13::Message::ContentType::HANDSHAKE,
97
+ messages: [ch],
98
+ cipher: TTTLS13::Cryptograph::Passer.new
99
+ )
100
+ )
101
+ @stack << inner
102
+ @stack << ch
103
+
104
+ recv, = conn.recv_message(TTTLS13::Cryptograph::Passer.new)
105
+ @stack << recv
106
+
107
+ recv
108
+ end
109
+
110
+ # @param socket [TCPSocket]
111
+ # @param hostname [String]
112
+ # @param ech_config [ECHConfig]
113
+ #
114
+ # @return [TTTLS13::Message::Record]
115
+ # rubocop: disable Metrics/AbcSize
116
+ # rubocop: disable Metrics/MethodLength
117
+ def send_ch_illegal_outer_ech_type(socket, hostname, ech_config)
118
+ conn = TLS13Client::Connection.new(socket, :client)
119
+ inner_ech = TTTLS13::Message::Extension::ECHClientHello.new_inner
120
+ exs, = TLS13Client.gen_ch_extensions(hostname)
121
+ inner = TTTLS13::Message::ClientHello.new(
122
+ cipher_suites: TTTLS13::CipherSuites.new(
123
+ [
124
+ TTTLS13::CipherSuite::TLS_AES_256_GCM_SHA384,
125
+ TTTLS13::CipherSuite::TLS_CHACHA20_POLY1305_SHA256,
126
+ TTTLS13::CipherSuite::TLS_AES_128_GCM_SHA256
127
+ ]
128
+ ),
129
+ extensions: exs.merge(
130
+ TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => inner_ech
131
+ )
132
+ )
133
+ @stack << inner
134
+
135
+ # offer_ech
136
+ selector = proc { |x| TLS13Client.select_ech_hpke_cipher_suite(x) }
137
+
138
+ # for ech_outer_extensions
139
+ replaced = \
140
+ inner.extensions.remove_and_replace!([])
141
+
142
+ # Encrypted ClientHello Configuration
143
+ ech_state, enc = TTTLS13::Ech.encrypted_ech_config(
144
+ ech_config,
145
+ selector
146
+ )
147
+ encoded = TTTLS13::Ech.encode_ch_inner(inner, ech_state.maximum_name_length, replaced)
148
+ overhead_len = TTTLS13::Ech.aead_id2overhead_len(
149
+ ech_state.cipher_suite.aead_id.uint16
150
+ )
151
+
152
+ # Encoding the ClientHelloInner
153
+ aad_ech = IllegalEchClientHello.new_outer(
154
+ cipher_suite: ech_state.cipher_suite,
155
+ config_id: ech_state.config_id,
156
+ enc:,
157
+ payload: '0' * (encoded.length + overhead_len)
158
+ )
159
+ aad = TTTLS13::Message::ClientHello.new(
160
+ legacy_version: inner.legacy_version,
161
+ legacy_session_id: inner.legacy_session_id,
162
+ cipher_suites: inner.cipher_suites,
163
+ legacy_compression_methods: inner.legacy_compression_methods,
164
+ extensions: inner.extensions.merge(
165
+ TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => aad_ech,
166
+ TTTLS13::Message::ExtensionType::SERVER_NAME => \
167
+ TTTLS13::Message::Extension::ServerName.new(ech_state.public_name)
168
+ )
169
+ )
170
+
171
+ # Authenticating the ClientHelloOuter
172
+ # which does not include the Handshake structure's four byte header.
173
+ outer_ech = IllegalEchClientHello.new_outer(
174
+ cipher_suite: ech_state.cipher_suite,
175
+ config_id: ech_state.config_id,
176
+ enc:,
177
+ payload: ech_state.ctx.seal(aad.serialize[4..], encoded)
178
+ )
179
+ outer = TTTLS13::Message::ClientHello.new(
180
+ legacy_version: aad.legacy_version,
181
+ random: aad.random,
182
+ legacy_session_id: aad.legacy_session_id,
183
+ cipher_suites: aad.cipher_suites,
184
+ legacy_compression_methods: aad.legacy_compression_methods,
185
+ extensions: aad.extensions.merge(
186
+ TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => outer_ech
187
+ )
188
+ )
189
+ conn.send_record(
190
+ TTTLS13::Message::Record.new(
191
+ type: TTTLS13::Message::ContentType::HANDSHAKE,
192
+ messages: [outer],
193
+ cipher: TTTLS13::Cryptograph::Passer.new
194
+ )
195
+ )
196
+ @stack << outer
197
+
198
+ recv, = conn.recv_message(TTTLS13::Cryptograph::Passer.new)
199
+ @stack << recv
200
+
201
+ recv
202
+ end
203
+ # rubocop: enable Metrics/AbcSize
204
+ # rubocop: enable Metrics/MethodLength
205
+
206
+ class IllegalEchClientHello < TTTLS13::Message::Extension::ECHClientHello
207
+ using TTTLS13::Refinements
208
+
209
+ ILLEGAL_OUTER = "\x02".freeze
210
+ ILLEGAL_INNER = "\x03".freeze
211
+
212
+ def self.new_inner
213
+ IllegalEchClientHello.new(type: ILLEGAL_INNER)
214
+ end
215
+
216
+ def self.new_outer(cipher_suite:, config_id:, enc:, payload:)
217
+ IllegalEchClientHello.new(
218
+ type: ILLEGAL_OUTER,
219
+ cipher_suite:,
220
+ config_id:,
221
+ enc:,
222
+ payload:
223
+ )
224
+ end
225
+
226
+ def serialize
227
+ case @type
228
+ when ILLEGAL_OUTER
229
+ binary = @type + @cipher_suite.encode + @config_id.to_uint8 \
230
+ + @enc.prefix_uint16_length + @payload.prefix_uint16_length
231
+ when ILLEGAL_INNER
232
+ binary = @type
233
+ else
234
+ return super
235
+ end
236
+
237
+ @extension_type + binary.prefix_uint16_length
238
+ end
239
+ end
240
+ end
241
+ end
242
+ end