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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +30 -0
- data/.gitignore +17 -0
- data/.rubocop.yml +36 -0
- data/.ruby-version +1 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +272 -0
- data/Rakefile +8 -0
- data/docs/echspec-demo.png +0 -0
- data/echspec.gemspec +26 -0
- data/exe/echspec +7 -0
- data/fixtures/echconfigs.pem +7 -0
- data/fixtures/server.crt +20 -0
- data/fixtures/server.key +27 -0
- data/lib/echspec/cli.rb +97 -0
- data/lib/echspec/error.rb +9 -0
- data/lib/echspec/log.rb +85 -0
- data/lib/echspec/result.rb +26 -0
- data/lib/echspec/spec/5.1-10.rb +222 -0
- data/lib/echspec/spec/5.1-9.rb +108 -0
- data/lib/echspec/spec/7-5.rb +242 -0
- data/lib/echspec/spec/7.1-11.rb +93 -0
- data/lib/echspec/spec/7.1-14.2.1.rb +133 -0
- data/lib/echspec/spec/7.1.1-2.rb +142 -0
- data/lib/echspec/spec/7.1.1-5.rb +83 -0
- data/lib/echspec/spec/9.rb +113 -0
- data/lib/echspec/spec.rb +170 -0
- data/lib/echspec/spec_case.rb +10 -0
- data/lib/echspec/spec_group.rb +10 -0
- data/lib/echspec/tls13_client.rb +167 -0
- data/lib/echspec/utils.rb +21 -0
- data/lib/echspec/version.rb +3 -0
- data/lib/echspec.rb +16 -0
- data/spec/9_spec.rb +13 -0
- data/spec/log_spec.rb +58 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/with_socket_spec.rb +77 -0
- metadata +141 -0
data/lib/echspec/log.rb
ADDED
@@ -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
|