tttls1.3 0.3.1 → 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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/Gemfile +1 -0
  4. data/example/helper.rb +43 -0
  5. data/example/https_client_using_0rtt.rb +5 -3
  6. data/example/https_client_using_ech.rb +6 -7
  7. data/example/https_client_using_grease_ech.rb +0 -2
  8. data/example/https_client_using_hrr.rb +2 -1
  9. data/example/https_client_using_hrr_and_ech.rb +6 -7
  10. data/example/https_client_using_hrr_and_ticket.rb +4 -2
  11. data/example/https_client_using_status_request.rb +2 -1
  12. data/example/https_client_using_ticket.rb +4 -2
  13. data/example/https_client_using_ticket_and_ech.rb +57 -0
  14. data/example/https_server.rb +14 -1
  15. data/lib/tttls1.3/client.rb +205 -418
  16. data/lib/tttls1.3/connection.rb +21 -362
  17. data/lib/tttls1.3/ech.rb +426 -0
  18. data/lib/tttls1.3/endpoint.rb +276 -0
  19. data/lib/tttls1.3/message/certificate_verify.rb +1 -1
  20. data/lib/tttls1.3/message/extension/ech.rb +21 -24
  21. data/lib/tttls1.3/message/extension/ech_outer_extensions.rb +52 -0
  22. data/lib/tttls1.3/message/extension/signature_algorithms.rb +2 -2
  23. data/lib/tttls1.3/message/extension/supported_versions.rb +3 -3
  24. data/lib/tttls1.3/message/extension/unknown_extension.rb +2 -2
  25. data/lib/tttls1.3/message/extensions.rb +30 -0
  26. data/lib/tttls1.3/message.rb +1 -0
  27. data/lib/tttls1.3/server.rb +125 -63
  28. data/lib/tttls1.3/utils.rb +37 -0
  29. data/lib/tttls1.3/version.rb +1 -1
  30. data/lib/tttls1.3.rb +2 -1
  31. data/spec/client_spec.rb +21 -60
  32. data/spec/ech_outer_extensions_spec.rb +42 -0
  33. data/spec/ech_spec.rb +41 -0
  34. data/spec/{connection_spec.rb → endpoint_spec.rb} +41 -49
  35. data/spec/extensions_spec.rb +65 -0
  36. data/spec/server_spec.rb +12 -12
  37. data/spec/spec_helper.rb +4 -0
  38. metadata +11 -6
  39. data/lib/tttls1.3/hpke.rb +0 -91
@@ -0,0 +1,426 @@
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
+ 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
17
+ # @param inner [TTTLS13::Message::ClientHello]
18
+ # @param ech_config [ECHConfig]
19
+ # @param hpke_cipher_suite_selector [Method]
20
+ #
21
+ # @return [TTTLS13::Message::ClientHello]
22
+ # @return [TTTLS13::Message::ClientHello]
23
+ # @return [TTTLS13::EchState]
24
+ # rubocop: disable Metrics/AbcSize
25
+ def self.offer_ech(inner, ech_config, hpke_cipher_suite_selector)
26
+ return [new_greased_ch(inner, new_grease_ech), nil, nil] \
27
+ if ech_config.nil? ||
28
+ !SUPPORTED_ECHCONFIG_VERSIONS.include?(ech_config.version)
29
+
30
+ # Encrypted ClientHello Configuration
31
+ ech_state, enc = encrypted_ech_config(
32
+ ech_config,
33
+ hpke_cipher_suite_selector
34
+ )
35
+ return [new_greased_ch(inner, new_grease_ech), nil, nil] \
36
+ if ech_state.nil? || enc.nil?
37
+
38
+ # for ech_outer_extensions
39
+ replaced = \
40
+ inner.extensions.remove_and_replace!(DEFAULT_ECH_OUTER_EXTENSIONS)
41
+
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
47
+ aad = new_ch_outer_aad(
48
+ inner,
49
+ ech_state.cipher_suite,
50
+ ech_state.config_id,
51
+ enc,
52
+ encoded.length + overhead_len,
53
+ ech_state.public_name
54
+ )
55
+
56
+ outer = new_ch_outer(
57
+ aad,
58
+ ech_state.cipher_suite,
59
+ ech_state.config_id,
60
+ enc,
61
+ # which does not include the Handshake structure's four byte header.
62
+ ech_state.ctx.seal(aad.serialize[4..], encoded)
63
+ )
64
+
65
+ [outer, inner, ech_state]
66
+ end
67
+ # rubocop: enable Metrics/AbcSize
68
+
69
+ # @param ech_config [ECHConfig]
70
+ # @param hpke_cipher_suite_selector [Method]
71
+ #
72
+ # @return [TTTLS13::EchState or nil]
73
+ # @return [String or nil]
74
+ # rubocop: disable Metrics/AbcSize
75
+ def self.encrypted_ech_config(ech_config, hpke_cipher_suite_selector)
76
+ public_name = ech_config.echconfig_contents.public_name
77
+ key_config = ech_config.echconfig_contents.key_config
78
+ public_key = key_config.public_key.opaque
79
+ kem_id = key_config&.kem_id&.uint16
80
+ config_id = key_config.config_id
81
+ cipher_suite = hpke_cipher_suite_selector.call(key_config)
82
+ aead_cipher = aead_id2aead_cipher(cipher_suite&.aead_id&.uint16)
83
+ kdf_hash = kdf_id2kdf_hash(cipher_suite&.kdf_id&.uint16)
84
+ return [nil, nil] \
85
+ if [kem_id, aead_cipher, kdf_hash].any?(&:nil?)
86
+
87
+ kem_curve_name, kem_hash = kem_id2dhkem(kem_id)
88
+ dhkem = kem_curve_name2dhkem(kem_curve_name)
89
+ pkr = dhkem&.new(kem_hash)&.deserialize_public_key(public_key)
90
+ return [nil, nil] if pkr.nil?
91
+
92
+ hpke = HPKE.new(kem_curve_name, kem_hash, kdf_hash, aead_cipher)
93
+ base_s = hpke.setup_base_s(pkr, "tls ech\x00" + ech_config.encode)
94
+ enc = base_s[:enc]
95
+ ctx = base_s[:context_s]
96
+ mnl = ech_config.echconfig_contents.maximum_name_length
97
+ ech_state = EchState.new(
98
+ mnl,
99
+ config_id,
100
+ cipher_suite,
101
+ public_name,
102
+ ctx
103
+ )
104
+
105
+ [ech_state, enc]
106
+ end
107
+ # rubocop: enable Metrics/AbcSize
108
+
109
+ # @param inner [TTTLS13::Message::ClientHello]
110
+ # @param ech_state [TTTLS13::EchState]
111
+ #
112
+ # @return [TTTLS13::Message::ClientHello]
113
+ # @return [TTTLS13::Message::ClientHello]
114
+ def self.offer_new_ech(inner, ech_state)
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)
123
+
124
+ # It encrypts EncodedClientHelloInner as described in Section 6.1.1, using
125
+ # the second partial ClientHelloOuterAAD, to obtain a second
126
+ # ClientHelloOuter. It reuses the original HPKE encryption context
127
+ # computed in Section 6.1 and uses the empty string for enc.
128
+ #
129
+ # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.1.5-4.4.1
130
+ aad = new_ch_outer_aad(
131
+ inner,
132
+ ech_state.cipher_suite,
133
+ ech_state.config_id,
134
+ '',
135
+ encoded.length + overhead_len,
136
+ ech_state.public_name
137
+ )
138
+
139
+ # Authenticating the ClientHelloOuter
140
+ outer = new_ch_outer(
141
+ aad,
142
+ ech_state.cipher_suite,
143
+ ech_state.config_id,
144
+ '',
145
+ # which does not include the Handshake structure's four byte header.
146
+ ech_state.ctx.seal(aad.serialize[4..], encoded)
147
+ )
148
+
149
+ [outer, inner]
150
+ end
151
+
152
+ # @param inner [TTTLS13::Message::ClientHello]
153
+ # @param maximum_name_length [Integer]
154
+ # @param replaced [TTTLS13::Message::Extensions]
155
+ #
156
+ # @return [String] EncodedClientHelloInner
157
+ def self.encode_ch_inner(inner, maximum_name_length, replaced)
158
+ encoded = Message::ClientHello.new(
159
+ legacy_version: inner.legacy_version,
160
+ random: inner.random,
161
+ legacy_session_id: '',
162
+ cipher_suites: inner.cipher_suites,
163
+ legacy_compression_methods: inner.legacy_compression_methods,
164
+ extensions: replaced
165
+ )
166
+ server_name_length = \
167
+ replaced[Message::ExtensionType::SERVER_NAME].server_name.length
168
+
169
+ padding_encoded_ch_inner(
170
+ # which does not include the Handshake structure's four byte header.
171
+ encoded.serialize[4..],
172
+ server_name_length,
173
+ maximum_name_length
174
+ )
175
+ end
176
+
177
+ # @param s [String]
178
+ # @param server_name_length [Integer]
179
+ # @param maximum_name_length [Integer]
180
+ #
181
+ # @return [String]
182
+ def self.padding_encoded_ch_inner(s,
183
+ server_name_length,
184
+ maximum_name_length)
185
+ padding_len =
186
+ if server_name_length.positive?
187
+ [maximum_name_length - server_name_length, 0].max
188
+ else
189
+ 9 + maximum_name_length
190
+ end
191
+
192
+ padding_len = 31 - ((s.length + padding_len - 1) % 32)
193
+ s + padding_len.zeros
194
+ end
195
+
196
+ # @param inner [TTTLS13::Message::ClientHello]
197
+ # @param cipher_suite [HpkeSymmetricCipherSuite]
198
+ # @param config_id [Integer]
199
+ # @param enc [String]
200
+ # @param payload_len [Integer]
201
+ # @param server_name [String]
202
+ #
203
+ # @return [TTTLS13::Message::ClientHello]
204
+ # rubocop: disable Metrics/ParameterLists
205
+ def self.new_ch_outer_aad(inner,
206
+ cipher_suite,
207
+ config_id,
208
+ enc,
209
+ payload_len,
210
+ server_name)
211
+ aad_ech = Message::Extension::ECHClientHello.new_outer(
212
+ cipher_suite: cipher_suite,
213
+ config_id: config_id,
214
+ enc: enc,
215
+ payload: payload_len.zeros
216
+ )
217
+ Message::ClientHello.new(
218
+ legacy_version: inner.legacy_version,
219
+ legacy_session_id: inner.legacy_session_id,
220
+ cipher_suites: inner.cipher_suites,
221
+ legacy_compression_methods: inner.legacy_compression_methods,
222
+ extensions: inner.extensions.merge(
223
+ Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => aad_ech,
224
+ Message::ExtensionType::SERVER_NAME => \
225
+ Message::Extension::ServerName.new(server_name)
226
+ )
227
+ )
228
+ end
229
+ # rubocop: enable Metrics/ParameterLists
230
+
231
+ # @param aad [TTTLS13::Message::ClientHello]
232
+ # @param cipher_suite [HpkeSymmetricCipherSuite]
233
+ # @param config_id [Integer]
234
+ # @param enc [String]
235
+ # @param payload [String]
236
+ #
237
+ # @return [TTTLS13::Message::ClientHello]
238
+ def self.new_ch_outer(aad, cipher_suite, config_id, enc, payload)
239
+ outer_ech = Message::Extension::ECHClientHello.new_outer(
240
+ cipher_suite: cipher_suite,
241
+ config_id: config_id,
242
+ enc: enc,
243
+ payload: payload
244
+ )
245
+ Message::ClientHello.new(
246
+ legacy_version: aad.legacy_version,
247
+ random: aad.random,
248
+ legacy_session_id: aad.legacy_session_id,
249
+ cipher_suites: aad.cipher_suites,
250
+ legacy_compression_methods: aad.legacy_compression_methods,
251
+ extensions: aad.extensions.merge(
252
+ Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => outer_ech
253
+ )
254
+ )
255
+ end
256
+
257
+ # @return [Message::Extension::ECHClientHello]
258
+ def self.new_grease_ech
259
+ # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#name-compliance-requirements
260
+ cipher_suite = HpkeSymmetricCipherSuite.new(
261
+ HpkeSymmetricCipherSuite::HpkeKdfId.new(
262
+ KdfId::HKDF_SHA256
263
+ ),
264
+ HpkeSymmetricCipherSuite::HpkeAeadId.new(
265
+ AeadId::AES_128_GCM
266
+ )
267
+ )
268
+ # Set the enc field to a randomly-generated valid encapsulated public key
269
+ # output by the HPKE KEM.
270
+ #
271
+ # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.2-2.3.1
272
+ public_key = OpenSSL::PKey.read(
273
+ OpenSSL::PKey.generate_key('X25519').public_to_pem
274
+ )
275
+ hpke = HPKE.new(:x25519, :sha256, :sha256, :aes_128_gcm)
276
+ enc = hpke.setup_base_s(public_key, '')[:enc]
277
+ # Set the payload field to a randomly-generated string of L+C bytes, where
278
+ # C is the ciphertext expansion of the selected AEAD scheme and L is the
279
+ # size of the EncodedClientHelloInner the client would compute when
280
+ # offering ECH, padded according to Section 6.1.3.
281
+ #
282
+ # https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-17#section-6.2-2.4.1
283
+ payload_len = placeholder_encoded_ch_inner_len \
284
+ + aead_id2overhead_len(AeadId::AES_128_GCM)
285
+
286
+ Message::Extension::ECHClientHello.new_outer(
287
+ cipher_suite: cipher_suite,
288
+ config_id: Convert.bin2i(OpenSSL::Random.random_bytes(1)),
289
+ enc: enc,
290
+ payload: OpenSSL::Random.random_bytes(payload_len)
291
+ )
292
+ end
293
+
294
+ # @return [Integer]
295
+ def self.placeholder_encoded_ch_inner_len
296
+ 448
297
+ end
298
+
299
+ # @param inner [TTTLS13::Message::ClientHello]
300
+ # @param ech [Message::Extension::ECHClientHello]
301
+ #
302
+ # @return [TTTLS13::Message::ClientHello]
303
+ def self.new_greased_ch(inner, ech)
304
+ Message::ClientHello.new(
305
+ legacy_version: inner.legacy_version,
306
+ random: inner.random,
307
+ legacy_session_id: inner.legacy_session_id,
308
+ cipher_suites: inner.cipher_suites,
309
+ legacy_compression_methods: inner.legacy_compression_methods,
310
+ extensions: inner.extensions.merge(
311
+ Message::ExtensionType::ENCRYPTED_CLIENT_HELLO => ech
312
+ )
313
+ )
314
+ end
315
+
316
+ module KemId
317
+ # https://www.iana.org/assignments/hpke/hpke.xhtml#hpke-kem-ids
318
+ P_256_SHA256 = 0x0010
319
+ P_384_SHA384 = 0x0011
320
+ P_521_SHA512 = 0x0012
321
+ X25519_SHA256 = 0x0020
322
+ X448_SHA512 = 0x0021
323
+ end
324
+
325
+ def self.kem_id2dhkem(kem_id)
326
+ case kem_id
327
+ when KemId::P_256_SHA256
328
+ %i[p_256 sha256]
329
+ when KemId::P_384_SHA384
330
+ %i[p_384 sha384]
331
+ when KemId::P_521_SHA512
332
+ %i[p_521 sha512]
333
+ when KemId::X25519_SHA256
334
+ %i[x25519 sha256]
335
+ when KemId::X448_SHA512
336
+ %i[x448 sha512]
337
+ end
338
+ end
339
+
340
+ def self.kem_curve_name2dhkem(kem_curve_name)
341
+ case kem_curve_name
342
+ when :p_256
343
+ HPKE::DHKEM::EC::P_256
344
+ when :p_384
345
+ HPKE::DHKEM::EC::P_384
346
+ when :p_521
347
+ HPKE::DHKEM::EC::P_521
348
+ when :x25519
349
+ HPKE::DHKEM::X25519
350
+ when :x448
351
+ HPKE::DHKEM::X448
352
+ end
353
+ end
354
+
355
+ module KdfId
356
+ # https://www.iana.org/assignments/hpke/hpke.xhtml#hpke-kdf-ids
357
+ HKDF_SHA256 = 0x0001
358
+ HKDF_SHA384 = 0x0002
359
+ HKDF_SHA512 = 0x0003
360
+ end
361
+
362
+ def self.kdf_id2kdf_hash(kdf_id)
363
+ case kdf_id
364
+ when KdfId::HKDF_SHA256
365
+ :sha256
366
+ when KdfId::HKDF_SHA384
367
+ :sha384
368
+ when KdfId::HKDF_SHA512
369
+ :sha512
370
+ end
371
+ end
372
+
373
+ module AeadId
374
+ # https://www.iana.org/assignments/hpke/hpke.xhtml#hpke-aead-ids
375
+ AES_128_GCM = 0x0001
376
+ AES_256_GCM = 0x0002
377
+ CHACHA20_POLY1305 = 0x0003
378
+ end
379
+
380
+ def self.aead_id2overhead_len(aead_id)
381
+ case aead_id
382
+ when AeadId::AES_128_GCM, AeadId::CHACHA20_POLY1305
383
+ 16
384
+ when AeadId::AES_256_GCM
385
+ 32
386
+ end
387
+ end
388
+
389
+ def self.aead_id2aead_cipher(aead_id)
390
+ case aead_id
391
+ when AeadId::AES_128_GCM
392
+ :aes_128_gcm
393
+ when AeadId::AES_256_GCM
394
+ :aes_256_gcm
395
+ when AeadId::CHACHA20_POLY1305
396
+ :chacha20_poly1305
397
+ end
398
+ end
399
+ end
400
+
401
+ class EchState
402
+ attr_reader :maximum_name_length
403
+ attr_reader :config_id
404
+ attr_reader :cipher_suite
405
+ attr_reader :public_name
406
+ attr_reader :ctx
407
+
408
+ # @param maximum_name_length [Integer]
409
+ # @param config_id [Integer]
410
+ # @param cipher_suite [HpkeSymmetricCipherSuite]
411
+ # @param public_name [String]
412
+ # @param ctx [HPKE::ContextS]
413
+ def initialize(maximum_name_length,
414
+ config_id,
415
+ cipher_suite,
416
+ public_name,
417
+ ctx)
418
+ @maximum_name_length = maximum_name_length
419
+ @config_id = config_id
420
+ @cipher_suite = cipher_suite
421
+ @public_name = public_name
422
+ @ctx = ctx
423
+ end
424
+ end
425
+ # rubocop: enable Metrics/ClassLength
426
+ 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, :internal_error \
50
+ raise Error::ErrorAlerts, :decode_error \
51
51
  unless signature_len + 4 == msg_len &&
52
52
  signature_len + 8 == binary.length
53
53