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 +4 -4
- data/.rubocop.yml +3 -0
- data/example/helper.rb +22 -0
- data/example/https_client_using_0rtt.rb +4 -2
- data/example/https_client_using_ech.rb +6 -7
- data/example/https_client_using_grease_ech.rb +0 -2
- data/example/https_client_using_hrr.rb +2 -1
- data/example/https_client_using_hrr_and_ech.rb +6 -7
- data/example/https_client_using_hrr_and_ticket.rb +4 -2
- data/example/https_client_using_status_request.rb +2 -1
- data/example/https_client_using_ticket.rb +4 -2
- data/example/https_client_using_ticket_and_ech.rb +57 -0
- data/lib/tttls1.3/ech.rb +35 -19
- data/lib/tttls1.3/message/extension/ech.rb +17 -22
- data/lib/tttls1.3/message/extension/ech_outer_extensions.rb +52 -0
- data/lib/tttls1.3/message/extensions.rb +30 -0
- data/lib/tttls1.3/message.rb +1 -0
- data/lib/tttls1.3/version.rb +1 -1
- data/spec/ech_outer_extensions_spec.rb +42 -0
- data/spec/ech_spec.rb +2 -0
- data/spec/extensions_spec.rb +65 -0
- data/spec/spec_helper.rb +4 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa5d7f448e0c984d75be22a995278853b4d4b0c505aaaa92fc5e99f48371fc82
|
4
|
+
data.tar.gz: 4a0d95465cabfd048ea1d7190c1c617dcc26a5395e06f7acc0173fcbbfe0bd04
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4035648fa81ae715315e1efbddd9b0b0506395180d0c7e994d6c8dd49d714e0754717b0cfa93bee702b4ef5a4d8e29bb765fae6045fdb7b24a3e25a1098eca38
|
7
|
+
data.tar.gz: 3c9d5286f1724b4998325ab811bf2e6ce44dbbbebb3ffb3c6c01ffe76f2d730797a599196b1ec6c3a894265b16ed0b12a78f2eeca53a36b52980de85db76f247
|
data/.rubocop.yml
CHANGED
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:
|
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'
|
@@ -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:
|
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
|
-
|
11
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
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:
|
164
|
+
extensions: replaced
|
151
165
|
)
|
152
166
|
server_name_length = \
|
153
|
-
|
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 [
|
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/
|
425
|
+
# rubocop: enable Metrics/ClassLength
|
410
426
|
end
|
@@ -26,12 +26,12 @@ module TTTLS13
|
|
26
26
|
# };
|
27
27
|
# } ECHClientHello;
|
28
28
|
class ECHClientHello
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
178
|
-
|
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
|
-
|
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
|
-
|
216
|
-
|
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
|
-
|
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
|
data/lib/tttls1.3/message.rb
CHANGED
@@ -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|
|
data/lib/tttls1.3/version.rb
CHANGED
@@ -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
data/spec/extensions_spec.rb
CHANGED
@@ -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.
|
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-
|
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
|