Paul Grinberg
Paul Grinberg

Reputation: 1407

Boost Beast Websocket verify client certificate more than once on connection

I have a Boost Beast asio based websocket server, which (in abbreviated form) starts like this

ssl_context_.set_options(
    boost::asio::ssl::context::default_workarounds |
    boost::asio::ssl::context::no_sslv2            |
    boost::asio::ssl::context::no_sslv3            |
    boost::asio::ssl::context::no_tlsv1            |
    boost::asio::ssl::context::no_tlsv1_1
    );

ssl_context_.use_certificate_file(
    auth->server_cert,
    boost::asio::ssl::context::file_format::pem
    );

ssl_context_.use_private_key_file(
    auth->server_key,
    boost::asio::ssl::context::file_format::pem
    );

ssl_context_.load_verify_file(auth->ca);

ssl_context_.set_verify_mode(
    boost::asio::ssl::verify_peer |
    boost::asio::ssl::verify_fail_if_no_peer_cert
    );

ssl_context_.set_verify_callback(
    std::bind(
      &verify_certificate_cb, std::placeholders::_1, std::placeholders::_2
      )
    );
    
try
{
  const boost::asio::ip::tcp::endpoint endpoint(boost::asio::ip::make_address(ip), port);
  acceptor_.open(endpoint.protocol());
  acceptor_.set_option(boost::asio::socket_base::reuse_address(true));
  acceptor_.bind(endpoint);
  acceptor_.listen();
}
catch(const boost::system::system_error &e)
{
  LOG_ERROR("Acceptor error: " << e.what());
  return;
}
 
acceptor_.async_accept(io_context_, boost::beast::bind_front_handler(&server::accept_handler, this));

and then, I have

static bool verify_certificate_cb(bool preverified, boost::asio::ssl::verify_context& ctx)
{
  char subject_name[256];
  X509* cert = X509_STORE_CTX_get_current_cert(ctx.native_handle());
  X509_NAME_oneline(X509_get_subject_name(cert), subject_name, 256);
  LOG_INFO("TLS connection verification for: " << subject_name);
  return preverified;
}

void server::accept_handler(const boost::system::error_code& error, boost::asio::ip::tcp::socket socket) noexcept
{
  if (!error)
  {
    // Using Boost's asio paradigm, websocket binds its completion handlers to a shared ptr of this object.
    // This allows the object to extend its lifetime beyond this scope until it's operations are done.
    // Once the all async operations are done, no reference remains, finally ending the lifetime of this object.
    const auto ws(std::make_shared<websocket>(std::move(socket), ssl_context_, auth_));
    
    // Initiate websocket handshake
    ws->handshake();

    // Continue with accepting the next available connection
    acceptor_.async_accept(io_context_, boost::beast::bind_front_handler(&server::accept_handler, this));
  }
  else
  {
    LOG_ERROR("Acceptor error: " << error.message());
  }
}

This works great for validating client certificates on initial connection. The problem is that this websocket server expects clients to be connected for a long time, possibly longer than the validity of the client certificate. In other words, I need to periodically re-check the client certificate after the initial connection is made. What is the best way to do that?

UPDATE 1: post @sehe comment I agree my question seems related to https://stackoverflow.com/a/77327517/4071435. I switched from ssl_context_.set_verify_mode() to SSL_CTX_set_verify() because the former does not support SSL_VERIFY_POST_HANDSHAKE. Then, in my downstream websocket async write handler (just a convenient place), I added SSL_verify_client_post_handshake(websocket_.next_layer().native_handle()); but that always returns 0, which is a failure. So, I seem to be missing something.

My test client is a python3-websockets based application

import websocket, ssl
import time

my_context = ssl.create_default_context()
my_context.load_verify_locations('/usr/share/www/daikin-txrx-ws/public_html/daikin-txrx-ws-ca.crt')
my_context.load_cert_chain(
        '/usr/share/www/daikin-txrx-ws/public_html/daikin-txrx-ws-client.crt',
        '/usr/share/www/daikin-txrx-ws/public_html/daikin-txrx-ws-client.key')
my_context.check_hostname = False
my_context.post_handshake_auth = True
my_context.verify_mode = ssl.CERT_REQUIRED

ws = websocket.WebSocket(sslopt={'context': my_context})
ws.connect('wss://localhost:889');

while True:
    ws.send("Hello, Server")
    print(ws.recv())
    time.sleep(1)

What else am I missing?

UPDATE 2: Better, but still missing something.

The problem with update1 above stemmed from the fact that SSL_verify_client_post_handshake() is a TLSv1.3 feature. The reason the code seems to not work is because openssl s_client and pythons-websocket both connect with TLSv1.2. When I add -tls1_3 to openssl s_client to force TLSv1.3, boost beast websocket rejects the connection with unsupported protocol.

This led me to find (an obvious in hindsight) problem. I had initialized my ssl_context with boost::asio::ssl::context::tlsv12. I changed it to boost::asio::ssl::context::tlsv13 and both clients were able to connect with TLSv1.3!

This also changed the behavior of SSL_verify_client_post_handshake(). The first invocation now returns 1 (YAY!!!), but the next invocation, about 1 second later returns 0. Seems like I'm very close now, but still missing something ...

UPDATE 3: It works! Actually, update 2 was working. The second call to SSL_verify_client_post_handshake() returning 0 is just an indication that the previous request wasn't yet completed.

Upvotes: 0

Views: 86

Answers (1)

farouq sonar
farouq sonar

Reputation: 1

To verify the client certificate more than once on a connection, you can implement a custom verification callback that checks the certificate at each handshake. Make sure to handle the state of the connection properly to avoid issues.

Upvotes: -3

Related Questions