tttls1.3 0.3.2 → 0.3.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/Gemfile +2 -1
  4. data/example/helper.rb +32 -1
  5. data/example/https_client_using_0rtt.rb +4 -2
  6. data/example/https_client_using_ech.rb +6 -7
  7. data/example/https_client_using_grease_ech.rb +0 -2
  8. data/example/https_client_using_hrr.rb +2 -1
  9. data/example/https_client_using_hrr_and_ech.rb +6 -7
  10. data/example/https_client_using_hrr_and_ticket.rb +4 -2
  11. data/example/https_client_using_status_request.rb +2 -1
  12. data/example/https_client_using_ticket.rb +4 -2
  13. data/example/https_client_using_ticket_and_ech.rb +57 -0
  14. data/lib/tttls1.3/client.rb +3 -5
  15. data/lib/tttls1.3/cryptograph/aead.rb +0 -5
  16. data/lib/tttls1.3/ech.rb +37 -21
  17. data/lib/tttls1.3/key_schedule.rb +1 -1
  18. data/lib/tttls1.3/message/application_data.rb +1 -1
  19. data/lib/tttls1.3/message/client_hello.rb +8 -0
  20. data/lib/tttls1.3/message/extension/alpn.rb +1 -1
  21. data/lib/tttls1.3/message/extension/ech.rb +17 -25
  22. data/lib/tttls1.3/message/extension/ech_outer_extensions.rb +51 -0
  23. data/lib/tttls1.3/message/extension/key_share.rb +3 -6
  24. data/lib/tttls1.3/message/extension/pre_shared_key.rb +0 -3
  25. data/lib/tttls1.3/message/extension/server_name.rb +0 -1
  26. data/lib/tttls1.3/message/extension/signature_algorithms_cert.rb +1 -1
  27. data/lib/tttls1.3/message/extension/unknown_extension.rb +0 -1
  28. data/lib/tttls1.3/message/extensions.rb +30 -2
  29. data/lib/tttls1.3/message/record.rb +0 -2
  30. data/lib/tttls1.3/message.rb +1 -0
  31. data/lib/tttls1.3/named_group.rb +1 -3
  32. data/lib/tttls1.3/server.rb +0 -1
  33. data/lib/tttls1.3/version.rb +1 -1
  34. data/spec/client_hello_spec.rb +19 -0
  35. data/spec/ech_outer_extensions_spec.rb +42 -0
  36. data/spec/ech_spec.rb +2 -0
  37. data/spec/extensions_spec.rb +65 -0
  38. data/spec/spec_helper.rb +4 -0
  39. metadata +6 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7245602faa9087e83b3e47484aa88dc368b153e98c76fd00e36ebb1a863af6ef
4
- data.tar.gz: c7362f39fc26763f712cdf61a29b3796686c50034d2a6431788d8dbf7dd505c9
3
+ metadata.gz: '0913f70f0bdbfd6740f41ce7902f55660ea1bd99533a8f765f9d98a1bd56a58c'
4
+ data.tar.gz: 611a063ae74498d19636ebf3ee3741b76164856341bb38ec9966a999579bdfb1
5
5
  SHA512:
6
- metadata.gz: 33168ef27007f9e6d73197ed3e1bae9b58dadfdf3042e732ddc6a05913db0aad48fb653ea7b7222716d70ebe42739f2d9dddee0ec75ee45eb33daba4be6f0229
7
- data.tar.gz: d05c73a49e0f5d568785c3b6af3d9bc3c286b35f1110c5f31860f8cf788a9f9e019112fd8647394672c279808e20c4f02d438a58c44f5bab9af1d5fb851ef41f
6
+ metadata.gz: 88f39003d30f4642c67a61169923fe248bc572b4a1c638eb36803dc4278aa91a499919784cd060fa35c522bcb935745d1b4542abb1963514aba1f86f1ea789fd
7
+ data.tar.gz: cd2ae8cd383cd9737a732d53c8ca27cbacdcd3c4a5b6bee62f5f43faba4fd8eb8067b0621df18d8b77db679823c3aa26947508fe5827939bf6741e6f417a1d80
data/.rubocop.yml CHANGED
@@ -4,6 +4,9 @@ AllCops:
4
4
  Gemspec/RequiredRubyVersion:
5
5
  Enabled: false
6
6
 
7
+ Semicolon:
8
+ AllowAsExpressionSeparator: true
9
+
7
10
  Style/ConditionalAssignment:
8
11
  Enabled: false
9
12
 
data/Gemfile CHANGED
@@ -9,11 +9,12 @@ gem 'openssl'
9
9
  gem 'rake'
10
10
 
11
11
  group :development do
12
+ gem 'base64'
12
13
  gem 'byebug'
13
14
  gem 'http_parser.rb'
15
+ gem 'resolv', '~> 0.4.0'
14
16
  gem 'rspec', '3.9.0'
15
17
  gem 'rubocop', '0.78.0'
16
- gem 'svcb_rr_patch'
17
18
  gem 'webrick'
18
19
  end
19
20
 
data/example/helper.rb CHANGED
@@ -2,13 +2,15 @@
2
2
 
3
3
  $LOAD_PATH << __dir__ + '/../lib'
4
4
 
5
+ require 'base64'
6
+ require 'resolv'
5
7
  require 'socket'
6
8
  require 'time'
7
9
  require 'uri'
8
10
  require 'webrick'
9
11
 
12
+ require 'ech_config'
10
13
  require 'http/parser'
11
- require 'svcb_rr_patch'
12
14
 
13
15
  require 'tttls1.3'
14
16
 
@@ -83,3 +85,32 @@ def transcript_htmlize(transcript)
83
85
  format(m[k], TTTLS13::Convert.obj2html(v.first))
84
86
  end.join('<br>')
85
87
  end
88
+
89
+ def parse_echconfigs_pem(pem)
90
+ # https://datatracker.ietf.org/doc/html/draft-farrell-tls-pemesni-08#section-3-4
91
+ s = pem.gsub(/-----(BEGIN|END) (ECH CONFIGS|ECHCONFIG)-----/, '')
92
+ .gsub("\n", '')
93
+ b = Base64.decode64(s)
94
+ raise 'failed to parse ECHConfigs' \
95
+ unless b.length == b.slice(0, 2).unpack1('n') + 2
96
+
97
+ ECHConfig.decode_vectors(b.slice(2..))
98
+ end
99
+
100
+ def resolve_echconfig(hostname)
101
+ rr = Resolv::DNS.new.getresources(
102
+ hostname,
103
+ Resolv::DNS::Resource::IN::HTTPS
104
+ )
105
+
106
+ # https://datatracker.ietf.org/doc/html/draft-ietf-tls-svcb-ech-01#section-6
107
+ ech = 5
108
+ raise "failed to resolve echconfig via #{hostname} HTTPS RR" \
109
+ if rr.first.nil? || rr.first.params[ech].nil?
110
+
111
+ octet = rr.first.params[ech].value
112
+ raise 'failed to parse ECHConfigs' \
113
+ unless octet.length == octet.slice(0, 2).unpack1('n') + 2
114
+
115
+ ECHConfig.decode_vectors(octet.slice(2..)).first
116
+ end
@@ -9,7 +9,8 @@ req = simple_http_request(uri.host, uri.path)
9
9
 
10
10
  settings_2nd = {
11
11
  ca_file: File.exist?(ca_file) ? ca_file : nil,
12
- alpn: ['http/1.1']
12
+ alpn: ['http/1.1'],
13
+ sslkeylogfile: '/tmp/sslkeylogfile.log'
13
14
  }
14
15
  process_new_session_ticket = lambda do |nst, rms, cs|
15
16
  return if Time.now.to_i - nst.timestamp > nst.ticket_lifetime
@@ -24,7 +25,8 @@ end
24
25
  settings_1st = {
25
26
  ca_file: File.exist?(ca_file) ? ca_file : nil,
26
27
  alpn: ['http/1.1'],
27
- process_new_session_ticket: process_new_session_ticket
28
+ process_new_session_ticket: process_new_session_ticket,
29
+ sslkeylogfile: '/tmp/sslkeylogfile.log'
28
30
  }
29
31
 
30
32
  succeed_early_data = false
@@ -2,22 +2,21 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative 'helper'
5
- HpkeSymmetricCipherSuite = \
6
- ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite
7
5
 
8
6
  uri = URI.parse(ARGV[0] || 'https://localhost:4433')
9
7
  ca_file = __dir__ + '/../tmp/ca.crt'
10
8
  req = simple_http_request(uri.host, uri.path)
9
+ ech_config = if ARGV.length > 1
10
+ parse_echconfigs_pem(File.open(ARGV[1]).read).first
11
+ else
12
+ resolve_echconfig(uri.host)
13
+ end
11
14
 
12
- rr = Resolv::DNS.new.getresources(
13
- uri.host,
14
- Resolv::DNS::Resource::IN::HTTPS
15
- )
16
15
  socket = TCPSocket.new(uri.host, uri.port)
17
16
  settings = {
18
17
  ca_file: File.exist?(ca_file) ? ca_file : nil,
19
18
  alpn: ['http/1.1'],
20
- ech_config: rr.first.svc_params['ech'].echconfiglist.first,
19
+ ech_config: ech_config,
21
20
  ech_hpke_cipher_suites:
22
21
  TTTLS13::STANDARD_CLIENT_ECH_HPKE_SYMMETRIC_CIPHER_SUITES,
23
22
  sslkeylogfile: '/tmp/sslkeylogfile.log'
@@ -2,8 +2,6 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative 'helper'
5
- HpkeSymmetricCipherSuite = \
6
- ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite
7
5
 
8
6
  uri = URI.parse(ARGV[0] || 'https://localhost:4433')
9
7
  ca_file = __dir__ + '/../tmp/ca.crt'
@@ -11,7 +11,8 @@ socket = TCPSocket.new(uri.host, uri.port)
11
11
  settings = {
12
12
  ca_file: File.exist?(ca_file) ? ca_file : nil,
13
13
  key_share_groups: [], # empty KeyShareClientHello.client_shares
14
- alpn: ['http/1.1']
14
+ alpn: ['http/1.1'],
15
+ sslkeylogfile: '/tmp/sslkeylogfile.log'
15
16
  }
16
17
  client = TTTLS13::Client.new(socket, uri.host, **settings)
17
18
  client.connect
@@ -2,23 +2,22 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative 'helper'
5
- HpkeSymmetricCipherSuite = \
6
- ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite
7
5
 
8
6
  uri = URI.parse(ARGV[0] || 'https://localhost:4433')
9
7
  ca_file = __dir__ + '/../tmp/ca.crt'
10
8
  req = simple_http_request(uri.host, uri.path)
9
+ ech_config = if ARGV.length > 1
10
+ parse_echconfigs_pem(File.open(ARGV[1]).read).first
11
+ else
12
+ resolve_echconfig(uri.host)
13
+ end
11
14
 
12
- rr = Resolv::DNS.new.getresources(
13
- uri.host,
14
- Resolv::DNS::Resource::IN::HTTPS
15
- )
16
15
  socket = TCPSocket.new(uri.host, uri.port)
17
16
  settings = {
18
17
  ca_file: File.exist?(ca_file) ? ca_file : nil,
19
18
  key_share_groups: [], # empty KeyShareClientHello.client_shares
20
19
  alpn: ['http/1.1'],
21
- ech_config: rr.first.svc_params['ech'].echconfiglist.first,
20
+ ech_config: ech_config,
22
21
  ech_hpke_cipher_suites:
23
22
  TTTLS13::STANDARD_CLIENT_ECH_HPKE_SYMMETRIC_CIPHER_SUITES,
24
23
  sslkeylogfile: '/tmp/sslkeylogfile.log'
@@ -9,7 +9,8 @@ req = simple_http_request(uri.host, uri.path)
9
9
 
10
10
  settings_2nd = {
11
11
  ca_file: File.exist?(ca_file) ? ca_file : nil,
12
- alpn: ['http/1.1']
12
+ alpn: ['http/1.1'],
13
+ sslkeylogfile: '/tmp/sslkeylogfile.log'
13
14
  }
14
15
  process_new_session_ticket = lambda do |nst, rms, cs|
15
16
  return if Time.now.to_i - nst.timestamp > nst.ticket_lifetime
@@ -25,7 +26,8 @@ end
25
26
  settings_1st = {
26
27
  ca_file: File.exist?(ca_file) ? ca_file : nil,
27
28
  alpn: ['http/1.1'],
28
- process_new_session_ticket: process_new_session_ticket
29
+ process_new_session_ticket: process_new_session_ticket,
30
+ sslkeylogfile: '/tmp/sslkeylogfile.log'
29
31
  }
30
32
 
31
33
  [
@@ -19,7 +19,8 @@ settings = {
19
19
  ca_file: File.exist?(ca_file) ? ca_file : nil,
20
20
  alpn: ['http/1.1'],
21
21
  check_certificate_status: true,
22
- process_certificate_status: process_certificate_status
22
+ process_certificate_status: process_certificate_status,
23
+ sslkeylogfile: '/tmp/sslkeylogfile.log'
23
24
  }
24
25
  client = TTTLS13::Client.new(socket, uri.host, **settings)
25
26
  client.connect
@@ -9,7 +9,8 @@ req = simple_http_request(uri.host, uri.path)
9
9
 
10
10
  settings_2nd = {
11
11
  ca_file: File.exist?(ca_file) ? ca_file : nil,
12
- alpn: ['http/1.1']
12
+ alpn: ['http/1.1'],
13
+ sslkeylogfile: '/tmp/sslkeylogfile.log'
13
14
  }
14
15
  process_new_session_ticket = lambda do |nst, rms, cs|
15
16
  return if Time.now.to_i - nst.timestamp > nst.ticket_lifetime
@@ -24,7 +25,8 @@ end
24
25
  settings_1st = {
25
26
  ca_file: File.exist?(ca_file) ? ca_file : nil,
26
27
  alpn: ['http/1.1'],
27
- process_new_session_ticket: process_new_session_ticket
28
+ process_new_session_ticket: process_new_session_ticket,
29
+ sslkeylogfile: '/tmp/sslkeylogfile.log'
28
30
  }
29
31
 
30
32
  [
@@ -0,0 +1,57 @@
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'helper'
5
+
6
+ uri = URI.parse(ARGV[0] || 'https://localhost:4433')
7
+ ca_file = __dir__ + '/../tmp/ca.crt'
8
+ req = simple_http_request(uri.host, uri.path)
9
+ ech_config = if ARGV.length > 1
10
+ parse_echconfigs_pem(File.open(ARGV[1]).read).first
11
+ else
12
+ resolve_echconfig(uri.host)
13
+ end
14
+
15
+ settings_2nd = {
16
+ ca_file: File.exist?(ca_file) ? ca_file : nil,
17
+ alpn: ['http/1.1'],
18
+ ech_config: ech_config,
19
+ ech_hpke_cipher_suites:
20
+ TTTLS13::STANDARD_CLIENT_ECH_HPKE_SYMMETRIC_CIPHER_SUITES,
21
+ sslkeylogfile: '/tmp/sslkeylogfile.log'
22
+ }
23
+ process_new_session_ticket = lambda do |nst, rms, cs|
24
+ return if Time.now.to_i - nst.timestamp > nst.ticket_lifetime
25
+
26
+ settings_2nd[:ticket] = nst.ticket
27
+ settings_2nd[:resumption_main_secret] = rms
28
+ settings_2nd[:psk_cipher_suite] = cs
29
+ settings_2nd[:ticket_nonce] = nst.ticket_nonce
30
+ settings_2nd[:ticket_age_add] = nst.ticket_age_add
31
+ settings_2nd[:ticket_timestamp] = nst.timestamp
32
+ end
33
+ settings_1st = {
34
+ ca_file: File.exist?(ca_file) ? ca_file : nil,
35
+ alpn: ['http/1.1'],
36
+ ech_config: ech_config,
37
+ ech_hpke_cipher_suites:
38
+ TTTLS13::STANDARD_CLIENT_ECH_HPKE_SYMMETRIC_CIPHER_SUITES,
39
+ process_new_session_ticket: process_new_session_ticket,
40
+ sslkeylogfile: '/tmp/sslkeylogfile.log'
41
+ }
42
+
43
+ [
44
+ # Initial Handshake:
45
+ settings_1st,
46
+ # Subsequent Handshake:
47
+ settings_2nd
48
+ ].each do |settings|
49
+ socket = TCPSocket.new(uri.host, uri.port)
50
+ client = TTTLS13::Client.new(socket, uri.host, **settings)
51
+ client.connect
52
+ client.write(req)
53
+
54
+ print recv_http_response(client)
55
+ client.close unless client.eof?
56
+ socket.close
57
+ end
@@ -89,8 +89,8 @@ module TTTLS13
89
89
  class Client
90
90
  include Logging
91
91
 
92
- HpkeSymmetricCipherSuit = \
93
- ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite
92
+ HpkeSymmetricCipherSuit \
93
+ = ECHConfig::ECHConfigContents::HpkeKeyConfig::HpkeSymmetricCipherSuite
94
94
 
95
95
  attr_reader :transcript
96
96
 
@@ -110,7 +110,6 @@ module TTTLS13
110
110
  raise Error::ConfigError unless valid_settings?
111
111
  end
112
112
 
113
- # NOTE:
114
113
  # START <----+
115
114
  # Send ClientHello | | Recv HelloRetryRequest
116
115
  # [K_send = early data] | |
@@ -327,7 +326,7 @@ module TTTLS13
327
326
  )
328
327
 
329
328
  # rejected ECH
330
- # NOTE: It can compute (hrr_)accept_ech until client selects the
329
+ # It can compute (hrr_)accept_ech until client selects the
331
330
  # cipher_suite.
332
331
  if !sh.hrr? && use_ech?
333
332
  if !@transcript.include?(HRR) && !key_schedule.accept_ech?
@@ -1008,7 +1007,6 @@ module TTTLS13
1008
1007
  [new_exs, priv_keys]
1009
1008
  end
1010
1009
 
1011
- # NOTE:
1012
1010
  # https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2
1013
1011
  #
1014
1012
  # @param ch1 [TTTLS13::Message::ClientHello]
@@ -23,8 +23,6 @@ module TTTLS13
23
23
  when CipherSuite::TLS_CHACHA20_POLY1305_SHA256
24
24
  @cipher = OpenSSL::Cipher.new('chacha20-poly1305')
25
25
  else
26
- # Note:
27
- # not supported
28
26
  # CipherSuite::TLS_AES_128_CCM_SHA256
29
27
  # CipherSuite::TLS_AES_128_CCM_8_SHA256
30
28
  raise Error::ErrorAlerts, :internal_error
@@ -36,7 +34,6 @@ module TTTLS13
36
34
  @auth_tag_len = CipherSuite.auth_tag_len(@cipher_suite)
37
35
  end
38
36
 
39
- # NOTE:
40
37
  # AEAD-Encrypt(write_key, nonce, additional_data, plaintext)
41
38
  #
42
39
  # @param content [String]
@@ -53,7 +50,6 @@ module TTTLS13
53
50
  encrypted_data + cipher.auth_tag
54
51
  end
55
52
 
56
- # NOTE:
57
53
  # AEAD-Decrypt(peer_write_key, nonce,
58
54
  # additional_data, AEADEncrypted)
59
55
  #
@@ -78,7 +74,6 @@ module TTTLS13
78
74
  [clear[0...-postfix_len], clear[-postfix_len]]
79
75
  end
80
76
 
81
- # NOTE:
82
77
  # struct {
83
78
  # opaque content[TLSPlaintext.length];
84
79
  # ContentType type;
data/lib/tttls1.3/ech.rb CHANGED
@@ -7,14 +7,19 @@ module TTTLS13
7
7
  SUPPORTED_ECHCONFIG_VERSIONS = ["\xfe\x0d"].freeze
8
8
  private_constant :SUPPORTED_ECHCONFIG_VERSIONS
9
9
 
10
- # rubocop: disable Metrics/ModuleLength
11
- module Ech
10
+ DEFAULT_ECH_OUTER_EXTENSIONS = [
11
+ Message::ExtensionType::KEY_SHARE
12
+ ].freeze
13
+ private_constant :DEFAULT_ECH_OUTER_EXTENSIONS
14
+
15
+ # rubocop: disable Metrics/ClassLength
16
+ class Ech
12
17
  # @param inner [TTTLS13::Message::ClientHello]
13
18
  # @param ech_config [ECHConfig]
14
19
  # @param hpke_cipher_suite_selector [Method]
15
20
  #
16
21
  # @return [TTTLS13::Message::ClientHello]
17
- # @return [TTTLS13::Message::ClientHello]
22
+ # @return [TTTLS13::Message::ClientHello] ClientHelloInner
18
23
  # @return [TTTLS13::EchState]
19
24
  # rubocop: disable Metrics/AbcSize
20
25
  def self.offer_ech(inner, ech_config, hpke_cipher_suite_selector)
@@ -30,12 +35,15 @@ module TTTLS13
30
35
  return [new_greased_ch(inner, new_grease_ech), nil, nil] \
31
36
  if ech_state.nil? || enc.nil?
32
37
 
33
- encoded = encode_ch_inner(inner, ech_state.maximum_name_length)
34
- overhead_len = aead_id2overhead_len(
35
- ech_state.cipher_suite.aead_id.uint16
36
- )
38
+ # for ech_outer_extensions
39
+ replaced = \
40
+ inner.extensions.remove_and_replace!(DEFAULT_ECH_OUTER_EXTENSIONS)
37
41
 
38
42
  # Encoding the ClientHelloInner
43
+ encoded = encode_ch_inner(inner, ech_state.maximum_name_length, replaced)
44
+ overhead_len = aead_id2overhead_len(ech_state.cipher_suite.aead_id.uint16)
45
+
46
+ # Authenticating the ClientHelloOuter
39
47
  aad = new_ch_outer_aad(
40
48
  inner,
41
49
  ech_state.cipher_suite,
@@ -44,13 +52,13 @@ module TTTLS13
44
52
  encoded.length + overhead_len,
45
53
  ech_state.public_name
46
54
  )
47
- # Authenticating the ClientHelloOuter
48
- # which does not include the Handshake structure's four byte header.
55
+
49
56
  outer = new_ch_outer(
50
57
  aad,
51
58
  ech_state.cipher_suite,
52
59
  ech_state.config_id,
53
60
  enc,
61
+ # which does not include the Handshake structure's four byte header.
54
62
  ech_state.ctx.seal(aad.serialize[4..], encoded)
55
63
  )
56
64
 
@@ -102,11 +110,16 @@ module TTTLS13
102
110
  # @param ech_state [TTTLS13::EchState]
103
111
  #
104
112
  # @return [TTTLS13::Message::ClientHello]
105
- # @return [TTTLS13::Message::ClientHello]
113
+ # @return [TTTLS13::Message::ClientHello] ClientHelloInner
106
114
  def self.offer_new_ech(inner, ech_state)
107
- encoded = encode_ch_inner(inner, ech_state.maximum_name_length)
108
- overhead_len \
109
- = aead_id2overhead_len(ech_state.cipher_suite.aead_id.uint16)
115
+ # for ech_outer_extensions
116
+ replaced = \
117
+ inner.extensions.remove_and_replace!(DEFAULT_ECH_OUTER_EXTENSIONS)
118
+
119
+ # Encoding the ClientHelloInner
120
+ encoded = encode_ch_inner(inner, ech_state.maximum_name_length, replaced)
121
+ overhead_len = \
122
+ aead_id2overhead_len(ech_state.cipher_suite.aead_id.uint16)
110
123
 
111
124
  # It encrypts EncodedClientHelloInner as described in Section 6.1.1, using
112
125
  # the second partial ClientHelloOuterAAD, to obtain a second
@@ -122,13 +135,14 @@ module TTTLS13
122
135
  encoded.length + overhead_len,
123
136
  ech_state.public_name
124
137
  )
138
+
125
139
  # Authenticating the ClientHelloOuter
126
- # which does not include the Handshake structure's four byte header.
127
140
  outer = new_ch_outer(
128
141
  aad,
129
142
  ech_state.cipher_suite,
130
143
  ech_state.config_id,
131
144
  '',
145
+ # which does not include the Handshake structure's four byte header.
132
146
  ech_state.ctx.seal(aad.serialize[4..], encoded)
133
147
  )
134
148
 
@@ -137,23 +151,23 @@ module TTTLS13
137
151
 
138
152
  # @param inner [TTTLS13::Message::ClientHello]
139
153
  # @param maximum_name_length [Integer]
154
+ # @param replaced [TTTLS13::Message::Extensions]
140
155
  #
141
156
  # @return [String] EncodedClientHelloInner
142
- def self.encode_ch_inner(inner, maximum_name_length)
143
- # TODO: ech_outer_extensions
157
+ def self.encode_ch_inner(inner, maximum_name_length, replaced)
144
158
  encoded = Message::ClientHello.new(
145
159
  legacy_version: inner.legacy_version,
146
160
  random: inner.random,
147
161
  legacy_session_id: '',
148
162
  cipher_suites: inner.cipher_suites,
149
163
  legacy_compression_methods: inner.legacy_compression_methods,
150
- extensions: inner.extensions
164
+ extensions: replaced
151
165
  )
152
166
  server_name_length = \
153
- inner.extensions[Message::ExtensionType::SERVER_NAME].server_name.length
167
+ replaced[Message::ExtensionType::SERVER_NAME].server_name.length
154
168
 
155
- # which does not include the Handshake structure's four byte header.
156
169
  padding_encoded_ch_inner(
170
+ # which does not include the Handshake structure's four byte header.
157
171
  encoded.serialize[4..],
158
172
  server_name_length,
159
173
  maximum_name_length
@@ -284,6 +298,8 @@ module TTTLS13
284
298
 
285
299
  # @param inner [TTTLS13::Message::ClientHello]
286
300
  # @param ech [Message::Extension::ECHClientHello]
301
+ #
302
+ # @return [TTTLS13::Message::ClientHello]
287
303
  def self.new_greased_ch(inner, ech)
288
304
  Message::ClientHello.new(
289
305
  legacy_version: inner.legacy_version,
@@ -393,7 +409,7 @@ module TTTLS13
393
409
  # @param config_id [Integer]
394
410
  # @param cipher_suite [HpkeSymmetricCipherSuite]
395
411
  # @param public_name [String]
396
- # @param ctx [[HPKE::ContextS]
412
+ # @param ctx [HPKE::ContextS]
397
413
  def initialize(maximum_name_length,
398
414
  config_id,
399
415
  cipher_suite,
@@ -406,5 +422,5 @@ module TTTLS13
406
422
  @ctx = ctx
407
423
  end
408
424
  end
409
- # rubocop: enable Metrics/ModuleLength
425
+ # rubocop: enable Metrics/ClassLength
410
426
  end
@@ -250,7 +250,7 @@ module TTTLS13
250
250
  #
251
251
  # @raise [TTTLS13::Error::ErrorAlerts]
252
252
  #
253
- # @param [String]
253
+ # @return [String]
254
254
  def self.hkdf_expand(secret, info, length, digest)
255
255
  hash_len = OpenSSL::Digest.new(digest).digest_length
256
256
  raise Error::ErrorAlerts, :internal_error if length > 255 * hash_len
@@ -6,7 +6,7 @@ module TTTLS13
6
6
  class ApplicationData
7
7
  attr_reader :fragment
8
8
 
9
- # @param [String]
9
+ # @param fragment [String]
10
10
  def initialize(fragment)
11
11
  @fragment = fragment
12
12
  end
@@ -160,6 +160,14 @@ module TTTLS13
160
160
  sg_groups.filter { |g| ks_groups.include?(g) } == ks_groups &&
161
161
  ks_groups.uniq == ks_groups
162
162
  end
163
+
164
+ # @return [Boolean]
165
+ def ch_inner?
166
+ ech = @extensions[Message::ExtensionType::ENCRYPTED_CLIENT_HELLO]
167
+ return false if ech.nil?
168
+
169
+ ech.type == Message::Extension::ECHClientHelloType::INNER
170
+ end
163
171
  end
164
172
  end
165
173
  end
@@ -9,7 +9,7 @@ module TTTLS13
9
9
  attr_reader :extension_type
10
10
  attr_reader :protocol_name_list
11
11
 
12
- # @param named_group_list [Array of String]
12
+ # @param protocol_name_list [Array] Array of String
13
13
  #
14
14
  # @raise [TTTLS13::Error::ErrorAlerts]
15
15
  #
@@ -12,7 +12,6 @@ module TTTLS13
12
12
  INNER = "\x01"
13
13
  end
14
14
 
15
- # NOTE:
16
15
  # struct {
17
16
  # ECHClientHelloType type;
18
17
  # select (ECHClientHello.type) {
@@ -26,12 +25,12 @@ module TTTLS13
26
25
  # };
27
26
  # } ECHClientHello;
28
27
  class ECHClientHello
29
- attr_accessor :extension_type
30
- attr_accessor :type
31
- attr_accessor :cipher_suite
32
- attr_accessor :config_id
33
- attr_accessor :enc
34
- attr_accessor :payload
28
+ attr_reader :extension_type
29
+ attr_reader :type
30
+ attr_reader :cipher_suite
31
+ attr_reader :config_id
32
+ attr_reader :enc
33
+ attr_reader :payload
35
34
 
36
35
  # @param type [TTTLS13::Message::Extension::ECHClientHelloType]
37
36
  # @param cipher_suite [HpkeSymmetricCipherSuite]
@@ -99,10 +98,10 @@ module TTTLS13
99
98
  # @raise [TTTLS13::Error::ErrorAlerts]
100
99
  #
101
100
  # @return [TTTLS13::Message::Extensions::ECHClientHello]
102
- # rubocop: disable Metrics/AbcSize
103
101
  def deserialize_outer_ech(binary)
104
102
  raise Error::ErrorAlerts, :internal_error if binary.nil?
105
- raise Error::ErrorAlerts, :decode_error if binary.length < 5
103
+
104
+ return nil if binary.length < 5
106
105
 
107
106
  kdf_id = \
108
107
  HpkeSymmetricCipherSuite::HpkeKdfId.decode(binary.slice(0, 2))
@@ -112,17 +111,14 @@ module TTTLS13
112
111
  cid = Convert.bin2i(binary.slice(4, 1))
113
112
  enc_len = Convert.bin2i(binary.slice(5, 2))
114
113
  i = 7
115
- raise Error::ErrorAlerts, :decode_error \
116
- if i + enc_len > binary.length
114
+ return nil if i + enc_len > binary.length
117
115
 
118
116
  enc = binary.slice(i, enc_len)
119
117
  i += enc_len
120
- raise Error::ErrorAlerts, :decode_error \
121
- if i + 2 > binary.length
118
+ return nil if i + 2 > binary.length
122
119
 
123
120
  payload_len = Convert.bin2i(binary.slice(i, 2))
124
- raise Error::ErrorAlerts, :decode_error \
125
- if i + payload_len > binary.length
121
+ return nil if i + payload_len > binary.length
126
122
 
127
123
  payload = binary.slice(i, payload_len)
128
124
  ECHClientHello.new(
@@ -133,7 +129,6 @@ module TTTLS13
133
129
  payload: payload
134
130
  )
135
131
  end
136
- # rubocop: enable Metrics/AbcSize
137
132
 
138
133
  # @param binary [String]
139
134
  #
@@ -169,13 +164,12 @@ module TTTLS13
169
164
  end
170
165
  end
171
166
 
172
- # NOTE:
173
167
  # struct {
174
168
  # ECHConfigList retry_configs;
175
169
  # } ECHEncryptedExtensions;
176
170
  class ECHEncryptedExtensions
177
- attr_accessor :extension_type
178
- attr_accessor :retry_configs
171
+ attr_reader :extension_type
172
+ attr_reader :retry_configs
179
173
 
180
174
  # @param retry_configs [Array of ECHConfig]
181
175
  def initialize(retry_configs)
@@ -198,8 +192,7 @@ module TTTLS13
198
192
  # @return [TTTLS13::Message::Extensions::ECHEncryptedExtensions]
199
193
  def self.deserialize(binary)
200
194
  raise Error::ErrorAlerts, :internal_error if binary.nil?
201
- raise Error::ErrorAlerts, :decode_error \
202
- if binary.length != binary.slice(0, 2).unpack1('n') + 2
195
+ return nil if binary.length != binary.slice(0, 2).unpack1('n') + 2
203
196
 
204
197
  ECHEncryptedExtensions.new(
205
198
  ECHConfig.decode_vectors(binary.slice(2..))
@@ -207,13 +200,12 @@ module TTTLS13
207
200
  end
208
201
  end
209
202
 
210
- # NOTE:
211
203
  # struct {
212
204
  # opaque confirmation[8];
213
205
  # } ECHHelloRetryRequest;
214
206
  class ECHHelloRetryRequest
215
- attr_accessor :extension_type
216
- attr_accessor :confirmation
207
+ attr_reader :extension_type
208
+ attr_reader :confirmation
217
209
 
218
210
  # @param confirmation [String]
219
211
  def initialize(confirmation)
@@ -233,7 +225,7 @@ module TTTLS13
233
225
  # @return [TTTLS13::Message::Extensions::ECHHelloRetryRequest]
234
226
  def self.deserialize(binary)
235
227
  raise Error::ErrorAlerts, :internal_error if binary.nil?
236
- raise Error::ErrorAlerts, :decode_error if binary.length != 8
228
+ return nil if binary.length != 8
237
229
 
238
230
  ECHHelloRetryRequest.new(binary)
239
231
  end
@@ -0,0 +1,51 @@
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+
4
+ module TTTLS13
5
+ using Refinements
6
+ module Message
7
+ module Extension
8
+ # ExtensionType OuterExtensions<2..254>;
9
+ class ECHOuterExtensions
10
+ attr_reader :extension_type
11
+ attr_reader :outer_extensions
12
+
13
+ # @param outer_extensions [Array of TTTLS13::Message::ExtensionType]
14
+ def initialize(outer_extensions)
15
+ @extension_type = ExtensionType::ECH_OUTER_EXTENSIONS
16
+ @outer_extensions = outer_extensions
17
+ end
18
+
19
+ # @raise [TTTLS13::Error::ErrorAlerts]
20
+ #
21
+ # @return [String]
22
+ def serialize
23
+ binary = @outer_extensions.join.prefix_uint8_length
24
+ @extension_type + binary.prefix_uint16_length
25
+ end
26
+
27
+ # @param binary [String]
28
+ #
29
+ # @raise [TTTLS13::Error::ErrorAlerts]
30
+ #
31
+ # @return [TTTLS13::Message::Extensions::ECHOuterExtensions]
32
+ def self.deserialize(binary)
33
+ raise Error::ErrorAlerts, :internal_error if binary.nil?
34
+
35
+ return nil if binary.length < 2
36
+
37
+ exlist_len = Convert.bin2i(binary.slice(0, 1))
38
+ i = 1
39
+ outer_extensions = []
40
+ while i < exlist_len + 1
41
+ outer_extensions << binary.slice(i, 2)
42
+ i += 2
43
+ end
44
+ return nil unless outer_extensions.length * 2 == exlist_len
45
+
46
+ ECHOuterExtensions.new(outer_extensions)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -11,7 +11,7 @@ module TTTLS13
11
11
  attr_reader :msg_type
12
12
  attr_reader :key_share_entry
13
13
 
14
- # @param msg_type [TTTLS13::Message::ContentType]
14
+ # @param msg_type [TTTLS13::Message::HandshakeType]
15
15
  # @param key_share_entry [Array of KeyShareEntry]
16
16
  #
17
17
  # @raise [TTTLS13::Error::ErrorAlerts]
@@ -108,7 +108,7 @@ module TTTLS13
108
108
  [key_share, priv_keys]
109
109
  end
110
110
 
111
- # @param groups [TTTLS13::NamedGroup]
111
+ # @param group [TTTLS13::NamedGroup]
112
112
  #
113
113
  # @return [TTTLS13::Message::Extensions::KeyShare]
114
114
  # @return [OpenSSL::PKey::EC.$Object]
@@ -129,7 +129,7 @@ module TTTLS13
129
129
  [key_share, ec]
130
130
  end
131
131
 
132
- # @param groups [TTTLS13::NamedGroup]
132
+ # @param group [TTTLS13::NamedGroup]
133
133
  #
134
134
  # @return [TTTLS13::Message::Extensions::KeyShare]
135
135
  def self.gen_hrr_key_share(group)
@@ -143,7 +143,6 @@ module TTTLS13
143
143
  class << self
144
144
  private
145
145
 
146
- # NOTE:
147
146
  # struct {
148
147
  # KeyShareEntry client_shares<0..2^16-1>;
149
148
  # } KeyShareClientHello;
@@ -178,7 +177,6 @@ module TTTLS13
178
177
  key_share_entry
179
178
  end
180
179
 
181
- # NOTE:
182
180
  # struct {
183
181
  # KeyShareEntry server_share;
184
182
  # } KeyShareServerHello;
@@ -201,7 +199,6 @@ module TTTLS13
201
199
  [KeyShareEntry.new(group: group, key_exchange: key_exchange)]
202
200
  end
203
201
 
204
- # NOTE:
205
202
  # struct {
206
203
  # NamedGroup selected_group;
207
204
  # } KeyShareHelloRetryRequest;
@@ -5,7 +5,6 @@ module TTTLS13
5
5
  using Refinements
6
6
  module Message
7
7
  module Extension
8
- # NOTE:
9
8
  # struct {
10
9
  # select (Handshake.msg_type) {
11
10
  # case client_hello: OfferedPsks;
@@ -83,7 +82,6 @@ module TTTLS13
83
82
  end
84
83
  end
85
84
 
86
- # NOTE:
87
85
  # opaque PskBinderEntry<32..255>;
88
86
  #
89
87
  # struct {
@@ -172,7 +170,6 @@ module TTTLS13
172
170
  # rubocop: enable Metrics/PerceivedComplexity
173
171
  end
174
172
 
175
- # NOTE:
176
173
  # struct {
177
174
  # opaque identity<1..2^16-1>;
178
175
  # uint32 obfuscated_ticket_age;
@@ -9,7 +9,6 @@ module TTTLS13
9
9
  HOST_NAME = "\x00"
10
10
  end
11
11
 
12
- # NOTE:
13
12
  # The extension_data field SHALL be empty when @server_name is empty.
14
13
  # Then, serialized extension_data is
15
14
  #
@@ -5,7 +5,7 @@ module TTTLS13
5
5
  module Message
6
6
  module Extension
7
7
  class SignatureAlgorithmsCert < SignatureAlgorithms
8
- # @param versions [Array of SignatureScheme]
8
+ # @param supported_signature_algorithms [Array] Array of SignatureScheme
9
9
  def initialize(supported_signature_algorithms)
10
10
  super(supported_signature_algorithms)
11
11
  @extension_type = ExtensionType::SIGNATURE_ALGORITHMS_CERT
@@ -5,7 +5,6 @@ module TTTLS13
5
5
  using Refinements
6
6
  module Message
7
7
  module Extension
8
- # NOTE:
9
8
  # Client/Server MUST ignore unrecognized extensions,
10
9
  # but transcript MUST include unrecognized extensions.
11
10
  class UnknownExtension
@@ -21,7 +21,6 @@ module TTTLS13
21
21
 
22
22
  alias super_fetch fetch
23
23
 
24
- # NOTE:
25
24
  # "pre_shared_key" MUST be the last extension in the ClientHello
26
25
  #
27
26
  # @return [String]
@@ -105,10 +104,37 @@ module TTTLS13
105
104
  store(ex.extension_type, ex)
106
105
  end
107
106
 
107
+ # removing and replacing extensions from EncodedClientHelloInner
108
+ # with a single "ech_outer_extensions"
109
+ #
110
+ # for example
111
+ # - before
112
+ # - self.keys: [A B C D E]
113
+ # - param : [D B]
114
+ # - after remove_and_replace!
115
+ # - self.keys: [A C E B D]
116
+ # - return : [A C E ech_outer_extensions[B D]]
117
+ # @param outer_extensions [Array of TTTLS13::Message::ExtensionType]
118
+ #
119
+ # @return [TTTLS13::Message::Extensions] for EncodedClientHelloInner
120
+ def remove_and_replace!(outer_extensions)
121
+ tmp1 = filter { |k, _| !outer_extensions.include?(k) }
122
+ tmp2 = filter { |k, _| outer_extensions.include?(k) }
123
+
124
+ clear
125
+ replaced = Message::Extensions.new
126
+
127
+ tmp1.each_value { |v| self << v; replaced << v }
128
+ tmp2.each_value { |v| self << v }
129
+ replaced << Message::Extension::ECHOuterExtensions.new(tmp2.keys) \
130
+ unless tmp2.keys.empty?
131
+
132
+ replaced
133
+ end
134
+
108
135
  class << self
109
136
  private
110
137
 
111
- # NOTE:
112
138
  # deserialize_extension ignores unparsable extension.
113
139
  # Received unparsable binary, returns nil, doesn't raise
114
140
  # ErrorAlerts :decode_error.
@@ -173,6 +199,8 @@ module TTTLS13
173
199
  else
174
200
  Extension::UnknownExtension.deserialize(binary, extension_type)
175
201
  end
202
+ when ExtensionType::ECH_OUTER_EXTENSIONS
203
+ Extension::ECHOuterExtensions.deserialize(binary)
176
204
  else
177
205
  Extension::UnknownExtension.deserialize(binary, extension_type)
178
206
  end
@@ -27,7 +27,6 @@ module TTTLS13
27
27
  @cipher = cipher
28
28
  end
29
29
 
30
- # NOTE:
31
30
  # serialize joins messages.
32
31
  # If serialize is received Server Parameters(EE, CT, CV),
33
32
  # it returns one binary.
@@ -50,7 +49,6 @@ module TTTLS13
50
49
  end.join
51
50
  end
52
51
 
53
- # NOTE:
54
52
  # If previous Record has surplus_binary,
55
53
  # surplus_binary should is given to Record.deserialize as buffered.
56
54
  #
@@ -74,6 +74,7 @@ module TTTLS13
74
74
  KEY_SHARE = "\x00\x33"
75
75
  # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-11.1
76
76
  ENCRYPTED_CLIENT_HELLO = "\xfe\x0d"
77
+ ECH_OUTER_EXTENSIONS = "\xfd\x00"
77
78
  end
78
79
 
79
80
  DEFINED_EXTENSIONS = ExtensionType.constants.map do |c|
@@ -17,7 +17,6 @@ module TTTLS13
17
17
  # ecdhe_private_use "\xfe\x00" ~ "\xfe\xff"
18
18
 
19
19
  class << self
20
- # NOTE:
21
20
  # For secp256r1, secp384r1, and secp521r1
22
21
  #
23
22
  # struct {
@@ -57,7 +56,6 @@ module TTTLS13
57
56
  end
58
57
  end
59
58
 
60
- # NOTE:
61
59
  # SECG | ANSI X9.62 | NIST
62
60
  # ------------+---------------+-------------
63
61
  # secp256r1 | prime256v1 | NIST P-256
@@ -66,7 +64,7 @@ module TTTLS13
66
64
  #
67
65
  # https://datatracker.ietf.org/doc/html/rfc4492#appendix-A
68
66
  #
69
- # @param groups [Array of TTTLS13::Message::Extension::NamedGroup]
67
+ # @param group [TTTLS13::Message::Extension::NamedGroup]
70
68
  #
71
69
  # @raise [TTTLS13::Error::ErrorAlerts]
72
70
  #
@@ -96,7 +96,6 @@ module TTTLS13
96
96
  end
97
97
  end
98
98
 
99
- # NOTE:
100
99
  # START <-----+
101
100
  # Recv ClientHello | | Send HelloRetryRequest
102
101
  # v |
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TTTLS13
4
- VERSION = '0.3.2'
4
+ VERSION = '0.3.4'
5
5
  end
@@ -37,6 +37,7 @@ RSpec.describe ClientHello do
37
37
  expect(message.legacy_compression_methods).to eq ["\x00"]
38
38
  expect(message.extensions).to be_empty
39
39
  expect(message.negotiated_tls_1_3?).to be false
40
+ expect(message.ch_inner?).to be false
40
41
  end
41
42
 
42
43
  it 'should be serialized' do
@@ -83,4 +84,22 @@ RSpec.describe ClientHello do
83
84
  expect(message.serialize).to eq TESTBINARY_0_RTT_CLIENT_HELLO
84
85
  end
85
86
  end
87
+
88
+ context 'valid inner client_hello' do
89
+ let(:message) do
90
+ cipher_suites = CipherSuites.new([TLS_AES_256_GCM_SHA384,
91
+ TLS_CHACHA20_POLY1305_SHA256,
92
+ TLS_AES_128_GCM_SHA256])
93
+ ch = ClientHello.new(random: OpenSSL::Random.random_bytes(32),
94
+ legacy_session_id: Array.new(32, 0).map(&:chr).join,
95
+ cipher_suites: cipher_suites)
96
+ ch.extensions[Message::ExtensionType::ENCRYPTED_CLIENT_HELLO] \
97
+ = Message::Extension::ECHClientHello.new_inner
98
+ ch
99
+ end
100
+
101
+ it 'should generate ClientHelloInner' do
102
+ expect(message.ch_inner?).to be true
103
+ end
104
+ end
86
105
  end
@@ -0,0 +1,42 @@
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'spec_helper'
5
+ using Refinements
6
+
7
+ RSpec.describe ECHOuterExtensions do
8
+ context 'valid ech_outer_extensions, [key_share]' do
9
+ let(:extension) do
10
+ ECHOuterExtensions.new([ExtensionType::KEY_SHARE])
11
+ end
12
+
13
+ it 'should be generated' do
14
+ expect(extension.extension_type).to eq ExtensionType::ECH_OUTER_EXTENSIONS
15
+ expect(extension.outer_extensions).to eq [ExtensionType::KEY_SHARE]
16
+ end
17
+
18
+ it 'should be serialized' do
19
+ expect(extension.serialize).to eq ExtensionType::ECH_OUTER_EXTENSIONS \
20
+ + 3.to_uint16 \
21
+ + 2.to_uint8 \
22
+ + ExtensionType::KEY_SHARE
23
+ end
24
+ end
25
+
26
+ context 'valid ech_outer_extensions binary' do
27
+ let(:extension) do
28
+ ECHOuterExtensions.deserialize(TESTBINARY_ECH_OUTER_EXTENSIONS)
29
+ end
30
+
31
+ it 'should generate valid object' do
32
+ expect(extension.extension_type).to be ExtensionType::ECH_OUTER_EXTENSIONS
33
+ expect(extension.outer_extensions).to eq [ExtensionType::KEY_SHARE]
34
+ end
35
+
36
+ it 'should generate serializable object' do
37
+ expect(extension.serialize)
38
+ .to eq ExtensionType::ECH_OUTER_EXTENSIONS \
39
+ + TESTBINARY_ECH_OUTER_EXTENSIONS.prefix_uint16_length
40
+ end
41
+ end
42
+ end
data/spec/ech_spec.rb CHANGED
@@ -78,7 +78,9 @@ RSpec.describe ECHClientHello do
78
78
  expect(extension.confirmation).to eq "\x00" * 8
79
79
  end
80
80
  end
81
+ end
81
82
 
83
+ RSpec.describe Ech do
82
84
  context 'EncodedClientHelloInner length' do
83
85
  let(:server_name) do
84
86
  'localhost'
@@ -182,4 +182,69 @@ RSpec.describe Extensions do
182
182
  .to raise_error(ErrorAlerts)
183
183
  end
184
184
  end
185
+
186
+ context 'removing and replacing extensions from EncodedClientHelloInner' do
187
+ let(:extensions) do
188
+ extensions, = Client.new(nil, 'localhost').send(:gen_ch_extensions)
189
+ extensions
190
+ end
191
+
192
+ let(:no_key_share_exs) do
193
+ Extensions.new(
194
+ extensions.filter { |k, _| k != ExtensionType::KEY_SHARE }.values
195
+ )
196
+ end
197
+
198
+ it 'should be equal remove_and_replace! with []' do
199
+ expected = extensions.clone
200
+ got = extensions.remove_and_replace!([])
201
+
202
+ expect(got.keys).to eq expected.keys
203
+ expect(got[ExtensionType::ECH_OUTER_EXTENSIONS]).to eq nil
204
+ expect(extensions.keys - got.keys).to eq []
205
+ end
206
+
207
+ it 'should be equal remove_and_replace! with [key_share]' do
208
+ expected = extensions.filter { |k, _| k != ExtensionType::KEY_SHARE }
209
+ expected[ExtensionType::ECH_OUTER_EXTENSIONS] = \
210
+ Extension::ECHOuterExtensions.new([ExtensionType::KEY_SHARE])
211
+ got = extensions.remove_and_replace!([ExtensionType::KEY_SHARE])
212
+
213
+ expect(got.keys).to eq expected.keys
214
+ expect(got[ExtensionType::ECH_OUTER_EXTENSIONS].outer_extensions)
215
+ .to eq expected[ExtensionType::ECH_OUTER_EXTENSIONS].outer_extensions
216
+ expect(extensions.keys - got.keys)
217
+ .to eq expected[ExtensionType::ECH_OUTER_EXTENSIONS].outer_extensions
218
+ end
219
+
220
+ it 'should be equal remove_and_replace! with' \
221
+ ' [key_share,supported_versions]' do
222
+ outer_extensions = [
223
+ ExtensionType::KEY_SHARE,
224
+ ExtensionType::SUPPORTED_VERSIONS
225
+ ]
226
+ expected = extensions.filter { |k, _| !outer_extensions.include?(k) }
227
+ expected[ExtensionType::ECH_OUTER_EXTENSIONS] = \
228
+ Extension::ECHOuterExtensions.new(
229
+ extensions.filter { |k, _| outer_extensions.include?(k) }.keys
230
+ )
231
+ got = extensions.remove_and_replace!(outer_extensions)
232
+
233
+ expect(got.keys).to eq expected.keys
234
+ expect(got[ExtensionType::ECH_OUTER_EXTENSIONS].outer_extensions)
235
+ .to eq expected[ExtensionType::ECH_OUTER_EXTENSIONS].outer_extensions
236
+ expect(extensions.keys - got.keys)
237
+ .to eq expected[ExtensionType::ECH_OUTER_EXTENSIONS].outer_extensions
238
+ end
239
+
240
+ it 'should be equal remove_and_replace! with no key_share extensions' \
241
+ ' & [key_share]' do
242
+ expected = no_key_share_exs.clone
243
+ got = no_key_share_exs.remove_and_replace!([ExtensionType::KEY_SHARE])
244
+
245
+ expect(got).to eq expected
246
+ expect(got[ExtensionType::ECH_OUTER_EXTENSIONS]).to eq nil
247
+ expect(no_key_share_exs.keys - got.keys).to eq []
248
+ end
249
+ end
185
250
  end
data/spec/spec_helper.rb CHANGED
@@ -245,6 +245,10 @@ TESTBINARY_ECH_HRR = <<BIN.split.map(&:hex).map(&:chr).join
245
245
  00 00 00 00 00 00 00 00
246
246
  BIN
247
247
 
248
+ TESTBINARY_ECH_OUTER_EXTENSIONS = <<BIN.split.map(&:hex).map(&:chr).join
249
+ 02 00 33
250
+ BIN
251
+
248
252
  # https://datatracker.ietf.org/doc/html/rfc8448#section-3
249
253
  # 3. Simple 1-RTT Handshake
250
254
  TESTBINARY_CLIENT_HELLO = <<BIN.split.map(&:hex).map(&:chr).join
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tttls1.3
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.3.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - thekuwayama
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-03-14 00:00:00.000000000 Z
11
+ date: 2024-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -108,6 +108,7 @@ files:
108
108
  - example/https_client_using_hrr_and_ticket.rb
109
109
  - example/https_client_using_status_request.rb
110
110
  - example/https_client_using_ticket.rb
111
+ - example/https_client_using_ticket_and_ech.rb
111
112
  - example/https_server.rb
112
113
  - interop/client_spec.rb
113
114
  - interop/server_spec.rb
@@ -139,6 +140,7 @@ files:
139
140
  - lib/tttls1.3/message/extension/cookie.rb
140
141
  - lib/tttls1.3/message/extension/early_data_indication.rb
141
142
  - lib/tttls1.3/message/extension/ech.rb
143
+ - lib/tttls1.3/message/extension/ech_outer_extensions.rb
142
144
  - lib/tttls1.3/message/extension/key_share.rb
143
145
  - lib/tttls1.3/message/extension/pre_shared_key.rb
144
146
  - lib/tttls1.3/message/extension/psk_key_exchange_modes.rb
@@ -176,6 +178,7 @@ files:
176
178
  - spec/compress_certificate_spec.rb
177
179
  - spec/cookie_spec.rb
178
180
  - spec/early_data_indication_spec.rb
181
+ - spec/ech_outer_extensions_spec.rb
179
182
  - spec/ech_spec.rb
180
183
  - spec/encrypted_extensions_spec.rb
181
184
  - spec/end_of_early_data_spec.rb
@@ -254,6 +257,7 @@ test_files:
254
257
  - spec/compress_certificate_spec.rb
255
258
  - spec/cookie_spec.rb
256
259
  - spec/early_data_indication_spec.rb
260
+ - spec/ech_outer_extensions_spec.rb
257
261
  - spec/ech_spec.rb
258
262
  - spec/encrypted_extensions_spec.rb
259
263
  - spec/end_of_early_data_spec.rb