class SSLTest::TestListener

TestListener is the real workhorse used by SSLTestCases. It builds on the SSLSmartProxy from PacketThief in order to intercept and forward SSL connections. It uses SSLSmartProxy because SSLSmartProxy provides a default behavior where it grabs the remote certificate from the destination and re-signs it before presenting it to the client.

TestListener expands on this by presenting the configured test chain instead of the re-signed remote certificate when the destination corresponds to the hostname the test suite is testing off of.

Public Class Methods

cert_matches_host(cert, hostname) click to toggle source

Return true if cert’s CNAME or subjectAltName matches hostname, otherwise return false.

# File lib/ssl_test/test_listener.rb, line 86
def self.cert_matches_host(cert, hostname)
  OpenSSL::SSL.verify_certificate_identity(cert, hostname)
end
new(tcpsocket, test_manager, logger=nil) click to toggle source

For all hosts that do not match hosttotest, we currently use the cacert and re-sign the original cert provided by the actual host. This will cause issues with certificate revocation.

  • cacert [OpenSSL::X509::Certificate] A CA that the client should trust.

  • cakey [OpenSSL::PKey::PKey] The CA's key, needed for resigning. It will also be the key used by the resigned certificates.

  • hosttotest [String] The hostname we want to apply the test chain to.

  • chaintotest [Array<OpenSSL::X509Certificate>] A chain of certs to present when the client attempts to connect to hostname.

  • keytotest [OpenSSL::PKey::PKey] The key corresponding to the leaf node in chaintotest.

# File lib/ssl_test/test_listener.rb, line 27
def initialize(tcpsocket, test_manager, logger=nil)
  @test_manager = test_manager

  if @test_manager.paused?
    @paused = true
  else
    @paused = false
    @test = @test_manager.current_test
    @hosttotest = @test.hosttotest
    chain = @test.certchain.dup
    @hostcert = chain.shift
    @hostkey = @test.keychain[0]
    @extrachain = chain
  end
  # Use the goodca for hosts we don't care to test against.
  super(tcpsocket, @test_manager.goodcacert, @test_manager.goodcakey, logger)

  @test_status = :running
  @testing_host = false
end

Public Instance Methods

check_for_hosttotest(actx) click to toggle source

Replaces the certificates used in the SSLContext with the test certificates if the destination matches the hostname we wish to test against. Otherwise, it leaves the context alone.

Additionally, if it matches, it sets @testing_host to true to check whether the test succeeds or not.

# File lib/ssl_test/test_listener.rb, line 69
def check_for_hosttotest(actx)
  if @paused
    logdebug "Testing is paused, not checking whether this is the host to test", :certcubject => actx.cert.subject
  elsif TestListener.cert_matches_host(actx.cert, @hosttotest)
    logdebug "Destination matches host-to-test", :hosttotest => @hosttotest, :certsubject => actx.cert.subject, :testname => @test.id
    actx.cert = @hostcert
    actx.key = @hostkey
    actx.extra_chain_cert = @extrachain
    @testing_host = true
  else
    logdebug "Destination does not match host-to-test", :hosttotest => @hosttotest, :certsubject => actx.cert.subject
  end
  actx
end
client_recv(data) click to toggle source

#client_recv means that the client sent data over the TLS connection, meaning they definately trusted our certificate chain.

# File lib/ssl_test/test_listener.rb, line 128
def client_recv(data)
  if @testing_host
    @test_status = :sentdata
    if @test_manager.testing_method == 'senddata'
      @test_manager.test_completed(@test, @test_status)
      @testing_host = false
    end
  end
  super(data)
end
post_init() click to toggle source

Checks whether the initial original destination certificate (without SNI hostname) matches the test hostname. We do this with #post_init to have the check happen after the parent class already added a re-signed certificate to +@ctx+.

# File lib/ssl_test/test_listener.rb, line 52
def post_init
  check_for_hosttotest(@ctx)
end
servername_cb(sslsock, hostname) click to toggle source

Checks whether the original destination certificate after we handle the SNI hostname matches the test hostname. Super already replaced the context with a certificate based on the remote host’s certificate.

# File lib/ssl_test/test_listener.rb, line 59
def servername_cb(sslsock, hostname)
  check_for_hosttotest(super(sslsock, hostname))
end
tls_failed_handshake(e) click to toggle source

If the handshake failed, then the client rejected our cert chain.

# File lib/ssl_test/test_listener.rb, line 106
def tls_failed_handshake(e)
  super
  logdebug "failed handshake"
  if @testing_host
    @test_status = :rejected
    @test_manager.test_completed(@test, @test_status)
    @testing_host = false
  end
end
tls_successful_handshake() click to toggle source

If the client completes connecting, we might consider that trusting our certificate chain. However, at least Java’s SSL client classes don’t reject until after completing the handshake.

# File lib/ssl_test/test_listener.rb, line 93
def tls_successful_handshake
  super
  logdebug "successful handshake"
  if @testing_host
    @test_status = :connected
    if @test_manager.testing_method == 'tlshandshake'
      @test_manager.test_completed(@test, @test_status)
      @testing_host = false
    end
  end
end
unbind() click to toggle source

Report our result.

# File lib/ssl_test/test_listener.rb, line 117
def unbind
  super
  logdebug "unbind"
  if @testing_host
    @test_manager.test_completed(@test, @test_status)
    @testing_host = false
  end
end