admin管理员组

文章数量:1313389

I'm porting a Java client library to a C++ static library. These libraries provide an SDK to communicate with a server that is written in Java using Netty.

I am using boost 1.87 with C++23.

#pragma once

#include "Config.hpp"
#include "util/concurrent/ThreadPool.hpp"
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <filesystem>
#include <memory>
#include <string>

using namespace boost::asio;
namespace fs = std::filesystem;

namespace sdk {

class HttpClient {
  public:
    explicit HttpClient(std::shared_ptr<ThreadPool> threadPool)
        : m_threadPool(std::move(threadPool)),
          m_ioContext(m_threadPool->getIoContext()),
          m_sslContext(ssl::context::tlsv12_client),
          m_sslSocket(m_ioContext, m_sslContext),
          m_host(Config::getData()->host),
          m_port(Config::getData()->port)
    {
        auto configData = Config::getData();

        // so dumb. why can't I just use the context directly?
        // i have to use the native handle to load the certificate and key
        // because boost::asio is not sending the client certificate
        SSL *sslHandle = m_sslSocket.native_handle();
        if (!sslHandle) {
            throw std::runtime_error("Failed to retrieve SSL handle!");
        }

        if (SSL_use_certificate_file(sslHandle, configData->keystore.path.c_str(), SSL_FILETYPE_PEM) <= 0) {
            // m_sslContext.use_certificate_chain_file(Config::getData()->keystore.path); // <== does not work
            throw std::runtime_error("Failed to load client certificate from: " + configData->keystore.path);
        }
        else if (SSL_use_PrivateKey_file(sslHandle, configData->key.path.c_str(), SSL_FILETYPE_PEM) <= 0) {
            // m_sslContext.use_private_key_file(Config::getData()->key.path, ssl::context::pem); // <== does not work
            throw std::runtime_error("Failed to load client private key from: " + configData->key.path);
        }
    }

    void connect()
    {
        ip::tcp::resolver resolver(m_ioContext);
        auto              endpoints = resolver.resolve(m_host, std::to_string(m_port));

        LOG(info) << "Resolving and connecting to: " << m_host << ":" << m_port;

        auto handshake_worker = [this](const boost::system::error_code &ec) {
            if (!ec) {
                LOG(info) << "SSL handshake successful!";
            }
            else {
                LOG(error) << "SSL handshake failed: " << ec.message();
                stop();
            }
        };

        auto connect_worker = [this, handshake_worker](const boost::system::error_code &ec,
                                                       const ip::tcp::endpoint         &endpoint) {
            if (!ec) {
                LOG(info) << "Connected to: " << endpoint;
                m_sslSocket.async_handshake(ssl::stream_base::client, handshake_worker);
            }
            else {
                LOG(error) << "Connection failed: " << ec.message();
                stop();
            }
        };

        async_connect(m_sslSocket.lowest_layer(), endpoints, connect_worker);
    }

    void stop()
    {
        if (m_sslSocket.lowest_layer().is_open()) {
            boost::system::error_code ec;
            m_sslSocket.lowest_layer().shutdown(ip::tcp::socket::shutdown_both, ec);
            m_sslSocket.lowest_layer().close();
        }
        m_threadPool->stop();
    }

    virtual ~HttpClient() { stop(); }

  private:
    std::shared_ptr<ThreadPool>  m_threadPool;
    io_context                  &m_ioContext;
    ssl::context                 m_sslContext;
    ssl::stream<ip::tcp::socket> m_sslSocket;
    std::string                  m_host;
    int                          m_port;
};

} // namespace sdk

For some strange reason calls to the boost::asio::ssl::context instance to set the certificate and privatekey file do not work. It fails in the handshaking.

The java server logs that no certificate chain has been sent.

javax.ssl|DEBUG|11|nioEventLoopGroup-2-2|2025-01-30 22:52:33.911 UTC|CertificateMessage.java:372|Consuming client Certificate handshake message (
"Certificates": <empty list>
)
javax.ssl|ERROR|11|nioEventLoopGroup-2-2|2025-01-30 22:52:33.913 UTC|TransportContext.java:358|Fatal (BAD_CERTIFICATE): Empty client certificate chain (
"throwable" : {

However, if I grab the native SSL handle from boost::asio::ssl::context and call the native APIs directly to set the certificate and private key files, everything works. The client certificate is sent and handshaking succeeds.

I can only conclude I'm missing a step or doing something in the wrong order, but I'm unable to tell what. I have also tried the following, with no success

// Configure SSL settings
m_sslContext.set_options(
    ssl::context::default_workarounds | 
    ssl::context::no_sslv2 | 
    ssl::context::no_sslv3 | 
    ssl::context::single_dh_use
);

// Enforce mutual TLS authentication
m_sslContext.set_verify_mode(
    ssl::verify_peer | 
    ssl::verify_fail_if_no_peer_cert);

I'm just too anal retentive to leave the work around in place and walk away from this.

I'm porting a Java client library to a C++ static library. These libraries provide an SDK to communicate with a server that is written in Java using Netty.

I am using boost 1.87 with C++23.

#pragma once

#include "Config.hpp"
#include "util/concurrent/ThreadPool.hpp"
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>
#include <filesystem>
#include <memory>
#include <string>

using namespace boost::asio;
namespace fs = std::filesystem;

namespace sdk {

class HttpClient {
  public:
    explicit HttpClient(std::shared_ptr<ThreadPool> threadPool)
        : m_threadPool(std::move(threadPool)),
          m_ioContext(m_threadPool->getIoContext()),
          m_sslContext(ssl::context::tlsv12_client),
          m_sslSocket(m_ioContext, m_sslContext),
          m_host(Config::getData()->host),
          m_port(Config::getData()->port)
    {
        auto configData = Config::getData();

        // so dumb. why can't I just use the context directly?
        // i have to use the native handle to load the certificate and key
        // because boost::asio is not sending the client certificate
        SSL *sslHandle = m_sslSocket.native_handle();
        if (!sslHandle) {
            throw std::runtime_error("Failed to retrieve SSL handle!");
        }

        if (SSL_use_certificate_file(sslHandle, configData->keystore.path.c_str(), SSL_FILETYPE_PEM) <= 0) {
            // m_sslContext.use_certificate_chain_file(Config::getData()->keystore.path); // <== does not work
            throw std::runtime_error("Failed to load client certificate from: " + configData->keystore.path);
        }
        else if (SSL_use_PrivateKey_file(sslHandle, configData->key.path.c_str(), SSL_FILETYPE_PEM) <= 0) {
            // m_sslContext.use_private_key_file(Config::getData()->key.path, ssl::context::pem); // <== does not work
            throw std::runtime_error("Failed to load client private key from: " + configData->key.path);
        }
    }

    void connect()
    {
        ip::tcp::resolver resolver(m_ioContext);
        auto              endpoints = resolver.resolve(m_host, std::to_string(m_port));

        LOG(info) << "Resolving and connecting to: " << m_host << ":" << m_port;

        auto handshake_worker = [this](const boost::system::error_code &ec) {
            if (!ec) {
                LOG(info) << "SSL handshake successful!";
            }
            else {
                LOG(error) << "SSL handshake failed: " << ec.message();
                stop();
            }
        };

        auto connect_worker = [this, handshake_worker](const boost::system::error_code &ec,
                                                       const ip::tcp::endpoint         &endpoint) {
            if (!ec) {
                LOG(info) << "Connected to: " << endpoint;
                m_sslSocket.async_handshake(ssl::stream_base::client, handshake_worker);
            }
            else {
                LOG(error) << "Connection failed: " << ec.message();
                stop();
            }
        };

        async_connect(m_sslSocket.lowest_layer(), endpoints, connect_worker);
    }

    void stop()
    {
        if (m_sslSocket.lowest_layer().is_open()) {
            boost::system::error_code ec;
            m_sslSocket.lowest_layer().shutdown(ip::tcp::socket::shutdown_both, ec);
            m_sslSocket.lowest_layer().close();
        }
        m_threadPool->stop();
    }

    virtual ~HttpClient() { stop(); }

  private:
    std::shared_ptr<ThreadPool>  m_threadPool;
    io_context                  &m_ioContext;
    ssl::context                 m_sslContext;
    ssl::stream<ip::tcp::socket> m_sslSocket;
    std::string                  m_host;
    int                          m_port;
};

} // namespace sdk

For some strange reason calls to the boost::asio::ssl::context instance to set the certificate and privatekey file do not work. It fails in the handshaking.

The java server logs that no certificate chain has been sent.

javax.ssl|DEBUG|11|nioEventLoopGroup-2-2|2025-01-30 22:52:33.911 UTC|CertificateMessage.java:372|Consuming client Certificate handshake message (
"Certificates": <empty list>
)
javax.ssl|ERROR|11|nioEventLoopGroup-2-2|2025-01-30 22:52:33.913 UTC|TransportContext.java:358|Fatal (BAD_CERTIFICATE): Empty client certificate chain (
"throwable" : {

However, if I grab the native SSL handle from boost::asio::ssl::context and call the native APIs directly to set the certificate and private key files, everything works. The client certificate is sent and handshaking succeeds.

I can only conclude I'm missing a step or doing something in the wrong order, but I'm unable to tell what. I have also tried the following, with no success

// Configure SSL settings
m_sslContext.set_options(
    ssl::context::default_workarounds | 
    ssl::context::no_sslv2 | 
    ssl::context::no_sslv3 | 
    ssl::context::single_dh_use
);

// Enforce mutual TLS authentication
m_sslContext.set_verify_mode(
    ssl::verify_peer | 
    ssl::verify_fail_if_no_peer_cert);

I'm just too anal retentive to leave the work around in place and walk away from this.

Share Improve this question edited Jan 31 at 0:10 tdemay asked Jan 30 at 23:59 tdemaytdemay 7489 silver badges27 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 1

The problem is you're setting context parameters AFTER constructing the ssl stream object.

The difference between what you're doing with OpenSSL directly is that you're using

  • SSL_use_certificate_file vs. SSL_CTX_use_PrivateKey_file
  • SSL_use_PrivateKey_file vs. SSL_CTX_use_PrivateKey_file

Here's a self-contained example that fixes the initialization order by using a create_ssl_context() factory. Note I've stubbed the verification to allow my self-signed test certs.

Live On Coliru

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

namespace asio = boost::asio;
namespace ssl  = asio::ssl;
using asio::ip::tcp;

#define LOG(level) std::osyncstream(std::cout) << "\n[" #level "] " << __FUNCTION__ << ":" << __LINE__ << " "

namespace sdk {
    static constexpr uint16_t SERVICE_PORT = 4433;

    namespace Config {
        struct Keystore {
            std::string path;
        };

        struct Key {
            std::string path;
        };

        struct Data {
            std::string host;
            int         port;
            Keystore    keystore;
            Key         key;
        };

        auto getData() -> std::shared_ptr<Data const> {
            static auto data = std::make_shared<Data>(Data{
                "localhost",
                SERVICE_PORT,
                Keystore{"certs/client_cert.pem"},
                Key{"certs/client_cert.pem"},
            });
            return data;
        }
    } // namespace Config

    using boost::system::error_code;

    asio::awaitable<void> server() try {
        auto ex = co_await asio::this_coro::executor;

        tcp::acceptor acc_(ex, {{}, SERVICE_PORT});

        ssl::context ctx(ssl::context::tlsv12_server);
        ctx.set_password_callback([](std::size_t, ssl::context_base::password_purpose) { return "test"; });
        ctx.set_verify_callback([](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);
            LOG(info) << "Server Verifying: " << subject_name << " preverified: " << preverified;
            return true;
        });

        ctx.set_verify_mode(ssl::context::verify_peer | ssl::context::verify_fail_if_no_peer_cert);
        ctx.use_certificate_file("certs/server_cert.pem", ssl::context::pem);
        ctx.use_private_key_file("certs/server_cert.pem", ssl::context::pem);

        for (;;) {
            auto s = co_await acc_.async_accept();
            LOG(info) << "Accepted connection from: " << s.remote_endpoint();

            ssl::stream<tcp::socket> ss(std::move(s), ctx);
            co_await ss.async_handshake(ssl::stream_base::server);

            LOG(info) << "Handshake successful";

            break; // let's just accept one connection
        };
    } catch(boost::system::system_error const& se) {
        LOG(error) << "Exception: " << se.code().message();
    }

    struct ThreadPool {
        explicit ThreadPool(size_t threads) : m_impl(threads) {}
        virtual ~ThreadPool() { join(); }

        void join() { m_impl.join(); }
        void stop() { m_impl.stop(); }

        using executor_type = asio::thread_pool::executor_type;
        executor_type get_executor() { return m_impl.get_executor(); }

      private:
        asio::thread_pool m_impl;
    };

    class HttpClient {
      public:
        explicit HttpClient(asio::any_io_executor ex)
            : m_io(ex)
            , m_ctx(create_ssl_context())
            , m_host(Config::getData()->host)
            , m_port(Config::getData()->port) {}

        void connect() {
            tcp::resolver resolver(m_io);
            auto          endpoints = resolver.resolve(m_host, std::to_string(m_port));

            LOG(info) << "Client resolving and connecting to: " << m_host << ":" << m_port;

            auto on_handshake = [this](error_code ec) {
                LOG(info) << "Client SSL handshake " << ec.message();
                stop();
            };

            auto on_connect = [this, on_handshake](error_code ec, tcp::endpoint const& endpoint) {
                if (!ec) {
                    LOG(info) << "Client Connected to: " << endpoint;
                    m_sslSocket.async_handshake(ssl::stream_base::client, on_handshake);
                } else {
                    LOG(error) << "Client Connection failed: " << ec.message();
                    stop();
                }
            };

            async_connect(m_sslSocket.lowest_layer(), endpoints, on_connect);
        }

        void stop() {
            if (m_sslSocket.lowest_layer().is_open()) {
                error_code ec;
                m_sslSocket.lowest_layer().shutdown(tcp::socket::shutdown_both, ec);
                m_sslSocket.lowest_layer().close();
            }
        }

        virtual ~HttpClient() { stop(); }

      private:
        asio::any_io_executor       m_io;
        ssl::context                m_ctx;
        ssl::stream<tcp::socket>    m_sslSocket{m_io, m_ctx};
        std::string                 m_host;
        int                         m_port;

        static ssl::context create_ssl_context() {
            ssl::context ctx(ssl::context::tlsv12_client);

            ctx.set_verify_callback([](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);
                LOG(info) << "Client Verifying: " << subject_name << " preverified: " << preverified;
                return true;
            });

            ctx.set_verify_mode(ssl::context::verify_peer | ssl::context::verify_fail_if_no_peer_cert);

            auto configData = Config::getData();
            ctx.use_private_key_file(configData->key.path, ssl::context::pem);
            ctx.use_certificate_file(Config::getData()->keystore.path, ssl::context::pem);
            return ctx;
        }
    };

} // namespace sdk

int main() {
    auto threadPool = std::make_shared<sdk::ThreadPool>(16);
    auto ex         = threadPool->get_executor();
    auto client     = std::make_unique<sdk::HttpClient>(ex);

    // start server
    co_spawn(ex, sdk::server, asio::detached);

    // connect the client
    client->connect();

    threadPool->join();

    LOG(info) << "Done!\n";
}

Printing e.g.

[info] connect:104 Client resolving and connecting to: localhost:4433
[info] operator():113 Client Connected to: 127.0.0.1:4433
[info] server:65 Accepted connection from: 127.0.0.1:51810
[info] operator():148 Client Verifying: /CN=root preverified: 0
[info] operator():148 Client Verifying: /CN=root preverified: 1
[info] operator():55 Server Verifying: /CN=Client_User preverified: 0
[info] operator():55 Server Verifying: /CN=Client_User preverified: 0
[info] operator():55 Server Verifying: /CN=Client_User preverified: 1
[info] server:70 Handshake successful
[info] operator():107 Client SSL handshake Success
[info] main:176 Done!

Live:

本文标签: cboostasiosslcontext not sending client certificatesStack Overflow