tttls1.3 0.2.9 → 0.2.14

Sign up to get free protection for your applications and to get access to all the features.
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 }