tttls1.3 0.3.2 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
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