tttls1.3 0.2.9 → 0.2.14

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +32 -0
  3. data/.rubocop.yml +9 -2
  4. data/Gemfile +1 -1
  5. data/README.md +5 -1
  6. data/Rakefile +66 -7
  7. data/example/helper.rb +6 -8
  8. data/example/https_client.rb +1 -1
  9. data/example/https_client_using_0rtt.rb +3 -3
  10. data/example/https_client_using_hrr.rb +1 -1
  11. data/example/https_client_using_hrr_and_ticket.rb +2 -2
  12. data/example/https_client_using_status_request.rb +31 -0
  13. data/example/https_client_using_ticket.rb +2 -2
  14. data/example/https_server.rb +6 -5
  15. data/interop/client_spec.rb +8 -8
  16. data/interop/helper.rb +10 -2
  17. data/interop/server_spec.rb +14 -10
  18. data/lib/tttls1.3.rb +1 -0
  19. data/lib/tttls1.3/client.rb +97 -12
  20. data/lib/tttls1.3/connection.rb +45 -12
  21. data/lib/tttls1.3/cryptograph.rb +1 -1
  22. data/lib/tttls1.3/cryptograph/aead.rb +20 -7
  23. data/lib/tttls1.3/message.rb +1 -1
  24. data/lib/tttls1.3/message/alert.rb +2 -2
  25. data/lib/tttls1.3/message/extension/status_request.rb +73 -17
  26. data/lib/tttls1.3/message/extensions.rb +35 -12
  27. data/lib/tttls1.3/server.rb +40 -13
  28. data/lib/tttls1.3/utils.rb +15 -0
  29. data/lib/tttls1.3/version.rb +1 -1
  30. data/spec/extensions_spec.rb +16 -0
  31. data/spec/fixtures/rsa_rsa.crt +15 -15
  32. data/spec/fixtures/rsa_rsa.key +25 -25
  33. data/spec/fixtures/rsa_rsa_ocsp.crt +18 -0
  34. data/spec/fixtures/rsa_rsa_ocsp.key +27 -0
  35. data/spec/server_hello_spec.rb +1 -1
  36. data/spec/spec_helper.rb +35 -1
  37. data/spec/status_request_spec.rb +77 -10
  38. data/tttls1.3.gemspec +1 -1
  39. metadata +14 -10
  40. data/.travis.yml +0 -18
  41. data/interop/Dockerfile +0 -28
@@ -12,6 +12,14 @@ include TTTLS13::Message::Extension
12
12
  include TTTLS13::Error
13
13
  # rubocop: enable Style/MixinUsage
14
14
 
15
- def wait_to_listen(port)
16
- sleep(0.2) while `lsof -ni :#{port}`.empty?
15
+ def wait_to_listen(host, port)
16
+ loop do
17
+ s = TCPSocket.open(host, port) # check by TCP handshake
18
+ rescue # rubocop: disable Style/RescueStandardError
19
+ sleep(0.2)
20
+ next
21
+ else
22
+ s.close
23
+ break
24
+ end
17
25
  end
@@ -9,14 +9,13 @@ PORT = 4433
9
9
  tcpserver = TCPServer.open(PORT)
10
10
 
11
11
  RSpec.describe Server do
12
- # testcases
13
12
  # normal [Boolean] Is this nominal scenarios?
14
13
  # opt [String] openssl s_client options
15
14
  # crt [String] server crt file path
16
15
  # key [String] server key file path
17
16
  # settings [Hash] TTTLS13::Client settins
18
- [
19
- # rubocop: disable Metrics/LineLength
17
+ # rubocop: disable Layout/LineLength
18
+ testcases = [
20
19
  [
21
20
  true,
22
21
  '-groups P-256:P-384:P-521 -ciphersuites TLS_AES_256_GCM_SHA384',
@@ -172,20 +171,24 @@ RSpec.describe Server do
172
171
  FIXTURES_DIR + '/rsa_rsa.key',
173
172
  compatibility_mode: false
174
173
  ]
175
- # rubocop: enable Metrics/LineLength
176
- ].each do |normal, opt, crt, key, settings|
174
+ ]
175
+ # rubocop: enable Layout/LineLength
176
+ testcases.each do |normal, opt, crt, key, settings|
177
177
  context 'server interop' do
178
178
  let(:server) do
179
- @socket = tcpserver.accept
179
+ loop do
180
+ @socket = tcpserver.accept
181
+ break unless @socket.eof?
182
+ end
180
183
  settings[:crt_file] = crt
181
184
  settings[:key_file] = key
182
- Server.new(@socket, settings)
185
+ Server.new(@socket, **settings)
183
186
  end
184
187
 
185
188
  let(:client) do
186
- wait_to_listen(PORT)
187
-
188
189
  ip = Socket.ip_address_list.find(&:ipv4_private?).ip_address
190
+ wait_to_listen(ip, PORT)
191
+
189
192
  cmd = 'echo -n ping | openssl s_client ' \
190
193
  + "-connect local:#{PORT} " \
191
194
  + '-tls1_3 ' \
@@ -195,7 +198,7 @@ RSpec.describe Server do
195
198
  + opt
196
199
  'docker run ' \
197
200
  + "--volume #{FIXTURES_DIR}:/tmp " \
198
- + "--add-host=local:#{ip} -it openssl " \
201
+ + "--add-host=local:#{ip} thekuwayama/openssl " \
199
202
  + "sh -c \"#{cmd}\" 2>&1 >/dev/null"
200
203
  end
201
204
 
@@ -216,6 +219,7 @@ RSpec.describe Server do
216
219
  it "should NOT accept request from openssl s_client ...#{opt}" do
217
220
  spawn(client)
218
221
  expect { server.accept }.to raise_error ErrorAlerts
222
+ expect { server.close }.to_not raise_error
219
223
  end
220
224
  end
221
225
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'openssl'
4
+ require 'net/http'
4
5
  require 'pp'
5
6
  require 'logger'
6
7
 
@@ -59,6 +59,8 @@ module TTTLS13
59
59
  ticket_age_add: nil,
60
60
  ticket_timestamp: nil,
61
61
  record_size_limit: nil,
62
+ check_certificate_status: false,
63
+ process_certificate_status: nil,
62
64
  compatibility_mode: true,
63
65
  loglevel: Logger::WARN
64
66
  }.freeze
@@ -300,7 +302,8 @@ module TTTLS13
300
302
  message = recv_message(receivable_ccs: true, cipher: hs_rcipher)
301
303
  if message.msg_type == Message::HandshakeType::CERTIFICATE
302
304
  ct = transcript[CT] = message
303
- terminate_invalid_certificate(ct, transcript[CH])
305
+ alert = check_invalid_certificate(ct, transcript[CH])
306
+ terminate(alert) unless alert.nil?
304
307
 
305
308
  @state = ClientState::WAIT_CV
306
309
  elsif message.msg_type == Message::HandshakeType::CERTIFICATE_REQUEST
@@ -314,7 +317,8 @@ module TTTLS13
314
317
  logger.debug('ClientState::WAIT_CERT')
315
318
 
316
319
  ct = transcript[CT] = recv_certificate(hs_rcipher)
317
- terminate_invalid_certificate(ct, transcript[CH])
320
+ alert = check_invalid_certificate(ct, transcript[CH])
321
+ terminate(alert) unless alert.nil?
318
322
 
319
323
  @state = ClientState::WAIT_CV
320
324
  when ClientState::WAIT_CV
@@ -392,6 +396,53 @@ module TTTLS13
392
396
  @succeed_early_data
393
397
  end
394
398
 
399
+ # @param res [OpenSSL::OCSP::Response]
400
+ # @param cert [OpenSSL::X509::Certificate]
401
+ # @param chain [Array of OpenSSL::X509::Certificate, nil]
402
+ #
403
+ # @return [Boolean]
404
+ #
405
+ # @example
406
+ # m = Client.method(:softfail_check_certificate_status)
407
+ # Client.new(
408
+ # socket,
409
+ # hostname,
410
+ # check_certificate_status: true,
411
+ # process_certificate_status: m
412
+ # )
413
+ def self.softfail_check_certificate_status(res, cert, chain)
414
+ ocsp_response = res
415
+ cid = OpenSSL::OCSP::CertificateId.new(cert, chain.first)
416
+
417
+ # When NOT received OCSPResponse in TLS handshake, this method will
418
+ # send OCSPRequest. If ocsp_uri is NOT presented in Certificate, return
419
+ # true. Also, if it sends OCSPRequest and does NOT receive a HTTPresponse
420
+ # within 2 seconds, return true.
421
+ if ocsp_response.nil?
422
+ uri = cert.ocsp_uris&.find { |u| URI::DEFAULT_PARSER.make_regexp =~ u }
423
+ return true if uri.nil?
424
+
425
+ begin
426
+ # send OCSP::Request
427
+ ocsp_request = gen_ocsp_request(cid)
428
+ Timeout.timeout(2) do
429
+ ocsp_response = send_ocsp_request(ocsp_request, uri)
430
+ end
431
+
432
+ # check nonce of OCSP::Response
433
+ check_nonce = ocsp_request.check_nonce(ocsp_response.basic)
434
+ return true unless [-1, 1].include?(check_nonce)
435
+ rescue StandardError
436
+ return true
437
+ end
438
+ end
439
+ return true \
440
+ if ocsp_response.status != OpenSSL::OCSP::RESPONSE_STATUS_SUCCESSFUL
441
+
442
+ status = ocsp_response.basic.status.find { |s| s.first.cmp(cid) }
443
+ status[1] != OpenSSL::OCSP::V_CERTSTATUS_REVOKED
444
+ end
445
+
395
446
  private
396
447
 
397
448
  # @return [Boolean]
@@ -423,6 +474,9 @@ module TTTLS13
423
474
  rsl = @settings[:record_size_limit]
424
475
  return false if !rsl.nil? && (rsl < 64 || rsl > 2**14 + 1)
425
476
 
477
+ return false if @settings[:check_certificate_status] &&
478
+ @settings[:process_certificate_status].nil?
479
+
426
480
  true
427
481
  end
428
482
  # rubocop: enable Metrics/AbcSize
@@ -471,9 +525,10 @@ module TTTLS13
471
525
  # @return [Hash of NamedGroup => OpenSSL::PKey::EC.$Object]
472
526
  # rubocop: disable Metrics/AbcSize
473
527
  # rubocop: disable Metrics/CyclomaticComplexity
528
+ # rubocop: disable Metrics/MethodLength
474
529
  # rubocop: disable Metrics/PerceivedComplexity
475
530
  def gen_ch_extensions
476
- exs = []
531
+ exs = Message::Extensions.new
477
532
  # server_name
478
533
  exs << Message::Extension::ServerName.new(@hostname)
479
534
 
@@ -519,10 +574,15 @@ module TTTLS13
519
574
  exs << Message::Extension::Alpn.new(@settings[:alpn].reject(&:empty?)) \
520
575
  if !@settings[:alpn].nil? && !@settings[:alpn].empty?
521
576
 
522
- [Message::Extensions.new(exs), priv_keys]
577
+ # status_request
578
+ exs << Message::Extension::OCSPStatusRequest.new \
579
+ if @settings[:check_certificate_status]
580
+
581
+ [exs, priv_keys]
523
582
  end
524
583
  # rubocop: enable Metrics/AbcSize
525
584
  # rubocop: enable Metrics/CyclomaticComplexity
585
+ # rubocop: enable Metrics/MethodLength
526
586
  # rubocop: enable Metrics/PerceivedComplexity
527
587
 
528
588
  # @param extensions [TTTLS13::Message::Extensions]
@@ -611,7 +671,7 @@ module TTTLS13
611
671
  # @return [TTTLS13::Message::Extensions]
612
672
  # @return [Hash of NamedGroup => OpenSSL::PKey::EC.$Object]
613
673
  def gen_newch_extensions(ch1, hrr)
614
- exs = []
674
+ exs = Message::Extensions.new
615
675
  # key_share
616
676
  if hrr.extensions.include?(Message::ExtensionType::KEY_SHARE)
617
677
  group = hrr.extensions[Message::ExtensionType::KEY_SHARE]
@@ -633,7 +693,7 @@ module TTTLS13
633
693
  if hrr.extensions.include?(Message::ExtensionType::COOKIE)
634
694
 
635
695
  # early_data
636
- new_exs = ch1.extensions.merge(Message::Extensions.new(exs))
696
+ new_exs = ch1.extensions.merge(exs)
637
697
  new_exs.delete(Message::ExtensionType::EARLY_DATA)
638
698
 
639
699
  [new_exs, priv_keys]
@@ -753,16 +813,32 @@ module TTTLS13
753
813
 
754
814
  # @param ct [TTTLS13::Message::Certificate]
755
815
  # @param ch [TTTLS13::Message::ClientHello]
756
- def terminate_invalid_certificate(ct, ch)
757
- terminate(:illegal_parameter) unless ct.appearable_extensions?
816
+ #
817
+ # @return [Symbol, nil] return key of ALERT_DESCRIPTION, if invalid
818
+ def check_invalid_certificate(ct, ch)
819
+ return :illegal_parameter unless ct.appearable_extensions?
758
820
 
759
- terminate(:unsupported_extension) \
821
+ return :unsupported_extension \
760
822
  unless ct.certificate_list.map(&:extensions)
761
823
  .all? { |e| (e.keys - ch.extensions.keys).empty? }
762
824
 
763
- terminate(:certificate_unknown) \
764
- unless trusted_certificate?(ct.certificate_list,
765
- @settings[:ca_file], @hostname)
825
+ return :certificate_unknown unless trusted_certificate?(
826
+ ct.certificate_list,
827
+ @settings[:ca_file],
828
+ @hostname
829
+ )
830
+
831
+ if @settings[:check_certificate_status]
832
+ ee = ct.certificate_list.first
833
+ ocsp_response = ee.extensions[Message::ExtensionType::STATUS_REQUEST]
834
+ &.ocsp_response
835
+ cert = ee.cert_data
836
+ chain = ct.certificate_list[1..]&.map(&:cert_data)
837
+ return :bad_certificate_status_response \
838
+ unless satisfactory_certificate_status?(ocsp_response, cert, chain)
839
+ end
840
+
841
+ nil
766
842
  end
767
843
 
768
844
  # @param ct [TTTLS13::Message::Certificate]
@@ -784,6 +860,15 @@ module TTTLS13
784
860
  )
785
861
  end
786
862
 
863
+ # @param ocsp_response [OpenSSL::OCSP::Response]
864
+ # @param cert [OpenSSL::X509::Certificate]
865
+ # @param chain [Array of OpenSSL::X509::Certificate, nil]
866
+ #
867
+ # @return [Boolean]
868
+ def satisfactory_certificate_status?(ocsp_response, cert, chain)
869
+ @settings[:process_certificate_status]&.call(ocsp_response, cert, chain)
870
+ end
871
+
787
872
  # @param nst [TTTLS13::Message::NewSessionTicket]
788
873
  #
789
874
  # @raise [TTTLS13::Error::ErrorAlerts]
@@ -254,7 +254,7 @@ module TTTLS13
254
254
  end
255
255
  # rubocop: enable Metrics/CyclomaticComplexity
256
256
 
257
- # @param wcipher [TTTLS13::Cryptograph::Aead, Passer]
257
+ # @param cipher [TTTLS13::Cryptograph::Aead, Passer]
258
258
  #
259
259
  # @return [TTTLS13::Message::Record]
260
260
  def recv_record(cipher)
@@ -273,7 +273,7 @@ module TTTLS13
273
273
 
274
274
  # Received a protected ccs, peer MUST abort the handshake.
275
275
  if record.type == Message::ContentType::APPLICATION_DATA &&
276
- record.messages.first.is_a?(Message::ChangeCipherSpec)
276
+ record.messages.any? { |m| m.is_a?(Message::ChangeCipherSpec) }
277
277
  terminate(:unexpected_message)
278
278
  end
279
279
 
@@ -474,8 +474,10 @@ module TTTLS13
474
474
  #
475
475
  # @return [Boolean]
476
476
  def trusted_certificate?(certificate_list, ca_file = nil, hostname = nil)
477
- cert_bin = certificate_list.first.cert_data
478
- cert = OpenSSL::X509::Certificate.new(cert_bin)
477
+ chain = certificate_list.map(&:cert_data).map do |c|
478
+ OpenSSL::X509::Certificate.new(c)
479
+ end
480
+ cert = chain.shift
479
481
 
480
482
  # not support CN matching, only support SAN matching
481
483
  return false if !hostname.nil? && !matching_san?(cert, hostname)
@@ -483,9 +485,6 @@ module TTTLS13
483
485
  store = OpenSSL::X509::Store.new
484
486
  store.set_default_paths
485
487
  store.add_file(ca_file) unless ca_file.nil?
486
- chain = certificate_list[1..].map(&:cert_data).map do |c|
487
- OpenSSL::X509::Certificate.new(c)
488
- end
489
488
  # TODO: parse authorityInfoAccess::CA Issuers
490
489
  ctx = OpenSSL::X509::StoreContext.new(store, cert, chain)
491
490
  now = Time.now
@@ -515,22 +514,22 @@ module TTTLS13
515
514
  def do_select_signature_algorithms(signature_algorithms, crt)
516
515
  spki = OpenSSL::Netscape::SPKI.new
517
516
  spki.public_key = crt.public_key
518
- oid_str = spki.to_text.split("\n")
519
- .find { |l| l.include?('Public Key Algorithm:') }
517
+ pka = OpenSSL::ASN1.decode(spki.to_der)
518
+ .value.first.value.first.value.first.value.first.value
520
519
  signature_algorithms.select do |sa|
521
520
  case sa
522
521
  when SignatureScheme::ECDSA_SECP256R1_SHA256,
523
522
  SignatureScheme::ECDSA_SECP384R1_SHA384,
524
523
  SignatureScheme::ECDSA_SECP521R1_SHA512
525
- oid_str.include?('id-ecPublicKey')
524
+ pka == 'id-ecPublicKey'
526
525
  when SignatureScheme::RSA_PSS_PSS_SHA256,
527
526
  SignatureScheme::RSA_PSS_PSS_SHA384,
528
527
  SignatureScheme::RSA_PSS_PSS_SHA512
529
- oid_str.include?('rsassaPss')
528
+ pka == 'rsassaPss'
530
529
  when SignatureScheme::RSA_PSS_RSAE_SHA256,
531
530
  SignatureScheme::RSA_PSS_RSAE_SHA384,
532
531
  SignatureScheme::RSA_PSS_RSAE_SHA512
533
- oid_str.include?('rsaEncryption')
532
+ pka == 'rsaEncryption'
534
533
  else
535
534
  # RSASSA-PKCS1-v1_5 algorithms refer solely to signatures which appear
536
535
  # in certificates and are not defined for use in signed TLS handshake
@@ -539,6 +538,40 @@ module TTTLS13
539
538
  end
540
539
  end
541
540
  end
541
+
542
+ class << self
543
+ # @param cid [OpenSSL::OCSP::CertificateId]
544
+ #
545
+ # @return [OpenSSL::OCSP::Request]
546
+ def gen_ocsp_request(cid)
547
+ ocsp_request = OpenSSL::OCSP::Request.new
548
+ ocsp_request.add_certid(cid)
549
+ ocsp_request.add_nonce
550
+ ocsp_request
551
+ end
552
+
553
+ # @param ocsp_request [OpenSSL::OCSP::Request]
554
+ # @param uri_string [String]
555
+ #
556
+ # @raise [Net::OpenTimeout, OpenSSL::OCSP::OCSPError, URI::$Exception]
557
+ #
558
+ # @return [OpenSSL::OCSP::Response, n
559
+ def send_ocsp_request(ocsp_request, uri_string)
560
+ # send HTTP POST
561
+ uri = URI.parse(uri_string)
562
+ path = uri.path
563
+ path = '/' if path.nil? || path.empty?
564
+ http_response = Net::HTTP.start(uri.host, uri.port) do |http|
565
+ http.post(
566
+ path,
567
+ ocsp_request.to_der,
568
+ 'content-type' => 'application/ocsp-request'
569
+ )
570
+ end
571
+
572
+ OpenSSL::OCSP::Response.new(http_response.body)
573
+ end
574
+ end
542
575
  end
543
576
  # rubocop: enable Metrics/ClassLength
544
577
  end
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- Dir[File.dirname(__FILE__) + '/cryptograph/*.rb'].each { |f| require f }
3
+ Dir[File.dirname(__FILE__) + '/cryptograph/*.rb'].sort.each { |f| require f }
@@ -44,8 +44,7 @@ module TTTLS13
44
44
  #
45
45
  # @return [String]
46
46
  def encrypt(content, type)
47
- reset_cipher
48
- cipher = @cipher.encrypt
47
+ cipher = reset_cipher
49
48
  plaintext = content + type + "\x00" * @length_of_padding
50
49
  cipher.auth_data = additional_data(plaintext.length)
51
50
  encrypted_data = cipher.update(plaintext) + cipher.final
@@ -66,8 +65,7 @@ module TTTLS13
66
65
  # @return [String]
67
66
  # @return [TTTLS13::Message::ContentType]
68
67
  def decrypt(encrypted_record, auth_data)
69
- reset_cipher
70
- decipher = @cipher.decrypt
68
+ decipher = reset_decipher
71
69
  auth_tag = encrypted_record[-@auth_tag_len..-1]
72
70
  decipher.auth_tag = auth_tag
73
71
  decipher.auth_data = auth_data # record header of TLSCiphertext
@@ -105,11 +103,26 @@ module TTTLS13
105
103
  + ciphertext_len.to_uint16
106
104
  end
107
105
 
106
+ # @return [OpenSSL::Cipher]
108
107
  def reset_cipher
109
- @cipher.reset
110
- @cipher.key = @write_key
108
+ cipher = @cipher.encrypt
109
+ cipher.reset
110
+ cipher.key = @write_key
111
111
  iv_len = CipherSuite.iv_len(@cipher_suite)
112
- @cipher.iv = @sequence_number.xor(@write_iv, iv_len)
112
+ cipher.iv = @sequence_number.xor(@write_iv, iv_len)
113
+
114
+ cipher
115
+ end
116
+
117
+ # @return [OpenSSL::Cipher]
118
+ def reset_decipher
119
+ decipher = @cipher.decrypt
120
+ decipher.reset
121
+ decipher.key = @write_key
122
+ iv_len = CipherSuite.iv_len(@cipher_suite)
123
+ decipher.iv = @sequence_number.xor(@write_iv, iv_len)
124
+
125
+ decipher
113
126
  end
114
127
 
115
128
  # @param clear [String]
@@ -78,4 +78,4 @@ module TTTLS13
78
78
  end
79
79
  end
80
80
 
81
- Dir[File.dirname(__FILE__) + '/message/*.rb'].each { |f| require f }
81
+ Dir[File.dirname(__FILE__) + '/message/*.rb'].sort.each { |f| require f }