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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -2
  3. data/.rubocop.yml +3 -0
  4. data/.ruby-version +1 -1
  5. data/Gemfile +1 -0
  6. data/README.md +2 -2
  7. data/example/README.md +1 -1
  8. data/example/helper.rb +22 -0
  9. data/example/https_client.rb +4 -4
  10. data/example/https_client_using_0rtt.rb +6 -5
  11. data/example/https_client_using_ech.rb +5 -6
  12. data/example/https_client_using_grease_ech.rb +3 -5
  13. data/example/https_client_using_grease_psk.rb +8 -16
  14. data/example/https_client_using_hrr.rb +5 -4
  15. data/example/https_client_using_hrr_and_ech.rb +6 -6
  16. data/example/https_client_using_hrr_and_ticket.rb +5 -4
  17. data/example/https_client_using_status_request.rb +4 -5
  18. data/example/https_client_using_ticket.rb +5 -4
  19. data/example/https_server.rb +14 -1
  20. data/lib/tttls1.3/client.rb +205 -418
  21. data/lib/tttls1.3/connection.rb +21 -362
  22. data/lib/tttls1.3/ech.rb +410 -0
  23. data/lib/tttls1.3/endpoint.rb +276 -0
  24. data/lib/tttls1.3/message/certificate_verify.rb +1 -1
  25. data/lib/tttls1.3/message/extension/ech.rb +12 -10
  26. data/lib/tttls1.3/message/extension/signature_algorithms.rb +2 -2
  27. data/lib/tttls1.3/message/extension/supported_versions.rb +3 -3
  28. data/lib/tttls1.3/message/extension/unknown_extension.rb +2 -2
  29. data/lib/tttls1.3/server.rb +125 -63
  30. data/lib/tttls1.3/utils.rb +37 -0
  31. data/lib/tttls1.3/version.rb +1 -1
  32. data/lib/tttls1.3.rb +2 -1
  33. data/spec/client_spec.rb +21 -60
  34. data/spec/ech_spec.rb +39 -0
  35. data/spec/{connection_spec.rb → endpoint_spec.rb} +41 -49
  36. data/spec/server_spec.rb +12 -12
  37. data/tttls1.3.gemspec +1 -1
  38. metadata +8 -7
  39. data/lib/tttls1.3/hpke.rb +0 -91
@@ -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, :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