Search code examples
c++opensslboost-asiohandshaketls1.3

TLSv1.3 post-handshake: server only verifies the client certificate when the connection is closed


Info / TL;DR:

The problem still exists but a workaround is located at the end of this post

The problem:

I have a problem where I just don't know what to do. I am using Boost 1.83 and OpenSSL 3.1 and I am trying to create a TLS server with post-handshake support. In other words, for certain client requests, I want to trigger client authentication after the TLS handshake. The TLS message exchange seems to work, but the order of processing is not correct. Currently, the server sends the response and then verifies the client's authenticity, because OpenSSL does not trigger the callback until the TLS connection is closed.

What am I doing wrong?

Sometimes the server still sends data after a "close notify" message, which is also strange. Can anyone help?

See below for minimal example of the server and a matching client. The problem also occurs with an asynchronous boost/asio implementation.

Here is the output after communicating with the client:

server ready
acceptor ok
handshake ok
read ok; client request message (30 bytes received): hello server, authenticate me!
2nd handshake ok
write ok; server response message (27 bytes transmitted): this is the server response
shutdown the connection...
client certificate valid: /C=DE/ST=RootCA_ST/L=RootCA_L/O=RootCA_O/OU=RootCA_OU/CN=RootCA_CN
client certificate valid: /C=DE/ST=TLS_Client_ST/L=TLS_Client_L/O=TLS_Client_O/OU=TLS_Client_OU/CN=TLS_Client_CN
shutdown: The operation was completed successfully

END

Here is the output with additional debug information:

server ready
acceptor ok
TLS status: before SSL initialization
TLS status: SSLv3/TLS read client hello
TLS status: SSLv3/TLS write server hello
TLS status: SSLv3/TLS write server hello
TLS status: SSLv3/TLS write change cipher spec
TLS status: SSLv3/TLS write change cipher spec
TLS status: TLSv1.3 write encrypted extensions
TLS status: TLSv1.3 write encrypted extensions
TLS status: TLSv1.3 write encrypted extensions
TLS status: SSLv3/TLS write certificate
TLS status: SSLv3/TLS write certificate
TLS status: SSLv3/TLS write certificate
TLS status: TLSv1.3 write server certificate verify
TLS status: TLSv1.3 write server certificate verify
TLS status: TLSv1.3 write server certificate verify
TLS status: SSLv3/TLS write finished
TLS status: SSLv3/TLS write finished
TLS status: SSLv3/TLS write finished
TLS status: TLSv1.3 early data
TLS status: TLSv1.3 early data
TLS status: TLSv1.3 early data
TLS status: SSLv3/TLS read finished
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
handshake ok
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
read ok; client request message (30 bytes received): hello server, authenticate me!
TLS status: SSLv3/TLS write certificate request
TLS status: SSLv3/TLS write certificate request
TLS status: SSLv3/TLS write certificate request
2nd handshake ok
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
write ok; server response message (27 bytes transmitted): this is the server response
shutdown the connection...
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
TLS status: SSLv3/TLS read client certificate
client certificate valid: /C=DE/ST=RootCA_ST/L=RootCA_L/O=RootCA_O/OU=RootCA_OU/CN=RootCA_CN
client certificate valid: /C=DE/ST=TLS_Client_ST/L=TLS_Client_L/O=TLS_Client_O/OU=TLS_Client_OU/CN=TLS_Client_CN
TLS status: SSLv3/TLS read client certificate
TLS status: SSLv3/TLS read client certificate
TLS status: SSLv3/TLS read certificate verify
TLS status: SSLv3/TLS read certificate verify
TLS status: SSLv3/TLS read certificate verify
TLS status: SSLv3/TLS read finished
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
shutdown: An existing connection was forcibly closed by the remote host

END

Here is the Wireshark output with the markers when certain code sections of the server are reached (127.0.0.1: client; 127.0.0.5: server):

No.     Time            Source     Destination   Protocol  Length   Info
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
      1 0.000662       127.0.0.1   127.0.0.5     TLSv1.3   287      Client Hello

<TCP: acceptor>
<TLS: handshake>

      2 16.029938      127.0.0.5   127.0.0.1     TLSv1.3   1478     Server Hello, Change Cipher Spec, Application Data, Application Data, Application Data, Application Data
      3 16.032142      127.0.0.1   127.0.0.5     TLSv1.3   124      Change Cipher Spec, Application Data
      4 16.032351      127.0.0.5   127.0.0.1     TLSv1.3   554      Application Data, Application Data
      5 16.032378      127.0.0.1   127.0.0.5     TLSv1.3   79       Application Data   --> this is the client request message

<TLS: read>
<TLS: post-handshake authentication of the client --> SSL_verify_client_post_handshake()>
<TLS: post-handshake authentication of the client --> 2nd handshake>

      6 51.594902      127.0.0.5   127.0.0.1     TLSv1.3   143      Application Data
      7 51.598791      127.0.0.1   127.0.0.5     TLSv1.3   1323     Application Data, Application Data, Application Data

<TLS: write>

      8 62.972860      127.0.0.5   127.0.0.1     TLSv1.3   93       Application Data   --> this is the server response message ()

<TLS: shutdown>

      9 68.777818      127.0.0.5   127.0.0.1     TLSv1.3   68       Application Data   --> close_notify of the server
     10 68.779306      127.0.0.1   127.0.0.5     TLSv1.3   68       Application Data   --> close_notify of the client
     11 68.780150      127.0.0.5   127.0.0.1     TLSv1.3   1674     Application Data, Application Data   --> well... I dont know how this happens here.

< --- verifyCertificate is invoked --- >
<end of function>

this is strange.... after the 'post-handshake authentication' in the code the server sends a certificate request and gets this also from the client, but the callback for the verification is called only after the shutdown(). ...there it is of course too late and it should happen before <TLS: write>.

any ideas? Do I still have to "instruct"/configure OpenSSL somehow?

Replying to sehe

Hello sehe. Thank you for your support and the time you spend for me. I also thought of using Cancellation, but unfortunately it doesn't work in my use case. I don't understand the behavior of my code anymore and that frustrates me. Maybe I'm blind and can't see the real problem. Unfortunately, I can't find any third-party code or examples to see how others have solved it.

The behavior of the server is strange.... If socket.write() is called after the second handshake (in the server code: 'TLS: post-handshake authentication'), then the client certificate is verified after shutdown() (see server trace above).

If a socket.read() is called after the second handshake, then the client certificate is verified immediately, but read() (or read_some()) blocks because the client sends nothing more:

server ready
acceptor ok
TLS status: before SSL initialization
TLS status: SSLv3/TLS read client hello
TLS status: SSLv3/TLS write server hello
TLS status: SSLv3/TLS write server hello
TLS status: SSLv3/TLS write change cipher spec
TLS status: SSLv3/TLS write change cipher spec
TLS status: TLSv1.3 write encrypted extensions
TLS status: TLSv1.3 write encrypted extensions
TLS status: TLSv1.3 write encrypted extensions
TLS status: SSLv3/TLS write certificate
TLS status: SSLv3/TLS write certificate
TLS status: SSLv3/TLS write certificate
TLS status: TLSv1.3 write server certificate verify
TLS status: TLSv1.3 write server certificate verify
TLS status: TLSv1.3 write server certificate verify
TLS status: SSLv3/TLS write finished
TLS status: SSLv3/TLS write finished
TLS status: SSLv3/TLS write finished
TLS status: TLSv1.3 early data
TLS status: TLSv1.3 early data
TLS status: TLSv1.3 early data
TLS status: SSLv3/TLS read finished
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
handshake ok
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
read ok; client request message (30 bytes received): hello server, authenticate me!
TLS status: SSLv3/TLS write certificate request
TLS status: SSLv3/TLS write certificate request
TLS status: SSLv3/TLS write certificate request
2nd handshake ok
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
TLS status: SSLv3/TLS read client certificate
client certificate valid: /C=DE/ST=RootCA_ST/L=RootCA_L/O=RootCA_O/OU=RootCA_OU/CN=RootCA_CN
client certificate valid: /C=DE/ST=TLS_Client_ST/L=TLS_Client_L/O=TLS_Client_O/OU=TLS_Client_OU/CN=TLS_Client_CN
TLS status: SSLv3/TLS read client certificate
TLS status: SSLv3/TLS read client certificate
TLS status: SSLv3/TLS read certificate verify
TLS status: SSLv3/TLS read certificate verify
TLS status: SSLv3/TLS read certificate verify
TLS status: SSLv3/TLS read finished
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket

If a socket.read() is called after the second handshake, and the client sends a second message after that, read() will no longer block, but the certificate will not be verified at all:

server ready
acceptor ok
TLS status: before SSL initialization
TLS status: SSLv3/TLS read client hello
TLS status: SSLv3/TLS write server hello
TLS status: SSLv3/TLS write server hello
TLS status: SSLv3/TLS write change cipher spec
TLS status: SSLv3/TLS write change cipher spec
TLS status: TLSv1.3 write encrypted extensions
TLS status: TLSv1.3 write encrypted extensions
TLS status: TLSv1.3 write encrypted extensions
TLS status: SSLv3/TLS write certificate
TLS status: SSLv3/TLS write certificate
TLS status: SSLv3/TLS write certificate
TLS status: TLSv1.3 write server certificate verify
TLS status: TLSv1.3 write server certificate verify
TLS status: TLSv1.3 write server certificate verify
TLS status: SSLv3/TLS write finished
TLS status: SSLv3/TLS write finished
TLS status: SSLv3/TLS write finished
TLS status: TLSv1.3 early data
TLS status: TLSv1.3 early data
TLS status: TLSv1.3 early data
TLS status: SSLv3/TLS read finished
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
handshake ok
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
read ok; client request message (30 bytes received): hello server, authenticate me!
TLS status: SSLv3/TLS write certificate request
TLS status: SSLv3/TLS write certificate request
TLS status: SSLv3/TLS write certificate request
2nd handshake ok
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
read ok; client request message (30 bytes received): hello server, authenticate me!
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
write ok; server response message (27 bytes transmitted): this is the server response
shutdown the connection...
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
shutdown: application data after close notify (SSL routines)

END

.... and with socket.async_read_some() the server will not block either, but the client certificate will also not be verified:

server ready
acceptor ok
TLS status: before SSL initialization
TLS status: SSLv3/TLS read client hello
TLS status: SSLv3/TLS write server hello
TLS status: SSLv3/TLS write server hello
TLS status: SSLv3/TLS write change cipher spec
TLS status: SSLv3/TLS write change cipher spec
TLS status: TLSv1.3 write encrypted extensions
TLS status: TLSv1.3 write encrypted extensions
TLS status: TLSv1.3 write encrypted extensions
TLS status: SSLv3/TLS write certificate
TLS status: SSLv3/TLS write certificate
TLS status: SSLv3/TLS write certificate
TLS status: TLSv1.3 write server certificate verify
TLS status: TLSv1.3 write server certificate verify
TLS status: TLSv1.3 write server certificate verify
TLS status: SSLv3/TLS write finished
TLS status: SSLv3/TLS write finished
TLS status: SSLv3/TLS write finished
TLS status: TLSv1.3 early data
TLS status: TLSv1.3 early data
TLS status: TLSv1.3 early data
TLS status: SSLv3/TLS read finished
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
TLS status: SSLv3/TLS write session ticket
handshake ok
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
read ok; client request message (30 bytes received): hello server, authenticate me!
TLS status: SSLv3/TLS write certificate request
TLS status: SSLv3/TLS write certificate request
TLS status: SSLv3/TLS write certificate request
2nd handshake ok
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
write ok; server response message (27 bytes transmitted): this is the server response
shutdown the connection...
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
TLS status: SSL negotiation finished successfully
shutdown: decryption failed or bad record mac (SSL routines)

Here the full example code and the test certificates plus keys

TLSv1.3 server

#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/bind.hpp>
#include <string>
#include <vector>
#include <iostream>

class TlsServer
{
public:
    TlsServer(boost::asio::io_service& ioService, uint16_t port)
        :
        m_ioService(ioService),
        m_acceptor(ioService, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)),
        m_context(boost::asio::ssl::context::tlsv13_server)
    {
        // set certificates
        m_context.load_verify_file("rootCA_cert.pem");
        m_context.use_certificate_chain_file("server_cert.pem");
        m_context.use_private_key_file("server_key.pem", boost::asio::ssl::context::pem);

        // certificate callback
        SSL_CTX_set_verify(m_context.native_handle(), SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT | SSL_VERIFY_POST_HANDSHAKE, nullptr);
        m_context.set_verify_callback(boost::bind(&TlsServer::verifyCertificate, this, _1, _2));

        std::cout << "server ready" << std::endl;
        wholeProcedure();
    }

    void wholeProcedure()
    {
        boost::asio::ssl::stream<boost::asio::ip::tcp::socket> socket(m_ioService, m_context);
        boost::system::error_code ec;

        // additional informations (optinal)
        SSL_set_msg_callback(socket.native_handle(),
            [](int write_p, int version, int content_type, const void* buf, size_t len, SSL* ssl, void* arg)
            {std::cout << "TLS status: \t" << SSL_state_string_long(ssl) << std::endl; });

        // TCP: acceptor
        m_acceptor.accept(socket.lowest_layer(), ec);
        if (ec)
        {
            std::cout << "acceptor error: " << ec.message() << std::endl;
            return;
        }
        else
        {
            std::cout << "acceptor ok" << std::endl;
        }

        // TLS: handshake
        socket.handshake(boost::asio::ssl::stream_base::server, ec);
        if (ec)
        {
            std::cout << "handshake error: " << ec.message() << std::endl;
            return;
        }
        else
        {
            std::cout << "handshake ok" << std::endl;
        }

        // TLS: read (receive client request)
        std::vector<char> buffer(2048);
        std::size_t bytesReceived = socket.read_some(boost::asio::buffer(buffer, buffer.size()), ec);
        if (ec)
        {
            std::cout << "read error: " << ec.message() << std::endl;
            return;
        }
        else
        {
            std::string requestStr(buffer.begin(), buffer.begin() + bytesReceived);
            std::cout << "read ok; client request message (" << bytesReceived << " bytes received): " << requestStr << std::endl;
        }

        // TLS: post-handshake authentication of the client
        if (SSL_verify_client_post_handshake(socket.native_handle()) == 0)
        {
            std::cout << "post-handshake error: " << ec.message() << std::endl;
            return;
        }
        socket.handshake(boost::asio::ssl::stream_base::server, ec);
        if (ec)
        {
            std::cout << "2nd handshake error: " << ec.message() << std::endl;
            return;
        }
        else
        {
            std::cout << "2nd handshake ok" << std::endl;
        }

        // TLS: 2nd read (receive client request)
        bytesReceived = socket.read_some(boost::asio::buffer(buffer, buffer.size()), ec);
        if (ec)
        {
            std::cout << "read error: " << ec.message() << std::endl;
            return;
        }
        else
        {
            std::string requestStr(buffer.begin(), buffer.begin() + bytesReceived);
            std::cout << "read ok; client request message (" << bytesReceived << " bytes received): " << requestStr << std::endl;
        }

        // TLS: write (send server response)
        std::string responseStr = "this is the server response";
        std::vector<unsigned char> responseMsg(responseStr.begin(), responseStr.end());
        std::size_t bytesTransmitted = boost::asio::write(socket, boost::asio::buffer(responseMsg, responseMsg.size()), ec);
        if (ec)
        {
            std::cout << "write error: " << ec.message() << std::endl;
            return;
        }
        else
        {
            std::cout << "write ok; server response message (" << bytesTransmitted << " bytes transmitted): " << responseStr << std::endl;
        }

        // TLS: shutdown
        std::cout << "shutdown the connection..." << std::endl;
        socket.shutdown(ec);
        std::cout << "shutdown: " << ec.message() << std::endl;
    }

    bool verifyCertificate(bool preverified, boost::asio::ssl::verify_context& ctx)
    {
        // get subject name of the certificate
        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);

        if (preverified == false)
        {
            std::cout << "client certificate invalid: " << subject_name << std::endl;
            return false;
        }
        else
        {
            std::cout << "client certificate valid: " << subject_name << std::endl;
            return true;
        }
    }

private:
    boost::asio::io_service& m_ioService;
    boost::asio::ip::tcp::acceptor m_acceptor;
    boost::asio::ssl::context m_context;
};

int main()
{
    try
    {
        uint16_t port = 2405;
        boost::asio::io_service ioService;
        TlsServer tlsServer(ioService, port);
        ioService.run();
    }
    catch (std::exception& e)
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }

    std::cout << "\nEND" << std::endl;
    std::cin.get();
    return 0;
}

TLSv1.3 client

#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <boost/bind.hpp>

class TlsClient
{
public:
    TlsClient(boost::asio::io_service& io_service, boost::asio::ssl::context& context, const boost::asio::ip::tcp::resolver::results_type& endpoint)
        :
        m_buffer(2048),
        m_socket(io_service, context)
    {
        m_socket.set_verify_mode(boost::asio::ssl::verify_peer);
        m_socket.set_verify_callback(boost::bind(&TlsClient::verifyCertificate, this, _1, _2));

        SSL_set_post_handshake_auth(m_socket.native_handle(), true); // enable post-handshake authentication
        TlsClient::connect(endpoint);
    }

    void connect(const boost::asio::ip::tcp::resolver::results_type& endpoint)
    {
        boost::asio::async_connect(m_socket.lowest_layer(), endpoint,
            [this](const boost::system::error_code& error, const boost::asio::ip::tcp::endpoint& endpoint)
            {
            if (error)
            {
            std::cout << "connect() failed: " << error.message() << std::endl;
            return;
            }
            handshake();
            }
        );
    }

    void handshake()
    {
        m_socket.async_handshake(boost::asio::ssl::stream_base::client,
            [this](const boost::system::error_code& error)
            {
            if (error)
            {
            std::cout << "handshake() failed: " << error.message() << std::endl;
            return;
            }
            sendRequest();
            }
        );
    }

    void sendRequest()
    {
        char request[] = "hello server, authenticate me!";
        boost::asio::async_write(m_socket, boost::asio::buffer(request, std::strlen(request)),
            [this](const boost::system::error_code& error, std::size_t bytesTransmitted)
            {
            if (error)
            {
            std::cout << "sendRequest() failed: " << error.message() << std::endl;
            return;
            }
            std::cout << "sendRequest() successful (" << bytesTransmitted << " bytes transmitted)" << std::endl;
            receiveResponse();
            }
        );
    }

    void receiveResponse()
    {
        m_socket.async_read_some(boost::asio::buffer(m_buffer, m_buffer.size()),
            [this](const boost::system::error_code& error, std::size_t bytesReceived)
            {
            if (!error)
            {
            std::cout << "receiveResponse() successful (" << bytesReceived << " bytes received)" << std::endl;
            std::cout << "server response message: ";
            std::cout.write(m_buffer.data(), bytesReceived);
            std::cout << std::endl;
            receiveResponse();
            }
            else if ((error) && (error == boost::asio::error::eof)) // 'end of file' is fine
            {
            std::cout << "receiveResponse(): close_notify received" << std::endl;
            shutdown();
            }
            else
            {
            std::cout << "receiveResponse() failed: " << error.message() << std::endl;
            shutdown();
            }
            }
        );
    }

    void shutdown()
    {
        m_socket.async_shutdown(
            [this](const boost::system::error_code& error)
            {
            if (error)
            {
            std::cout << "shutdown() failed: " << error.message() << std::endl;
            }
            }
        );
    }

    bool verifyCertificate(bool preverified, boost::asio::ssl::verify_context& ctx)
    {
        // get subject name of the certificate
        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);
        if (preverified == false)
        {
            std::cout << "certificate invalid: " << subject_name << std::endl;
            return false;
        }
        else
        {
            std::cout << "certificate valid: " << subject_name << std::endl;
            return true;
        }
    }

private:
    boost::asio::ssl::stream<boost::asio::ip::tcp::socket> m_socket;
    std::vector<char> m_buffer;
};

int main()
{
    try
    {
        boost::asio::io_context io_context;
        boost::asio::ip::tcp::resolver resolver(io_context);
        auto endpoint = resolver.resolve("127.0.0.5", "2405");

        boost::asio::ssl::context ctx(boost::asio::ssl::context::tlsv13_client);
        ctx.load_verify_file("rootCA_cert.pem");
        ctx.use_certificate_chain_file("client_cert.pem");
        ctx.use_private_key_file("client_key.pem", boost::asio::ssl::context::pem);

        TlsClient client(io_context, ctx, endpoint);
        io_context.run();
    }
    catch (std::exception& e)
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }

    std::cout << "\nEND" << std::endl;
    std::cin.get();
    return 0;
}

Certificates and key (Base64/PEM format)

client_cert.pem
================================================================
-----BEGIN CERTIFICATE-----
MIICKzCCAd2gAwIBAgIIaFncvW99ybMwBQYDK2VwMG8xCzAJBgNVBAYTAkRFMRIw
EAYDVQQIDAlSb290Q0FfU1QxETAPBgNVBAcMCFJvb3RDQV9MMREwDwYDVQQKDAhS
b290Q0FfTzESMBAGA1UECwwJUm9vdENBX09VMRIwEAYDVQQDDAlSb290Q0FfQ04w
IBcNMjMwMTAxMDAwMDAwWhgPMjA5OTEyMzEyMzU5NTlaMIGDMQswCQYDVQQGEwJE
RTEWMBQGA1UECAwNVExTX0NsaWVudF9TVDEVMBMGA1UEBwwMVExTX0NsaWVudF9M
MRUwEwYDVQQKDAxUTFNfQ2xpZW50X08xFjAUBgNVBAsMDVRMU19DbGllbnRfT1Ux
FjAUBgNVBAMMDVRMU19DbGllbnRfQ04wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
AAQE/ZKwGG1daBXWmH9bS8Y3cEOHBkkjJVs+BAv+9lFfB99Doc/UdS1ERcIS7B0I
KpZCs1PB3XYIz2jcXaZ/0GSqo1EwTzAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBRP
hb4ZxX4LXb13WkFlfC7G3wc9TzALBgNVHQ8EBAMCA7gwEwYDVR0lBAwwCgYIKwYB
BQUHAwIwBQYDK2VwA0EA6awYvVghrPtPsDcAVzKSuZKojxiuoVXFTE+nIcB1uCBp
5ZGJ8Uj5qWrxe55Xq+M4ffOZZUsb4dBKGxUdTy1BAQ==
-----END CERTIFICATE-----

client_key.pem
================================================================
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEINcbzvL7XAAT2iZVFzjsOqWd7TvDW9jMF/w6E/2+cXkboAoGCCqGSM49
AwEHoUQDQgAEBP2SsBhtXWgV1ph/W0vGN3BDhwZJIyVbPgQL/vZRXwffQ6HP1HUt
REXCEuwdCCqWQrNTwd12CM9o3F2mf9Bkqg==
-----END EC PRIVATE KEY-----

server_cert.pem
================================================================
-----BEGIN CERTIFICATE-----
MIICRTCCAfegAwIBAgIIe2Nr+QCIaNgwBQYDK2VwMG8xCzAJBgNVBAYTAkRFMRIw
EAYDVQQIDAlSb290Q0FfU1QxETAPBgNVBAcMCFJvb3RDQV9MMREwDwYDVQQKDAhS
b290Q0FfTzESMBAGA1UECwwJUm9vdENBX09VMRIwEAYDVQQDDAlSb290Q0FfQ04w
IBcNMjMwMTAxMDAwMDAwWhgPMjA5OTEyMzEyMzU5NTlaMIGDMQswCQYDVQQGEwJE
RTEWMBQGA1UECAwNVExTX1NlcnZlcl9TVDEVMBMGA1UEBwwMVExTX1NlcnZlcl9M
MRUwEwYDVQQKDAxUTFNfU2VydmVyX08xFjAUBgNVBAsMDVRMU19TZXJ2ZXJfT1Ux
FjAUBgNVBAMMDVRMU19TZXJ2ZXJfQ04wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNC
AAQqNsRBYBNkqPYV2TZHNQx5jLF+2gUwaeTYVqGfxJILqwQpIO3hmcEncPtvm9wI
gL8TnV32r53M+d4Dh4GnL9puo2swaTAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBR/
ETSLqThTfoMF4mJosj8yjkFQyDALBgNVHQ8EBAMCA+gwEwYDVR0lBAwwCgYIKwYB
BQUHAwEwGAYDVR0RBBEwD4INVExTX1NlcnZlcl9DTjAFBgMrZXADQQD/6dOs1YHU
q3BnvtI9WI4vPVsuLlhblWR3z6mea3ZXUufvkPsyr2NdNFBMX+GHHxkLvOFNIhX+
dsp7k2j2wLoK
-----END CERTIFICATE-----

server_key.pem
================================================================
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIMfzK4phpnO22uVPYxCg7EjnNFub9OH/ds61chjxX0CAoAoGCCqGSM49
AwEHoUQDQgAEKjbEQWATZKj2Fdk2RzUMeYyxftoFMGnk2Fahn8SSC6sEKSDt4ZnB
J3D7b5vcCIC/E51d9q+dzPneA4eBpy/abg==
-----END EC PRIVATE KEY-----

rootCA_cert.pem
================================================================
-----BEGIN CERTIFICATE-----
MIIB1TCCAYegAwIBAgIIdq9JvkPbtBAwBQYDK2VwMG8xCzAJBgNVBAYTAkRFMRIw
EAYDVQQIDAlSb290Q0FfU1QxETAPBgNVBAcMCFJvb3RDQV9MMREwDwYDVQQKDAhS
b290Q0FfTzESMBAGA1UECwwJUm9vdENBX09VMRIwEAYDVQQDDAlSb290Q0FfQ04w
IBcNMjMwMTAxMDAwMDAwWhgPMjA5OTEyMzEyMzU5NTlaMG8xCzAJBgNVBAYTAkRF
MRIwEAYDVQQIDAlSb290Q0FfU1QxETAPBgNVBAcMCFJvb3RDQV9MMREwDwYDVQQK
DAhSb290Q0FfTzESMBAGA1UECwwJUm9vdENBX09VMRIwEAYDVQQDDAlSb290Q0Ff
Q04wKjAFBgMrZXADIQA56tHp15pu+Y+/oQi5czzu2+uWEDZNZKMkyZMVgfKCjKM/
MD0wDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqECiKkuXkVhe3HXeSlOoAFb1
iUowCwYDVR0PBAQDAgEGMAUGAytlcANBAMdLUzNMj4YPiNNbU+tCHRUyU3z7HgvD
LxRJvjGsgDx/wt/ATK5FgM1Oyo8H5PtUle42AFNYf/mABtH7QPGFmQI=
-----END CERTIFICATE-----

Workaround

What do I want:

Here is a very brief description of what is important to me:

I have a TLSv1.3 server and some TLSv1.3 clients, some of which need to be authenticated and some of which do not. The need is decided after the client request is processed. This is the case, for example, when certain resources or data are requested by the client.

The basic idea was to solve this with post-handshake authentication (PHA). However, it is currently causing problems.

Workaround:

With OpenSSL/Boost, it is possible to have the server request the client certificate at the beginning of the TLS handshake. The server can be configured to continue the handshake even if the client does not send a certificate:

context.set_verify_mode(boost::asio::ssl::verify_peer); // boost variant
SSL_CTX_set_verify(context, SSL_VERIFY_PEER); // OpenSSL variant

If the client does this, it is authenticated instantly.

If I process the client request message after the handshake and authentication is required, then I can check if this has been done:

    if (SSL_get_peer_certificate(m_socket.native_handle()) != NULL)
    {
        if (SSL_get_verify_result(m_socket.native_handle()) == X509_V_OK) // warning: also returns 'X509_V_OK' if no cert present 
        {
            // ok: certificate received and valid
        }
        else
        {
            // fail: certificate received, but invalid
        }
    }
    else
    {
        // fail: no certificate received
    }

For my purposes this solution is good enough and maybe it will help others too.

kind regards, SBond

PS: ...thank you sehe for your help and time :)


Solution

  • After toying with this for a long time I reached the conclusion that the only way to ensure the verification preceeds the authentication response is to write another message in your protocol.

    • I opted for a simple "VERIFICATION REQUEST"/"VERIFICATION RESPONSE" roundtrip,
    • simply async_wait(read|write) on the lowest level socket wasn't enough
    • neither was issueing an empty write (send(""))

    I used an event to be able to asynchronously await the verification response. This may be overcomplicated when you do not want to allow any other messages to be sent before completing the verification step. In that case, simply close the connection when receiving an invalid verification response.

    • File server.cpp

       #include <boost/asio.hpp>
       #include <boost/asio/ssl.hpp>
       #include <deque>
       #include <iomanip>
       #include <iostream>
      
       namespace asio = boost::asio;
       namespace ssl  = asio::ssl;
       using asio::ip::tcp;
       using boost::system::error_code;
       using Socket = ssl::stream<tcp::socket>;
      
       static bool verifyCertificate(bool preverified, ssl::verify_context& ctx) {
           // get subject name of the certificate
           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);
      
           std::cout << "client certificate " << (preverified ? "valid" : "invalid") << ": " << subject_name
                     << std::endl;
           return preverified;
       }
      
       static void tlsDebug(int, int, int, void const*, size_t, SSL* ssl, void*) {
           static_cast<void>(ssl);
           // std::cout << "TLS status: " << SSL_state_string_long(ssl) << std::endl;
       }
      
       struct Session : std::enable_shared_from_this<Session> {
           Session(Socket socket) : socket(std::move(socket)) {}
      
           void Run() {
               // TLS: handshake
               auto self = shared_from_this();
               socket.async_handshake(ssl::stream_base::server, [this, self](error_code ec) {
                   std::cout << "handshake: " << ec.message() << std::endl;
                   reader();
               });
           }
      
         private:
           using time_point = std::chrono::steady_clock::time_point;
           Socket                  socket;
           asio::steady_timer      verification_response{socket.get_executor(), time_point::max()};
           size_t                  request_counter = 0;
           std::deque<std::string> outbox;
           std::string             req;
      
           void writer() {
               if (outbox.empty()) {
                   std::cout << "writer idle (queue: " << outbox.size() << ")" << std::endl;
                   return;
               }
      
               auto self = shared_from_this();
               // TLS: write
               asio::async_write(                //
                   socket,                       //
                   asio::buffer(outbox.front()), //
                   [this, self](error_code ec, size_t /*n*/) {
                       std::cout << "wrote: '" << outbox.front() << "' (" << ec.message() << ")" << std::endl;
                       if (!ec) {
                           outbox.pop_front();
                           writer();
                       }
                   });
           }
      
           void shutdown() {
               // TLS: shutdown
               std::cout << "shutdown the connection..." << std::endl;
               auto self = shared_from_this();
               socket.async_shutdown(
                   [self](error_code ec) { std::cout << "shutdown: " << ec.message() << std::endl; });
           }
      
           void send(std::string msg) {
               outbox.push_back(std::move(msg));
               if (outbox.size() == 1) {
                   std::cout << "writer start (queue: " << outbox.size() << ")" << std::endl;
                   writer();
               }
           }
      
           bool is_verified() const { return verification_response.expiry() <= std::chrono::steady_clock::now(); }
      
           void reader() {
               request_counter += 1;
               // TLS: read (receive client request)
               req.resize(2048);
               auto self = shared_from_this();
               socket.async_read_some(asio::buffer(req), [this, self](error_code ec, size_t n) {
                   std::cout << "read #" << request_counter << ": " << ec.message() << std::endl;
      
                   if (ec) {
                       std::cout << "reader completed" << std::endl;
                       return shutdown();
                   }
      
                   req.resize(n);
                   std::cout << "client request #" << request_counter << ": '" << req << "'" << std::endl;
      
                   if (req == "authentication request") {
                       if (is_verified()) {
                           send("ALREADY AUTHENTICATED");
                           return reader();
                       }
      
                       std::cout << "--- initiating post-handshake peer auth" << std::endl;
                       bool ok = SSL_verify_client_post_handshake(socket.native_handle());
                       std::cout << "post-handshake: " << (ok ? "OK" : "Failed") << std::endl;
      
                       send("VERIFICATION REQUEST");
                       reader(); // await VERIFICATION RESPONSE
      
                       verification_response.async_wait([this, self](error_code ec) {
                           if (ec == asio::error::operation_aborted) {
                               socket.async_handshake( //
                                   ssl::stream_base::server, [this, self](error_code ec) {
                                       std::cout << "2nd handshake: " << ec.message() << std::endl;
                                       if (!ec) {
                                           verification_response.expires_at(std::chrono::steady_clock::now());
                                           send("AUTHENTICATED");
                                           reader();
                                       }
                                   });
                           } else {
                               std::cerr << "Verification failed" << std::endl;
                               shutdown();
                           }
                       });
                   } else if (req == "VERIFICATION RESPONSE") {
                       verification_response.cancel_one(); // can only signal pending auth
                   } else {
                       if (is_verified())
                           send("Authenticated response to '" + req + "'");
                       else
                           send("Generic response to '" + req + "'");
                       reader();
                   }
               });
           }
       };
      
       struct TlsServer {
           TlsServer(asio::any_io_executor ex, uint16_t port) : acc(ex, {{}, port}) {}
      
           void Start() {
               // set certificates
               ctx.load_verify_file("rootCA_cert.pem");
               ctx.use_certificate_chain_file("server_cert.pem");
               ctx.use_private_key_file("server_key.pem", ssl::context::pem);
      
               // certificate callback
               SSL_CTX_set_verify(ctx.native_handle(),
                                  SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT | SSL_VERIFY_POST_HANDSHAKE,
                                  nullptr);
               ctx.set_verify_callback(verifyCertificate);
      
               std::cout << "server ready" << std::endl;
               accept_loop();
           }
      
         private:
           tcp::acceptor            acc;
           ssl::context             ctx{ssl::context::tlsv13_server};
           ssl::stream<tcp::socket> s{acc.get_executor(), ctx};
      
           void accept_loop() {
               s = {acc.get_executor(), ctx};
               SSL_set_msg_callback(s.native_handle(), tlsDebug);
      
               acc.async_accept(s.lowest_layer(), [this](error_code ec) {
                   std::cout << "acceptor: " << ec.message() << std::endl;
      
                   if (!ec) {
                       std::make_shared<Session>(std::move(s))->Run();
                       accept_loop();
                   }
               });
           }
       };
      
       int main() {
           try {
               asio::io_context ioc;
               TlsServer        s(ioc.get_executor(), 2405);
               s.Start();
               ioc.run();
           } catch (std::exception const& e) {
               std::cerr << "Exception: " << e.what() << "\n";
           }
      
           std::cout << "\nEND" << std::endl;
       }
      
    • File client.cpp

       #include <boost/asio.hpp>
       #include <boost/asio/ssl.hpp>
       #include <deque>
       #include <iomanip>
       #include <iostream>
      
       namespace asio = boost::asio;
       namespace ssl  = asio::ssl;
       using asio::ip::tcp;
       using boost::system::error_code;
      
       class TlsClient {
         public:
           TlsClient(asio::any_io_executor ex, ssl::context& context, tcp::resolver::results_type endpoint)
               : m_socket(ex, context) {
               m_socket.set_verify_mode(ssl::verify_peer);
               m_socket.set_verify_callback(verifyCertificate);
      
               SSL_set_post_handshake_auth(m_socket.native_handle(), true); // enable post-handshake authentication
               connect(endpoint);
           }
      
           void connect(tcp::resolver::results_type endpoint) {
               asio::async_connect( //
                   m_socket.lowest_layer(), endpoint, [this](error_code ec, tcp::endpoint) {
                       if (ec) {
                           std::cout << "connect() failed: " << ec.message() << std::endl;
                           return;
                       }
                       handshake();
                   });
           }
      
           void handshake() {
               m_socket.async_handshake( //
                   ssl::stream_base::client, [this](error_code ec) {
                       if (ec) {
                           std::cout << "handshake() failed: " << ec.message() << std::endl;
                           return;
                       }
                       sendRequest();
                   });
           }
      
           void send(std::string msg, bool immediate = false) {
               if (immediate) {
                   m_queue.push_front(std::move(msg));
                   if (m_queue.size() == 1)
                       sendRequest();
               } else {
                   m_queue.push_back(std::move(msg));
                   if (m_queue.size() == 1)
                   delayRequest();
               }
           }
      
           void sendRequest() {
               if (m_queue.empty()) {
                   std::cout << "sendRequest() completed" << std::endl;
                   return shutdown();
               }
      
               asio::async_write( //
                   m_socket, asio::buffer(m_queue.front()), [this](error_code ec, size_t n) {
                       if (ec) {
                           std::cout << "sendRequest() failed: " << ec.message() << std::endl;
                           return;
                       }
                       std::cout << "sendRequest() successful (" << n << " bytes transmitted)" << std::endl;
                       m_queue.pop_front();
                       receiveResponse();
                   });
               std::cout << "sendRequest() initiated for '" << m_queue.front() << "'" << std::endl;
           }
      
           void receiveResponse() {
               m_buffer.resize(2048);
               m_socket.async_read_some(asio::buffer(m_buffer), [this](error_code ec, size_t n) {
                   std::cout << "receiveResponse() " << ec.message() << " (" << n << " bytes received)" << std::endl;
                   if (!ec) {
                       m_buffer.resize(n);
                       std::cout << "server message: '" << m_buffer << "'" << std::endl;
                       if (m_buffer == "VERIFICATION REQUEST") {
                           std::cout << "allowing 2nd hanshake" << std::endl;
                           send("VERIFICATION RESPONSE", true);
                       }
                       return delayRequest();
                   } else if (ec == asio::error::eof) { // 'end of file' is fine
                       std::cout << "receiveResponse(): close_notify received" << std::endl;
                       shutdown();
                   }
               });
           }
      
           void delayRequest() {
               m_timer.expires_from_now((rand() % 3 + 1) * std::chrono::milliseconds(500));
               m_timer.async_wait([this](error_code) { sendRequest(); });
           }
      
           void shutdown() {
               m_socket.async_shutdown([/*this*/](error_code ec) {
                   if (ec) {
                       std::cout << "shutdown() failed: " << ec.message() << std::endl;
                   }
               });
           }
      
           static bool verifyCertificate(bool preverified, 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);
      
               std::cout << "certificate " << (preverified ? "valid" : "invalid") << ": " << subject_name
                         << std::endl;
               return preverified;
           }
      
         private:
           std::deque<std::string> m_queue{
               "non-auth request", "ping heartbeat",         "authentication request",
               "first query",      "authentication request", "second query",
           };
           ssl::stream<tcp::socket> m_socket;
           asio::steady_timer       m_timer{m_socket.get_executor()};
           std::string              m_buffer;
       };
      
       int main() {
           try {
               asio::io_context ioc;
               auto             endpoint = tcp::resolver(ioc).resolve("127.0.0.5", "2405");
      
               ssl::context ctx(ssl::context::tlsv13_client);
               ctx.load_verify_file("rootCA_cert.pem");
               ctx.use_certificate_chain_file("client_cert.pem");
               ctx.use_private_key_file("client_key.pem", ssl::context::pem);
      
               TlsClient client(ioc.get_executor(), ctx, endpoint);
               ioc.run();
           } catch (std::exception const& e) {
               std::cerr << "Exception: " << e.what() << "\n";
           }
      
           std::cout << "\nEND" << std::endl;
       }
      

    Demo, first with correct client certificate, then disabling the rootCA so it fails:

    enter image description here

    In plain text:

    server ready
    acceptor: Success
    handshake: Success
    read #1: Success
    client request #1: 'non-auth request'
    writer start (queue: 1)
    wrote: 'Generic response to 'non-auth request'' (Success)
    writer idle (queue: 0)
    read #2: Success
    client request #2: 'ping heartbeat'
    writer start (queue: 1)
    wrote: 'Generic response to 'ping heartbeat'' (Success)
    writer idle (queue: 0)
    read #3: Success
    client request #3: 'authentication request'
    --- initiating post-handshake peer auth
    post-handshake: OK
    writer start (queue: 1)
    wrote: 'VERIFICATION REQUEST' (Success)
    writer idle (queue: 0)
    client certificate valid: /C=DE/ST=RootCA_ST/L=RootCA_L/O=RootCA_O/OU=RootCA_OU/CN=RootCA_CN
    client certificate valid: /C=DE/ST=TLS_Client_ST/L=TLS_Client_L/O=TLS_Client_O/OU=TLS_Client_OU/CN=TLS_Client_CN
    read #4: Success
    client request #4: 'VERIFICATION RESPONSE'
    2nd handshake: Success
    writer start (queue: 1)
    wrote: 'AUTHENTICATED' (Success)
    writer idle (queue: 0)
    read #5: Success
    client request #5: 'first query'
    writer start (queue: 1)
    wrote: 'Authenticated response to 'first query'' (Success)
    writer idle (queue: 0)
    read #6: Success
    client request #6: 'authentication request'
    writer start (queue: 1)
    wrote: 'ALREADY AUTHENTICATED' (Success)
    writer idle (queue: 0)
    read #7: Success
    client request #7: 'second query'
    writer start (queue: 1)
    wrote: 'Authenticated response to 'second query'' (Success)
    writer idle (queue: 0)
    read #8: End of file
    reader completed
    shutdown the connection...
    shutdown: Success
    

    vs.

    server ready
    acceptor: Success
    handshake: Success
    read #1: Success
    client request #1: 'non-auth request'
    writer start (queue: 1)
    wrote: 'Generic response to 'non-auth request'' (Success)
    writer idle (queue: 0)
    read #2: Success
    client request #2: 'ping heartbeat'
    writer start (queue: 1)
    wrote: 'Generic response to 'ping heartbeat'' (Success)
    writer idle (queue: 0)
    read #3: Success
    client request #3: 'authentication request'
    --- initiating post-handshake peer auth
    post-handshake: OK
    writer start (queue: 1)
    wrote: 'VERIFICATION REQUEST' (Success)
    writer idle (queue: 0)
    client certificate invalid: /C=DE/ST=RootCA_ST/L=RootCA_L/O=RootCA_O/OU=RootCA_OU/CN=RootCA_CN
    read #4: certificate verify failed (SSL routines)
    reader completed
    shutdown the connection...
    shutdown: shutdown while in init (SSL routines)