Search code examples
rubysslthincertificate-authority

How do I choose a certificate authority file when using thin?


I know I can choose to verify the client with --ssl-verify, but how can I specify which CA chain that I want to use? I'm used to providing a file (like with curl's --cacert or WEBrick's :SSLCACertificateFile), so I've got one ready, but I can't seem to find documentation on how to pass it to thin.


Solution

  • Short answer: You can't.

    Long answer: You could, but you'd have to update EventMachine's C++ extension that builds the ssl connection, and update the call stack up through EventMachine and Thin to pass the certificate authority file along.

    How I found this out: Source Code! It's all on github

    • thin's command line opts are parsed in thin:lib/thin/runner.rb

      opts.separator "SSL options:"
      
      opts.on(      "--ssl", "Enables SSL")                                           { @options[:ssl] = true }
      opts.on(      "--ssl-key-file PATH", "Path to private key")                     { |path| @options[:ssl_key_file] = path }
      opts.on(      "--ssl-cert-file PATH", "Path to certificate")                    { |path| @options[:ssl_cert_file] = path }
      opts.on(      "--ssl-verify", "Enables SSL certificate verification")           { @options[:ssl_verify] = true }
      
    • and then used to create a controller

      controller = case
      when cluster? then Controllers::Cluster.new(@options)
      when service? then Controllers::Service.new(@options)
      else               Controllers::Controller.new(@options)
      end
      
    • In thin:lib/controllers/controller.rb the ssl options are pulled back out to be stored with the server object

      # ssl support
      if @options[:ssl]
        server.ssl = true
        server.ssl_options = { :private_key_file => @options[:ssl_key_file], :cert_chain_file => @options[:ssl_cert_file], :verify_peer => @options[:ssl_verify] }
      end
      
    • and are finally used to initialize the connection to the client

      def initialize_connection(connection)
        connection.backend                 = self
        connection.app                     = @server.app
        connection.comm_inactivity_timeout = @timeout
        connection.threaded                = @threaded
      
        if @ssl
          connection.start_tls(@ssl_options)
        end
      
    • This connection is an EventMachine::Connection, defined in eventmachine:lib/em/connection.rb. EventMachine::Connection#start_tls passes the parameters along to EventMachine::set_tls_parms.

      def start_tls args={}
        priv_key, cert_chain, verify_peer = args.values_at(:private_key_file, :cert_chain_file, :verify_peer)
      
        [priv_key, cert_chain].each do |file|
          next if file.nil? or file.empty?
          raise FileNotFoundException,
          "Could not find #{file} for start_tls" unless File.exists? file
        end 
      
        EventMachine::set_tls_parms(@signature, priv_key || '', cert_chain || '', verify_peer)
        EventMachine::start_tls @signature
      end 
      
    • EventMachine::set_tls_parms is part of the C++ extension and is defined in eventmachine:ext/rubymain.cpp as the five argument C function t_set_tls_parms

      rb_define_module_function (EmModule, "set_tls_parms", (VALUE(*)(...))t_set_tls_parms, 4);
      
    • And t_set_tls_parms defined elsewhere in the same file just passes the ssl options on to evma_set_tls_parms.

      static VALUE t_set_tls_parms (VALUE self, VALUE signature, VALUE privkeyfile, VALUE certchainfile, VALUE verify_peer)
      {
        /* set_tls_parms takes a series of positional arguments for specifying such things
         * as private keys and certificate chains.
         * It's expected that the parameter list will grow as we add more supported features.
         * ALL of these parameters are optional, and can be specified as empty or NULL strings.
         */
        evma_set_tls_parms (NUM2ULONG (signature), StringValuePtr (privkeyfile), StringValuePtr (certchainfile), (verify_peer == Qtrue ? 1 : 0));
        return Qnil;
      }
      
    • The vanilla C function evma_set_tls_parms is defined in eventmachine:ext/cmain.cpp. It passes the ssl options on to EventableDescriptor's SetTlsParms method:

      extern "C" void evma_set_tls_parms (const unsigned long binding, const char *privatekey_filename, const char *certchain_filename, int verify_peer)
      {
        ensure_eventmachine("evma_set_tls_parms");
        EventableDescriptor *ed = dynamic_cast <EventableDescriptor*> (Bindable_t::GetObject (binding));
        if (ed)
          ed->SetTlsParms (privatekey_filename, certchain_filename, (verify_peer == 1 ? true : false));
      } 
      
    • That SetTlsParms instance method is defined in eventmachine:ed.cpp, and all it really does is cache the ssl options in some instance variables.

      void ConnectionDescriptor::SetTlsParms (const char *privkey_filename, const char *certchain_filename, bool verify_peer)
      {
        #ifdef WITH_SSL
        if (SslBox)
          throw std::runtime_error ("call SetTlsParms before calling StartTls");
        if (privkey_filename && *privkey_filename)
          PrivateKeyFilename = privkey_filename;
        if (certchain_filename && *certchain_filename)
          CertChainFilename = certchain_filename;
        bSslVerifyPeer = verify_peer;
        #endif
      
        #ifdef WITHOUT_SSL
        throw std::runtime_error ("Encryption not available on this event-machine");
        #endif
      }
      
    • Those instance variables are used later in the StartTls instance method (defined in the same file), and passed on to initialize a new SslBox_t

      void ConnectionDescriptor::StartTls()
      {
        #ifdef WITH_SSL
        if (SslBox)
          throw std::runtime_error ("SSL/TLS already running on connection");
      
        SslBox = new SslBox_t (bIsServer, PrivateKeyFilename, CertChainFilename, bSslVerifyPeer, GetBinding());
        _DispatchCiphertext();
        #endif
      
    • The SslBox_t constructor is defined in eventmachine:ext/ssl.cpp, where it uses the ssl options to initialize a new SslContext_t.

      SslBox_t::SslBox_t (bool is_server, const string &privkeyfile, const string &certchainfile, bool verify_peer, const unsigned long binding):
        bIsServer (is_server),
        bHandshakeCompleted (false),
        bVerifyPeer (verify_peer),
        pSSL (NULL),
        pbioRead (NULL),
        pbioWrite (NULL)
      {
        /* TODO someday: make it possible to re-use SSL contexts so we don't have to create
         * a new one every time we come here.
         */
      
        Context = new SslContext_t (bIsServer, privkeyfile, certchainfile);
        assert (Context);
      
    • The SslContext_t constructor is defined in the same file where it uses those options with the standard OpenSSL C bindings:

      // The SSL_CTX calls here do NOT allocate memory.
      int e;
      if (privkeyfile.length() > 0)
        e = SSL_CTX_use_PrivateKey_file (pCtx, privkeyfile.c_str(), SSL_FILETYPE_PEM);
      else
        e = SSL_CTX_use_PrivateKey (pCtx, DefaultPrivateKey);
      if (e <= 0) ERR_print_errors_fp(stderr);
      assert (e > 0);
      
      if (certchainfile.length() > 0)
        e = SSL_CTX_use_certificate_chain_file (pCtx, certchainfile.c_str());
      else
        e = SSL_CTX_use_certificate (pCtx, DefaultCertificate);
      if (e <= 0) ERR_print_errors_fp(stderr);
      assert (e > 0);
      

    So now we know how the ssl options are used. If the call chain were modified to pass a CA file name along with the rest down to this point, say as const string &certauthfile, we could use just a couple more OpenSSL calls to add the authority file:

    if (certauthfile.length() > 0)
      e = SSL_CTX_load_verify_locations(pCtx, certauthfile.c_str(), NULL);
    else
      ;// no default necessary
    if (e <= 0) ERR_print_errors_fp(stderr);
    assert (e > 0);
    

    Submitting a patch to do this is left as an exercise for the sufficiently motivated.