echspec 0.0.2 → 0.0.4

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: 664b02dfe5659032ff19242937be686085ab762f223fd950a1332da55fd69e12
4
- data.tar.gz: 0146c242937094ff8a4cb1c494d27af14a55d34e8967c4bf174b6eb76132b8c6
3
+ metadata.gz: 9da986ad5207e26d816ebe08d7f8620981836c13d0dd9d695447269276a13453
4
+ data.tar.gz: ad7effc41c7349b011fc386ec4195d1d2dd86d51af68317d4034ad9538a9d31f
5
5
  SHA512:
6
- metadata.gz: ca2add160c37c4997ee2f76172c1b4edc598d85149fecfae5fd6861dce34455916fbee67d02bbf60e8d6f121e583ca284fee4bde73a215c735c05465e30b8704
7
- data.tar.gz: 832197c0251d27bf060a4ac774126117eca42fc11615e1078edbf4db84109a24125e6ab2417cba393223a87632701deea876a90e0e1cf5d9a8d7c7b324a40dc8
6
+ metadata.gz: 53a725b8f4e8404f579b4b87b7d434c6146fa054f80a67147a2df2477102ecd3f7ca32db9b8827a5f00bfd1c51c0c70ec2c1575747c8a53716f682111a6d1c23
7
+ data.tar.gz: a0c8940990d04ff038e1ecdc4f1505a8b27f4ca87232daf08daecc9263f0a34c0cb8f14fee896d254a729102cc01d1cb7bb127f754c3ad9119b6bfb39c1487e7
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.4.3
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,7 +8,7 @@
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
14
  ## Installation
@@ -24,7 +24,13 @@ $ gem install echspec
24
24
 
25
25
  ```sh-session
26
26
  $ echspec --help
27
- Usage: echspec [OPTIONS] <HOSTNAME>
27
+ Usage: echspec {SUBCOMMAND}
28
+
29
+ Available subcommands: run, gen_configs, version, help.
30
+ ```
31
+ ```sh-session
32
+ $ echspec run --help
33
+ Usage: echspec run [OPTIONS...] {HOSTNAME}
28
34
  -f, --file FILE path to ECHConfigs PEM file (default resolve ECHConfigs via DNS)
29
35
  -p, --port VALUE server port number (default 443)
30
36
  -n, --not-force-compliant-hpke not force compliant ECHConfig HPKE cipher suite
@@ -35,7 +41,7 @@ Usage: echspec [OPTIONS] <HOSTNAME>
35
41
  You can run it the following:
36
42
 
37
43
  ```sh-session
38
- $ echspec research.cloudflare.com
44
+ $ echspec run research.cloudflare.com
39
45
  TLS Encrypted Client Hello Server
40
46
  ✔ MUST implement the following HPKE cipher suite: KEM: DHKEM(X25519, HKDF-SHA256), KDF: HKDF-SHA256 and AEAD: AES-128-GCM. [9]
41
47
  ✔ MUST abort with an "illegal_parameter" alert, if EncodedClientHelloInner is padded with non-zero values. [5.1-9]
@@ -54,7 +60,7 @@ TLS Encrypted Client Hello Server
54
60
  Failures:
55
61
 
56
62
  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/draft-ietf-tls-esni-22#section-7-5
63
+ https://datatracker.ietf.org/doc/html/rfc9849#section-7-5
58
64
  did not send expected alert: illegal_parameter
59
65
 
60
66
  1 failure
@@ -63,7 +69,7 @@ Failures:
63
69
  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:
64
70
 
65
71
  ```sh-session
66
- $ echspec -f fixtures/echconfigs.pem -p 4433 localhost
72
+ $ echspec run -f fixtures/echconfigs.pem -p 4433 localhost
67
73
  ```
68
74
 
69
75
  By default, `echspec` uses the following HPKE cipher suite
@@ -78,13 +84,13 @@ By default, `echspec` uses the following HPKE cipher suite
78
84
  Using the `-n` or `--not-force-compliant-hpke`, you can not enforce the HPKE cipher suite.
79
85
 
80
86
  ```sh-session
81
- $ echspec -f fixtures/echconfigs.pem -p 4433 -n localhost
87
+ $ echspec run -f fixtures/echconfigs.pem -p 4433 -n localhost
82
88
  ```
83
89
 
84
90
  If you specify the SECTIONS, you can run only SECTIONS the following:
85
91
 
86
92
  ```sh-session
87
- $ echspec -f fixtures/echconfigs.pem -p 4433 -n -s 7.1.1-2,7.1.1-5 localhost
93
+ $ echspec run -f fixtures/echconfigs.pem -p 4433 -n -s 7.1.1-2,7.1.1-5 localhost
88
94
  TLS Encrypted Client Hello Server
89
95
  ✔ MUST abort with a "missing_extension" alert, if 2nd ClientHelloOuter does not contains the "encrypted_client_hello" extension. [7.1.1-2]
90
96
  ✔ MUST abort with an "illegal_parameter" alert, if 2nd ClientHelloOuter "encrypted_client_hello" enc is empty. [7.1.1-2]
@@ -94,7 +100,7 @@ TLS Encrypted Client Hello Server
94
100
  Using the `-v` or `--verbose` option provides a message stack if an error occurs. The message stack is formatted as JSON.
95
101
 
96
102
  ```sh-session
97
- $ echspec -s 7-5 -v research.cloudflare.com 2>&1 > /dev/null | jq .
103
+ $ echspec run -s 7-5 -v research.cloudflare.com 2>&1 > /dev/null | jq .
98
104
  ````
99
105
 
100
106
  <details>
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.5'
29
+ spec.add_dependency 'tttls1.3', '~> 0.3.6'
26
30
  end
data/exe/echspec CHANGED
@@ -4,4 +4,4 @@ $LOAD_PATH << "#{__dir__}/../lib"
4
4
 
5
5
  require 'echspec'
6
6
 
7
- EchSpec::CLI.new.run
7
+ EchSpec::CLI.new.execute
@@ -0,0 +1,61 @@
1
+ module EchSpec
2
+ class CLI
3
+ class GenConfigs
4
+ def execute(argv)
5
+ fpath = parse_options(argv)
6
+ write(fpath)
7
+ end
8
+
9
+ def parse_options(argv)
10
+ op = OptionParser.new
11
+
12
+ op.banner = 'Usage: echspec gen_configs {FILE}'
13
+
14
+ begin
15
+ args = op.parse(argv)
16
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
17
+ warn op
18
+ warn "** #{e.message}"
19
+ exit 1
20
+ end
21
+
22
+ if args.length != 1
23
+ warn op
24
+ warn '** {FILE} argument is not specified'
25
+ exit 1
26
+ end
27
+ args[0]
28
+ end
29
+
30
+ def write(fpath)
31
+ hostname = 'localhost'
32
+
33
+ key = OpenSSL::PKey.generate_key('X25519')
34
+ echconfigs = ECHConfigList.new(
35
+ [
36
+ ECHConfig.new(
37
+ "\xfe\x0d".b,
38
+ ECHConfig::ECHConfigContents.new(
39
+ ECHConfig::ECHConfigContents::HpkeKeyConfig.new(
40
+ 123,
41
+ ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeKemId.new(HPKE::DHKEM_X25519_HKDF_SHA256),
42
+ ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkePublicKey.new(key.raw_public_key),
43
+ [
44
+ ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite.new(
45
+ ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite::HpkeKdfId.new(HPKE::HKDF_SHA256),
46
+ ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite::HpkeAeadId.new(HPKE::AES_128_GCM)
47
+ )
48
+ ]
49
+ ),
50
+ 32,
51
+ hostname.b,
52
+ ECHConfig::ECHConfigContents::Extensions.new('')
53
+ )
54
+ )
55
+ ]
56
+ )
57
+ File.write(fpath, key.private_to_pem + echconfigs.to_pem)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,100 @@
1
+ module EchSpec
2
+ class CLI
3
+ class Run
4
+ def execute(argv)
5
+ fpath, port, force_compliant, verbose, hostname, sections = parse_options(argv)
6
+
7
+ if sections.nil?
8
+ Spec.run(fpath, port, hostname, force_compliant, verbose)
9
+ else
10
+ Spec.run_only(fpath, port, hostname, sections, verbose)
11
+ end
12
+ end
13
+
14
+ # rubocop: disable Metrics/AbcSize
15
+ # rubocop: disable Metrics/MethodLength
16
+ def parse_options(argv)
17
+ op = OptionParser.new
18
+
19
+ # default value
20
+ fpath = nil
21
+ port = 443
22
+ force_compliant = true
23
+ verbose = false
24
+ sections = nil
25
+
26
+ op.on(
27
+ '-f',
28
+ '--file FILE',
29
+ 'path to ECHConfigs PEM file (default resolve ECHConfigs via DNS)'
30
+ ) do |v|
31
+ fpath = v
32
+ end
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.on(
43
+ '-n',
44
+ '--not-force-compliant-hpke',
45
+ 'not force compliant ECHConfig HPKE cipher suite'
46
+ ) do
47
+ force_compliant = false
48
+ end
49
+
50
+ op.on(
51
+ '-v',
52
+ '--verbose',
53
+ 'verbose mode; prints message stack if raised an error'
54
+ ) do
55
+ verbose = true
56
+ end
57
+
58
+ op.on(
59
+ '-s',
60
+ '--sections SECTIONS',
61
+ 'sections to test; by the default, test all sections'
62
+ ) do |v|
63
+ sections = v.split(',')
64
+ end
65
+
66
+ op.banner = 'Usage: echspec run [OPTIONS...] {HOSTNAME}'
67
+
68
+ begin
69
+ args = op.parse(argv)
70
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
71
+ warn op
72
+ warn "** #{e.message}"
73
+ exit 1
74
+ end
75
+
76
+ if !fpath.nil? && !File.exist?(fpath)
77
+ warn '** {FILE} is not found'
78
+ exit 1
79
+ end
80
+
81
+ unknowns = sections.nil? ? [] : sections - Spec.sections
82
+ unless unknowns.empty?
83
+ warn "** #{unknowns} are unknown sections"
84
+ exit 1
85
+ end
86
+
87
+ if args.length != 1
88
+ warn op
89
+ warn '** {HOSTNAME} argument is not specified'
90
+ exit 1
91
+ end
92
+ hostname = args[0]
93
+
94
+ [fpath, port, force_compliant, verbose, hostname, sections]
95
+ end
96
+ # rubocop: enable Metrics/AbcSize
97
+ # rubocop: enable Metrics/MethodLength
98
+ end
99
+ end
100
+ end
data/lib/echspec/cli.rb CHANGED
@@ -1,96 +1,33 @@
1
+ require_relative 'cli/gen_configs'
2
+ require_relative 'cli/run'
3
+
1
4
  module EchSpec
2
5
  class CLI
3
- # rubocop: disable Metrics/AbcSize
4
- # rubocop: disable Metrics/MethodLength
5
- def parse_options(argv = ARGV)
6
- op = OptionParser.new
7
-
8
- # default value
9
- fpath = nil
10
- port = 443
11
- force_compliant = true
12
- verbose = false
13
- sections = nil
14
-
15
- op.on(
16
- '-f',
17
- '--file FILE',
18
- 'path to ECHConfigs PEM file (default resolve ECHConfigs via DNS)'
19
- ) do |v|
20
- fpath = v
21
- end
22
-
23
- op.on(
24
- '-p',
25
- '--port VALUE',
26
- "server port number (default #{port})"
27
- ) do |v|
28
- port = v
29
- end
30
-
31
- op.on(
32
- '-n',
33
- '--not-force-compliant-hpke',
34
- 'not force compliant ECHConfig HPKE cipher suite'
35
- ) do
36
- force_compliant = false
37
- end
38
-
39
- op.on(
40
- '-v',
41
- '--verbose',
42
- 'verbose mode; prints message stack if raised an error'
43
- ) do
44
- verbose = true
45
- end
46
-
47
- op.on(
48
- '-s',
49
- '--sections SECTIONS',
50
- 'sections to test; by the default, test all sections'
51
- ) do |v|
52
- sections = v.split(',')
53
- end
54
-
55
- op.banner = 'Usage: echspec [OPTIONS] <HOSTNAME>'
56
- begin
57
- args = op.parse(argv)
58
- rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
59
- warn op
60
- warn "** #{e.message}"
61
- exit 1
62
- end
6
+ using Refinements
63
7
 
64
- if !fpath.nil? && !File.exist?(fpath)
65
- warn '** <FILE> is not found'
66
- exit 1
67
- end
8
+ def execute(argv = ARGV)
9
+ subcommands = %i[run gen_configs]
68
10
 
69
- unknowns = sections.nil? ? [] : sections - Spec.sections
70
- unless unknowns.empty?
71
- warn "** #{unknowns} are unknown sections"
72
- exit 1
73
- end
11
+ op = OptionParser.new
74
12
 
75
- if args.length != 1
76
- warn op
77
- warn '** <HOSTNAME> argument is not specified'
78
- exit 1
79
- end
80
- hostname = args[0]
13
+ op.banner = <<~USAGE
14
+ Usage: echspec {SUBCOMMAND}
81
15
 
82
- [fpath, port, force_compliant, verbose, hostname, sections]
83
- end
84
- # rubocop: enable Metrics/AbcSize
85
- # rubocop: enable Metrics/MethodLength
16
+ Available subcommands: #{subcommands.join(', ')}, version, help.
17
+ USAGE
86
18
 
87
- def run
88
- fpath, port, force_compliant, verbose, hostname, sections = parse_options
19
+ op.version = EchSpec::VERSION
20
+ op.order!(argv)
89
21
 
90
- if sections.nil?
91
- Spec.run(fpath, port, hostname, force_compliant, verbose)
22
+ subcommand = argv.shift
23
+ case subcommand&.to_sym
24
+ when :version
25
+ puts EchSpec::VERSION
26
+ when *subcommands
27
+ klass = self.class.const_get(subcommand.to_camel)
28
+ klass.new.__send__(:execute, argv)
92
29
  else
93
- Spec.run_only(fpath, port, hostname, sections, verbose)
30
+ puts op
94
31
  end
95
32
  end
96
33
  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
@@ -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
@@ -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
@@ -47,9 +47,9 @@ module EchSpec
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
@@ -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
 
@@ -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
@@ -14,7 +14,7 @@ module EchSpec
14
14
 
15
15
  ResultDescURL = Struct.new(:result, :desc, :url)
16
16
 
17
- # @param rds [Array<ResultDescURL>] 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
19
  def print_results(rdus, verbose)
20
20
  rdus.each { |rdu| print_summary(rdu.result, rdu.desc) }
@@ -148,7 +148,7 @@ module EchSpec
148
148
  #
149
149
  # @return [String]
150
150
  def url(section)
151
- "https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-#{section}"
151
+ "https://datatracker.ietf.org/doc/html/rfc9849#section-#{section}"
152
152
  end
153
153
 
154
154
  # @param fpath [String | NilClass]
@@ -157,7 +157,7 @@ module EchSpec
157
157
  #
158
158
  # @return [ECHConfig]
159
159
  def try_get_ech_config(fpath, hostname, force_compliant)
160
- # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-9
160
+ # https://datatracker.ietf.org/doc/html/rfc9849#section-9
161
161
  result = Spec9.try_get_ech_config(fpath, hostname, force_compliant)
162
162
  desc = desc(Spec9.description, Spec9.section)
163
163
  url = url(Spec9.section)
@@ -175,10 +175,10 @@ module EchSpec
175
175
  end
176
176
 
177
177
  def spec_groups
178
- # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-5
178
+ # https://datatracker.ietf.org/doc/html/rfc9849#section-5
179
179
  groups = [Spec5_1_9, Spec5_1_10]
180
180
 
181
- # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-22#section-7
181
+ # https://datatracker.ietf.org/doc/html/rfc9849#section-7
182
182
  groups += [Spec7_5, Spec7_1_11, Spec7_1_14_2_1, Spec7_1_1_2, Spec7_1_1_5]
183
183
 
184
184
  groups.map(&:spec_group)
data/lib/echspec/utils.rb CHANGED
@@ -20,6 +20,10 @@ module EchSpec
20
20
  def yellow
21
21
  colorize(33)
22
22
  end
23
+
24
+ def to_camel
25
+ gsub(/(?:^|_)(.)/) { Regexp.last_match(1).upcase }
26
+ end
23
27
  end
24
28
  end
25
29
  end
@@ -1,3 +1,3 @@
1
1
  module EchSpec
2
- VERSION = '0.0.2'.freeze
2
+ VERSION = '0.0.4'.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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: echspec
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - thekuwayama
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
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
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: resolv
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -57,14 +71,14 @@ dependencies:
57
71
  requirements:
58
72
  - - "~>"
59
73
  - !ruby/object:Gem::Version
60
- version: 0.3.5
74
+ version: 0.3.6
61
75
  type: :runtime
62
76
  prerelease: false
63
77
  version_requirements: !ruby/object:Gem::Requirement
64
78
  requirements:
65
79
  - - "~>"
66
80
  - !ruby/object:Gem::Version
67
- version: 0.3.5
81
+ version: 0.3.6
68
82
  description: A conformance testing tool for ECH implementation
69
83
  email:
70
84
  - thekuwayama@gmail.com
@@ -73,8 +87,6 @@ executables:
73
87
  extensions: []
74
88
  extra_rdoc_files: []
75
89
  files:
76
- - ".github/workflows/ci.yml"
77
- - ".gitignore"
78
90
  - ".rubocop.yml"
79
91
  - ".ruby-version"
80
92
  - Gemfile
@@ -89,6 +101,8 @@ files:
89
101
  - fixtures/server.key
90
102
  - lib/echspec.rb
91
103
  - lib/echspec/cli.rb
104
+ - lib/echspec/cli/gen_configs.rb
105
+ - lib/echspec/cli/run.rb
92
106
  - lib/echspec/error.rb
93
107
  - lib/echspec/log.rb
94
108
  - lib/echspec/result.rb
@@ -106,10 +120,6 @@ files:
106
120
  - lib/echspec/tls13_client.rb
107
121
  - lib/echspec/utils.rb
108
122
  - lib/echspec/version.rb
109
- - spec/9_spec.rb
110
- - spec/log_spec.rb
111
- - spec/spec_helper.rb
112
- - spec/with_socket_spec.rb
113
123
  homepage: https://github.com/thekuwayama/echspec
114
124
  licenses:
115
125
  - MIT
@@ -121,18 +131,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
121
131
  requirements:
122
132
  - - ">="
123
133
  - !ruby/object:Gem::Version
124
- version: '3.2'
134
+ version: '4.0'
125
135
  required_rubygems_version: !ruby/object:Gem::Requirement
126
136
  requirements:
127
137
  - - ">="
128
138
  - !ruby/object:Gem::Version
129
139
  version: '0'
130
140
  requirements: []
131
- rubygems_version: 3.6.7
141
+ rubygems_version: 4.0.7
132
142
  specification_version: 4
133
143
  summary: A conformance testing tool for ECH implementation
134
- test_files:
135
- - spec/9_spec.rb
136
- - spec/log_spec.rb
137
- - spec/spec_helper.rb
138
- - spec/with_socket_spec.rb
144
+ 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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
19
- - name: Set up Ruby
20
- uses: ruby/setup-ruby@eaecf785f6a34567a6d97f686bbb7bccc1ac1e5c # v1.237.0
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