tttls1.3 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7245602faa9087e83b3e47484aa88dc368b153e98c76fd00e36ebb1a863af6ef
4
- data.tar.gz: c7362f39fc26763f712cdf61a29b3796686c50034d2a6431788d8dbf7dd505c9
3
+ metadata.gz: fa5d7f448e0c984d75be22a995278853b4d4b0c505aaaa92fc5e99f48371fc82
4
+ data.tar.gz: 4a0d95465cabfd048ea1d7190c1c617dcc26a5395e06f7acc0173fcbbfe0bd04
5
5
  SHA512:
6
- metadata.gz: 33168ef27007f9e6d73197ed3e1bae9b58dadfdf3042e732ddc6a05913db0aad48fb653ea7b7222716d70ebe42739f2d9dddee0ec75ee45eb33daba4be6f0229
7
- data.tar.gz: d05c73a49e0f5d568785c3b6af3d9bc3c286b35f1110c5f31860f8cf788a9f9e019112fd8647394672c279808e20c4f02d438a58c44f5bab9af1d5fb851ef41f
6
+ metadata.gz: 4035648fa81ae715315e1efbddd9b0b0506395180d0c7e994d6c8dd49d714e0754717b0cfa93bee702b4ef5a4d8e29bb765fae6045fdb7b24a3e25a1098eca38
7
+ data.tar.gz: 3c9d5286f1724b4998325ab811bf2e6ce44dbbbebb3ffb3c6c01ffe76f2d730797a599196b1ec6c3a894265b16ed0b12a78f2eeca53a36b52980de85db76f247
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/example/helper.rb CHANGED
@@ -7,6 +7,7 @@ require 'time'
7
7
  require 'uri'
8
8
  require 'webrick'
9
9
 
10
+ require 'ech_config'
10
11
  require 'http/parser'
11
12
  require 'svcb_rr_patch'
12
13
 
@@ -83,3 +84,24 @@ def transcript_htmlize(transcript)
83
84
  format(m[k], TTTLS13::Convert.obj2html(v.first))
84
85
  end.join('<br>')
85
86
  end
87
+
88
+ def parse_echconfigs_pem(pem)
89
+ s = pem.gsub(/-----(BEGIN|END) ECH CONFIGS-----/, '')
90
+ .gsub("\n", '')
91
+ b = Base64.decode64(s)
92
+ raise 'failed to parse ECHConfigs' \
93
+ unless b.length == b.slice(0, 2).unpack1('n') + 2
94
+
95
+ ECHConfig.decode_vectors(b.slice(2..))
96
+ end
97
+
98
+ def resolve_echconfig(hostname)
99
+ rr = Resolv::DNS.new.getresources(
100
+ hostname,
101
+ Resolv::DNS::Resource::IN::HTTPS
102
+ )
103
+ raise "failed to resolve echconfig via #{hostname} HTTPS RR" \
104
+ if rr.first.nil? || !rr.first.svc_params.keys.include?('ech')
105
+
106
+ rr.first.svc_params['ech'].echconfiglist.first
107
+ 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
data/lib/tttls1.3/ech.rb CHANGED
@@ -7,8 +7,13 @@ 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]
@@ -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
 
@@ -104,9 +112,14 @@ module TTTLS13
104
112
  # @return [TTTLS13::Message::ClientHello]
105
113
  # @return [TTTLS13::Message::ClientHello]
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
@@ -26,12 +26,12 @@ module TTTLS13
26
26
  # };
27
27
  # } ECHClientHello;
28
28
  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
29
+ attr_reader :extension_type
30
+ attr_reader :type
31
+ attr_reader :cipher_suite
32
+ attr_reader :config_id
33
+ attr_reader :enc
34
+ attr_reader :payload
35
35
 
36
36
  # @param type [TTTLS13::Message::Extension::ECHClientHelloType]
37
37
  # @param cipher_suite [HpkeSymmetricCipherSuite]
@@ -99,10 +99,10 @@ module TTTLS13
99
99
  # @raise [TTTLS13::Error::ErrorAlerts]
100
100
  #
101
101
  # @return [TTTLS13::Message::Extensions::ECHClientHello]
102
- # rubocop: disable Metrics/AbcSize
103
102
  def deserialize_outer_ech(binary)
104
103
  raise Error::ErrorAlerts, :internal_error if binary.nil?
105
- raise Error::ErrorAlerts, :decode_error if binary.length < 5
104
+
105
+ return nil if binary.length < 5
106
106
 
107
107
  kdf_id = \
108
108
  HpkeSymmetricCipherSuite::HpkeKdfId.decode(binary.slice(0, 2))
@@ -112,17 +112,14 @@ module TTTLS13
112
112
  cid = Convert.bin2i(binary.slice(4, 1))
113
113
  enc_len = Convert.bin2i(binary.slice(5, 2))
114
114
  i = 7
115
- raise Error::ErrorAlerts, :decode_error \
116
- if i + enc_len > binary.length
115
+ return nil if i + enc_len > binary.length
117
116
 
118
117
  enc = binary.slice(i, enc_len)
119
118
  i += enc_len
120
- raise Error::ErrorAlerts, :decode_error \
121
- if i + 2 > binary.length
119
+ return nil if i + 2 > binary.length
122
120
 
123
121
  payload_len = Convert.bin2i(binary.slice(i, 2))
124
- raise Error::ErrorAlerts, :decode_error \
125
- if i + payload_len > binary.length
122
+ return nil if i + payload_len > binary.length
126
123
 
127
124
  payload = binary.slice(i, payload_len)
128
125
  ECHClientHello.new(
@@ -133,7 +130,6 @@ module TTTLS13
133
130
  payload: payload
134
131
  )
135
132
  end
136
- # rubocop: enable Metrics/AbcSize
137
133
 
138
134
  # @param binary [String]
139
135
  #
@@ -174,8 +170,8 @@ module TTTLS13
174
170
  # ECHConfigList retry_configs;
175
171
  # } ECHEncryptedExtensions;
176
172
  class ECHEncryptedExtensions
177
- attr_accessor :extension_type
178
- attr_accessor :retry_configs
173
+ attr_reader :extension_type
174
+ attr_reader :retry_configs
179
175
 
180
176
  # @param retry_configs [Array of ECHConfig]
181
177
  def initialize(retry_configs)
@@ -198,8 +194,7 @@ module TTTLS13
198
194
  # @return [TTTLS13::Message::Extensions::ECHEncryptedExtensions]
199
195
  def self.deserialize(binary)
200
196
  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
197
+ return nil if binary.length != binary.slice(0, 2).unpack1('n') + 2
203
198
 
204
199
  ECHEncryptedExtensions.new(
205
200
  ECHConfig.decode_vectors(binary.slice(2..))
@@ -212,8 +207,8 @@ module TTTLS13
212
207
  # opaque confirmation[8];
213
208
  # } ECHHelloRetryRequest;
214
209
  class ECHHelloRetryRequest
215
- attr_accessor :extension_type
216
- attr_accessor :confirmation
210
+ attr_reader :extension_type
211
+ attr_reader :confirmation
217
212
 
218
213
  # @param confirmation [String]
219
214
  def initialize(confirmation)
@@ -233,7 +228,7 @@ module TTTLS13
233
228
  # @return [TTTLS13::Message::Extensions::ECHHelloRetryRequest]
234
229
  def self.deserialize(binary)
235
230
  raise Error::ErrorAlerts, :internal_error if binary.nil?
236
- raise Error::ErrorAlerts, :decode_error if binary.length != 8
231
+ return nil if binary.length != 8
237
232
 
238
233
  ECHHelloRetryRequest.new(binary)
239
234
  end
@@ -0,0 +1,52 @@
1
+ # encoding: ascii-8bit
2
+ # frozen_string_literal: true
3
+
4
+ module TTTLS13
5
+ using Refinements
6
+ module Message
7
+ module Extension
8
+ # NOTE:
9
+ # ExtensionType OuterExtensions<2..254>;
10
+ class ECHOuterExtensions
11
+ attr_reader :extension_type
12
+ attr_reader :outer_extensions
13
+
14
+ # @param outer_extensions [Array of TTTLS13::Message::ExtensionType]
15
+ def initialize(outer_extensions)
16
+ @extension_type = ExtensionType::ECH_OUTER_EXTENSIONS
17
+ @outer_extensions = outer_extensions
18
+ end
19
+
20
+ # @raise [TTTLS13::Error::ErrorAlerts]
21
+ #
22
+ # @return [String]
23
+ def serialize
24
+ binary = @outer_extensions.join.prefix_uint8_length
25
+ @extension_type + binary.prefix_uint16_length
26
+ end
27
+
28
+ # @param binary [String]
29
+ #
30
+ # @raise [TTTLS13::Error::ErrorAlerts]
31
+ #
32
+ # @return [TTTLS13::Message::Extensions::ECHOuterExtensions]
33
+ def self.deserialize(binary)
34
+ raise Error::ErrorAlerts, :internal_error if binary.nil?
35
+
36
+ return nil if binary.length < 2
37
+
38
+ exlist_len = Convert.bin2i(binary.slice(0, 1))
39
+ i = 1
40
+ outer_extensions = []
41
+ while i < exlist_len + 1
42
+ outer_extensions << binary.slice(i, 2)
43
+ i += 2
44
+ end
45
+ return nil unless outer_extensions.length * 2 == exlist_len
46
+
47
+ ECHOuterExtensions.new(outer_extensions)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -105,6 +105,34 @@ module TTTLS13
105
105
  store(ex.extension_type, ex)
106
106
  end
107
107
 
108
+ # removing and replacing extensions from EncodedClientHelloInner
109
+ # with a single "ech_outer_extensions"
110
+ #
111
+ # for example
112
+ # - before
113
+ # - self.keys: [A B C D E]
114
+ # - param : [D B]
115
+ # - after remove_and_replace!
116
+ # - self.keys: [A C E B D]
117
+ # - return : [A C E ech_outer_extensions[B D]]
118
+ # @param outer_extensions [Array of TTTLS13::Message::ExtensionType]
119
+ #
120
+ # @return [TTTLS13::Message::Extensions] for EncodedClientHelloInner
121
+ def remove_and_replace!(outer_extensions)
122
+ tmp1 = filter { |k, _| !outer_extensions.include?(k) }
123
+ tmp2 = filter { |k, _| outer_extensions.include?(k) }
124
+
125
+ clear
126
+ replaced = Message::Extensions.new
127
+
128
+ tmp1.each_value { |v| self << v; replaced << v }
129
+ tmp2.each_value { |v| self << v }
130
+ replaced << Message::Extension::ECHOuterExtensions.new(tmp2.keys) \
131
+ unless tmp2.keys.empty?
132
+
133
+ replaced
134
+ end
135
+
108
136
  class << self
109
137
  private
110
138
 
@@ -173,6 +201,8 @@ module TTTLS13
173
201
  else
174
202
  Extension::UnknownExtension.deserialize(binary, extension_type)
175
203
  end
204
+ when ExtensionType::ECH_OUTER_EXTENSIONS
205
+ Extension::ECHOuterExtensions.deserialize(binary)
176
206
  else
177
207
  Extension::UnknownExtension.deserialize(binary, extension_type)
178
208
  end
@@ -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|
@@ -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.3'
5
5
  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.3
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-04-19 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