echspec 0.0.6 → 0.0.8
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/.ruby-version +1 -1
- data/README.md +107 -2
- data/lib/echspec/cli/ech_config_printer.rb +55 -0
- data/lib/echspec/cli/gen_configs.rb +1 -25
- 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/ech_config.rb +36 -0
- data/lib/echspec/spec/5-11.rb +70 -0
- data/lib/echspec/spec/5.1-9.rb +3 -8
- data/lib/echspec/spec/7.1-14.2.1.rb +20 -74
- data/lib/echspec/spec/7.2-1.rb +112 -0
- data/lib/echspec/spec/9.rb +80 -2
- data/lib/echspec/spec.rb +2 -2
- data/lib/echspec/tls13_client.rb +80 -0
- data/lib/echspec/version.rb +1 -1
- data/lib/echspec.rb +3 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b4b19de1ec6feae0372b847a16805e3f2711283341d85b8b1e309ba5034c4d1b
|
|
4
|
+
data.tar.gz: edb1bc1ad4ca9489a2de82a6406d3f7e6dd57ecb785c2063a024ccfea5cc995a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 93d42d067d4839ff7ba052a81b3b587be0b7fa10f0acb372a97f6cfee4240a3dec35fd76899116164cb8a3276114b3bf5ba315cab1228af72a56b5a8cc841867
|
|
7
|
+
data.tar.gz: f5fa1e47ed16a24708583c6a2fa6419616c6df389c7d993bb0b6e52b5626581d6d9140b974e87dcd64d923c08ed2fb24cc8ef8cd412502c4fc921246578071a5
|
data/.ruby-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
4.0.
|
|
1
|
+
4.0.7
|
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}
|
|
@@ -53,6 +56,7 @@ Example output:
|
|
|
53
56
|
$ echspec run research.cloudflare.com
|
|
54
57
|
TLS Encrypted Client Hello Server
|
|
55
58
|
✔ MUST implement the following HPKE cipher suite: KEM: DHKEM(X25519, HKDF-SHA256), KDF: HKDF-SHA256 and AEAD: AES-128-GCM. [9]
|
|
59
|
+
✔ MUST include an "encrypted_client_hello" extension with an 8-byte confirmation payload in HelloRetryRequest, if ClientHello offers the inner variant. [5-11]
|
|
56
60
|
✔ MUST abort with an "illegal_parameter" alert, if EncodedClientHelloInner is padded with non-zero values. [5.1-9]
|
|
57
61
|
✔ MUST abort with an "illegal_parameter" alert, if any referenced extension is missing in ClientHelloOuter. [5.1-10]
|
|
58
62
|
✔ MUST abort with an "illegal_parameter" alert, if any extension is referenced in OuterExtensions more than once. [5.1-10]
|
|
@@ -65,6 +69,7 @@ TLS Encrypted Client Hello Server
|
|
|
65
69
|
✔ MUST abort with a "missing_extension" alert, if 2nd ClientHelloOuter does not contains the "encrypted_client_hello" extension. [7.1.1-2]
|
|
66
70
|
✔ MUST abort with an "illegal_parameter" alert, if 2nd ClientHelloOuter "encrypted_client_hello" enc is empty. [7.1.1-2]
|
|
67
71
|
✔ MUST abort with a "decrypt_error" alert, if fails to decrypt 2nd ClientHelloOuter. [7.1.1-5]
|
|
72
|
+
✔ MUST confirm ECH acceptance in ServerHello, if it confirmed ECH acceptance in HelloRetryRequest. [7.2-1]
|
|
68
73
|
|
|
69
74
|
Failures:
|
|
70
75
|
|
|
@@ -75,7 +80,7 @@ Failures:
|
|
|
75
80
|
1 failure
|
|
76
81
|
```
|
|
77
82
|
|
|
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:
|
|
83
|
+
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
84
|
|
|
80
85
|
```sh-session
|
|
81
86
|
$ echspec run -f fixtures/echconfigs.pem -p 4433 localhost
|
|
@@ -282,6 +287,8 @@ $ echspec run -s 7-5 -v research.cloudflare.com 2>&1 > /dev/null | jq .
|
|
|
282
287
|
|
|
283
288
|
</details>
|
|
284
289
|
|
|
290
|
+
### gen_configs
|
|
291
|
+
|
|
285
292
|
You can generate an ECHConfig PEM file the following:
|
|
286
293
|
|
|
287
294
|
```sh-session
|
|
@@ -297,6 +304,104 @@ AQALZXhhbXBsZS5jb20AAA==
|
|
|
297
304
|
-----END ECHCONFIG-----
|
|
298
305
|
```
|
|
299
306
|
|
|
307
|
+
The generated file contains the following two PEM blocks:
|
|
308
|
+
|
|
309
|
+
- `PRIVATE KEY`
|
|
310
|
+
- X25519
|
|
311
|
+
- `ECHCONFIG`
|
|
312
|
+
- version
|
|
313
|
+
- 0xfe0d
|
|
314
|
+
- config_id
|
|
315
|
+
- 123
|
|
316
|
+
- KEM
|
|
317
|
+
- DHKEM(X25519, HKDF-SHA256)
|
|
318
|
+
- KDF
|
|
319
|
+
- HKDF-SHA256
|
|
320
|
+
- AEAD
|
|
321
|
+
- AES-128-GCM
|
|
322
|
+
- maximum\_name\_length
|
|
323
|
+
- 32
|
|
324
|
+
- public_name
|
|
325
|
+
- "localhost"
|
|
326
|
+
- extensions
|
|
327
|
+
- []
|
|
328
|
+
|
|
329
|
+
### resolve
|
|
330
|
+
|
|
331
|
+
You can resolve ECHConfigs for a hostname and print fields using the `resolve` subcommand.
|
|
332
|
+
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:
|
|
333
|
+
|
|
334
|
+
```sh-session
|
|
335
|
+
$ echspec resolve --help
|
|
336
|
+
Usage: echspec resolve [OPTIONS...] [{HOSTNAME}]
|
|
337
|
+
|
|
338
|
+
Resolve ECHConfigs for a hostname and print fields.
|
|
339
|
+
{HOSTNAME} is required unless -f is specified.
|
|
340
|
+
|
|
341
|
+
Examples:
|
|
342
|
+
|
|
343
|
+
$ echspec resolve localhost
|
|
344
|
+
$ echspec resolve -w localhost
|
|
345
|
+
$ echspec resolve -f echconfigs.pem
|
|
346
|
+
|
|
347
|
+
Options:
|
|
348
|
+
-f, --file FILE path to ECHConfigs PEM file (default resolve ECHConfigs via DNS)
|
|
349
|
+
-w, --well-known resolve ECHConfigs via well-known URI, instead of DNS
|
|
350
|
+
```
|
|
351
|
+
```sh-session
|
|
352
|
+
$ echspec resolve research.cloudflare.com
|
|
353
|
+
ECHConfig: │
|
|
354
|
+
version(hex): │ fe 0d
|
|
355
|
+
length(integer): │ 65
|
|
356
|
+
contents: │
|
|
357
|
+
key_config: │
|
|
358
|
+
config_id(integer): │ 104
|
|
359
|
+
kem_id(hex): │ 00 20
|
|
360
|
+
public_key(hex): │ 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
|
|
361
|
+
cipher_suites: │
|
|
362
|
+
kdf_id(hex): │ 00 01
|
|
363
|
+
aead_id(hex): │ 00 01
|
|
364
|
+
maximum_name_length(integer): │ 0
|
|
365
|
+
public_name(string): │ cloudflare-ech.com
|
|
366
|
+
extensions(hex): │
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### grease
|
|
370
|
+
|
|
371
|
+
You can send a GREASE ECH to a server to trigger `retry_configs` and print the retry ECHConfigs fields using the `grease` subcommand:
|
|
372
|
+
|
|
373
|
+
```sh-session
|
|
374
|
+
$ echspec grease --help
|
|
375
|
+
Usage: echspec grease [OPTIONS...] {HOSTNAME}
|
|
376
|
+
|
|
377
|
+
Send GREASE ECH to a server and display retry_configs.
|
|
378
|
+
|
|
379
|
+
Examples:
|
|
380
|
+
|
|
381
|
+
$ echspec grease localhost
|
|
382
|
+
$ echspec grease -p 4433 localhost
|
|
383
|
+
|
|
384
|
+
Options:
|
|
385
|
+
-p, --port VALUE server port number (default 443)
|
|
386
|
+
```
|
|
387
|
+
```sh-session
|
|
388
|
+
$ echspec grease research.cloudflare.com
|
|
389
|
+
ECHConfig: │
|
|
390
|
+
version(hex): │ fe 0d
|
|
391
|
+
length(integer): │ 65
|
|
392
|
+
contents: │
|
|
393
|
+
key_config: │
|
|
394
|
+
config_id(integer): │ 72
|
|
395
|
+
kem_id(hex): │ 00 20
|
|
396
|
+
public_key(hex): │ 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
|
|
397
|
+
cipher_suites: │
|
|
398
|
+
kdf_id(hex): │ 00 01
|
|
399
|
+
aead_id(hex): │ 00 01
|
|
400
|
+
maximum_name_length(integer): │ 0
|
|
401
|
+
public_name(string): │ cloudflare-ech.com
|
|
402
|
+
extensions(hex): │
|
|
403
|
+
```
|
|
404
|
+
|
|
300
405
|
|
|
301
406
|
## Note
|
|
302
407
|
|
|
@@ -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(hex):', cs.kdf_id.encode.unpack1('H4').scan(/.{2}/).join(' ')],
|
|
32
|
+
[' aead_id(hex):', cs.aead_id.encode.unpack1('H4').scan(/.{2}/).join(' ')]
|
|
33
|
+
]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
[
|
|
37
|
+
['ECHConfig:', ''],
|
|
38
|
+
[' version(hex):', c.version.unpack1('H4').scan(/.{2}/).join(' ')],
|
|
39
|
+
[' length(integer):', ec.encode.length],
|
|
40
|
+
[' contents:', ''],
|
|
41
|
+
[' key_config:', ''],
|
|
42
|
+
[' config_id(integer):', kc.config_id],
|
|
43
|
+
[' kem_id(hex):', kc.kem_id.encode.unpack1('H4').scan(/.{2}/).join(' ')],
|
|
44
|
+
[' public_key(hex):', kc.public_key.opaque.unpack1('H*').scan(/.{2}/).join(' ')],
|
|
45
|
+
[' cipher_suites:', ''],
|
|
46
|
+
*cipher_suite_pairs,
|
|
47
|
+
[' maximum_name_length(integer):', ec.maximum_name_length],
|
|
48
|
+
[' public_name(string):', ec.public_name],
|
|
49
|
+
[' extensions(hex):', ext]
|
|
50
|
+
]
|
|
51
|
+
end
|
|
52
|
+
# rubocop: enable Metrics/AbcSize
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -36,32 +36,8 @@ module EchSpec
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def write(fpath)
|
|
39
|
-
hostname = 'localhost'
|
|
40
|
-
|
|
41
39
|
key = OpenSSL::PKey.generate_key('X25519')
|
|
42
|
-
echconfigs = ECHConfigList.new(
|
|
43
|
-
[
|
|
44
|
-
ECHConfig.new(
|
|
45
|
-
"\xfe\x0d".b,
|
|
46
|
-
ECHConfig::ECHConfigContents.new(
|
|
47
|
-
ECHConfig::ECHConfigContents::HpkeKeyConfig.new(
|
|
48
|
-
123,
|
|
49
|
-
ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeKemId.new(HPKE::DHKEM_X25519_HKDF_SHA256),
|
|
50
|
-
ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkePublicKey.new(key.raw_public_key),
|
|
51
|
-
[
|
|
52
|
-
ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite.new(
|
|
53
|
-
ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite::HpkeKdfId.new(HPKE::HKDF_SHA256),
|
|
54
|
-
ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite::HpkeAeadId.new(HPKE::AES_128_GCM)
|
|
55
|
-
)
|
|
56
|
-
]
|
|
57
|
-
),
|
|
58
|
-
32,
|
|
59
|
-
hostname.b,
|
|
60
|
-
ECHConfig::ECHConfigContents::Extensions.new('')
|
|
61
|
-
)
|
|
62
|
-
)
|
|
63
|
-
]
|
|
64
|
-
)
|
|
40
|
+
echconfigs = ECHConfigList.new([EchConfig.gen_ech_config(key.raw_public_key)])
|
|
65
41
|
File.write(fpath, key.private_to_pem + echconfigs.to_pem)
|
|
66
42
|
end
|
|
67
43
|
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
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module EchSpec
|
|
2
|
+
module EchConfig
|
|
3
|
+
# @param public_key [String]
|
|
4
|
+
# @param kem_id [Integer]
|
|
5
|
+
# @param kdf_id [Integer]
|
|
6
|
+
# @param aead_id [Integer]
|
|
7
|
+
#
|
|
8
|
+
# @return [ECHConfig]
|
|
9
|
+
def self.gen_ech_config(public_key, kem_id: HPKE::DHKEM_X25519_HKDF_SHA256, kdf_id: HPKE::HKDF_SHA256, aead_id: HPKE::AES_128_GCM)
|
|
10
|
+
version = "\xfe\x0d".b
|
|
11
|
+
config_id = 123
|
|
12
|
+
maximum_name_length = 32
|
|
13
|
+
hostname = 'localhost'
|
|
14
|
+
|
|
15
|
+
ECHConfig.new(
|
|
16
|
+
version,
|
|
17
|
+
ECHConfig::ECHConfigContents.new(
|
|
18
|
+
ECHConfig::ECHConfigContents::HpkeKeyConfig.new(
|
|
19
|
+
config_id,
|
|
20
|
+
ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeKemId.new(kem_id),
|
|
21
|
+
ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkePublicKey.new(public_key),
|
|
22
|
+
[
|
|
23
|
+
ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite.new(
|
|
24
|
+
ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite::HpkeKdfId.new(kdf_id),
|
|
25
|
+
ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite::HpkeAeadId.new(aead_id)
|
|
26
|
+
)
|
|
27
|
+
]
|
|
28
|
+
),
|
|
29
|
+
maximum_name_length,
|
|
30
|
+
hostname.b,
|
|
31
|
+
ECHConfig::ECHConfigContents::Extensions.new('')
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module EchSpec
|
|
2
|
+
module Spec
|
|
3
|
+
class Spec5_11 < WithSocket
|
|
4
|
+
# Finally, when the client offers the "encrypted_client_hello", if the
|
|
5
|
+
# payload is the inner variant and the server responds with
|
|
6
|
+
# HelloRetryRequest, it MUST include an "encrypted_client_hello"
|
|
7
|
+
# extension with the following payload:
|
|
8
|
+
#
|
|
9
|
+
# struct {
|
|
10
|
+
# opaque confirmation[8];
|
|
11
|
+
# } ECHHelloRetryRequest;
|
|
12
|
+
#
|
|
13
|
+
# The value of ECHHelloRetryRequest.confirmation is set to
|
|
14
|
+
# hrr_accept_confirmation as described in Section 7.2.1.
|
|
15
|
+
#
|
|
16
|
+
# https://datatracker.ietf.org/doc/html/rfc9849#section-5-11
|
|
17
|
+
|
|
18
|
+
# @return [EchSpec::SpecGroup]
|
|
19
|
+
def self.spec_group
|
|
20
|
+
SpecGroup.new(
|
|
21
|
+
'5-11',
|
|
22
|
+
[
|
|
23
|
+
SpecCase.new(
|
|
24
|
+
'MUST include an "encrypted_client_hello" extension with an 8-byte confirmation payload in HelloRetryRequest, if ClientHello offers the inner variant.',
|
|
25
|
+
method(:validate_hrr_ech_length)
|
|
26
|
+
)
|
|
27
|
+
]
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @param hostname [String]
|
|
32
|
+
# @param port [Integer]
|
|
33
|
+
# @param ech_config [ECHConfig]
|
|
34
|
+
#
|
|
35
|
+
# @return [EchSpec::Ok | Err]
|
|
36
|
+
def self.validate_hrr_ech_length(hostname, port, ech_config)
|
|
37
|
+
Spec5_11.new.do_validate_hrr_ech_length(hostname, port, ech_config)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param hostname [String]
|
|
41
|
+
# @param port [Integer]
|
|
42
|
+
# @param ech_config [ECHConfig]
|
|
43
|
+
#
|
|
44
|
+
# @return [EchSpec::Ok | Err]
|
|
45
|
+
def do_validate_hrr_ech_length(hostname, port, ech_config)
|
|
46
|
+
with_socket(hostname, port) do |socket|
|
|
47
|
+
_conn, _inner, _ch, hrr, = TLS13Client.recv_hrr(socket, hostname, ech_config, @stack)
|
|
48
|
+
validate_hrr_ech(hrr)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param hrr [TTTLS13::Message::ServerHello]
|
|
53
|
+
#
|
|
54
|
+
# @return [EchSpec::Ok | Err]
|
|
55
|
+
def validate_hrr_ech(hrr)
|
|
56
|
+
# Extensions#[] returns nil for UnknownExtension, so use super_fetch.
|
|
57
|
+
ex = hrr.extensions.super_fetch(TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO, nil)
|
|
58
|
+
case ex
|
|
59
|
+
in TTTLS13::Message::Extension::ECHHelloRetryRequest
|
|
60
|
+
Ok.new(nil)
|
|
61
|
+
in nil
|
|
62
|
+
Err.new('HelloRetryRequest did not include "encrypted_client_hello" extension', message_stack)
|
|
63
|
+
in TTTLS13::Message::Extension::UnknownExtension
|
|
64
|
+
# tttls1.3 deserializes the extension whose length is not 8 as UnknownExtension
|
|
65
|
+
Err.new("HelloRetryRequest \"encrypted_client_hello\" extension has #{ex.extension_data.length}-byte payload, expected 8 bytes", message_stack)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/echspec/spec/5.1-9.rb
CHANGED
|
@@ -92,14 +92,9 @@ module EchSpec
|
|
|
92
92
|
def self.padding_encoded_ch_inner(s,
|
|
93
93
|
server_name_length,
|
|
94
94
|
maximum_name_length)
|
|
95
|
-
padding_len =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
else
|
|
99
|
-
9 + maximum_name_length
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
padding_len = 31 - ((s.length + padding_len - 1) % 32)
|
|
95
|
+
padding_len = super.length - s.length
|
|
96
|
+
# at least one non-zero byte is needed for this test case
|
|
97
|
+
padding_len = 32 if padding_len.zero?
|
|
103
98
|
s + NON_ZERO * padding_len # padding with non-zero value
|
|
104
99
|
end
|
|
105
100
|
end
|
|
@@ -43,86 +43,32 @@ 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)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return Err.new('ECHConfigs did not have "retry_configs"', message_stack) \
|
|
51
|
-
if ex.retry_configs.nil? || ex.retry_configs.empty?
|
|
52
|
-
|
|
53
|
-
Ok.new(nil)
|
|
46
|
+
recv = TLS13Client.send_ch_with_greased_ech(socket, hostname, @stack)
|
|
47
|
+
validate_ee_ech(recv)
|
|
48
|
+
rescue ECHConfig::DecodeError
|
|
49
|
+
Err.new('EncryptedExtensions "encrypted_client_hello" extension could not be decoded', message_stack)
|
|
54
50
|
end
|
|
55
51
|
end
|
|
56
52
|
|
|
57
|
-
#
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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?
|
|
53
|
+
# @param ee [TTTLS13::Message::EncryptedExtensions]
|
|
54
|
+
#
|
|
55
|
+
# @return [EchSpec::Ok | Err]
|
|
56
|
+
def validate_ee_ech(ee)
|
|
57
|
+
# Extensions#[] returns nil for UnknownExtension, so use super_fetch.
|
|
58
|
+
ex = ee.extensions.super_fetch(TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO, nil)
|
|
59
|
+
case ex
|
|
60
|
+
in TTTLS13::Message::Extension::ECHEncryptedExtensions
|
|
61
|
+
return Err.new('EncryptedExtensions "encrypted_client_hello" extension did not have "retry_configs"', message_stack) \
|
|
62
|
+
if ex.retry_configs.nil? || ex.retry_configs.empty?
|
|
93
63
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
64
|
+
Ok.new(nil)
|
|
65
|
+
in nil
|
|
66
|
+
Err.new('EncryptedExtensions did not include "encrypted_client_hello" extension', message_stack)
|
|
67
|
+
in TTTLS13::Message::Extension::UnknownExtension
|
|
68
|
+
# tttls1.3 deserializes the extension whose length field mismatches as UnknownExtension
|
|
69
|
+
Err.new('EncryptedExtensions "encrypted_client_hello" extension could not be decoded', message_stack)
|
|
117
70
|
end
|
|
118
|
-
|
|
119
|
-
raise Error::BeforeTargetSituationError, 'not received EncryptedExtensions' \
|
|
120
|
-
unless recv.is_a?(TTTLS13::Message::EncryptedExtensions)
|
|
121
|
-
|
|
122
|
-
recv
|
|
123
71
|
end
|
|
124
|
-
# rubocop: enable Metrics/AbcSize
|
|
125
|
-
# rubocop: enable Metrics/MethodLength
|
|
126
72
|
end
|
|
127
73
|
end
|
|
128
74
|
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
module EchSpec
|
|
2
|
+
module Spec
|
|
3
|
+
class Spec7_2_1 < WithSocket
|
|
4
|
+
# Upon receipt of an "encrypted_client_hello" extension of type inner
|
|
5
|
+
# in a ClientHello, if the backend server negotiates TLS 1.3 or higher,
|
|
6
|
+
# then it MUST confirm ECH acceptance to the client by computing its
|
|
7
|
+
# ServerHello as described here.
|
|
8
|
+
#
|
|
9
|
+
# https://datatracker.ietf.org/doc/html/rfc9849#section-7.2-1
|
|
10
|
+
|
|
11
|
+
# @return [EchSpec::SpecGroup]
|
|
12
|
+
def self.spec_group
|
|
13
|
+
SpecGroup.new(
|
|
14
|
+
'7.2-1',
|
|
15
|
+
[
|
|
16
|
+
SpecCase.new(
|
|
17
|
+
'MUST confirm ECH acceptance in ServerHello, if it confirmed ECH acceptance in HelloRetryRequest.',
|
|
18
|
+
method(:validate_sh_confirmation_after_hrr)
|
|
19
|
+
)
|
|
20
|
+
]
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @param hostname [String]
|
|
25
|
+
# @param port [Integer]
|
|
26
|
+
# @param ech_config [ECHConfig]
|
|
27
|
+
#
|
|
28
|
+
# @return [EchSpec::Ok | Err]
|
|
29
|
+
def self.validate_sh_confirmation_after_hrr(hostname, port, ech_config)
|
|
30
|
+
Spec7_2_1.new.do_validate_sh_confirmation_after_hrr(hostname, port, ech_config)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param hostname [String]
|
|
34
|
+
# @param port [Integer]
|
|
35
|
+
# @param ech_config [ECHConfig]
|
|
36
|
+
#
|
|
37
|
+
# @return [EchSpec::Ok | Err]
|
|
38
|
+
def do_validate_sh_confirmation_after_hrr(hostname, port, ech_config)
|
|
39
|
+
with_socket(hostname, port) do |socket|
|
|
40
|
+
recv, confirmation = send_2nd_ch_after_hrr(socket, hostname, ech_config)
|
|
41
|
+
return Err.new('did not send expected handshake message: ServerHello', message_stack) \
|
|
42
|
+
unless recv.is_a?(TTTLS13::Message::ServerHello) && !recv.hrr?
|
|
43
|
+
|
|
44
|
+
validate_sh_confirmation(confirmation, recv)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @param expected [String] accept_confirmation
|
|
49
|
+
# @param sh [TTTLS13::Message::ServerHello]
|
|
50
|
+
#
|
|
51
|
+
# @return [EchSpec::Ok | Err]
|
|
52
|
+
def validate_sh_confirmation(expected, sh)
|
|
53
|
+
return Ok.new(nil) if sh.random[-8..] == expected
|
|
54
|
+
|
|
55
|
+
Err.new('the last 8 bytes of ServerHello.random did not match accept_confirmation, although HelloRetryRequest confirmed ECH acceptance', message_stack)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# rubocop: disable Metrics/AbcSize
|
|
59
|
+
# rubocop: disable Metrics/MethodLength
|
|
60
|
+
def send_2nd_ch_after_hrr(socket, hostname, ech_config)
|
|
61
|
+
conn, inner1, _ch1, hrr, ech_state = TLS13Client.recv_hrr(socket, hostname, ech_config, @stack)
|
|
62
|
+
transcript = TTTLS13::Transcript.new
|
|
63
|
+
transcript[TTTLS13::CH1] = [inner1, inner1.serialize]
|
|
64
|
+
transcript[TTTLS13::HRR] = [hrr, hrr.serialize]
|
|
65
|
+
# shared_secret is not used to compute (hrr_)accept_confirmation
|
|
66
|
+
key_schedule = TTTLS13::KeySchedule.new(
|
|
67
|
+
shared_secret: nil,
|
|
68
|
+
cipher_suite: hrr.cipher_suite,
|
|
69
|
+
transcript:
|
|
70
|
+
)
|
|
71
|
+
ex = hrr.extensions[TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO]
|
|
72
|
+
raise Error::BeforeTargetSituationError, 'HelloRetryRequest did not confirm ECH acceptance' \
|
|
73
|
+
if ex.nil? || ex.confirmation != key_schedule.hrr_accept_confirmation
|
|
74
|
+
|
|
75
|
+
# send 2nd ClientHello; ClientHelloInner2 is based on ClientHelloInner1,
|
|
76
|
+
# and its "encrypted_client_hello" extension of type inner is left
|
|
77
|
+
# unmodified.
|
|
78
|
+
inner2 = TTTLS13::Message::ClientHello.new(
|
|
79
|
+
legacy_version: inner1.legacy_version,
|
|
80
|
+
random: inner1.random,
|
|
81
|
+
legacy_session_id: inner1.legacy_session_id,
|
|
82
|
+
cipher_suites: inner1.cipher_suites,
|
|
83
|
+
legacy_compression_methods: inner1.legacy_compression_methods,
|
|
84
|
+
extensions: TLS13Client.gen_newch_extensions(inner1, hrr)
|
|
85
|
+
)
|
|
86
|
+
ch, inner2 = TTTLS13::Ech.offer_new_ech(inner2, ech_state)
|
|
87
|
+
conn.send_record(
|
|
88
|
+
TTTLS13::Message::Record.new(
|
|
89
|
+
type: TTTLS13::Message::ContentType::HANDSHAKE,
|
|
90
|
+
messages: [ch],
|
|
91
|
+
cipher: TTTLS13::Cryptograph::Passer.new
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
@stack << inner2
|
|
95
|
+
@stack << ch
|
|
96
|
+
|
|
97
|
+
recv, = conn.recv_message(TTTLS13::Cryptograph::Passer.new)
|
|
98
|
+
recv, = conn.recv_message(TTTLS13::Cryptograph::Passer.new) \
|
|
99
|
+
if recv.is_a?(TTTLS13::Message::ChangeCipherSpec)
|
|
100
|
+
@stack << recv
|
|
101
|
+
return [recv, nil] unless recv.is_a?(TTTLS13::Message::ServerHello) && !recv.hrr?
|
|
102
|
+
|
|
103
|
+
transcript[TTTLS13::CH] = [inner2, inner2.serialize]
|
|
104
|
+
sh = recv
|
|
105
|
+
transcript[TTTLS13::SH] = [sh, sh.serialize]
|
|
106
|
+
[recv, key_schedule.accept_confirmation]
|
|
107
|
+
end
|
|
108
|
+
# rubocop: enable Metrics/AbcSize
|
|
109
|
+
# rubocop: enable Metrics/MethodLength
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
data/lib/echspec/spec/9.rb
CHANGED
|
@@ -77,12 +77,87 @@ module EchSpec
|
|
|
77
77
|
return Err.new("HTTPS resource record for #{hostname} does NOT have ech SvcParams.", nil) if rr.params[ech].nil?
|
|
78
78
|
|
|
79
79
|
octet = rr.params[ech].value
|
|
80
|
-
Err.new('Failed to parse ECHConfig on HTTPS resource record.', nil) \
|
|
81
|
-
unless octet.length == octet.slice(0, 2).unpack1('n') + 2
|
|
80
|
+
return Err.new('Failed to parse ECHConfig on HTTPS resource record.', nil) \
|
|
81
|
+
unless octet.length >= 2 && octet.length == octet.slice(0, 2).unpack1('n') + 2
|
|
82
82
|
|
|
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
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
139
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
|
140
|
+
def parse_origin_svcb(body)
|
|
141
|
+
h = JSON.parse(body)
|
|
142
|
+
echs = h['endpoints']&.filter_map { |e| e.dig('params', 'ech') }
|
|
143
|
+
return Err.new('The origin-svcb well-known resource does NOT have ech SvcParams.', nil) if echs.nil? || echs.empty?
|
|
144
|
+
|
|
145
|
+
# if parsing any one of the ech values fails, return Err
|
|
146
|
+
ech_configs = echs.flat_map do |ech|
|
|
147
|
+
octet = Base64.decode64(ech)
|
|
148
|
+
return Err.new('Failed to parse ECHConfig on the origin-svcb well-known resource.', nil) \
|
|
149
|
+
unless octet.length >= 2 && octet.length == octet.slice(0, 2).unpack1('n') + 2
|
|
150
|
+
|
|
151
|
+
ECHConfig.decode_vectors(octet.slice(2..))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
Ok.new(ech_configs)
|
|
155
|
+
rescue StandardError => e
|
|
156
|
+
Err.new(e.message, nil)
|
|
157
|
+
end
|
|
158
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
159
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
|
160
|
+
|
|
86
161
|
# @param pem [String]
|
|
87
162
|
#
|
|
88
163
|
# @return [EchSpec::Ok<Array<ECHConfig>> | Err]
|
|
@@ -92,6 +167,9 @@ module EchSpec
|
|
|
92
167
|
.first
|
|
93
168
|
.gsub("\n", '')
|
|
94
169
|
b = Base64.decode64(s)
|
|
170
|
+
return Err.new('Failed to parse ECHConfig on the PEM file.', nil) \
|
|
171
|
+
unless b.length >= 2 && b.length == b.slice(0, 2).unpack1('n') + 2
|
|
172
|
+
|
|
95
173
|
ech_configs = ECHConfig.decode_vectors(b.slice(2..))
|
|
96
174
|
Ok.new(ech_configs)
|
|
97
175
|
rescue StandardError
|
data/lib/echspec/spec.rb
CHANGED
|
@@ -176,10 +176,10 @@ module EchSpec
|
|
|
176
176
|
|
|
177
177
|
def spec_groups
|
|
178
178
|
# https://datatracker.ietf.org/doc/html/rfc9849#section-5
|
|
179
|
-
groups = [Spec5_1_9, Spec5_1_10]
|
|
179
|
+
groups = [Spec5_11, Spec5_1_9, Spec5_1_10]
|
|
180
180
|
|
|
181
181
|
# https://datatracker.ietf.org/doc/html/rfc9849#section-7
|
|
182
|
-
groups += [Spec7_5, Spec7_1_11, Spec7_1_14_2_1, Spec7_1_1_2, Spec7_1_1_5]
|
|
182
|
+
groups += [Spec7_5, Spec7_1_11, Spec7_1_14_2_1, Spec7_1_1_2, Spec7_1_1_5, Spec7_2_1]
|
|
183
183
|
|
|
184
184
|
groups.map(&:spec_group)
|
|
185
185
|
end
|
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
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
require 'base64'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'net/http'
|
|
2
4
|
require 'optparse'
|
|
3
5
|
require 'pp'
|
|
4
6
|
require 'resolv'
|
|
@@ -10,6 +12,7 @@ require_relative 'echspec/utils'
|
|
|
10
12
|
require_relative 'echspec/log'
|
|
11
13
|
require_relative 'echspec/error'
|
|
12
14
|
require_relative 'echspec/result'
|
|
15
|
+
require_relative 'echspec/ech_config'
|
|
13
16
|
require_relative 'echspec/tls13_client'
|
|
14
17
|
require_relative 'echspec/spec_case'
|
|
15
18
|
require_relative 'echspec/spec_group'
|
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.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- thekuwayama
|
|
@@ -101,12 +101,17 @@ 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
|
|
109
|
+
- lib/echspec/ech_config.rb
|
|
106
110
|
- lib/echspec/error.rb
|
|
107
111
|
- lib/echspec/log.rb
|
|
108
112
|
- lib/echspec/result.rb
|
|
109
113
|
- lib/echspec/spec.rb
|
|
114
|
+
- lib/echspec/spec/5-11.rb
|
|
110
115
|
- lib/echspec/spec/5.1-10.rb
|
|
111
116
|
- lib/echspec/spec/5.1-9.rb
|
|
112
117
|
- lib/echspec/spec/7-5.rb
|
|
@@ -114,6 +119,7 @@ files:
|
|
|
114
119
|
- lib/echspec/spec/7.1-14.2.1.rb
|
|
115
120
|
- lib/echspec/spec/7.1.1-2.rb
|
|
116
121
|
- lib/echspec/spec/7.1.1-5.rb
|
|
122
|
+
- lib/echspec/spec/7.2-1.rb
|
|
117
123
|
- lib/echspec/spec/9.rb
|
|
118
124
|
- lib/echspec/spec_case.rb
|
|
119
125
|
- lib/echspec/spec_group.rb
|
|
@@ -138,7 +144,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
138
144
|
- !ruby/object:Gem::Version
|
|
139
145
|
version: '0'
|
|
140
146
|
requirements: []
|
|
141
|
-
rubygems_version: 4.0.
|
|
147
|
+
rubygems_version: 4.0.20
|
|
142
148
|
specification_version: 4
|
|
143
149
|
summary: A conformance testing tool for ECH implementation
|
|
144
150
|
test_files: []
|