ech_config 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +34 -0
  3. data/.gitignore +16 -0
  4. data/.rubocop.yml +34 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +17 -0
  7. data/Gemfile.lock +73 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +22 -0
  10. data/Rakefile +16 -0
  11. data/ech_config.gemspec +24 -0
  12. data/example/README.md +9 -0
  13. data/example/well_known_url_client.rb +29 -0
  14. data/lib/ech_config/ech_config_contents/extensions.rb +24 -0
  15. data/lib/ech_config/ech_config_contents/hpke_key_config/hpke_kem_id.rb +24 -0
  16. data/lib/ech_config/ech_config_contents/hpke_key_config/hpke_public_key.rb +22 -0
  17. data/lib/ech_config/ech_config_contents/hpke_key_config/hpke_symmetric_cipher_suite/hpke_aead_id.rb +24 -0
  18. data/lib/ech_config/ech_config_contents/hpke_key_config/hpke_symmetric_cipher_suite/hpke_kdf_id.rb +24 -0
  19. data/lib/ech_config/ech_config_contents/hpke_key_config/hpke_symmetric_cipher_suite.rb +41 -0
  20. data/lib/ech_config/ech_config_contents/hpke_key_config.rb +82 -0
  21. data/lib/ech_config/ech_config_contents.rb +78 -0
  22. data/lib/ech_config/error.rb +11 -0
  23. data/lib/ech_config/version.rb +6 -0
  24. data/lib/ech_config.rb +62 -0
  25. data/sorbet/config +3 -0
  26. data/sorbet/rbi/gems/ast.rbi +49 -0
  27. data/sorbet/rbi/gems/byebug.rbi +1041 -0
  28. data/sorbet/rbi/gems/ech_config.rbi +22 -0
  29. data/sorbet/rbi/gems/parallel.rbi +86 -0
  30. data/sorbet/rbi/gems/parser.rbi +1477 -0
  31. data/sorbet/rbi/gems/rainbow.rbi +122 -0
  32. data/sorbet/rbi/gems/rake.rbi +646 -0
  33. data/sorbet/rbi/gems/regexp_parser.rbi +984 -0
  34. data/sorbet/rbi/gems/rexml.rbi +599 -0
  35. data/sorbet/rbi/gems/rspec-core.rbi +1947 -0
  36. data/sorbet/rbi/gems/rspec-expectations.rbi +1178 -0
  37. data/sorbet/rbi/gems/rspec-mocks.rbi +1096 -0
  38. data/sorbet/rbi/gems/rspec-support.rbi +282 -0
  39. data/sorbet/rbi/gems/rspec.rbi +15 -0
  40. data/sorbet/rbi/gems/rubocop-ast.rbi +1399 -0
  41. data/sorbet/rbi/gems/rubocop-sorbet.rbi +218 -0
  42. data/sorbet/rbi/gems/rubocop.rbi +9351 -0
  43. data/sorbet/rbi/gems/ruby-progressbar.rbi +304 -0
  44. data/sorbet/rbi/gems/unicode-display_width.rbi +23 -0
  45. data/sorbet/rbi/gems/webrick.rbi +662 -0
  46. data/sorbet/rbi/hidden-definitions/errors.txt +22890 -0
  47. data/sorbet/rbi/hidden-definitions/hidden.rbi +12118 -0
  48. data/sorbet/rbi/sorbet-typed/lib/rainbow/all/rainbow.rbi +276 -0
  49. data/sorbet/rbi/sorbet-typed/lib/rake/all/rake.rbi +645 -0
  50. data/sorbet/rbi/sorbet-typed/lib/rspec-core/all/rspec-core.rbi +24 -0
  51. data/sorbet/rbi/sorbet-typed/lib/rubocop/>=1.8/rubocop.rbi +12 -0
  52. data/sorbet/rbi/todo.rbi +6 -0
  53. data/spec/hpke_key_config_spec.rb +32 -0
  54. data/spec/spec_helper.rb +6 -0
  55. metadata +126 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 669b29a1c5c1ec227e23a1f417b18a694449c21b1fd2f8ca90ea2afc67ad7e02
4
+ data.tar.gz: 2e748100fb8990fa38e1bd784868dff959da998a727164bf27869cdc21b35b0c
5
+ SHA512:
6
+ metadata.gz: da1970fab247c1428188b088a26462f8fcf5cc150363e85b57bd1336bac1146bbf46b1396437045f2c190481d7636def112ef4d8ebf1edc95a1e9e7b5e47dad8
7
+ data.tar.gz: 73448f4886946b2f969ce9f45830a0bb798d26d5f8d34dd59f85b5f7b99ada2d02afba322d29229b44658bcbb9f00bc2f52d341e677f15ad6e18a6dbbd7c6bce
@@ -0,0 +1,34 @@
1
+ name: CI
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: ['2.7.x', '3.0.x', '3.1.x', '3.2.x']
17
+ steps:
18
+ - uses: actions/checkout@v3
19
+ - name: Set up Ruby
20
+ uses: ruby/setup-ruby@v1
21
+ with:
22
+ ruby-version: ${{ matrix.ruby }}
23
+ - name: Install dependencies
24
+ run: |
25
+ gem --version
26
+ gem install bundler
27
+ bundle --version
28
+ bundle install
29
+ - name: Run Rubocop
30
+ run: |
31
+ bundle exec rake rubocop
32
+ - name: Run Sorbet type checker
33
+ run: |
34
+ bundle exec rake sorbet
data/.gitignore ADDED
@@ -0,0 +1,16 @@
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
+ *.rdb
data/.rubocop.yml ADDED
@@ -0,0 +1,34 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.7
3
+
4
+ Style/ConditionalAssignment:
5
+ Enabled: false
6
+
7
+ Style/Documentation:
8
+ Enabled: false
9
+
10
+ Style/NumericLiterals:
11
+ Enabled: false
12
+
13
+ Style/ClassAndModuleChildren:
14
+ EnforcedStyle: compact
15
+
16
+ Metrics/AbcSize:
17
+ Max: 30
18
+
19
+ Metrics/MethodLength:
20
+ Max: 30
21
+
22
+ Naming/MethodParameterName:
23
+ MinNameLength: 1
24
+
25
+ Metrics/BlockLength:
26
+ Exclude:
27
+ - 'spec/*.rb'
28
+ - 'sorbet/**/*'
29
+
30
+ require:
31
+ - rubocop-sorbet
32
+
33
+ Sorbet:
34
+ Enabled: true
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.1.2
data/Gemfile ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'sorbet-runtime'
8
+
9
+ group :test do
10
+ gem 'byebug'
11
+ gem 'rake'
12
+ gem 'rspec', '3.11'
13
+ gem 'rubocop', '1.42'
14
+ gem 'rubocop-sorbet'
15
+ gem 'sorbet'
16
+ gem 'webrick'
17
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,73 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ech_config (0.0.1)
5
+ sorbet-runtime
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ast (2.4.2)
11
+ byebug (11.1.3)
12
+ diff-lcs (1.5.0)
13
+ json (2.6.3)
14
+ parallel (1.22.1)
15
+ parser (3.2.0.0)
16
+ ast (~> 2.4.1)
17
+ rainbow (3.1.1)
18
+ rake (13.0.6)
19
+ regexp_parser (2.6.1)
20
+ rexml (3.2.5)
21
+ rspec (3.11.0)
22
+ rspec-core (~> 3.11.0)
23
+ rspec-expectations (~> 3.11.0)
24
+ rspec-mocks (~> 3.11.0)
25
+ rspec-core (3.11.0)
26
+ rspec-support (~> 3.11.0)
27
+ rspec-expectations (3.11.1)
28
+ diff-lcs (>= 1.2.0, < 2.0)
29
+ rspec-support (~> 3.11.0)
30
+ rspec-mocks (3.11.2)
31
+ diff-lcs (>= 1.2.0, < 2.0)
32
+ rspec-support (~> 3.11.0)
33
+ rspec-support (3.11.1)
34
+ rubocop (1.42.0)
35
+ json (~> 2.3)
36
+ parallel (~> 1.10)
37
+ parser (>= 3.1.2.1)
38
+ rainbow (>= 2.2.2, < 4.0)
39
+ regexp_parser (>= 1.8, < 3.0)
40
+ rexml (>= 3.2.5, < 4.0)
41
+ rubocop-ast (>= 1.24.1, < 2.0)
42
+ ruby-progressbar (~> 1.7)
43
+ unicode-display_width (>= 1.4.0, < 3.0)
44
+ rubocop-ast (1.24.1)
45
+ parser (>= 3.1.1.0)
46
+ rubocop-sorbet (0.6.11)
47
+ rubocop (>= 0.90.0)
48
+ ruby-progressbar (1.11.0)
49
+ sorbet (0.5.10607)
50
+ sorbet-static (= 0.5.10607)
51
+ sorbet-runtime (0.5.10607)
52
+ sorbet-static (0.5.10607-universal-darwin-21)
53
+ unicode-display_width (2.4.2)
54
+ webrick (1.7.0)
55
+
56
+ PLATFORMS
57
+ arm64-darwin-21
58
+ x86_64-darwin-21
59
+
60
+ DEPENDENCIES
61
+ bundler
62
+ byebug
63
+ ech_config!
64
+ rake
65
+ rspec (= 3.11)
66
+ rubocop (= 1.42)
67
+ rubocop-sorbet
68
+ sorbet
69
+ sorbet-runtime
70
+ webrick
71
+
72
+ BUNDLED WITH
73
+ 2.3.7
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Tomoya Kuwayama
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # ech_config
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/ech_config.svg)](https://badge.fury.io/rb/ech_config)
4
+ [![CI](https://github.com/thekuwayama/ech_config/workflows/CI/badge.svg)](https://github.com/thekuwayama/ech_config/actions?workflow=CI)
5
+
6
+ `ech_config` is Ruby implementation of [Encrypted ClientHello Configuration](https://datatracker.ietf.org/doc/draft-ietf-tls-esni/).
7
+
8
+ `ech_config` supports `0xfe0b` ~ `0xfe0f` ECHConfig.version.
9
+
10
+
11
+ ## Installation
12
+
13
+ The gem is available at [rubygems.org](https://rubygems.org/gems/ech_config). You can install with:
14
+
15
+ ```sh-session
16
+ $ gem install ech_config
17
+ ```
18
+
19
+
20
+ ## License
21
+
22
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # typed: false
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/gem_tasks'
5
+ require 'rubocop/rake_task'
6
+ require 'rspec/core/rake_task'
7
+
8
+ RuboCop::RakeTask.new
9
+ RSpec::Core::RakeTask.new(:spec)
10
+
11
+ desc 'Run Sorbet type checker'
12
+ task :sorbet do
13
+ sh 'srb tc'
14
+ end
15
+
16
+ task default: %i[rubocop sorbet spec]
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'ech_config/version'
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = 'ech_config'
9
+ spec.version = ECHConfig::VERSION
10
+ spec.authors = ['thekuwayama']
11
+ spec.email = ['thekuwayama@gmail.com']
12
+ spec.summary = 'ECHConfig'
13
+ spec.description = spec.summary
14
+ spec.homepage = 'https://github.com/thekuwayama/ech_config'
15
+ spec.license = 'MIT'
16
+ spec.required_ruby_version = '>=2.7.0'
17
+
18
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = ['lib']
21
+
22
+ spec.add_development_dependency 'bundler'
23
+ spec.add_runtime_dependency 'sorbet-runtime'
24
+ end
data/example/README.md ADDED
@@ -0,0 +1,9 @@
1
+ ## Usage
2
+
3
+ ```sh-session
4
+ $ cd /path/to/ech_config/example
5
+
6
+ $ ruby well_known_url_client.rb cover.defo.ie draft-13.esni.defo.ie
7
+ ```
8
+
9
+ https://datatracker.ietf.org/meeting/113/materials/slides-113-dispatch-a-well-known-url-for-publishing-echconfiglists-00
@@ -0,0 +1,29 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require 'base64'
5
+ require 'json'
6
+ require 'net/http'
7
+ require 'uri'
8
+
9
+ require "#{File.dirname(__FILE__)}/../lib/ech_config"
10
+
11
+ front = ARGV[0] || 'localhost'
12
+ backend = ARGV[1] || 'draft-13.esni.localhost'
13
+ uri = URI("https://#{front}/.well-known/ech/#{backend}.json")
14
+
15
+ res = Net::HTTP.get_response(uri)
16
+ unless res.is_a?(Net::HTTPSuccess)
17
+ warn 'error: HTTP Status is not 200'
18
+ exit 1
19
+ end
20
+
21
+ JSON.parse(res.body, symbolize_names: true).each do |h|
22
+ octet = Base64.decode64(h[:echconfiglist])
23
+ unless octet.length - 2 == octet.slice(0, 2)&.unpack1('n')
24
+ warn 'error: failed to parse echconfiglist'
25
+ exit 1
26
+ end
27
+
28
+ pp ECHConfig.decode_vectors(octet.slice(2..) || '')
29
+ end
@@ -0,0 +1,24 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ class ECHConfig::ECHConfigContents::Extensions
5
+ extend T::Sig
6
+ attr_reader :octet
7
+
8
+ sig { params(octet: String).void }
9
+ def initialize(octet)
10
+ # Note taht ECHConfig::ECHConfigContents::Extension only has octets.
11
+ # If you need, deserialize octets to get TLS Extension objects.
12
+ @octet = octet
13
+ end
14
+
15
+ sig { returns(String) }
16
+ def load
17
+ @octet
18
+ end
19
+
20
+ sig { params(octet: String).returns(T.attached_class) }
21
+ def self.store(octet)
22
+ new(octet)
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ class ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeKemId
5
+ extend T::Sig
6
+ attr_reader :uint16
7
+
8
+ sig { params(uint16: Integer).void }
9
+ def initialize(uint16)
10
+ @uint16 = uint16
11
+ end
12
+
13
+ sig { returns(String) }
14
+ def encode
15
+ [@uint16].pack('n')
16
+ end
17
+
18
+ sig { params(octet: String).returns(T.attached_class) }
19
+ def self.decode(octet)
20
+ raise ::ECHConfig::DecodeError if octet.length != 2
21
+
22
+ new(octet.unpack1('n'))
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ class ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkePublicKey
5
+ extend T::Sig
6
+ attr_reader :opaque
7
+
8
+ sig { params(opaque: String).void }
9
+ def initialize(opaque)
10
+ @opaque = opaque
11
+ end
12
+
13
+ sig { returns(String) }
14
+ def encode
15
+ @opaque.then { |s| [s.length].pack('n') + s }
16
+ end
17
+
18
+ sig { params(octet: String).returns(T.attached_class) }
19
+ def self.decode(octet)
20
+ new(octet)
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ class ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite::HpkeAeadId
5
+ extend T::Sig
6
+ attr_reader :uint16
7
+
8
+ sig { params(uint16: Integer).void }
9
+ def initialize(uint16)
10
+ @uint16 = uint16
11
+ end
12
+
13
+ sig { returns(String) }
14
+ def encode
15
+ [@uint16].pack('n')
16
+ end
17
+
18
+ sig { params(octet: String).returns(T.attached_class) }
19
+ def self.decode(octet)
20
+ raise ::ECHConfig::DecodeError if octet.length != 2
21
+
22
+ new(octet.unpack1('n'))
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ class ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite::HpkeKdfId
5
+ extend T::Sig
6
+ attr_reader :uint16
7
+
8
+ sig { params(uint16: Integer).void }
9
+ def initialize(uint16)
10
+ @uint16 = uint16
11
+ end
12
+
13
+ sig { returns(String) }
14
+ def encode
15
+ [@uint16].pack('n')
16
+ end
17
+
18
+ sig { params(octet: String).returns(T.attached_class) }
19
+ def self.decode(octet)
20
+ raise ::ECHConfig::DecodeError if octet.length != 2
21
+
22
+ new(octet.unpack1('n'))
23
+ end
24
+ end
@@ -0,0 +1,41 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ class ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite
5
+ extend T::Sig
6
+ # define class
7
+ end
8
+
9
+ Dir["#{File.dirname(__FILE__)}/hpke_symmetric_cipher_suite/*.rb"]
10
+ .sort.each { |f| require f }
11
+
12
+ class ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite
13
+ attr_reader :kdf_id, :aead_id
14
+
15
+ sig { params(kdf_id: HpkeKdfId, aead_id: HpkeAeadId).void }
16
+ def initialize(kdf_id, aead_id)
17
+ @kdf_id = kdf_id
18
+ @aead_id = aead_id
19
+ end
20
+
21
+ sig { returns(String) }
22
+ def encode
23
+ @kdf_id.encode + @aead_id.encode
24
+ end
25
+
26
+ sig { params(octet: String).returns(T::Array[T.attached_class]) }
27
+ def self.decode_vectors(octet)
28
+ i = 0
29
+ cipher_suites = []
30
+ while i < octet.length
31
+ raise ::ECHConfig::DecodeError if i + 4 > octet.length
32
+
33
+ kdf_id = HpkeKdfId.decode(octet.slice(i, 2) || '')
34
+ aead_id = HpkeAeadId.decode(octet.slice(i + 2, 2) || '')
35
+ i += 4
36
+ cipher_suites << new(kdf_id, aead_id)
37
+ end
38
+
39
+ cipher_suites
40
+ end
41
+ end
@@ -0,0 +1,82 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ class ECHConfig::ECHConfigContents::HpkeKeyConfig
5
+ # define class
6
+ end
7
+
8
+ Dir["#{File.dirname(__FILE__)}/hpke_key_config/*.rb"]
9
+ .sort.each { |f| require f }
10
+
11
+ class ECHConfig::ECHConfigContents::HpkeKeyConfig
12
+ extend T::Sig
13
+ attr_reader :config_id, :kem_id, :public_key, :cipher_suites
14
+
15
+ sig do
16
+ params(
17
+ config_id: Integer,
18
+ kem_id: HpkeKemId,
19
+ public_key: HpkePublicKey,
20
+ cipher_suites: T::Array[HpkeSymmetricCipherSuite]
21
+ ).void
22
+ end
23
+ def initialize(config_id,
24
+ kem_id,
25
+ public_key,
26
+ cipher_suites)
27
+ @config_id = config_id
28
+ @kem_id = kem_id
29
+ @public_key = public_key
30
+ @cipher_suites = cipher_suites
31
+ end
32
+
33
+ sig { returns(String) }
34
+ def encode
35
+ [@config_id].pack('C') \
36
+ + @kem_id.encode \
37
+ + @public_key.encode \
38
+ + @cipher_suites.map(&:encode).join.then { |s| [s.length].pack('n') + s }
39
+ end
40
+
41
+ sig { params(octet: String).returns([T.attached_class, T.nilable(String)]) }
42
+ # rubocop:disable Metrics/AbcSize
43
+ # rubocop:disable Metrics/CyclomaticComplexity
44
+ # rubocop:disable Metrics/PerceivedComplexity
45
+ def self.decode(octet)
46
+ raise ::ECHConfig::DecodeError if octet.empty?
47
+
48
+ config_id = octet.slice(0, 1)&.unpack1('C')
49
+ i = 1
50
+ raise ::ECHConfig::DecodeError if i + 2 > octet.length
51
+
52
+ kem_id = HpkeKemId.decode(octet.slice(i, 2) || '')
53
+ i += 2
54
+ raise ::ECHConfig::DecodeError if i + 2 > octet.length
55
+
56
+ pk_len = octet.slice(i, 2)&.unpack1('n')
57
+ i += 2
58
+ raise ::ECHConfig::DecodeError if i + pk_len > octet.length
59
+
60
+ public_key = HpkePublicKey.decode(octet.slice(i, pk_len) || '')
61
+ i += pk_len
62
+ raise ::ECHConfig::DecodeError if i + 2 > octet.length
63
+
64
+ cs_len = octet.slice(i, 2)&.unpack1('n')
65
+ i += 2
66
+ raise ::ECHConfig::DecodeError if i + 2 > octet.length
67
+
68
+ cs_bin = octet.slice(i, cs_len)
69
+ i += cs_len
70
+ cipher_suites = HpkeSymmetricCipherSuite.decode_vectors(cs_bin || '')
71
+ hpke_key_config = new(
72
+ config_id,
73
+ kem_id,
74
+ public_key,
75
+ cipher_suites
76
+ )
77
+ [hpke_key_config, octet[i..]]
78
+ end
79
+ # rubocop:enable Metrics/AbcSize
80
+ # rubocop:enable Metrics/CyclomaticComplexity
81
+ # rubocop:enable Metrics/PerceivedComplexity
82
+ end
@@ -0,0 +1,78 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ class ECHConfig::ECHConfigContents
5
+ # define class
6
+ end
7
+
8
+ Dir["#{File.dirname(__FILE__)}/ech_config_contents/*.rb"]
9
+ .sort.each { |f| require f }
10
+
11
+ class ECHConfig::ECHConfigContents
12
+ extend T::Sig
13
+ attr_reader :key_config, :maximum_name_length, :public_name, :extensions
14
+
15
+ sig do
16
+ params(
17
+ key_config: HpkeKeyConfig,
18
+ maximum_name_length: Integer,
19
+ public_name: String,
20
+ extensions: Extensions
21
+ ).void
22
+ end
23
+ def initialize(key_config,
24
+ maximum_name_length,
25
+ public_name,
26
+ extensions)
27
+ @key_config = key_config
28
+ @maximum_name_length = maximum_name_length
29
+ @public_name = public_name
30
+ @extensions = extensions
31
+ end
32
+
33
+ sig { returns(String) }
34
+ def encode
35
+ @key_config.encode \
36
+ + [@maximum_name_length].pack('C') \
37
+ + @public_name.then { |s| [s.length].pack('C') + s } \
38
+ + @extensions.load.then { |s| [s.length].pack('n') + s }
39
+ end
40
+
41
+ sig { params(octet: String).returns(T.attached_class) }
42
+ # rubocop:disable Metrics/AbcSize
43
+ # rubocop:disable Metrics/CyclomaticComplexity
44
+ # rubocop:disable Metrics/PerceivedComplexity
45
+ def self.decode(octet)
46
+ key_config, octet = HpkeKeyConfig.decode(octet)
47
+ raise ::ECHConfig::DecodeError if octet.nil?
48
+ raise ::ECHConfig::DecodeError if octet.length < 2
49
+
50
+ maximum_name_length = octet.slice(0, 1)&.unpack1('C')
51
+ raise ::ECHConfig::DecodeError if maximum_name_length.nil?
52
+
53
+ pn_len = octet.slice(1, 1)&.unpack1('C')
54
+ i = 2
55
+ raise ::ECHConfig::DecodeError if i + pn_len > octet.length
56
+
57
+ public_name = octet.slice(i, pn_len)
58
+ raise ::ECHConfig::DecodeError if public_name.nil?
59
+
60
+ i += pn_len
61
+ raise ::ECHConfig::DecodeError if i + 2 > octet.length
62
+
63
+ ex_len = octet.slice(i, 2)&.unpack1('n')
64
+ i += 2
65
+ raise ::ECHConfig::DecodeError if i + ex_len != octet.length
66
+
67
+ extensions = Extensions.store(octet)
68
+ new(
69
+ key_config,
70
+ maximum_name_length,
71
+ public_name,
72
+ extensions
73
+ )
74
+ end
75
+ # rubocop:enable Metrics/AbcSize
76
+ # rubocop:enable Metrics/CyclomaticComplexity
77
+ # rubocop:enable Metrics/PerceivedComplexity
78
+ end
@@ -0,0 +1,11 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ # Generic error, common for all classes under ECHConfig module.
5
+ class ECHConfig::Error < StandardError; end
6
+
7
+ # Raised if configure is invalid.
8
+ class ECHConfig::DecodeError < ECHConfig::Error; end
9
+
10
+ # Raised if version is unsupported.
11
+ class ECHConfig::UnsupportedVersion < ECHConfig::Error; end
@@ -0,0 +1,6 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ class ECHConfig
5
+ VERSION = '0.0.1'
6
+ end