echspec 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|