echspec 0.0.7 → 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 +52 -28
- data/lib/echspec/cli/ech_config_printer.rb +13 -13
- data/lib/echspec/cli/gen_configs.rb +1 -25
- 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 -4
- data/lib/echspec/spec/7.2-1.rb +112 -0
- data/lib/echspec/spec/9.rb +12 -2
- data/lib/echspec/spec.rb +2 -2
- data/lib/echspec/version.rb +1 -1
- data/lib/echspec.rb +1 -0
- metadata +5 -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
|
@@ -56,6 +56,7 @@ Example output:
|
|
|
56
56
|
$ echspec run research.cloudflare.com
|
|
57
57
|
TLS Encrypted Client Hello Server
|
|
58
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]
|
|
59
60
|
✔ MUST abort with an "illegal_parameter" alert, if EncodedClientHelloInner is padded with non-zero values. [5.1-9]
|
|
60
61
|
✔ MUST abort with an "illegal_parameter" alert, if any referenced extension is missing in ClientHelloOuter. [5.1-10]
|
|
61
62
|
✔ MUST abort with an "illegal_parameter" alert, if any extension is referenced in OuterExtensions more than once. [5.1-10]
|
|
@@ -68,6 +69,7 @@ TLS Encrypted Client Hello Server
|
|
|
68
69
|
✔ MUST abort with a "missing_extension" alert, if 2nd ClientHelloOuter does not contains the "encrypted_client_hello" extension. [7.1.1-2]
|
|
69
70
|
✔ MUST abort with an "illegal_parameter" alert, if 2nd ClientHelloOuter "encrypted_client_hello" enc is empty. [7.1.1-2]
|
|
70
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]
|
|
71
73
|
|
|
72
74
|
Failures:
|
|
73
75
|
|
|
@@ -302,6 +304,28 @@ AQALZXhhbXBsZS5jb20AAA==
|
|
|
302
304
|
-----END ECHCONFIG-----
|
|
303
305
|
```
|
|
304
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
|
+
|
|
305
329
|
### resolve
|
|
306
330
|
|
|
307
331
|
You can resolve ECHConfigs for a hostname and print fields using the `resolve` subcommand.
|
|
@@ -326,20 +350,20 @@ Options:
|
|
|
326
350
|
```
|
|
327
351
|
```sh-session
|
|
328
352
|
$ echspec resolve research.cloudflare.com
|
|
329
|
-
ECHConfig:
|
|
330
|
-
version(
|
|
331
|
-
length(
|
|
332
|
-
contents
|
|
333
|
-
key_config
|
|
334
|
-
config_id(
|
|
335
|
-
kem_id(
|
|
336
|
-
public_key(
|
|
337
|
-
cipher_suites
|
|
338
|
-
kdf_id(
|
|
339
|
-
aead_id(
|
|
340
|
-
maximum_name_length(
|
|
341
|
-
public_name(
|
|
342
|
-
extensions(
|
|
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): │
|
|
343
367
|
```
|
|
344
368
|
|
|
345
369
|
### grease
|
|
@@ -362,20 +386,20 @@ Options:
|
|
|
362
386
|
```
|
|
363
387
|
```sh-session
|
|
364
388
|
$ echspec grease research.cloudflare.com
|
|
365
|
-
ECHConfig:
|
|
366
|
-
version(
|
|
367
|
-
length(
|
|
368
|
-
contents
|
|
369
|
-
key_config
|
|
370
|
-
config_id(
|
|
371
|
-
kem_id(
|
|
372
|
-
public_key(
|
|
373
|
-
cipher_suites
|
|
374
|
-
kdf_id(
|
|
375
|
-
aead_id(
|
|
376
|
-
maximum_name_length(
|
|
377
|
-
public_name(
|
|
378
|
-
extensions(
|
|
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): │
|
|
379
403
|
```
|
|
380
404
|
|
|
381
405
|
|
|
@@ -28,25 +28,25 @@ module EchSpec
|
|
|
28
28
|
ext = ec.extensions.octet.unpack1('H*').scan(/.{2}/).join(' ')
|
|
29
29
|
cipher_suite_pairs = kc.cipher_suites.flat_map do |cs|
|
|
30
30
|
[
|
|
31
|
-
[' kdf_id(
|
|
32
|
-
[' aead_id(
|
|
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
33
|
]
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
[
|
|
37
37
|
['ECHConfig:', ''],
|
|
38
|
-
[' version(
|
|
39
|
-
[' length(
|
|
40
|
-
[' contents
|
|
41
|
-
[' key_config
|
|
42
|
-
[' config_id(
|
|
43
|
-
[' kem_id(
|
|
44
|
-
[' public_key(
|
|
45
|
-
[' cipher_suites
|
|
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
46
|
*cipher_suite_pairs,
|
|
47
|
-
[' maximum_name_length(
|
|
48
|
-
[' public_name(
|
|
49
|
-
[' extensions(
|
|
47
|
+
[' maximum_name_length(integer):', ec.maximum_name_length],
|
|
48
|
+
[' public_name(string):', ec.public_name],
|
|
49
|
+
[' extensions(hex):', ext]
|
|
50
50
|
]
|
|
51
51
|
end
|
|
52
52
|
# rubocop: enable Metrics/AbcSize
|
|
@@ -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,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
|
|
@@ -44,13 +44,29 @@ module EchSpec
|
|
|
44
44
|
def do_validate_ee_retry_configs(hostname, port)
|
|
45
45
|
with_socket(hostname, port) do |socket|
|
|
46
46
|
recv = TLS13Client.send_ch_with_greased_ech(socket, hostname, @stack)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
validate_ee_ech(recv)
|
|
48
|
+
rescue ECHConfig::DecodeError
|
|
49
|
+
Err.new('EncryptedExtensions "encrypted_client_hello" extension could not be decoded', message_stack)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
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) \
|
|
51
62
|
if ex.retry_configs.nil? || ex.retry_configs.empty?
|
|
52
63
|
|
|
53
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)
|
|
54
70
|
end
|
|
55
71
|
end
|
|
56
72
|
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,8 +77,8 @@ 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
|
|
@@ -135,6 +135,8 @@ module EchSpec
|
|
|
135
135
|
# @param body [String]
|
|
136
136
|
#
|
|
137
137
|
# @return [EchSpec::Ok | Err]
|
|
138
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
|
139
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
|
138
140
|
def parse_origin_svcb(body)
|
|
139
141
|
h = JSON.parse(body)
|
|
140
142
|
echs = h['endpoints']&.filter_map { |e| e.dig('params', 'ech') }
|
|
@@ -143,6 +145,9 @@ module EchSpec
|
|
|
143
145
|
# if parsing any one of the ech values fails, return Err
|
|
144
146
|
ech_configs = echs.flat_map do |ech|
|
|
145
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
|
+
|
|
146
151
|
ECHConfig.decode_vectors(octet.slice(2..))
|
|
147
152
|
end
|
|
148
153
|
|
|
@@ -150,6 +155,8 @@ module EchSpec
|
|
|
150
155
|
rescue StandardError => e
|
|
151
156
|
Err.new(e.message, nil)
|
|
152
157
|
end
|
|
158
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
|
159
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
|
153
160
|
|
|
154
161
|
# @param pem [String]
|
|
155
162
|
#
|
|
@@ -160,6 +167,9 @@ module EchSpec
|
|
|
160
167
|
.first
|
|
161
168
|
.gsub("\n", '')
|
|
162
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
|
+
|
|
163
173
|
ech_configs = ECHConfig.decode_vectors(b.slice(2..))
|
|
164
174
|
Ok.new(ech_configs)
|
|
165
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/version.rb
CHANGED
data/lib/echspec.rb
CHANGED
|
@@ -12,6 +12,7 @@ require_relative 'echspec/utils'
|
|
|
12
12
|
require_relative 'echspec/log'
|
|
13
13
|
require_relative 'echspec/error'
|
|
14
14
|
require_relative 'echspec/result'
|
|
15
|
+
require_relative 'echspec/ech_config'
|
|
15
16
|
require_relative 'echspec/tls13_client'
|
|
16
17
|
require_relative 'echspec/spec_case'
|
|
17
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
|
|
@@ -106,10 +106,12 @@ files:
|
|
|
106
106
|
- lib/echspec/cli/grease.rb
|
|
107
107
|
- lib/echspec/cli/resolve.rb
|
|
108
108
|
- lib/echspec/cli/run.rb
|
|
109
|
+
- lib/echspec/ech_config.rb
|
|
109
110
|
- lib/echspec/error.rb
|
|
110
111
|
- lib/echspec/log.rb
|
|
111
112
|
- lib/echspec/result.rb
|
|
112
113
|
- lib/echspec/spec.rb
|
|
114
|
+
- lib/echspec/spec/5-11.rb
|
|
113
115
|
- lib/echspec/spec/5.1-10.rb
|
|
114
116
|
- lib/echspec/spec/5.1-9.rb
|
|
115
117
|
- lib/echspec/spec/7-5.rb
|
|
@@ -117,6 +119,7 @@ files:
|
|
|
117
119
|
- lib/echspec/spec/7.1-14.2.1.rb
|
|
118
120
|
- lib/echspec/spec/7.1.1-2.rb
|
|
119
121
|
- lib/echspec/spec/7.1.1-5.rb
|
|
122
|
+
- lib/echspec/spec/7.2-1.rb
|
|
120
123
|
- lib/echspec/spec/9.rb
|
|
121
124
|
- lib/echspec/spec_case.rb
|
|
122
125
|
- lib/echspec/spec_group.rb
|
|
@@ -141,7 +144,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
141
144
|
- !ruby/object:Gem::Version
|
|
142
145
|
version: '0'
|
|
143
146
|
requirements: []
|
|
144
|
-
rubygems_version: 4.0.
|
|
147
|
+
rubygems_version: 4.0.20
|
|
145
148
|
specification_version: 4
|
|
146
149
|
summary: A conformance testing tool for ECH implementation
|
|
147
150
|
test_files: []
|