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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3902d0c840b18babccf1c9e7c764c570fd6b29e6f9688e0f9f0c5696bae4e871
4
- data.tar.gz: c01a13d625bc07918e8f80db55266019ff3031c597e30e9be0dbd4515755bcb5
3
+ metadata.gz: b4b19de1ec6feae0372b847a16805e3f2711283341d85b8b1e309ba5034c4d1b
4
+ data.tar.gz: edb1bc1ad4ca9489a2de82a6406d3f7e6dd57ecb785c2063a024ccfea5cc995a
5
5
  SHA512:
6
- metadata.gz: 22d2c6085da85d18cbbf3d5e52f7f4f8511f2d878da9642ce1ad53817e4a33d8429c132456305592dd37e547c53998aa2db0bc091df276b4a216f4b2283b9492
7
- data.tar.gz: 9fa6ac8c64727d9e64418b7bcd3e38a98dc693b59ff52f6c7a42b4c7dc7635420d5dc60ef7f11ecf9837cd8d85a0fe7b5fd59987d81f57539ded979f330c4938
6
+ metadata.gz: 93d42d067d4839ff7ba052a81b3b587be0b7fa10f0acb372a97f6cfee4240a3dec35fd76899116164cb8a3276114b3bf5ba315cab1228af72a56b5a8cc841867
7
+ data.tar.gz: f5fa1e47ed16a24708583c6a2fa6419616c6df389c7d993bb0b6e52b5626581d6d9140b974e87dcd64d923c08ed2fb24cc8ef8cd412502c4fc921246578071a5
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 4.0.2
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(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): │
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(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): │
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(uint16):', cs.kdf_id.encode.unpack1('H4').scan(/.{2}/).join(' ')],
32
- [' aead_id(uint16):', cs.aead_id.encode.unpack1('H4').scan(/.{2}/).join(' ')]
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(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):', ''],
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(uint8):', ec.maximum_name_length],
48
- [' public_name(opaque):', ec.public_name],
49
- [' extensions(opaque):', ext]
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
@@ -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
- if server_name_length.positive?
97
- [maximum_name_length - server_name_length, 0].max
98
- else
99
- 9 + maximum_name_length
100
- end
101
-
102
- padding_len = 31 - ((s.length + padding_len - 1) % 32)
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
- ex = recv.extensions[TTTLS13::Message::ExtensionType::ENCRYPTED_CLIENT_HELLO]
48
- return Err.new('did not send expected alert: encrypted_client_hello', message_stack) \
49
- unless ex.is_a?(TTTLS13::Message::Extension::ECHEncryptedExtensions)
50
- return Err.new('ECHConfigs did not have "retry_configs"', message_stack) \
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module EchSpec
2
- VERSION = '0.0.7'.freeze
2
+ VERSION = '0.0.8'.freeze
3
3
  end
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.7
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.6
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: []