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