tttls1.3 0.3.0 → 0.3.2
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/.github/workflows/ci.yml +2 -2
- data/.rubocop.yml +3 -0
- data/.ruby-version +1 -1
- data/Gemfile +1 -0
- data/README.md +2 -2
- data/example/README.md +1 -1
- data/example/helper.rb +22 -0
- data/example/https_client.rb +4 -4
- data/example/https_client_using_0rtt.rb +6 -5
- data/example/https_client_using_ech.rb +5 -6
- data/example/https_client_using_grease_ech.rb +3 -5
- data/example/https_client_using_grease_psk.rb +8 -16
- data/example/https_client_using_hrr.rb +5 -4
- data/example/https_client_using_hrr_and_ech.rb +6 -6
- data/example/https_client_using_hrr_and_ticket.rb +5 -4
- data/example/https_client_using_status_request.rb +4 -5
- data/example/https_client_using_ticket.rb +5 -4
- data/example/https_server.rb +14 -1
- data/lib/tttls1.3/client.rb +205 -418
- data/lib/tttls1.3/connection.rb +21 -362
- data/lib/tttls1.3/ech.rb +410 -0
- data/lib/tttls1.3/endpoint.rb +276 -0
- data/lib/tttls1.3/message/certificate_verify.rb +1 -1
- data/lib/tttls1.3/message/extension/ech.rb +12 -10
- data/lib/tttls1.3/message/extension/signature_algorithms.rb +2 -2
- data/lib/tttls1.3/message/extension/supported_versions.rb +3 -3
- data/lib/tttls1.3/message/extension/unknown_extension.rb +2 -2
- data/lib/tttls1.3/server.rb +125 -63
- data/lib/tttls1.3/utils.rb +37 -0
- data/lib/tttls1.3/version.rb +1 -1
- data/lib/tttls1.3.rb +2 -1
- data/spec/client_spec.rb +21 -60
- data/spec/ech_spec.rb +39 -0
- data/spec/{connection_spec.rb → endpoint_spec.rb} +41 -49
- data/spec/server_spec.rb +12 -12
- data/tttls1.3.gemspec +1 -1
- metadata +8 -7
- data/lib/tttls1.3/hpke.rb +0 -91
data/lib/tttls1.3/ech.rb
ADDED
@@ -0,0 +1,410 @@
|
|
1
|
+
# encoding: ascii-8bit
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module TTTLS13
|
5
|
+
using Refinements
|
6
|
+
|
7
|
+
SUPPORTED_ECHCONFIG_VERSIONS = ["\xfe\x0d"].freeze
|
8
|
+
private_constant :SUPPORTED_ECHCONFIG_VERSIONS
|
9
|
+
|
10
|
+
# rubocop: disable Metrics/ModuleLength
|
11
|
+
module Ech
|
12
|
+
# @param inner [TTTLS13::Message::ClientHello]
|
13
|
+
# @param ech_config [ECHConfig]
|
14
|
+
# @param hpke_cipher_suite_selector [Method]
|
15
|
+
#
|
16
|
+
# @return [TTTLS13::Message::ClientHello]
|
17
|
+
# @return [TTTLS13::Message::ClientHello]
|
18
|
+
# @return [TTTLS13::EchState]
|
19
|
+
# rubocop: disable Metrics/AbcSize
|
20
|
+
def self.offer_ech(inner, ech_config, hpke_cipher_suite_selector)
|
21
|
+
return [new_greased_ch(inner, new_grease_ech), nil, nil] \
|
22
|
+
if ech_config.nil? ||
|
23
|
+
!SUPPORTED_ECHCONFIG_VERSIONS.include?(ech_config.version)
|
24
|
+
|
25
|
+
# Encrypted ClientHello Configuration
|
26
|
+
ech_state, enc = encrypted_ech_config(
|
27
|
+
ech_config,
|
28
|
+
hpke_cipher_suite_selector
|
29
|
+
)
|
30
|
+
return [new_greased_ch(inner, new_grease_ech), nil, nil] \
|
31
|
+
if ech_state.nil? || enc.nil?
|
32
|
+
|
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
|
+
)
|
37
|
+
|
38
|
+
# Encoding the ClientHelloInner
|
39
|
+
aad = new_ch_outer_aad(
|
40
|
+
inner,
|
41
|
+
ech_state.cipher_suite,
|
42
|
+
ech_state.config_id,
|
43
|
+
enc,
|
44
|
+
encoded.length + overhead_len,
|
45
|
+
ech_state.public_name
|
46
|
+
)
|
47
|
+
# Authenticating the ClientHelloOuter
|
48
|
+
# which does not include the Handshake structure's four byte header.
|
49
|
+
outer = new_ch_outer(
|
50
|
+
aad,
|
51
|
+
ech_state.cipher_suite,
|
52
|
+
ech_state.config_id,
|
53
|
+
enc,
|
54
|
+
ech_state.ctx.seal(aad.serialize[4..], encoded)
|
55
|
+
)
|
56
|
+
|
57
|
+
[outer, inner, ech_state]
|
58
|
+
end
|
59
|
+
# rubocop: enable Metrics/AbcSize
|
60
|
+
|
61
|
+
# @param ech_config [ECHConfig]
|
62
|
+
# @param hpke_cipher_suite_selector [Method]
|
63
|
+
#
|
64
|
+
# @return [TTTLS13::EchState or nil]
|
65
|
+
# @return [String or nil]
|
66
|
+
# rubocop: disable Metrics/AbcSize
|
67
|
+
def self.encrypted_ech_config(ech_config, hpke_cipher_suite_selector)
|
68
|
+
public_name = ech_config.echconfig_contents.public_name
|
69
|
+
key_config = ech_config.echconfig_contents.key_config
|
70
|
+
public_key = key_config.public_key.opaque
|
71
|
+
kem_id = key_config&.kem_id&.uint16
|
72
|
+
config_id = key_config.config_id
|
73
|
+
cipher_suite = hpke_cipher_suite_selector.call(key_config)
|
74
|
+
aead_cipher = aead_id2aead_cipher(cipher_suite&.aead_id&.uint16)
|
75
|
+
kdf_hash = kdf_id2kdf_hash(cipher_suite&.kdf_id&.uint16)
|
76
|
+
return [nil, nil] \
|
77
|
+
if [kem_id, aead_cipher, kdf_hash].any?(&:nil?)
|
78
|
+
|
79
|
+
kem_curve_name, kem_hash = kem_id2dhkem(kem_id)
|
80
|
+
dhkem = kem_curve_name2dhkem(kem_curve_name)
|
81
|
+
pkr = dhkem&.new(kem_hash)&.deserialize_public_key(public_key)
|
82
|
+
return [nil, nil] if pkr.nil?
|
83
|
+
|
84
|
+
hpke = HPKE.new(kem_curve_name, kem_hash, kdf_hash, aead_cipher)
|
85
|
+
base_s = hpke.setup_base_s(pkr, "tls ech\x00" + ech_config.encode)
|
86
|
+
enc = base_s[:enc]
|
87
|
+
ctx = base_s[:context_s]
|
88
|
+
mnl = ech_config.echconfig_contents.maximum_name_length
|
89
|
+
ech_state = EchState.new(
|
90
|
+
mnl,
|
91
|
+
config_id,
|
92
|
+
cipher_suite,
|
93
|
+
public_name,
|
94
|
+
ctx
|
95
|
+
)
|
96
|
+
|
97
|
+
[ech_state, enc]
|
98
|
+
end
|
99
|
+
# rubocop: enable Metrics/AbcSize
|
100
|
+
|
101
|
+
# @param inner [TTTLS13::Message::ClientHello]
|
102
|
+
# @param ech_state [TTTLS13::EchState]
|
103
|
+
#
|
104
|
+
# @return [TTTLS13::Message::ClientHello]
|
105
|
+
# @return [TTTLS13::Message::ClientHello]
|
106
|
+
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)
|
110
|
+
|
111
|
+
# It encrypts EncodedClientHelloInner as described in Section 6.1.1, using
|
112
|
+
# the second partial ClientHelloOuterAAD, to obtain a second
|
113
|
+
# ClientHelloOuter. It reuses the original HPKE encryption context
|
114
|
+
# computed in Section 6.1 and uses the empty string for enc.
|
115
|
+
#
|
116
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.1.5-4.4.1
|
117
|
+
aad = new_ch_outer_aad(
|
118
|
+
inner,
|
119
|
+
ech_state.cipher_suite,
|
120
|
+
ech_state.config_id,
|
121
|
+
'',
|
122
|
+
encoded.length + overhead_len,
|
123
|
+
ech_state.public_name
|
124
|
+
)
|
125
|
+
# Authenticating the ClientHelloOuter
|
126
|
+
# which does not include the Handshake structure's four byte header.
|
127
|
+
outer = new_ch_outer(
|
128
|
+
aad,
|
129
|
+
ech_state.cipher_suite,
|
130
|
+
ech_state.config_id,
|
131
|
+
'',
|
132
|
+
ech_state.ctx.seal(aad.serialize[4..], encoded)
|
133
|
+
)
|
134
|
+
|
135
|
+
[outer, inner]
|
136
|
+
end
|
137
|
+
|
138
|
+
# @param inner [TTTLS13::Message::ClientHello]
|
139
|
+
# @param maximum_name_length [Integer]
|
140
|
+
#
|
141
|
+
# @return [String] EncodedClientHelloInner
|
142
|
+
def self.encode_ch_inner(inner, maximum_name_length)
|
143
|
+
# TODO: ech_outer_extensions
|
144
|
+
encoded = Message::ClientHello.new(
|
145
|
+
legacy_version: inner.legacy_version,
|
146
|
+
random: inner.random,
|
147
|
+
legacy_session_id: '',
|
148
|
+
cipher_suites: inner.cipher_suites,
|
149
|
+
legacy_compression_methods: inner.legacy_compression_methods,
|
150
|
+
extensions: inner.extensions
|
151
|
+
)
|
152
|
+
server_name_length = \
|
153
|
+
inner.extensions[Message::ExtensionType::SERVER_NAME].server_name.length
|
154
|
+
|
155
|
+
# which does not include the Handshake structure's four byte header.
|
156
|
+
padding_encoded_ch_inner(
|
157
|
+
encoded.serialize[4..],
|
158
|
+
server_name_length,
|
159
|
+
maximum_name_length
|
160
|
+
)
|
161
|
+
end
|
162
|
+
|
163
|
+
# @param s [String]
|
164
|
+
# @param server_name_length [Integer]
|
165
|
+
# @param maximum_name_length [Integer]
|
166
|
+
#
|
167
|
+
# @return [String]
|
168
|
+
def self.padding_encoded_ch_inner(s,
|
169
|
+
server_name_length,
|
170
|
+
maximum_name_length)
|
171
|
+
padding_len =
|
172
|
+
if server_name_length.positive?
|
173
|
+
[maximum_name_length - server_name_length, 0].max
|
174
|
+
else
|
175
|
+
9 + maximum_name_length
|
176
|
+
end
|
177
|
+
|
178
|
+
padding_len = 31 - ((s.length + padding_len - 1) % 32)
|
179
|
+
s + padding_len.zeros
|
180
|
+
end
|
181
|
+
|
182
|
+
# @param inner [TTTLS13::Message::ClientHello]
|
183
|
+
# @param cipher_suite [HpkeSymmetricCipherSuite]
|
184
|
+
# @param config_id [Integer]
|
185
|
+
# @param enc [String]
|
186
|
+
# @param payload_len [Integer]
|
187
|
+
# @param server_name [String]
|
188
|
+
#
|
189
|
+
# @return [TTTLS13::Message::ClientHello]
|
190
|
+
# rubocop: disable Metrics/ParameterLists
|
191
|
+
def self.new_ch_outer_aad(inner,
|
192
|
+
cipher_suite,
|
193
|
+
config_id,
|
194
|
+
enc,
|
195
|
+
payload_len,
|
196
|
+
server_name)
|
197
|
+
aad_ech = Message::Extension::ECHClientHello.new_outer(
|
198
|
+
cipher_suite: cipher_suite,
|
199
|
+
config_id: config_id,
|
200
|
+
enc: enc,
|
201
|
+
payload: payload_len.zeros
|
202
|
+
)
|
203
|
+
Message::ClientHello.new(
|
204
|
+
legacy_version: inner.legacy_version,
|
205
|
+
legacy_session_id: inner.legacy_session_id,
|
206
|
+
cipher_suites: inner.cipher_suites,
|
207
|
+
legacy_compression_methods: inner.legacy_compression_methods,
|
208
|
+
extensions: inner.extensions.merge(
|
209
|
+
Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => aad_ech,
|
210
|
+
Message::ExtensionType::SERVER_NAME => \
|
211
|
+
Message::Extension::ServerName.new(server_name)
|
212
|
+
)
|
213
|
+
)
|
214
|
+
end
|
215
|
+
# rubocop: enable Metrics/ParameterLists
|
216
|
+
|
217
|
+
# @param aad [TTTLS13::Message::ClientHello]
|
218
|
+
# @param cipher_suite [HpkeSymmetricCipherSuite]
|
219
|
+
# @param config_id [Integer]
|
220
|
+
# @param enc [String]
|
221
|
+
# @param payload [String]
|
222
|
+
#
|
223
|
+
# @return [TTTLS13::Message::ClientHello]
|
224
|
+
def self.new_ch_outer(aad, cipher_suite, config_id, enc, payload)
|
225
|
+
outer_ech = Message::Extension::ECHClientHello.new_outer(
|
226
|
+
cipher_suite: cipher_suite,
|
227
|
+
config_id: config_id,
|
228
|
+
enc: enc,
|
229
|
+
payload: payload
|
230
|
+
)
|
231
|
+
Message::ClientHello.new(
|
232
|
+
legacy_version: aad.legacy_version,
|
233
|
+
random: aad.random,
|
234
|
+
legacy_session_id: aad.legacy_session_id,
|
235
|
+
cipher_suites: aad.cipher_suites,
|
236
|
+
legacy_compression_methods: aad.legacy_compression_methods,
|
237
|
+
extensions: aad.extensions.merge(
|
238
|
+
Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => outer_ech
|
239
|
+
)
|
240
|
+
)
|
241
|
+
end
|
242
|
+
|
243
|
+
# @return [Message::Extension::ECHClientHello]
|
244
|
+
def self.new_grease_ech
|
245
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#name-compliance-requirements
|
246
|
+
cipher_suite = HpkeSymmetricCipherSuite.new(
|
247
|
+
HpkeSymmetricCipherSuite::HpkeKdfId.new(
|
248
|
+
KdfId::HKDF_SHA256
|
249
|
+
),
|
250
|
+
HpkeSymmetricCipherSuite::HpkeAeadId.new(
|
251
|
+
AeadId::AES_128_GCM
|
252
|
+
)
|
253
|
+
)
|
254
|
+
# Set the enc field to a randomly-generated valid encapsulated public key
|
255
|
+
# output by the HPKE KEM.
|
256
|
+
#
|
257
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.2-2.3.1
|
258
|
+
public_key = OpenSSL::PKey.read(
|
259
|
+
OpenSSL::PKey.generate_key('X25519').public_to_pem
|
260
|
+
)
|
261
|
+
hpke = HPKE.new(:x25519, :sha256, :sha256, :aes_128_gcm)
|
262
|
+
enc = hpke.setup_base_s(public_key, '')[:enc]
|
263
|
+
# Set the payload field to a randomly-generated string of L+C bytes, where
|
264
|
+
# C is the ciphertext expansion of the selected AEAD scheme and L is the
|
265
|
+
# size of the EncodedClientHelloInner the client would compute when
|
266
|
+
# offering ECH, padded according to Section 6.1.3.
|
267
|
+
#
|
268
|
+
# https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.2-2.4.1
|
269
|
+
payload_len = placeholder_encoded_ch_inner_len \
|
270
|
+
+ aead_id2overhead_len(AeadId::AES_128_GCM)
|
271
|
+
|
272
|
+
Message::Extension::ECHClientHello.new_outer(
|
273
|
+
cipher_suite: cipher_suite,
|
274
|
+
config_id: Convert.bin2i(OpenSSL::Random.random_bytes(1)),
|
275
|
+
enc: enc,
|
276
|
+
payload: OpenSSL::Random.random_bytes(payload_len)
|
277
|
+
)
|
278
|
+
end
|
279
|
+
|
280
|
+
# @return [Integer]
|
281
|
+
def self.placeholder_encoded_ch_inner_len
|
282
|
+
448
|
283
|
+
end
|
284
|
+
|
285
|
+
# @param inner [TTTLS13::Message::ClientHello]
|
286
|
+
# @param ech [Message::Extension::ECHClientHello]
|
287
|
+
def self.new_greased_ch(inner, ech)
|
288
|
+
Message::ClientHello.new(
|
289
|
+
legacy_version: inner.legacy_version,
|
290
|
+
random: inner.random,
|
291
|
+
legacy_session_id: inner.legacy_session_id,
|
292
|
+
cipher_suites: inner.cipher_suites,
|
293
|
+
legacy_compression_methods: inner.legacy_compression_methods,
|
294
|
+
extensions: inner.extensions.merge(
|
295
|
+
Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => ech
|
296
|
+
)
|
297
|
+
)
|
298
|
+
end
|
299
|
+
|
300
|
+
module KemId
|
301
|
+
# https://www.iana.org/assignments/hpke/hpke.xhtml#hpke-kem-ids
|
302
|
+
P_256_SHA256 = 0x0010
|
303
|
+
P_384_SHA384 = 0x0011
|
304
|
+
P_521_SHA512 = 0x0012
|
305
|
+
X25519_SHA256 = 0x0020
|
306
|
+
X448_SHA512 = 0x0021
|
307
|
+
end
|
308
|
+
|
309
|
+
def self.kem_id2dhkem(kem_id)
|
310
|
+
case kem_id
|
311
|
+
when KemId::P_256_SHA256
|
312
|
+
%i[p_256 sha256]
|
313
|
+
when KemId::P_384_SHA384
|
314
|
+
%i[p_384 sha384]
|
315
|
+
when KemId::P_521_SHA512
|
316
|
+
%i[p_521 sha512]
|
317
|
+
when KemId::X25519_SHA256
|
318
|
+
%i[x25519 sha256]
|
319
|
+
when KemId::X448_SHA512
|
320
|
+
%i[x448 sha512]
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def self.kem_curve_name2dhkem(kem_curve_name)
|
325
|
+
case kem_curve_name
|
326
|
+
when :p_256
|
327
|
+
HPKE::DHKEM::EC::P_256
|
328
|
+
when :p_384
|
329
|
+
HPKE::DHKEM::EC::P_384
|
330
|
+
when :p_521
|
331
|
+
HPKE::DHKEM::EC::P_521
|
332
|
+
when :x25519
|
333
|
+
HPKE::DHKEM::X25519
|
334
|
+
when :x448
|
335
|
+
HPKE::DHKEM::X448
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
module KdfId
|
340
|
+
# https://www.iana.org/assignments/hpke/hpke.xhtml#hpke-kdf-ids
|
341
|
+
HKDF_SHA256 = 0x0001
|
342
|
+
HKDF_SHA384 = 0x0002
|
343
|
+
HKDF_SHA512 = 0x0003
|
344
|
+
end
|
345
|
+
|
346
|
+
def self.kdf_id2kdf_hash(kdf_id)
|
347
|
+
case kdf_id
|
348
|
+
when KdfId::HKDF_SHA256
|
349
|
+
:sha256
|
350
|
+
when KdfId::HKDF_SHA384
|
351
|
+
:sha384
|
352
|
+
when KdfId::HKDF_SHA512
|
353
|
+
:sha512
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
module AeadId
|
358
|
+
# https://www.iana.org/assignments/hpke/hpke.xhtml#hpke-aead-ids
|
359
|
+
AES_128_GCM = 0x0001
|
360
|
+
AES_256_GCM = 0x0002
|
361
|
+
CHACHA20_POLY1305 = 0x0003
|
362
|
+
end
|
363
|
+
|
364
|
+
def self.aead_id2overhead_len(aead_id)
|
365
|
+
case aead_id
|
366
|
+
when AeadId::AES_128_GCM, AeadId::CHACHA20_POLY1305
|
367
|
+
16
|
368
|
+
when AeadId::AES_256_GCM
|
369
|
+
32
|
370
|
+
end
|
371
|
+
end
|
372
|
+
|
373
|
+
def self.aead_id2aead_cipher(aead_id)
|
374
|
+
case aead_id
|
375
|
+
when AeadId::AES_128_GCM
|
376
|
+
:aes_128_gcm
|
377
|
+
when AeadId::AES_256_GCM
|
378
|
+
:aes_256_gcm
|
379
|
+
when AeadId::CHACHA20_POLY1305
|
380
|
+
:chacha20_poly1305
|
381
|
+
end
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
class EchState
|
386
|
+
attr_reader :maximum_name_length
|
387
|
+
attr_reader :config_id
|
388
|
+
attr_reader :cipher_suite
|
389
|
+
attr_reader :public_name
|
390
|
+
attr_reader :ctx
|
391
|
+
|
392
|
+
# @param maximum_name_length [Integer]
|
393
|
+
# @param config_id [Integer]
|
394
|
+
# @param cipher_suite [HpkeSymmetricCipherSuite]
|
395
|
+
# @param public_name [String]
|
396
|
+
# @param ctx [[HPKE::ContextS]
|
397
|
+
def initialize(maximum_name_length,
|
398
|
+
config_id,
|
399
|
+
cipher_suite,
|
400
|
+
public_name,
|
401
|
+
ctx)
|
402
|
+
@maximum_name_length = maximum_name_length
|
403
|
+
@config_id = config_id
|
404
|
+
@cipher_suite = cipher_suite
|
405
|
+
@public_name = public_name
|
406
|
+
@ctx = ctx
|
407
|
+
end
|
408
|
+
end
|
409
|
+
# rubocop: enable Metrics/ModuleLength
|
410
|
+
end
|
@@ -0,0 +1,276 @@
|
|
1
|
+
# encoding: ascii-8bit
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module TTTLS13
|
5
|
+
# rubocop: disable Metrics/ClassLength
|
6
|
+
class Endpoint
|
7
|
+
# @param label [String]
|
8
|
+
# @param context [String]
|
9
|
+
# @param key_length [Integer]
|
10
|
+
# @param exporter_secret [String]
|
11
|
+
# @param cipher_suite [TTTLS13::CipherSuite]
|
12
|
+
#
|
13
|
+
# @return [String, nil]
|
14
|
+
def self.exporter(label, context, key_length, exporter_secret, cipher_suite)
|
15
|
+
return nil if exporter_secret.nil? || cipher_suite.nil?
|
16
|
+
|
17
|
+
digest = CipherSuite.digest(cipher_suite)
|
18
|
+
do_exporter(exporter_secret, digest, label, context, key_length)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param cipher_suite [TTTLS13::CipherSuite]
|
22
|
+
# @param write_key [String]
|
23
|
+
# @param write_iv [String]
|
24
|
+
#
|
25
|
+
# @return [TTTLS13::Cryptograph::Aead]
|
26
|
+
def self.gen_cipher(cipher_suite, write_key, write_iv)
|
27
|
+
seq_num = SequenceNumber.new
|
28
|
+
Cryptograph::Aead.new(
|
29
|
+
cipher_suite: cipher_suite,
|
30
|
+
write_key: write_key,
|
31
|
+
write_iv: write_iv,
|
32
|
+
sequence_number: seq_num
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param ch1 [TTTLS13::Message::ClientHello]
|
37
|
+
# @param hrr [TTTLS13::Message::ServerHello]
|
38
|
+
# @param ch [TTTLS13::Message::ClientHello]
|
39
|
+
# @param binder_key [String]
|
40
|
+
# @param digest [String] name of digest algorithm
|
41
|
+
#
|
42
|
+
# @return [String]
|
43
|
+
def self.sign_psk_binder(ch1:, hrr:, ch:, binder_key:, digest:)
|
44
|
+
# TODO: ext binder
|
45
|
+
hash_len = OpenSSL::Digest.new(digest).digest_length
|
46
|
+
tt = Transcript.new
|
47
|
+
tt[CH1] = [ch1, ch1.serialize] unless ch1.nil?
|
48
|
+
tt[HRR] = [hrr, hrr.serialize] unless hrr.nil?
|
49
|
+
tt[CH] = [ch, ch.serialize]
|
50
|
+
# transcript-hash (CH1 + HRR +) truncated-CH
|
51
|
+
hash = tt.truncate_hash(digest, CH, hash_len + 3)
|
52
|
+
OpenSSL::HMAC.digest(digest, binder_key, hash)
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param key [OpenSSL::PKey::PKey]
|
56
|
+
# @param signature_scheme [TTTLS13::SignatureScheme]
|
57
|
+
# @param context [String]
|
58
|
+
# @param hash [String]
|
59
|
+
#
|
60
|
+
# @raise [TTTLS13::Error::ErrorAlerts]
|
61
|
+
#
|
62
|
+
# @return [String]
|
63
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
64
|
+
def self.sign_certificate_verify(key:, signature_scheme:, context:, hash:)
|
65
|
+
content = "\x20" * 64 + context + "\x00" + hash
|
66
|
+
|
67
|
+
# RSA signatures MUST use an RSASSA-PSS algorithm, regardless of whether
|
68
|
+
# RSASSA-PKCS1-v1_5 algorithms appear in "signature_algorithms".
|
69
|
+
case signature_scheme
|
70
|
+
when SignatureScheme::RSA_PKCS1_SHA256,
|
71
|
+
SignatureScheme::RSA_PSS_RSAE_SHA256,
|
72
|
+
SignatureScheme::RSA_PSS_PSS_SHA256
|
73
|
+
key.sign_pss('SHA256', content, salt_length: :digest,
|
74
|
+
mgf1_hash: 'SHA256')
|
75
|
+
when SignatureScheme::RSA_PKCS1_SHA384,
|
76
|
+
SignatureScheme::RSA_PSS_RSAE_SHA384,
|
77
|
+
SignatureScheme::RSA_PSS_PSS_SHA384
|
78
|
+
key.sign_pss('SHA384', content, salt_length: :digest,
|
79
|
+
mgf1_hash: 'SHA384')
|
80
|
+
when SignatureScheme::RSA_PKCS1_SHA512,
|
81
|
+
SignatureScheme::RSA_PSS_RSAE_SHA512,
|
82
|
+
SignatureScheme::RSA_PSS_PSS_SHA512
|
83
|
+
key.sign_pss('SHA512', content, salt_length: :digest,
|
84
|
+
mgf1_hash: 'SHA512')
|
85
|
+
when SignatureScheme::ECDSA_SECP256R1_SHA256
|
86
|
+
key.sign('SHA256', content)
|
87
|
+
when SignatureScheme::ECDSA_SECP384R1_SHA384
|
88
|
+
key.sign('SHA384', content)
|
89
|
+
when SignatureScheme::ECDSA_SECP521R1_SHA512
|
90
|
+
key.sign('SHA512', content)
|
91
|
+
else # TODO: ED25519, ED448
|
92
|
+
terminate(:internal_error)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
96
|
+
|
97
|
+
# @param public_key [OpenSSL::PKey::PKey]
|
98
|
+
# @param signature_scheme [TTTLS13::SignatureScheme]
|
99
|
+
# @param signature [String]
|
100
|
+
# @param context [String]
|
101
|
+
# @param hash [String]
|
102
|
+
#
|
103
|
+
# @raise [TTTLS13::Error::ErrorAlerts]
|
104
|
+
#
|
105
|
+
# @return [Boolean]
|
106
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
107
|
+
def self.verified_certificate_verify?(public_key:, signature_scheme:,
|
108
|
+
signature:, context:, hash:)
|
109
|
+
content = "\x20" * 64 + context + "\x00" + hash
|
110
|
+
|
111
|
+
# RSA signatures MUST use an RSASSA-PSS algorithm, regardless of whether
|
112
|
+
# RSASSA-PKCS1-v1_5 algorithms appear in "signature_algorithms".
|
113
|
+
case signature_scheme
|
114
|
+
when SignatureScheme::RSA_PKCS1_SHA256,
|
115
|
+
SignatureScheme::RSA_PSS_RSAE_SHA256,
|
116
|
+
SignatureScheme::RSA_PSS_PSS_SHA256
|
117
|
+
public_key.verify_pss('SHA256', signature, content, salt_length: :auto,
|
118
|
+
mgf1_hash: 'SHA256')
|
119
|
+
when SignatureScheme::RSA_PKCS1_SHA384,
|
120
|
+
SignatureScheme::RSA_PSS_RSAE_SHA384,
|
121
|
+
SignatureScheme::RSA_PSS_PSS_SHA384
|
122
|
+
public_key.verify_pss('SHA384', signature, content, salt_length: :auto,
|
123
|
+
mgf1_hash: 'SHA384')
|
124
|
+
when SignatureScheme::RSA_PKCS1_SHA512,
|
125
|
+
SignatureScheme::RSA_PSS_RSAE_SHA512,
|
126
|
+
SignatureScheme::RSA_PSS_PSS_SHA512
|
127
|
+
public_key.verify_pss('SHA512', signature, content, salt_length: :auto,
|
128
|
+
mgf1_hash: 'SHA512')
|
129
|
+
when SignatureScheme::ECDSA_SECP256R1_SHA256
|
130
|
+
public_key.verify('SHA256', signature, content)
|
131
|
+
when SignatureScheme::ECDSA_SECP384R1_SHA384
|
132
|
+
public_key.verify('SHA384', signature, content)
|
133
|
+
when SignatureScheme::ECDSA_SECP521R1_SHA512
|
134
|
+
public_key.verify('SHA512', signature, content)
|
135
|
+
else # TODO: ED25519, ED448
|
136
|
+
terminate(:internal_error)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
140
|
+
|
141
|
+
# @param digest [String] name of digest algorithm
|
142
|
+
# @param finished_key [String]
|
143
|
+
# @param hash [String]
|
144
|
+
#
|
145
|
+
# @return [String]
|
146
|
+
def self.sign_finished(digest:, finished_key:, hash:)
|
147
|
+
OpenSSL::HMAC.digest(digest, finished_key, hash)
|
148
|
+
end
|
149
|
+
|
150
|
+
# @param finished [TTTLS13::Message::Finished]
|
151
|
+
# @param digest [String] name of digest algorithm
|
152
|
+
# @param finished_key [String]
|
153
|
+
# @param hash [String]
|
154
|
+
#
|
155
|
+
# @return [Boolean]
|
156
|
+
def self.verified_finished?(finished:, digest:, finished_key:, hash:)
|
157
|
+
sign_finished(digest: digest, finished_key: finished_key, hash: hash) \
|
158
|
+
== finished.verify_data
|
159
|
+
end
|
160
|
+
|
161
|
+
# @param key_exchange [String]
|
162
|
+
# @param priv_key [OpenSSL::PKey::$Object]
|
163
|
+
# @param group [TTTLS13::NamedGroup]
|
164
|
+
#
|
165
|
+
# @return [String]
|
166
|
+
def self.gen_shared_secret(key_exchange, priv_key, group)
|
167
|
+
curve = NamedGroup.curve_name(group)
|
168
|
+
terminate(:internal_error) if curve.nil?
|
169
|
+
|
170
|
+
pub_key = OpenSSL::PKey::EC::Point.new(
|
171
|
+
OpenSSL::PKey::EC::Group.new(curve),
|
172
|
+
OpenSSL::BN.new(key_exchange, 2)
|
173
|
+
)
|
174
|
+
|
175
|
+
priv_key.dh_compute_key(pub_key)
|
176
|
+
end
|
177
|
+
|
178
|
+
# @param certificate_list [Array of CertificateEntry]
|
179
|
+
# @param ca_file [String] path to ca.crt
|
180
|
+
# @param hostname [String]
|
181
|
+
#
|
182
|
+
# @return [Boolean]
|
183
|
+
def self.trusted_certificate?(certificate_list,
|
184
|
+
ca_file = nil,
|
185
|
+
hostname = nil)
|
186
|
+
chain = certificate_list.map(&:cert_data).map do |c|
|
187
|
+
OpenSSL::X509::Certificate.new(c)
|
188
|
+
end
|
189
|
+
cert = chain.shift
|
190
|
+
|
191
|
+
# not support CN matching, only support SAN matching
|
192
|
+
return false if !hostname.nil? && !matching_san?(cert, hostname)
|
193
|
+
|
194
|
+
store = OpenSSL::X509::Store.new
|
195
|
+
store.set_default_paths
|
196
|
+
store.add_file(ca_file) unless ca_file.nil?
|
197
|
+
# TODO: parse authorityInfoAccess::CA Issuers
|
198
|
+
ctx = OpenSSL::X509::StoreContext.new(store, cert, chain)
|
199
|
+
now = Time.now
|
200
|
+
ctx.verify && cert.not_before < now && now < cert.not_after
|
201
|
+
end
|
202
|
+
|
203
|
+
# @param signature_algorithms [Array of SignatureAlgorithms]
|
204
|
+
# @param crt [OpenSSL::X509::Certificate]
|
205
|
+
#
|
206
|
+
# @return [Array of TTTLS13::Message::Extension::SignatureAlgorithms]
|
207
|
+
def self.select_signature_algorithms(signature_algorithms, crt)
|
208
|
+
pka = OpenSSL::ASN1.decode(crt.public_key.to_der)
|
209
|
+
.value.first.value.first.value
|
210
|
+
signature_algorithms.select do |sa|
|
211
|
+
case sa
|
212
|
+
when SignatureScheme::ECDSA_SECP256R1_SHA256,
|
213
|
+
SignatureScheme::ECDSA_SECP384R1_SHA384,
|
214
|
+
SignatureScheme::ECDSA_SECP521R1_SHA512
|
215
|
+
pka == 'id-ecPublicKey'
|
216
|
+
when SignatureScheme::RSA_PSS_PSS_SHA256,
|
217
|
+
SignatureScheme::RSA_PSS_PSS_SHA384,
|
218
|
+
SignatureScheme::RSA_PSS_PSS_SHA512
|
219
|
+
pka == 'rsassaPss'
|
220
|
+
when SignatureScheme::RSA_PSS_RSAE_SHA256,
|
221
|
+
SignatureScheme::RSA_PSS_RSAE_SHA384,
|
222
|
+
SignatureScheme::RSA_PSS_RSAE_SHA512
|
223
|
+
pka == 'rsaEncryption'
|
224
|
+
else
|
225
|
+
# RSASSA-PKCS1-v1_5 algorithms refer solely to signatures which appear
|
226
|
+
# in certificates and are not defined for use in signed TLS handshake
|
227
|
+
# messages
|
228
|
+
false
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# @param cert [OpenSSL::X509::Certificate]
|
234
|
+
# @param name [String]
|
235
|
+
#
|
236
|
+
# @return [Boolean]
|
237
|
+
def self.matching_san?(cert, name)
|
238
|
+
san = cert.extensions.find { |ex| ex.oid == 'subjectAltName' }
|
239
|
+
return false if san.nil?
|
240
|
+
|
241
|
+
ostr = OpenSSL::ASN1.decode(san.to_der).value.last
|
242
|
+
OpenSSL::ASN1.decode(ostr.value)
|
243
|
+
.map(&:value)
|
244
|
+
.map { |s| s.gsub('.', '\.').gsub('*', '.*') }
|
245
|
+
.any? { |s| name.match(/#{s}/) }
|
246
|
+
end
|
247
|
+
|
248
|
+
class << self
|
249
|
+
# @param secret [String] (early_)exporter_secret
|
250
|
+
# @param digest [String] name of digest algorithm
|
251
|
+
# @param label [String]
|
252
|
+
# @param context [String]
|
253
|
+
# @param key_length [Integer]
|
254
|
+
#
|
255
|
+
# @return [String]
|
256
|
+
def do_exporter(secret, digest, label, context, key_length)
|
257
|
+
derived_secret = KeySchedule.hkdf_expand_label(
|
258
|
+
secret,
|
259
|
+
label,
|
260
|
+
OpenSSL::Digest.digest(digest, ''),
|
261
|
+
OpenSSL::Digest.new(digest).digest_length,
|
262
|
+
digest
|
263
|
+
)
|
264
|
+
|
265
|
+
KeySchedule.hkdf_expand_label(
|
266
|
+
derived_secret,
|
267
|
+
'exporter',
|
268
|
+
OpenSSL::Digest.digest(digest, context),
|
269
|
+
key_length,
|
270
|
+
digest
|
271
|
+
)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
# rubocop: enable Metrics/ClassLength
|
276
|
+
end
|
@@ -47,7 +47,7 @@ module TTTLS13
|
|
47
47
|
signature_scheme = binary.slice(4, 2)
|
48
48
|
signature_len = Convert.bin2i(binary.slice(6, 2))
|
49
49
|
signature = binary.slice(8, signature_len)
|
50
|
-
raise Error::ErrorAlerts, :
|
50
|
+
raise Error::ErrorAlerts, :decode_error \
|
51
51
|
unless signature_len + 4 == msg_len &&
|
52
52
|
signature_len + 8 == binary.length
|
53
53
|
|