echspec 0.0.6 → 0.0.7
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/README.md +83 -2
- data/lib/echspec/cli/ech_config_printer.rb +55 -0
- data/lib/echspec/cli/grease.rb +73 -0
- data/lib/echspec/cli/resolve.rb +86 -0
- data/lib/echspec/cli.rb +4 -1
- data/lib/echspec/spec/7.1-14.2.1.rb +1 -71
- data/lib/echspec/spec/9.rb +68 -0
- data/lib/echspec/tls13_client.rb +80 -0
- data/lib/echspec/version.rb +1 -1
- data/lib/echspec.rb +2 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3902d0c840b18babccf1c9e7c764c570fd6b29e6f9688e0f9f0c5696bae4e871
|
|
4
|
+
data.tar.gz: c01a13d625bc07918e8f80db55266019ff3031c597e30e9be0dbd4515755bcb5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22d2c6085da85d18cbbf3d5e52f7f4f8511f2d878da9642ce1ad53817e4a33d8429c132456305592dd37e547c53998aa2db0bc091df276b4a216f4b2283b9492
|
|
7
|
+
data.tar.gz: 9fa6ac8c64727d9e64418b7bcd3e38a98dc693b59ff52f6c7a42b4c7dc7635420d5dc60ef7f11ecf9837cd8d85a0fe7b5fd59987d81f57539ded979f330c4938
|
data/README.md
CHANGED
|
@@ -26,8 +26,11 @@ $ gem install echspec
|
|
|
26
26
|
$ echspec --help
|
|
27
27
|
Usage: echspec {SUBCOMMAND}
|
|
28
28
|
|
|
29
|
-
Available subcommands: run, gen_configs, version, help.
|
|
29
|
+
Available subcommands: run, gen_configs, resolve, grease, version, help.
|
|
30
30
|
```
|
|
31
|
+
|
|
32
|
+
### run
|
|
33
|
+
|
|
31
34
|
```sh-session
|
|
32
35
|
$ echspec run --help
|
|
33
36
|
Usage: echspec run [OPTIONS...] {HOSTNAME}
|
|
@@ -75,7 +78,7 @@ Failures:
|
|
|
75
78
|
1 failure
|
|
76
79
|
```
|
|
77
80
|
|
|
78
|
-
By default, `echspec` retrieves ECHConfigs via DNS HTTPS records. You can specify a local PEM file using the `-f, --file FILE` option. To test a server on localhost:
|
|
81
|
+
By default, `echspec` retrieves ECHConfigs via DNS HTTPS resource records. You can specify a local PEM file using the `-f, --file FILE` option. To test a server on localhost:
|
|
79
82
|
|
|
80
83
|
```sh-session
|
|
81
84
|
$ echspec run -f fixtures/echconfigs.pem -p 4433 localhost
|
|
@@ -282,6 +285,8 @@ $ echspec run -s 7-5 -v research.cloudflare.com 2>&1 > /dev/null | jq .
|
|
|
282
285
|
|
|
283
286
|
</details>
|
|
284
287
|
|
|
288
|
+
### gen_configs
|
|
289
|
+
|
|
285
290
|
You can generate an ECHConfig PEM file the following:
|
|
286
291
|
|
|
287
292
|
```sh-session
|
|
@@ -297,6 +302,82 @@ AQALZXhhbXBsZS5jb20AAA==
|
|
|
297
302
|
-----END ECHCONFIG-----
|
|
298
303
|
```
|
|
299
304
|
|
|
305
|
+
### resolve
|
|
306
|
+
|
|
307
|
+
You can resolve ECHConfigs for a hostname and print fields using the `resolve` subcommand.
|
|
308
|
+
With `-w`, ECHConfigs are resolved from the well-known URI (`https://{HOSTNAME}/.well-known/origin-svcb`) defined in [draft-ietf-tls-wkech](https://datatracker.ietf.org/doc/html/draft-ietf-tls-wkech-12), instead of DNS HTTPS resource records:
|
|
309
|
+
|
|
310
|
+
```sh-session
|
|
311
|
+
$ echspec resolve --help
|
|
312
|
+
Usage: echspec resolve [OPTIONS...] [{HOSTNAME}]
|
|
313
|
+
|
|
314
|
+
Resolve ECHConfigs for a hostname and print fields.
|
|
315
|
+
{HOSTNAME} is required unless -f is specified.
|
|
316
|
+
|
|
317
|
+
Examples:
|
|
318
|
+
|
|
319
|
+
$ echspec resolve localhost
|
|
320
|
+
$ echspec resolve -w localhost
|
|
321
|
+
$ echspec resolve -f echconfigs.pem
|
|
322
|
+
|
|
323
|
+
Options:
|
|
324
|
+
-f, --file FILE path to ECHConfigs PEM file (default resolve ECHConfigs via DNS)
|
|
325
|
+
-w, --well-known resolve ECHConfigs via well-known URI, instead of DNS
|
|
326
|
+
```
|
|
327
|
+
```sh-session
|
|
328
|
+
$ echspec resolve research.cloudflare.com
|
|
329
|
+
ECHConfig: │
|
|
330
|
+
version(uint16): │ fe 0d
|
|
331
|
+
length(uint16): │ 65
|
|
332
|
+
contents(ECHConfigContents): │
|
|
333
|
+
key_config(HpkeKeyConfig): │
|
|
334
|
+
config_id(uint8): │ 104
|
|
335
|
+
kem_id(uint16): │ 00 20
|
|
336
|
+
public_key(opaque): │ e8 49 25 b3 6c e3 fb 9c 72 c7 71 e2 7a b2 8c 65 84 ba c2 2a 90 c2 cb 59 23 b2 27 d8 c1 85 84 17
|
|
337
|
+
cipher_suites(HpkeSymmetricCipherSuite): │
|
|
338
|
+
kdf_id(uint16): │ 00 01
|
|
339
|
+
aead_id(uint16): │ 00 01
|
|
340
|
+
maximum_name_length(uint8): │ 0
|
|
341
|
+
public_name(opaque): │ cloudflare-ech.com
|
|
342
|
+
extensions(opaque): │
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
### grease
|
|
346
|
+
|
|
347
|
+
You can send a GREASE ECH to a server to trigger `retry_configs` and print the retry ECHConfigs fields using the `grease` subcommand:
|
|
348
|
+
|
|
349
|
+
```sh-session
|
|
350
|
+
$ echspec grease --help
|
|
351
|
+
Usage: echspec grease [OPTIONS...] {HOSTNAME}
|
|
352
|
+
|
|
353
|
+
Send GREASE ECH to a server and display retry_configs.
|
|
354
|
+
|
|
355
|
+
Examples:
|
|
356
|
+
|
|
357
|
+
$ echspec grease localhost
|
|
358
|
+
$ echspec grease -p 4433 localhost
|
|
359
|
+
|
|
360
|
+
Options:
|
|
361
|
+
-p, --port VALUE server port number (default 443)
|
|
362
|
+
```
|
|
363
|
+
```sh-session
|
|
364
|
+
$ echspec grease research.cloudflare.com
|
|
365
|
+
ECHConfig: │
|
|
366
|
+
version(uint16): │ fe 0d
|
|
367
|
+
length(uint16): │ 65
|
|
368
|
+
contents(ECHConfigContents): │
|
|
369
|
+
key_config(HpkeKeyConfig): │
|
|
370
|
+
config_id(uint8): │ 72
|
|
371
|
+
kem_id(uint16): │ 00 20
|
|
372
|
+
public_key(opaque): │ 0a 89 2a 9f 05 0a d5 21 da cc 42 65 df cf 70 38 22 7e 85 44 93 1b 68 fa a2 b5 37 ad d8 2c 89 39
|
|
373
|
+
cipher_suites(HpkeSymmetricCipherSuite): │
|
|
374
|
+
kdf_id(uint16): │ 00 01
|
|
375
|
+
aead_id(uint16): │ 00 01
|
|
376
|
+
maximum_name_length(uint8): │ 0
|
|
377
|
+
public_name(opaque): │ cloudflare-ech.com
|
|
378
|
+
extensions(opaque): │
|
|
379
|
+
```
|
|
380
|
+
|
|
300
381
|
|
|
301
382
|
## Note
|
|
302
383
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module EchSpec
|
|
2
|
+
class CLI
|
|
3
|
+
module EchConfigPrinter
|
|
4
|
+
def print_ech_configs(ech_configs)
|
|
5
|
+
all_pairs = ech_configs.map { |c| field_pairs(c) }
|
|
6
|
+
label_width, value_width = evaluate_widths(all_pairs)
|
|
7
|
+
|
|
8
|
+
all_pairs.each_with_index do |pairs, i|
|
|
9
|
+
puts "#{'─' * label_width}┼#{'─' * value_width}" if i.positive?
|
|
10
|
+
pairs.each { |label, value| print_field(label, value, label_width) }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def evaluate_widths(all_pairs)
|
|
15
|
+
label_width = all_pairs.flat_map { |pairs| pairs.map { |label, _| label.length } }.max + 1
|
|
16
|
+
value_width = all_pairs.flat_map { |pairs| pairs.map { |_, value| value.to_s.length } }.max + 1
|
|
17
|
+
[label_width, value_width]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def print_field(label, value, label_width)
|
|
21
|
+
puts "#{label.ljust(label_width)}│ #{value}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# rubocop: disable Metrics/AbcSize
|
|
25
|
+
def field_pairs(c)
|
|
26
|
+
ec = c.echconfig_contents
|
|
27
|
+
kc = ec.key_config
|
|
28
|
+
ext = ec.extensions.octet.unpack1('H*').scan(/.{2}/).join(' ')
|
|
29
|
+
cipher_suite_pairs = kc.cipher_suites.flat_map do |cs|
|
|
30
|
+
[
|
|
31
|
+
[' kdf_id(uint16):', cs.kdf_id.encode.unpack1('H4').scan(/.{2}/).join(' ')],
|
|
32
|
+
[' aead_id(uint16):', cs.aead_id.encode.unpack1('H4').scan(/.{2}/).join(' ')]
|
|
33
|
+
]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
[
|
|
37
|
+
['ECHConfig:', ''],
|
|
38
|
+
[' version(uint16):', c.version.unpack1('H4').scan(/.{2}/).join(' ')],
|
|
39
|
+
[' length(uint16):', ec.encode.length],
|
|
40
|
+
[' contents(ECHConfigContents):', ''],
|
|
41
|
+
[' key_config(HpkeKeyConfig):', ''],
|
|
42
|
+
[' config_id(uint8):', kc.config_id],
|
|
43
|
+
[' kem_id(uint16):', kc.kem_id.encode.unpack1('H4').scan(/.{2}/).join(' ')],
|
|
44
|
+
[' public_key(opaque):', kc.public_key.opaque.unpack1('H*').scan(/.{2}/).join(' ')],
|
|
45
|
+
[' cipher_suites(HpkeSymmetricCipherSuite):', ''],
|
|
46
|
+
*cipher_suite_pairs,
|
|
47
|
+
[' maximum_name_length(uint8):', ec.maximum_name_length],
|
|
48
|
+
[' public_name(opaque):', ec.public_name],
|
|
49
|
+
[' extensions(opaque):', ext]
|
|
50
|
+
]
|
|
51
|
+
end
|
|
52
|
+
# rubocop: enable Metrics/AbcSize
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module EchSpec
|
|
2
|
+
class CLI
|
|
3
|
+
class Grease
|
|
4
|
+
include EchConfigPrinter
|
|
5
|
+
|
|
6
|
+
def execute(argv)
|
|
7
|
+
port, hostname = parse_options(argv)
|
|
8
|
+
|
|
9
|
+
TTTLS13::Logging.logger.level = Logger::WARN
|
|
10
|
+
socket = TCPSocket.new(hostname, port)
|
|
11
|
+
recv = TLS13Client.send_ch_with_greased_ech(socket, hostname)
|
|
12
|
+
ex = recv.extensions[TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO]
|
|
13
|
+
|
|
14
|
+
if ex.nil? || ex.retry_configs.nil? || ex.retry_configs.empty?
|
|
15
|
+
warn "** #{hostname}:#{port} did not send retry_configs"
|
|
16
|
+
exit 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
print_ech_configs(ex.retry_configs)
|
|
20
|
+
rescue Timeout::Error
|
|
21
|
+
warn "** #{hostname}:#{port} connection timeout"
|
|
22
|
+
exit 1
|
|
23
|
+
rescue Errno::ECONNREFUSED
|
|
24
|
+
warn "** #{hostname}:#{port} connection refused"
|
|
25
|
+
exit 1
|
|
26
|
+
ensure
|
|
27
|
+
socket&.close
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse_options(argv)
|
|
31
|
+
op = OptionParser.new
|
|
32
|
+
port = 443
|
|
33
|
+
|
|
34
|
+
op.on(
|
|
35
|
+
'-p',
|
|
36
|
+
'--port VALUE',
|
|
37
|
+
"server port number (default #{port})"
|
|
38
|
+
) do |v|
|
|
39
|
+
port = v
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
op.banner = <<~USAGE
|
|
43
|
+
Usage: echspec grease [OPTIONS...] {HOSTNAME}
|
|
44
|
+
|
|
45
|
+
Send GREASE ECH to a server and display retry_configs.
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
|
|
49
|
+
$ echspec grease localhost
|
|
50
|
+
$ echspec grease -p 4433 localhost
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
USAGE
|
|
54
|
+
|
|
55
|
+
begin
|
|
56
|
+
args = op.parse(argv)
|
|
57
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
|
58
|
+
warn op
|
|
59
|
+
warn "** #{e.message}"
|
|
60
|
+
exit 1
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if args.length != 1
|
|
64
|
+
warn op
|
|
65
|
+
warn '** {HOSTNAME} argument is not specified'
|
|
66
|
+
exit 1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
[port, args[0]]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module EchSpec
|
|
2
|
+
class CLI
|
|
3
|
+
class Resolve
|
|
4
|
+
include EchConfigPrinter
|
|
5
|
+
def execute(argv)
|
|
6
|
+
fpath, well_known, hostname = parse_options(argv)
|
|
7
|
+
|
|
8
|
+
result = if !fpath.nil?
|
|
9
|
+
Spec::Spec9.parse_pem(File.read(fpath))
|
|
10
|
+
elsif well_known
|
|
11
|
+
Spec::Spec9.well_known_origin_svcb(hostname)
|
|
12
|
+
else
|
|
13
|
+
Spec::Spec9.resolve_ech_configs(hostname)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
case result
|
|
17
|
+
in Ok(ech_configs)
|
|
18
|
+
print_ech_configs(ech_configs)
|
|
19
|
+
in Err(details, _)
|
|
20
|
+
warn "** #{details}"
|
|
21
|
+
exit 1
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# rubocop: disable Metrics/MethodLength
|
|
26
|
+
def parse_options(argv)
|
|
27
|
+
op = OptionParser.new
|
|
28
|
+
fpath = nil
|
|
29
|
+
well_known = false
|
|
30
|
+
|
|
31
|
+
op.on(
|
|
32
|
+
'-f',
|
|
33
|
+
'--file FILE',
|
|
34
|
+
'path to ECHConfigs PEM file (default resolve ECHConfigs via DNS)'
|
|
35
|
+
) do |v|
|
|
36
|
+
fpath = v
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
op.on(
|
|
40
|
+
'-w',
|
|
41
|
+
'--well-known',
|
|
42
|
+
'resolve ECHConfigs via well-known URI, instead of DNS'
|
|
43
|
+
) do
|
|
44
|
+
well_known = true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
op.banner = <<~USAGE
|
|
48
|
+
Usage: echspec resolve [OPTIONS...] [{HOSTNAME}]
|
|
49
|
+
|
|
50
|
+
Resolve ECHConfigs for a hostname and print fields.
|
|
51
|
+
{HOSTNAME} is required unless -f is specified.
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
|
|
55
|
+
$ echspec resolve localhost
|
|
56
|
+
$ echspec resolve -w localhost
|
|
57
|
+
$ echspec resolve -f echconfigs.pem
|
|
58
|
+
|
|
59
|
+
Options:
|
|
60
|
+
USAGE
|
|
61
|
+
|
|
62
|
+
begin
|
|
63
|
+
args = op.parse(argv)
|
|
64
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
|
65
|
+
warn op
|
|
66
|
+
warn "** #{e.message}"
|
|
67
|
+
exit 1
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if !fpath.nil? && !File.exist?(fpath)
|
|
71
|
+
warn '** {FILE} is not found'
|
|
72
|
+
exit 1
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
if fpath.nil? && args.length != 1
|
|
76
|
+
warn op
|
|
77
|
+
warn '** {HOSTNAME} argument is not specified'
|
|
78
|
+
exit 1
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
[fpath, well_known, args[0]]
|
|
82
|
+
end
|
|
83
|
+
# rubocop: enable Metrics/MethodLength
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/echspec/cli.rb
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
require_relative 'cli/ech_config_printer'
|
|
1
2
|
require_relative 'cli/gen_configs'
|
|
3
|
+
require_relative 'cli/grease'
|
|
4
|
+
require_relative 'cli/resolve'
|
|
2
5
|
require_relative 'cli/run'
|
|
3
6
|
|
|
4
7
|
module EchSpec
|
|
@@ -6,7 +9,7 @@ module EchSpec
|
|
|
6
9
|
using Refinements
|
|
7
10
|
|
|
8
11
|
def execute(argv = ARGV)
|
|
9
|
-
subcommands = %i[run gen_configs]
|
|
12
|
+
subcommands = %i[run gen_configs resolve grease]
|
|
10
13
|
|
|
11
14
|
op = OptionParser.new
|
|
12
15
|
|
|
@@ -43,7 +43,7 @@ module EchSpec
|
|
|
43
43
|
# @return [EchSpec::Ok | Err]
|
|
44
44
|
def do_validate_ee_retry_configs(hostname, port)
|
|
45
45
|
with_socket(hostname, port) do |socket|
|
|
46
|
-
recv = send_ch_with_greased_ech(socket, hostname)
|
|
46
|
+
recv = TLS13Client.send_ch_with_greased_ech(socket, hostname, @stack)
|
|
47
47
|
ex = recv.extensions[TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO]
|
|
48
48
|
return Err.new('did not send expected alert: encrypted_client_hello', message_stack) \
|
|
49
49
|
unless ex.is_a?(TTTLS13::Message::Extension::ECHEncryptedExtensions)
|
|
@@ -53,76 +53,6 @@ module EchSpec
|
|
|
53
53
|
Ok.new(nil)
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
|
-
|
|
57
|
-
# rubocop: disable Metrics/AbcSize
|
|
58
|
-
# rubocop: disable Metrics/MethodLength
|
|
59
|
-
def send_ch_with_greased_ech(socket, hostname)
|
|
60
|
-
# send ClientHello
|
|
61
|
-
conn = TLS13Client::Connection.new(socket, :client)
|
|
62
|
-
inner_ech = TTTLS13::Message::Extension::ECHClientHello.new_inner
|
|
63
|
-
exs, shared_secret = TLS13Client.gen_ch_extensions(hostname)
|
|
64
|
-
inner = TTTLS13::Message::ClientHello.new(
|
|
65
|
-
cipher_suites: TTTLS13::CipherSuites.new(
|
|
66
|
-
[
|
|
67
|
-
TTTLS13::CipherSuite::TLS_AES_256_GCM_SHA384,
|
|
68
|
-
TTTLS13::CipherSuite::TLS_CHACHA20_POLY1305_SHA256,
|
|
69
|
-
TTTLS13::CipherSuite::TLS_AES_128_GCM_SHA256
|
|
70
|
-
]
|
|
71
|
-
),
|
|
72
|
-
extensions: exs.merge(
|
|
73
|
-
TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => inner_ech
|
|
74
|
-
)
|
|
75
|
-
)
|
|
76
|
-
@stack << inner
|
|
77
|
-
|
|
78
|
-
ch = TTTLS13::Ech.new_greased_ch(inner, TTTLS13::Ech.new_grease_ech)
|
|
79
|
-
conn.send_record(
|
|
80
|
-
TTTLS13::Message::Record.new(
|
|
81
|
-
type: TTTLS13::Message::ContentType::HANDSHAKE,
|
|
82
|
-
messages: [ch],
|
|
83
|
-
cipher: TTTLS13::Cryptograph::Passer.new
|
|
84
|
-
)
|
|
85
|
-
)
|
|
86
|
-
@stack << ch
|
|
87
|
-
|
|
88
|
-
# receive ServerHello
|
|
89
|
-
recv, = conn.recv_message(TTTLS13::Cryptograph::Passer.new)
|
|
90
|
-
@stack << recv
|
|
91
|
-
raise Error::BeforeTargetSituationError, 'not received ServerHello' \
|
|
92
|
-
unless recv.is_a?(TTTLS13::Message::ServerHello) && !recv.hrr?
|
|
93
|
-
|
|
94
|
-
# receive EncryptedExtensions
|
|
95
|
-
transcript = TTTLS13::Transcript.new
|
|
96
|
-
transcript[TTTLS13::CH] = [ch, ch.serialize]
|
|
97
|
-
sh = recv
|
|
98
|
-
transcript[TTTLS13::SH] = [sh, sh.serialize]
|
|
99
|
-
kse = sh.extensions[TTTLS13::Message::ExtensionType::KEY_SHARE]
|
|
100
|
-
.key_share_entry.first
|
|
101
|
-
key_schedule = TTTLS13::KeySchedule.new(
|
|
102
|
-
psk: nil,
|
|
103
|
-
shared_secret: shared_secret.build(kse.group, kse.key_exchange),
|
|
104
|
-
cipher_suite: sh.cipher_suite,
|
|
105
|
-
transcript:
|
|
106
|
-
)
|
|
107
|
-
hs_rcipher = TTTLS13::Endpoint.gen_cipher(
|
|
108
|
-
sh.cipher_suite,
|
|
109
|
-
key_schedule.server_handshake_write_key,
|
|
110
|
-
key_schedule.server_handshake_write_iv
|
|
111
|
-
)
|
|
112
|
-
recv, = conn.recv_message(hs_rcipher)
|
|
113
|
-
@stack << recv
|
|
114
|
-
if recv.is_a?(TTTLS13::Message::ChangeCipherSpec)
|
|
115
|
-
recv, = conn.recv_message(hs_rcipher)
|
|
116
|
-
@stack << recv
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
raise Error::BeforeTargetSituationError, 'not received EncryptedExtensions' \
|
|
120
|
-
unless recv.is_a?(TTTLS13::Message::EncryptedExtensions)
|
|
121
|
-
|
|
122
|
-
recv
|
|
123
|
-
end
|
|
124
|
-
# rubocop: enable Metrics/AbcSize
|
|
125
|
-
# rubocop: enable Metrics/MethodLength
|
|
126
56
|
end
|
|
127
57
|
end
|
|
128
58
|
end
|
data/lib/echspec/spec/9.rb
CHANGED
|
@@ -83,6 +83,74 @@ module EchSpec
|
|
|
83
83
|
Ok.new(ECHConfig.decode_vectors(octet.slice(2..)))
|
|
84
84
|
end
|
|
85
85
|
|
|
86
|
+
# @param hostname [String]
|
|
87
|
+
#
|
|
88
|
+
# @return [EchSpec::Ok<Array<ECHConfig>> | Err]
|
|
89
|
+
def well_known_origin_svcb(hostname)
|
|
90
|
+
res = fetch_well_known(hostname)
|
|
91
|
+
body = case res
|
|
92
|
+
in Ok(obj)
|
|
93
|
+
obj
|
|
94
|
+
in Err
|
|
95
|
+
return res
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
parse_origin_svcb(body)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-wkech-12
|
|
102
|
+
WK_ECH_PATH = '/.well-known/origin-svcb'.freeze
|
|
103
|
+
private_constant :WK_ECH_PATH
|
|
104
|
+
|
|
105
|
+
# @param hostname [String]
|
|
106
|
+
#
|
|
107
|
+
# @return [EchSpec::Ok | Err]
|
|
108
|
+
def fetch_well_known(hostname)
|
|
109
|
+
uri = URI("https://#{hostname}#{WK_ECH_PATH}")
|
|
110
|
+
res = Net::HTTP.get_response(uri)
|
|
111
|
+
return Err.new("GET #{uri} returned #{res.code}.", nil) unless res.is_a?(Net::HTTPSuccess)
|
|
112
|
+
|
|
113
|
+
Ok.new(res.body)
|
|
114
|
+
rescue StandardError => e
|
|
115
|
+
Err.new(e.message, nil)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# {
|
|
119
|
+
# "regeninterval": 3600,
|
|
120
|
+
# "endpoints": [
|
|
121
|
+
# {
|
|
122
|
+
# "priority": 1,
|
|
123
|
+
# "params": {
|
|
124
|
+
# "ipv4hint": [ "192.0.2.1", "192.0.2.254" ],
|
|
125
|
+
# "ech": "AD7+DQA6NAAgACCn3zNTeX/WOD...AAA==",
|
|
126
|
+
# "ipv6hint": [ "2001:DB::ec4" ],
|
|
127
|
+
# "alpn": [ "h2", "http/1.1" ]
|
|
128
|
+
# }
|
|
129
|
+
# }
|
|
130
|
+
# ]
|
|
131
|
+
# }
|
|
132
|
+
#
|
|
133
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-wkech-12#section-5
|
|
134
|
+
#
|
|
135
|
+
# @param body [String]
|
|
136
|
+
#
|
|
137
|
+
# @return [EchSpec::Ok | Err]
|
|
138
|
+
def parse_origin_svcb(body)
|
|
139
|
+
h = JSON.parse(body)
|
|
140
|
+
echs = h['endpoints']&.filter_map { |e| e.dig('params', 'ech') }
|
|
141
|
+
return Err.new('The origin-svcb well-known resource does NOT have ech SvcParams.', nil) if echs.nil? || echs.empty?
|
|
142
|
+
|
|
143
|
+
# if parsing any one of the ech values fails, return Err
|
|
144
|
+
ech_configs = echs.flat_map do |ech|
|
|
145
|
+
octet = Base64.decode64(ech)
|
|
146
|
+
ECHConfig.decode_vectors(octet.slice(2..))
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
Ok.new(ech_configs)
|
|
150
|
+
rescue StandardError => e
|
|
151
|
+
Err.new(e.message, nil)
|
|
152
|
+
end
|
|
153
|
+
|
|
86
154
|
# @param pem [String]
|
|
87
155
|
#
|
|
88
156
|
# @return [EchSpec::Ok<Array<ECHConfig>> | Err]
|
data/lib/echspec/tls13_client.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
module EchSpec
|
|
2
|
+
# rubocop: disable Metrics/ModuleLength
|
|
2
3
|
module TLS13Client
|
|
3
4
|
class Connection < TTTLS13::Connection
|
|
4
5
|
# @param socket [Socket]
|
|
@@ -95,6 +96,84 @@ module EchSpec
|
|
|
95
96
|
ch1.extensions.merge(exs)
|
|
96
97
|
end
|
|
97
98
|
|
|
99
|
+
# @param socket [TCPSocket]
|
|
100
|
+
# @param hostname [String]
|
|
101
|
+
# @param stack [EchSpec::Log::MessageStack | nil]
|
|
102
|
+
#
|
|
103
|
+
# @raise [EchSpec::Error::BeforeTargetSituationError]
|
|
104
|
+
#
|
|
105
|
+
# @return [TTTLS13::Message::EncryptedExtensions]
|
|
106
|
+
# rubocop: disable Metrics/AbcSize
|
|
107
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
108
|
+
# rubocop: disable Metrics/MethodLength
|
|
109
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
|
110
|
+
def send_ch_with_greased_ech(socket, hostname, stack = nil)
|
|
111
|
+
conn = TLS13Client::Connection.new(socket, :client)
|
|
112
|
+
inner_ech = TTTLS13::Message::Extension::ECHClientHello.new_inner
|
|
113
|
+
exs, shared_secret = TLS13Client.gen_ch_extensions(hostname)
|
|
114
|
+
inner = TTTLS13::Message::ClientHello.new(
|
|
115
|
+
cipher_suites: TTTLS13::CipherSuites.new(
|
|
116
|
+
[
|
|
117
|
+
TTTLS13::CipherSuite::TLS_AES_256_GCM_SHA384,
|
|
118
|
+
TTTLS13::CipherSuite::TLS_CHACHA20_POLY1305_SHA256,
|
|
119
|
+
TTTLS13::CipherSuite::TLS_AES_128_GCM_SHA256
|
|
120
|
+
]
|
|
121
|
+
),
|
|
122
|
+
extensions: exs.merge(
|
|
123
|
+
TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => inner_ech
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
stack << inner if stack
|
|
127
|
+
|
|
128
|
+
ch = TTTLS13::Ech.new_greased_ch(inner, TTTLS13::Ech.new_grease_ech)
|
|
129
|
+
conn.send_record(
|
|
130
|
+
TTTLS13::Message::Record.new(
|
|
131
|
+
type: TTTLS13::Message::ContentType::HANDSHAKE,
|
|
132
|
+
messages: [ch],
|
|
133
|
+
cipher: TTTLS13::Cryptograph::Passer.new
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
stack << ch if stack
|
|
137
|
+
|
|
138
|
+
recv, = conn.recv_message(TTTLS13::Cryptograph::Passer.new)
|
|
139
|
+
stack << recv if stack
|
|
140
|
+
raise Error::BeforeTargetSituationError, 'not received ServerHello' \
|
|
141
|
+
unless recv.is_a?(TTTLS13::Message::ServerHello) && !recv.hrr?
|
|
142
|
+
|
|
143
|
+
transcript = TTTLS13::Transcript.new
|
|
144
|
+
transcript[TTTLS13::CH] = [ch, ch.serialize]
|
|
145
|
+
sh = recv
|
|
146
|
+
transcript[TTTLS13::SH] = [sh, sh.serialize]
|
|
147
|
+
kse = sh.extensions[TTTLS13::Message::ExtensionType::KEY_SHARE]
|
|
148
|
+
.key_share_entry.first
|
|
149
|
+
key_schedule = TTTLS13::KeySchedule.new(
|
|
150
|
+
psk: nil,
|
|
151
|
+
shared_secret: shared_secret.build(kse.group, kse.key_exchange),
|
|
152
|
+
cipher_suite: sh.cipher_suite,
|
|
153
|
+
transcript:
|
|
154
|
+
)
|
|
155
|
+
hs_rcipher = TTTLS13::Endpoint.gen_cipher(
|
|
156
|
+
sh.cipher_suite,
|
|
157
|
+
key_schedule.server_handshake_write_key,
|
|
158
|
+
key_schedule.server_handshake_write_iv
|
|
159
|
+
)
|
|
160
|
+
recv, = conn.recv_message(hs_rcipher)
|
|
161
|
+
stack << recv if stack
|
|
162
|
+
if recv.is_a?(TTTLS13::Message::ChangeCipherSpec)
|
|
163
|
+
recv, = conn.recv_message(hs_rcipher)
|
|
164
|
+
stack << recv if stack
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
raise Error::BeforeTargetSituationError, 'not received EncryptedExtensions' \
|
|
168
|
+
unless recv.is_a?(TTTLS13::Message::EncryptedExtensions)
|
|
169
|
+
|
|
170
|
+
recv
|
|
171
|
+
end
|
|
172
|
+
# rubocop: enable Metrics/AbcSize
|
|
173
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
174
|
+
# rubocop: enable Metrics/MethodLength
|
|
175
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
|
176
|
+
|
|
98
177
|
# @param conf [ECHConfig::ECHConfigContents::HpkeKeyConfig]
|
|
99
178
|
#
|
|
100
179
|
# @return [Boolean]
|
|
@@ -164,4 +243,5 @@ module EchSpec
|
|
|
164
243
|
# rubocop: enable Metrics/MethodLength
|
|
165
244
|
end
|
|
166
245
|
end
|
|
246
|
+
# rubocop: enable Metrics/ModuleLength
|
|
167
247
|
end
|
data/lib/echspec/version.rb
CHANGED
data/lib/echspec.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: echspec
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thekuwayama
|
|
@@ -101,7 +101,10 @@ files:
|
|
|
101
101
|
- fixtures/server.key
|
|
102
102
|
- lib/echspec.rb
|
|
103
103
|
- lib/echspec/cli.rb
|
|
104
|
+
- lib/echspec/cli/ech_config_printer.rb
|
|
104
105
|
- lib/echspec/cli/gen_configs.rb
|
|
106
|
+
- lib/echspec/cli/grease.rb
|
|
107
|
+
- lib/echspec/cli/resolve.rb
|
|
105
108
|
- lib/echspec/cli/run.rb
|
|
106
109
|
- lib/echspec/error.rb
|
|
107
110
|
- lib/echspec/log.rb
|