tttls1.3 0.3.2 → 0.3.3

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