echspec 0.0.1 → 0.0.3

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: 942fd582bb741ca465bb5255b458ef3cbf3ff8cd09f20b45957df8c64d32f55a
4
- data.tar.gz: e5748a921bc1ab45c053884690fe0e7f5718b8847c6376f87100f22173f8dd9c
3
+ metadata.gz: 4ff77e42c8a7e2d787372fe6f96f3c00619fb6bcb3feea8df89c5400a838aef6
4
+ data.tar.gz: d317448071982052658909b19a99f535ce99e1c9f063de65be9cecae1ab4179f
5
5
  SHA512:
6
- metadata.gz: a9597e9295d2b6a8d2b836a59a9ec1013edb42b2fc605e2dacd9ba274510ccb9982f1971d6aa4d092f1896da19a4d2317386d72f9d690639049e288e96fe1fe5
7
- data.tar.gz: d104c73ca8124af91cbaeb172757d8353e3a328668c89a9d1b66f01281ac6d8e5bacd298ac96b05fa27d1b70dadceb9a216c8b873db9607286a7e0767f304c58
6
+ metadata.gz: 7c4eea37fd0a7002bf6fd2a68b0a5c299bf84b43a71933053fb6cf2df59aedc17836e3299e809d1e5889130ccfcbd6ec19a276ffda556da8cf58b9e28042e211
7
+ data.tar.gz: c56a875c0547d3456958a5cbd9b8d3ec7d4e921033d176c2b2835b80657b59d9d207e9dc0342564506f89d9cee313168427e906e9d6249f6298a0e90d6a9a5cf
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 3.2
2
+ TargetRubyVersion: 4.0
3
3
 
4
4
  Layout/LineLength:
5
5
  Max: 200
@@ -9,6 +9,7 @@ Metrics/AbcSize:
9
9
 
10
10
  Metrics/BlockLength:
11
11
  Exclude:
12
+ - 'Rakefile'
12
13
  - 'spec/*.rb'
13
14
 
14
15
  Metrics/ClassLength:
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.3.6
1
+ 4.0.1
data/Gemfile CHANGED
@@ -1,13 +1,14 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
3
  gem 'base64'
4
+ gem 'ech_config', github: 'thekuwayama/ech_config'
4
5
  gem 'resolv', '> 0.4.0'
5
6
  gem 'tttls1.3', github: 'thekuwayama/tttls1.3'
6
7
 
7
8
  group :development do
8
9
  gem 'rake', '13.2.1'
9
10
  gem 'rspec'
10
- gem 'rubocop', '1.62.0'
11
+ gem 'rubocop', '1.82.1'
11
12
  end
12
13
 
13
14
  gemspec
data/README.md CHANGED
@@ -8,10 +8,10 @@
8
8
 
9
9
  ![echspec demo](docs/echspec-demo.png)
10
10
 
11
- - https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22
11
+ - https://datatracker.ietf.org/doc/html/rfc9849
12
12
 
13
13
 
14
- ## Initial Setup
14
+ ## Installation
15
15
 
16
16
  The gem is available at [rubygems.org](https://rubygems.org/gems/echspec). You can install it the following:
17
17
 
@@ -44,12 +44,20 @@ TLS Encrypted Client Hello Server
44
44
  ✔ MUST abort with an "illegal_parameter" alert, if "encrypted_client_hello" is referenced in OuterExtensions. [5.1-10]
45
45
  ✔ MUST abort with an "illegal_parameter" alert, if the extensions in ClientHelloOuter corresponding to those in OuterExtensions do not occur in the same order. [5.1-10]
46
46
  ✔ MUST abort with an "illegal_parameter" alert, if ECHClientHello.type is not a valid ECHClientHelloType in ClientHelloInner. [7-5]
47
- MUST abort with an "illegal_parameter" alert, if ECHClientHello.type is not a valid ECHClientHelloType in ClientHelloOuter. [7-5]
47
+ x MUST abort with an "illegal_parameter" alert, if ECHClientHello.type is not a valid ECHClientHelloType in ClientHelloOuter. [7-5]
48
48
  ✔ MUST abort with an "illegal_parameter" alert, if ClientHelloInner offers TLS 1.2 or below. [7.1-11]
49
49
  ✔ MUST include the "encrypted_client_hello" extension in its EncryptedExtensions with the "retry_configs" field set to one or more ECHConfig. [7.1-14.2.1]
50
50
  ✔ MUST abort with a "missing_extension" alert, if 2nd ClientHelloOuter does not contains the "encrypted_client_hello" extension. [7.1.1-2]
51
51
  ✔ MUST abort with an "illegal_parameter" alert, if 2nd ClientHelloOuter "encrypted_client_hello" enc is empty. [7.1.1-2]
52
52
  ✔ MUST abort with a "decrypt_error" alert, if fails to decrypt 2nd ClientHelloOuter. [7.1.1-5]
53
+
54
+ Failures:
55
+
56
+ 1) MUST abort with an "illegal_parameter" alert, if ECHClientHello.type is not a valid ECHClientHelloType in ClientHelloOuter. [7-5]
57
+ https://datatracker.ietf.org/doc/html/rfc9849#section-7-5
58
+ did not send expected alert: illegal_parameter
59
+
60
+ 1 failure
53
61
  ```
54
62
 
55
63
  By default, `echspec` retrieves ECHConfigs via HTTPS records. By using the `-f, --file FILE` option, you can specify an ECHConfig pem file. If you need to test the server on localhost, you can run it the following:
data/Rakefile CHANGED
@@ -1,8 +1,49 @@
1
1
  require 'bundler/gem_tasks'
2
+ require 'ech_config'
3
+ require 'hpke'
4
+ require 'openssl'
2
5
  require 'rspec/core/rake_task'
3
6
  require 'rubocop/rake_task'
4
7
 
5
8
  RuboCop::RakeTask.new
6
9
  RSpec::Core::RakeTask.new(:spec)
7
10
 
11
+ TMP_DIR = "#{__dir__}/tmp".freeze
12
+ ECHCONFIGS = "#{TMP_DIR}/echconfigs.pem".freeze
13
+
14
+ directory TMP_DIR
15
+
16
+ file ECHCONFIGS => TMP_DIR do
17
+ puts "generate #{ECHCONFIGS}..."
18
+
19
+ key = OpenSSL::PKey.generate_key('X25519')
20
+ echconfigs = ECHConfigList.new(
21
+ [
22
+ ECHConfig.new(
23
+ "\xfe\x0d".b,
24
+ ECHConfig::ECHConfigContents.new(
25
+ ECHConfig::ECHConfigContents::HpkeKeyConfig.new(
26
+ 123,
27
+ ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeKemId.new(HPKE::DHKEM_X25519_HKDF_SHA256),
28
+ ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkePublicKey.new(key.raw_public_key),
29
+ [
30
+ ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite.new(
31
+ ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite::HpkeKdfId.new(HPKE::HKDF_SHA256),
32
+ ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite::HpkeAeadId.new(HPKE::AES_128_GCM)
33
+ )
34
+ ]
35
+ ),
36
+ 32,
37
+ 'localhost'.b,
38
+ ECHConfig::ECHConfigContents::Extensions.new('')
39
+ )
40
+ )
41
+ ]
42
+ )
43
+ File.write(ECHCONFIGS, key.private_to_pem + echconfigs.to_pem)
44
+ end
45
+
46
+ desc 'generate echconfigs file'
47
+ task gen_echconfigs: ECHCONFIGS
48
+
8
49
  task default: %i[rubocop spec]
Binary file
data/echspec.gemspec CHANGED
@@ -11,16 +11,20 @@ Gem::Specification.new do |spec|
11
11
  spec.description = spec.summary
12
12
  spec.homepage = 'https://github.com/thekuwayama/echspec'
13
13
  spec.license = 'MIT'
14
- spec.required_ruby_version = '>=3.2'
14
+ spec.required_ruby_version = '>=4.0'
15
15
 
16
- spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
17
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
16
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
17
+ `git ls-files -z`.split("\x0").reject do |f|
18
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
19
+ end
20
+ end
18
21
  spec.require_paths = ['lib']
19
22
  spec.bindir = 'exe'
20
23
  spec.executables = ['echspec']
21
24
 
22
25
  spec.add_development_dependency 'bundler'
23
26
  spec.add_dependency 'base64'
27
+ spec.add_dependency 'ech_config', '~> 0.0.4'
24
28
  spec.add_dependency 'resolv', '> 0.4.0'
25
- spec.add_dependency 'tttls1.3', '~> 0.3.4'
29
+ spec.add_dependency 'tttls1.3', '~> 0.3.6'
26
30
  end
data/lib/echspec/log.rb CHANGED
@@ -34,12 +34,16 @@ module EchSpec
34
34
  'EncryptedExtensions'
35
35
  in TTTLS13::Message::Certificate
36
36
  'Certificate'
37
+ in TTTLS13::Message::CompressedCertificate
38
+ 'CompressedCertificate'
37
39
  in TTTLS13::Message::CertificateVerify
38
40
  'CertificateVerify'
39
41
  in TTTLS13::Message::Finished
40
42
  'Finished'
41
43
  in TTTLS13::Message::EndOfEarlyData
42
44
  'EndOfEarlyData'
45
+ in TTTLS13::Message::NewSessionTicket
46
+ 'NewSessionTicket'
43
47
  in TTTLS13::Message::Alert
44
48
  'Alert'
45
49
  end
@@ -1,7 +1,7 @@
1
1
  module EchSpec
2
2
  module Spec
3
3
  class Spec5_1_10 < WithSocket
4
- # Next it makes a copy of the client_hello field and copies the
4
+ # Next, it makes a copy of the client_hello field and copies the
5
5
  # legacy_session_id field from ClientHelloOuter. It then looks for an
6
6
  # "ech_outer_extensions" extension. If found, it replaces the extension
7
7
  # with the corresponding sequence of extensions in the
@@ -14,7 +14,7 @@ module EchSpec
14
14
  # * The extensions in ClientHelloOuter corresponding to those in
15
15
  # OuterExtensions do not occur in the same order.
16
16
  #
17
- # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-5.1-10
17
+ # https://datatracker.ietf.org/doc/html/rfc9849#section-5.1-10
18
18
 
19
19
  # @return [EchSpec::SpecGroup]
20
20
  def self.spec_group
@@ -130,7 +130,7 @@ module EchSpec
130
130
  end
131
131
 
132
132
  class MissingReferencedExtensions < TTTLS13::Message::Extensions
133
- # @param _ [Array of TTTLS13::Message::ExtensionType]
133
+ # @param _ [Array<TTTLS13::Message::ExtensionType>]
134
134
  #
135
135
  # @return [TTTLS13::Message::Extensions] for EncodedClientHelloInner
136
136
  def remove_and_replace!(_)
@@ -150,7 +150,7 @@ module EchSpec
150
150
  end
151
151
 
152
152
  class DuplicatedOuterExtensions < TTTLS13::Message::Extensions
153
- # @param _ [Array of TTTLS13::Message::ExtensionType]
153
+ # @param _ [Array<TTTLS13::Message::ExtensionType>]
154
154
  #
155
155
  # @return [TTTLS13::Message::Extensions] for EncodedClientHelloInner
156
156
  def remove_and_replace!(_)
@@ -172,7 +172,7 @@ module EchSpec
172
172
  end
173
173
 
174
174
  class ReferencedEncryptedClientHello < TTTLS13::Message::Extensions
175
- # @param _ [Array of TTTLS13::Message::ExtensionType]
175
+ # @param _ [Array<TTTLS13::Message::ExtensionType>]
176
176
  #
177
177
  # @return [TTTLS13::Message::Extensions] for EncodedClientHelloInner
178
178
  def remove_and_replace!(_)
@@ -194,7 +194,7 @@ module EchSpec
194
194
  end
195
195
 
196
196
  class NotSameOrderExtensions < TTTLS13::Message::Extensions
197
- # @param _ [Array of TTTLS13::Message::ExtensionType]
197
+ # @param _ [Array<TTTLS13::Message::ExtensionType>]
198
198
  #
199
199
  # @return [TTTLS13::Message::Extensions] for EncodedClientHelloInner
200
200
  def remove_and_replace!(_)
@@ -2,12 +2,12 @@ module EchSpec
2
2
  module Spec
3
3
  class Spec5_1_9 < WithSocket
4
4
  # The client-facing server computes ClientHelloInner by reversing this
5
- # process. First it parses EncodedClientHelloInner, interpreting all
6
- # bytes after client_hello as padding. If any padding byte is non-
7
- # zero, the server MUST abort the connection with an
8
- # "illegal_parameter" alert.
5
+ # process. First, it parses EncodedClientHelloInner, interpreting all
6
+ # bytes after client_hello as padding. If any padding byte is non-zero,
7
+ # the server MUST abort the connection with an "illegal_parameter"
8
+ # alert.
9
9
  #
10
- # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-5.1-9
10
+ # https://datatracker.ietf.org/doc/html/rfc9849#section-5.1-9
11
11
 
12
12
  # @return [EchSpec::SpecGroup]
13
13
  def self.spec_group
@@ -1,10 +1,14 @@
1
1
  module EchSpec
2
2
  module Spec
3
3
  class Spec7_5 < WithSocket
4
- # If ECHClientHello.type is not a valid ECHClientHelloType, then the
5
- # server MUST abort with an "illegal_parameter" alert.
4
+ # In shared mode, a server plays both roles, first decrypting the
5
+ # ClientHelloOuter and then using the contents of the ClientHelloInner.
6
+ # A shared mode server which receives a ClientHello with
7
+ # ECHClientHello.type of inner MUST abort with an "illegal_parameter"
8
+ # alert, because such a ClientHello should never be received directly
9
+ # from the network.
6
10
  #
7
- # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-7-5
11
+ # https://datatracker.ietf.org/doc/html/rfc9849#section-7-5
8
12
 
9
13
  # @return [EchSpec::SpecGroup]
10
14
  def self.spec_group
@@ -7,7 +7,7 @@ module EchSpec
7
7
  # offer TLS 1.2 or below. If either of these checks fails, the client-
8
8
  # facing server MUST abort with an "illegal_parameter" alert.
9
9
  #
10
- # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-7.1-11
10
+ # https://datatracker.ietf.org/doc/html/rfc9849#section-7.1-11
11
11
 
12
12
  # @return [SpecGroup]
13
13
  def self.spec_group
@@ -3,8 +3,8 @@ module EchSpec
3
3
  class Spec7_1_14_2_1 < WithSocket
4
4
  # Otherwise, if all candidate ECHConfig values fail to decrypt the
5
5
  # extension, the client-facing server MUST ignore the extension and
6
- # proceed with the connection using ClientHelloOuter, with the
7
- # following modifications:
6
+ # proceed with the connection using ClientHelloOuter with the following
7
+ # modifications:
8
8
  #
9
9
  # * If the server is configured with any ECHConfigs, it MUST include
10
10
  # the "encrypted_client_hello" extension in its EncryptedExtensions
@@ -13,7 +13,7 @@ module EchSpec
13
13
  # ECHConfig values of different versions. This allows a server to
14
14
  # support multiple versions at once.
15
15
  #
16
- # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-7.1-14.2.1
16
+ # https://datatracker.ietf.org/doc/html/rfc9849#section-7.1-14.2.1
17
17
 
18
18
  # @return [EchSpec::SpecGroup]
19
19
  def self.spec_group
@@ -60,7 +60,7 @@ module EchSpec
60
60
  # send ClientHello
61
61
  conn = TLS13Client::Connection.new(socket, :client)
62
62
  inner_ech = TTTLS13::Message::Extension::ECHClientHello.new_inner
63
- exs, priv_keys = TLS13Client.gen_ch_extensions(hostname)
63
+ exs, shared_secret = TLS13Client.gen_ch_extensions(hostname)
64
64
  inner = TTTLS13::Message::ClientHello.new(
65
65
  cipher_suites: TTTLS13::CipherSuites.new(
66
66
  [
@@ -98,14 +98,9 @@ module EchSpec
98
98
  transcript[TTTLS13::SH] = [sh, sh.serialize]
99
99
  kse = sh.extensions[TTTLS13::Message::ExtensionType::KEY_SHARE]
100
100
  .key_share_entry.first
101
- shared_secret = TTTLS13::Endpoint.gen_shared_secret(
102
- kse.key_exchange,
103
- priv_keys[kse.group],
104
- kse.group
105
- )
106
101
  key_schedule = TTTLS13::KeySchedule.new(
107
102
  psk: nil,
108
- shared_secret:,
103
+ shared_secret: shared_secret.build(kse.group, kse.key_exchange),
109
104
  cipher_suite: sh.cipher_suite,
110
105
  transcript:
111
106
  )
@@ -1,7 +1,7 @@
1
1
  module EchSpec
2
2
  module Spec
3
3
  class Spec7_1_1_2 < WithSocket
4
- # If the client-facing server accepted ECH, it checks the second
4
+ # If the client-facing server accepted ECH, it checks that the second
5
5
  # ClientHelloOuter also contains the "encrypted_client_hello"
6
6
  # extension. If not, it MUST abort the handshake with a
7
7
  # "missing_extension" alert. Otherwise, it checks that
@@ -9,7 +9,7 @@ module EchSpec
9
9
  # unchanged, and that ECHClientHello.enc is empty. If not, it MUST
10
10
  # abort the handshake with an "illegal_parameter" alert.
11
11
  #
12
- # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-7.1.1-2
12
+ # https://datatracker.ietf.org/doc/html/rfc9849#section-7.1.1-2
13
13
 
14
14
  # @return [EchSpec::SpecGroup]
15
15
  def self.spec_group
@@ -5,10 +5,10 @@ module EchSpec
5
5
  # using the second ClientHelloOuter. If decryption fails, the client-
6
6
  # facing server MUST abort the handshake with a "decrypt_error" alert.
7
7
  # Otherwise, it reconstructs the second ClientHelloInner from the new
8
- # EncodedClientHelloInner as described in Section 5.1, using the
9
- # second ClientHelloOuter for any referenced extensions.
8
+ # EncodedClientHelloInner as described in Section 5.1, using the second
9
+ # ClientHelloOuter for any referenced extensions.
10
10
  #
11
- # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-7.1.1-5
11
+ # https://datatracker.ietf.org/doc/html/rfc9849#section-7.1.1-5
12
12
 
13
13
  # @return [EchSpec::SpecGroup]
14
14
  def self.spec_group
@@ -9,7 +9,7 @@ module EchSpec
9
9
  # * KDF: HKDF-SHA256 (see Section 7.2 of [HPKE])
10
10
  # * AEAD: AES-128-GCM (see Section 7.3 of [HPKE])
11
11
  #
12
- # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-9
12
+ # https://datatracker.ietf.org/doc/html/rfc9849#section-9
13
13
  @section = '9'
14
14
  @description = 'MUST implement the following HPKE cipher suite: KEM: DHKEM(X25519, HKDF-SHA256), KDF: HKDF-SHA256 and AEAD: AES-128-GCM.'
15
15
  class << self
@@ -41,15 +41,15 @@ module EchSpec
41
41
  end
42
42
  end
43
43
 
44
- # @param ech_configs [Array of ECHConfig]
44
+ # @param ech_configs [Array<ECHConfig>]
45
45
  #
46
- # @return [EchSpec::Ok<ECHConfig> | Err]
46
+ # @return [EchSpec::Ok | Err]
47
47
  def validate_compliant_ech_configs(ech_configs)
48
48
  ech_config = ech_configs.find do |c|
49
49
  kconfig = c.echconfig_contents.key_config
50
- valid_kem_id = kconfig.kem_id.uint16 == 0x0020
50
+ valid_kem_id = kconfig.kem_id.uint16 == HPKE::DHKEM_X25519_HKDF_SHA256
51
51
  valid_cipher_suite = kconfig.cipher_suites.any? do |cs|
52
- cs.kdf_id.uint16 == 0x0001 && cs.aead_id.uint16 == 0x0001
52
+ cs.kdf_id.uint16 == HPKE::HKDF_SHA256 && cs.aead_id.uint16 == HPKE::AES_128_GCM
53
53
  end
54
54
 
55
55
  valid_kem_id && valid_cipher_suite
@@ -61,7 +61,7 @@ module EchSpec
61
61
 
62
62
  # @param hostname [String]
63
63
  #
64
- # @return [EchSpec::Ok<Array of ECHConfig> | Err]
64
+ # @return [EchSpec::Ok<Array<ECHConfig>> | Err]
65
65
  def resolve_ech_configs(hostname)
66
66
  begin
67
67
  rr = Resolv::DNS.new.getresource(
@@ -72,7 +72,7 @@ module EchSpec
72
72
  return Err.new(e.message, nil)
73
73
  end
74
74
 
75
- # https://datatracker.ietf.org/doc/html/draft-ietf-tls-svcb-ech-01#section-6
75
+ # https://datatracker.ietf.org/doc/html/rfc9934#section-3
76
76
  ech = 5
77
77
  return Err.new("HTTPS resource record for #{hostname} does NOT have ech SvcParams.", nil) if rr.params[ech].nil?
78
78
 
@@ -85,7 +85,7 @@ module EchSpec
85
85
 
86
86
  # @param pem [String]
87
87
  #
88
- # @return [EchSpec::Ok<Array of ECHConfig> | Err]
88
+ # @return [EchSpec::Ok<Array<ECHConfig>> | Err]
89
89
  def parse_pem(pem)
90
90
  s = pem.scan(/-----BEGIN ECHCONFIG-----(.*)-----END ECHCONFIG-----/m)
91
91
  .first
@@ -95,7 +95,7 @@ module EchSpec
95
95
  ech_configs = ECHConfig.decode_vectors(b.slice(2..))
96
96
  Ok.new(ech_configs)
97
97
  rescue StandardError
98
- # https://datatracker.ietf.org/doc/html/draft-farrell-tls-pemesni-08#section-3
98
+ # https://datatracker.ietf.org/doc/html/rfc9934#section-3
99
99
  example = <<~PEM
100
100
  -----BEGIN PRIVATE KEY-----
101
101
  MC4CAQAwBQYDK2VuBCIEICjd4yGRdsoP9gU7YT7My8DHx1Tjme8GYDXrOMCi8v1V
data/lib/echspec/spec.rb CHANGED
@@ -12,20 +12,20 @@ module EchSpec
12
12
  msg.description == TTTLS13::Message::ALERT_DESCRIPTION[desc]
13
13
  end
14
14
 
15
- ResultDesc = Struct.new(:result, :desc)
15
+ ResultDescURL = Struct.new(:result, :desc, :url)
16
16
 
17
- # @param rds [Array of ResultDesc] result: EchSpec::Ok | Err, desc: String
17
+ # @param rdus [Array<ResultDescURL>] result: EchSpec::Ok | Err, desc: String, url: URI
18
18
  # @param verbose [Boolean]
19
- def print_results(rds, verbose)
20
- rds.each { |rd| print_summary(rd.result, rd.desc) }
21
- failures = rds.filter { |rd| rd.result.is_a? Err }
19
+ def print_results(rdus, verbose)
20
+ rdus.each { |rdu| print_summary(rdu.result, rdu.desc) }
21
+ failures = rdus.filter { |rdu| rdu.result.is_a? Err }
22
22
  return if failures.empty?
23
23
 
24
24
  puts
25
25
  puts 'Failures:'
26
26
  puts
27
27
  failures.each
28
- .with_index { |rd, idx| print_err_details(rd.result, idx, rd.desc, verbose) }
28
+ .with_index { |rdu, idx| print_err_details(rdu.result, rdu.url, idx, rdu.desc, verbose) }
29
29
  puts "#{failures.length} failure".red
30
30
  end
31
31
 
@@ -36,20 +36,22 @@ module EchSpec
36
36
  cross = "\u0078"
37
37
  summary = case result
38
38
  in Ok
39
- "\t#{check} #{desc}".green
39
+ "#{check} #{desc}".green.indent
40
40
  in Err
41
- "\t#{cross} #{desc}".red
41
+ "#{cross} #{desc}".red.indent
42
42
  end
43
43
  puts summary
44
44
  end
45
45
 
46
46
  # @param err [EchSpec::Err]
47
+ # @param url [String]
47
48
  # @param idx [Integer]
48
49
  # @param desc [String]
49
50
  # @param verbose [Boolean]
50
- def print_err_details(err, idx, desc, verbose)
51
- puts "\t#{idx + 1}) #{desc}"
52
- puts "\t\t#{err.details}"
51
+ def print_err_details(err, url, idx, desc, verbose)
52
+ puts "#{idx + 1}) #{desc}".indent
53
+ puts url.indent.indent
54
+ puts err.details.indent.indent
53
55
  warn err.message_stack if verbose && !err.message_stack.nil?
54
56
  puts
55
57
  end
@@ -103,7 +105,7 @@ module EchSpec
103
105
  # @param fpath [String | NilClass]
104
106
  # @param port [Integer]
105
107
  # @param hostname [String]
106
- # @param sections [Array of String]
108
+ # @param sections [Array<String>]
107
109
  # @param verbose [Boolean]
108
110
  def run_only(fpath, port, hostname, sections, verbose)
109
111
  targets = spec_groups.filter { |g| sections.include?(g.section) }
@@ -119,18 +121,34 @@ module EchSpec
119
121
  # @param port [Integer]
120
122
  # @param hostname [String]
121
123
  # @param ech_config [ECHConfig]
122
- # @param targets [Array of EchSpec::SpecGroup]
124
+ # @param targets [Array<EchSpec::SpecGroup>]
123
125
  # @param verbose [Boolean]
124
126
  def do_run(port, hostname, ech_config, targets, verbose)
125
- rds = targets.flat_map do |g|
127
+ rdus = targets.flat_map do |g|
126
128
  g.spec_cases.map do |sc|
127
- r = sc.method.call(hostname, port, ech_config)
128
- d = "#{sc.description} [#{g.section}]"
129
- ResultDesc.new(result: r, desc: d)
129
+ result = sc.method.call(hostname, port, ech_config)
130
+ desc = desc(sc.description, g.section)
131
+ url = url(g.section)
132
+ ResultDescURL.new(result:, desc:, url:)
130
133
  end
131
134
  end
132
135
 
133
- print_results(rds, verbose)
136
+ print_results(rdus, verbose)
137
+ end
138
+
139
+ # @param description [String]
140
+ # @param section [String]
141
+ #
142
+ # @return [String]
143
+ def desc(description, section)
144
+ "#{description} [#{section}]"
145
+ end
146
+
147
+ # @param section [String]
148
+ #
149
+ # @return [String]
150
+ def url(section)
151
+ "https://datatracker.ietf.org/doc/html/rfc9849#section-#{section}"
134
152
  end
135
153
 
136
154
  # @param fpath [String | NilClass]
@@ -139,24 +157,28 @@ module EchSpec
139
157
  #
140
158
  # @return [ECHConfig]
141
159
  def try_get_ech_config(fpath, hostname, force_compliant)
142
- # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-9
143
- case result = Spec9.try_get_ech_config(fpath, hostname, force_compliant)
144
- in Ok(obj) if force_compliant
145
- result.tap { |r| print_summary(r, "#{Spec9.description} [#{Spec9.section}]") }
146
- obj
147
- in Ok(obj)
148
- obj
160
+ # https://datatracker.ietf.org/doc/html/rfc9849#section-9
161
+ result = Spec9.try_get_ech_config(fpath, hostname, force_compliant)
162
+ desc = desc(Spec9.description, Spec9.section)
163
+ url = url(Spec9.section)
164
+
165
+ case result
166
+ in Ok(ech_config) if force_compliant
167
+ print_summary(result, desc)
168
+ ech_config
169
+ in Ok(ech_config)
170
+ ech_config
149
171
  in Err(details, _)
150
- puts "\t#{details}".red
172
+ print_results([ResultDescURL.new(result:, desc:, url:)], true)
151
173
  exit 1
152
174
  end
153
175
  end
154
176
 
155
177
  def spec_groups
156
- # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-5
178
+ # https://datatracker.ietf.org/doc/html/rfc9849#section-5
157
179
  groups = [Spec5_1_9, Spec5_1_10]
158
180
 
159
- # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-7
181
+ # https://datatracker.ietf.org/doc/html/rfc9849#section-7
160
182
  groups += [Spec7_5, Spec7_1_11, Spec7_1_14_2_1, Spec7_1_1_2, Spec7_1_1_5]
161
183
 
162
184
  groups.map(&:spec_group)
@@ -66,12 +66,12 @@ module EchSpec
66
66
  exs << TTTLS13::Message::Extension::SupportedGroups.new(groups)
67
67
 
68
68
  # key_share
69
- key_share, priv_keys = TTTLS13::Message::Extension::KeyShare.gen_ch_key_share(
69
+ key_share, shared_secret = TTTLS13::Message::Extension::KeyShare.gen_ch_key_share(
70
70
  groups
71
71
  )
72
72
  exs << key_share
73
73
 
74
- [exs, priv_keys]
74
+ [exs, shared_secret]
75
75
  end
76
76
 
77
77
  # @param ch1 [TTTLS13::Message::ClientHello]
data/lib/echspec/utils.rb CHANGED
@@ -1,6 +1,10 @@
1
1
  module EchSpec
2
2
  module Refinements
3
3
  refine String do
4
+ def indent
5
+ "\t#{self}"
6
+ end
7
+
4
8
  def colorize(code)
5
9
  "\e[#{code}m#{self}\e[0m"
6
10
  end
@@ -1,3 +1,3 @@
1
1
  module EchSpec
2
- VERSION = '0.0.1'.freeze
2
+ VERSION = '0.0.3'.freeze
3
3
  end
data/lib/echspec.rb CHANGED
@@ -1,16 +1,17 @@
1
1
  require 'base64'
2
2
  require 'optparse'
3
+ require 'pp'
3
4
  require 'resolv'
4
5
  require 'timeout'
5
6
  require 'tttls1.3'
6
7
 
7
- require 'echspec/version'
8
- require 'echspec/utils'
9
- require 'echspec/log'
10
- require 'echspec/error'
11
- require 'echspec/result'
12
- require 'echspec/tls13_client'
13
- require 'echspec/spec_case'
14
- require 'echspec/spec_group'
15
- require 'echspec/spec'
16
- require 'echspec/cli'
8
+ require_relative 'echspec/version'
9
+ require_relative 'echspec/utils'
10
+ require_relative 'echspec/log'
11
+ require_relative 'echspec/error'
12
+ require_relative 'echspec/result'
13
+ require_relative 'echspec/tls13_client'
14
+ require_relative 'echspec/spec_case'
15
+ require_relative 'echspec/spec_group'
16
+ require_relative 'echspec/spec'
17
+ require_relative 'echspec/cli'
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: echspec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - thekuwayama
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-01-12 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: bundler
@@ -38,6 +37,20 @@ dependencies:
38
37
  - - ">="
39
38
  - !ruby/object:Gem::Version
40
39
  version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: ech_config
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.0.4
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 0.0.4
41
54
  - !ruby/object:Gem::Dependency
42
55
  name: resolv
43
56
  requirement: !ruby/object:Gem::Requirement
@@ -58,14 +71,14 @@ dependencies:
58
71
  requirements:
59
72
  - - "~>"
60
73
  - !ruby/object:Gem::Version
61
- version: 0.3.4
74
+ version: 0.3.6
62
75
  type: :runtime
63
76
  prerelease: false
64
77
  version_requirements: !ruby/object:Gem::Requirement
65
78
  requirements:
66
79
  - - "~>"
67
80
  - !ruby/object:Gem::Version
68
- version: 0.3.4
81
+ version: 0.3.6
69
82
  description: A conformance testing tool for ECH implementation
70
83
  email:
71
84
  - thekuwayama@gmail.com
@@ -74,8 +87,6 @@ executables:
74
87
  extensions: []
75
88
  extra_rdoc_files: []
76
89
  files:
77
- - ".github/workflows/ci.yml"
78
- - ".gitignore"
79
90
  - ".rubocop.yml"
80
91
  - ".ruby-version"
81
92
  - Gemfile
@@ -107,15 +118,10 @@ files:
107
118
  - lib/echspec/tls13_client.rb
108
119
  - lib/echspec/utils.rb
109
120
  - lib/echspec/version.rb
110
- - spec/9_spec.rb
111
- - spec/log_spec.rb
112
- - spec/spec_helper.rb
113
- - spec/with_socket_spec.rb
114
121
  homepage: https://github.com/thekuwayama/echspec
115
122
  licenses:
116
123
  - MIT
117
124
  metadata: {}
118
- post_install_message:
119
125
  rdoc_options: []
120
126
  require_paths:
121
127
  - lib
@@ -123,19 +129,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
123
129
  requirements:
124
130
  - - ">="
125
131
  - !ruby/object:Gem::Version
126
- version: '3.2'
132
+ version: '4.0'
127
133
  required_rubygems_version: !ruby/object:Gem::Requirement
128
134
  requirements:
129
135
  - - ">="
130
136
  - !ruby/object:Gem::Version
131
137
  version: '0'
132
138
  requirements: []
133
- rubygems_version: 3.5.22
134
- signing_key:
139
+ rubygems_version: 4.0.7
135
140
  specification_version: 4
136
141
  summary: A conformance testing tool for ECH implementation
137
- test_files:
138
- - spec/9_spec.rb
139
- - spec/log_spec.rb
140
- - spec/spec_helper.rb
141
- - spec/with_socket_spec.rb
142
+ test_files: []
@@ -1,30 +0,0 @@
1
- name: lint & test
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
- pull_request:
8
- branches:
9
- - '*'
10
-
11
- jobs:
12
- ci:
13
- runs-on: ubuntu-latest
14
- strategy:
15
- matrix:
16
- ruby-version: ['3.2', '3.3', '3.4']
17
- steps:
18
- - uses: actions/checkout@v4
19
- - name: Set up Ruby
20
- uses: ruby/setup-ruby@v1
21
- with:
22
- ruby-version: ${{ matrix.ruby-version }}
23
- - name: Install dependencies
24
- run: |
25
- gem --version
26
- gem install bundler
27
- bundle --version
28
- bundle install
29
- - name: Run rubocop & rspec
30
- run: bundle exec rake
data/.gitignore DELETED
@@ -1,17 +0,0 @@
1
- *.gem
2
- *.rbc
3
- .config
4
- .rvmrc
5
- /.bundle/
6
- /vendor/
7
- /lib/bundler/man/
8
- /pkg/
9
- /.yardoc/
10
- /_yardoc/
11
- /doc/
12
- /rdoc/
13
- /coverage/
14
- /spec/reports/
15
- /tmp/
16
- .DS_Store
17
- Gemfile.lock
data/spec/9_spec.rb DELETED
@@ -1,13 +0,0 @@
1
- require_relative 'spec_helper'
2
-
3
- RSpec.describe EchSpec::Spec::Spec9 do
4
- context 'parse_pem' do
5
- let(:pem) do
6
- File.open("#{__dir__}/../fixtures/echconfigs.pem").read
7
- end
8
-
9
- it 'could parse' do
10
- expect(EchSpec::Spec::Spec9.send(:parse_pem, pem)).to be_a EchSpec::Ok
11
- end
12
- end
13
- end
data/spec/log_spec.rb DELETED
@@ -1,58 +0,0 @@
1
- require_relative 'spec_helper'
2
-
3
- RSpec.describe EchSpec::Log::MessageStack do
4
- context 'obj2json' do
5
- let(:crt) do
6
- File.open("#{__dir__}/../fixtures/server.crt").read
7
- end
8
-
9
- it 'should convert' do
10
- expect(EchSpec::Log::MessageStack.obj2json(OpenSSL::X509::Certificate.new(crt)))
11
- .to eq "#{crt.split("\n").join('\n')}\\n"
12
- end
13
-
14
- it 'should convert' do
15
- expect(EchSpec::Log::MessageStack.obj2json(1)).to eq '1'
16
- end
17
-
18
- it 'should convert' do
19
- expect(EchSpec::Log::MessageStack.obj2json(0.1)).to eq '0.1'
20
- end
21
-
22
- it 'should convert' do
23
- expect(EchSpec::Log::MessageStack.obj2json(true)).to eq 'true'
24
- end
25
-
26
- it 'should convert' do
27
- expect(EchSpec::Log::MessageStack.obj2json(false)).to eq 'false'
28
- end
29
-
30
- it 'should convert' do
31
- expect(EchSpec::Log::MessageStack.obj2json('')).to eq '""'
32
- end
33
-
34
- it 'should convert' do
35
- expect(EchSpec::Log::MessageStack.obj2json('string')).to eq '"0x737472696e67"'
36
- end
37
-
38
- it 'should convert' do
39
- expect(EchSpec::Log::MessageStack.obj2json(nil)).to eq 'null'
40
- end
41
-
42
- it 'should convert' do
43
- expect(EchSpec::Log::MessageStack.obj2json([1, true, '', 'string', nil])).to eq '[1,true,"","0x737472696e67",null]'
44
- end
45
-
46
- it 'should convert' do
47
- expect(EchSpec::Log::MessageStack.obj2json(1 => true, '' => 'string', nil => [])).to eq '{1:true,"":"0x737472696e67",null:[]}'
48
- end
49
-
50
- it 'should convert' do
51
- expect(EchSpec::Log::MessageStack.obj2json(C.new('string'))).to eq '{"name":"0x737472696e67"}'
52
- end
53
-
54
- it 'should convert' do
55
- expect(EchSpec::Log::MessageStack.obj2json(D.new)).to eq '"$D"'
56
- end
57
- end
58
- end
data/spec/spec_helper.rb DELETED
@@ -1,12 +0,0 @@
1
- RSpec.configure(&:disable_monkey_patching!)
2
-
3
- require 'echspec'
4
-
5
- class C
6
- def initialize(name)
7
- @name = name
8
- end
9
- end
10
-
11
- class D
12
- end
@@ -1,77 +0,0 @@
1
- require_relative 'spec_helper'
2
-
3
- module EchSpec
4
- module Spec
5
- class SpecX < WithSocket
6
- def validate(hostname, port)
7
- with_socket(hostname, port) do |_socket|
8
- # not return
9
- end
10
- end
11
- end
12
-
13
- class SpecY < WithSocket
14
- def validate(hostname, port)
15
- with_socket(hostname, port) do |_socket|
16
- return EchSpec::Ok.new(1)
17
- end
18
- end
19
- end
20
-
21
- class SpecZ < WithSocket
22
- def validate(hostname, port)
23
- with_socket(hostname, port) do |_socket|
24
- msg = TTTLS13::Message::Alert.new(
25
- level: TTTLS13::Message::AlertLevel::FATAL,
26
- description: "\x0a"
27
- )
28
- return EchSpec::Err.new('details', [msg])
29
- end
30
- end
31
- end
32
-
33
- class SpecW < WithSocket
34
- def validate(hostname, port)
35
- with_socket(hostname, port) do |_socket|
36
- raise EchSpec::Error::BeforeTargetSituationError, 'not received ClientHello'
37
- end
38
- end
39
- end
40
- end
41
- end
42
-
43
- RSpec.describe EchSpec::Spec::WithSocket do
44
- context 'with_socket' do
45
- before do
46
- socket = StringIO.new
47
- allow(TCPSocket).to receive(:new).and_return(socket)
48
- end
49
-
50
- it 'should return nil' do
51
- result = EchSpec::Spec::SpecX.new.validate('localhost', 4433)
52
- expect(result).to eq nil
53
- end
54
-
55
- it 'should return Ok(1)' do
56
- result = EchSpec::Spec::SpecY.new.validate('localhost', 4433)
57
- expect(result).to be_a EchSpec::Ok
58
- expect(result.obj).to eq 1
59
- end
60
-
61
- it 'should return Err(details, message_stack)' do
62
- result = EchSpec::Spec::SpecZ.new.validate('localhost', 4433)
63
- expect(result).to be_a EchSpec::Err
64
- expect(result.details).to eq 'details'
65
- expect(result.message_stack.length).to be 1
66
- expect(result.message_stack.first.level).to be TTTLS13::Message::AlertLevel::FATAL
67
- expect(result.message_stack.first.description).to eq "\x0a"
68
- end
69
-
70
- it 'should return Err(details, message_stack), raised BeforeTargetSituationError' do
71
- result = EchSpec::Spec::SpecW.new.validate('localhost', 4433)
72
- expect(result).to be_a EchSpec::Err
73
- expect(result.details).to eq 'not received ClientHello'
74
- expect(result.message_stack).to eq '{}'
75
- end
76
- end
77
- end