Skip to content

TLS Implementation Discovery and Selection Protocol #90

@sgerbino

Description

@sgerbino

Summary

Provide a service-based mechanism for callers to inspect an execution context at runtime, determine which TLS implementations are available, and obtain a polymorphic TLS stream using a selected implementation.

Motivation

Currently, TLS support requires compile-time knowledge of which implementation to use (boost::corosio::wolfssl::stream or boost::corosio::openssl::stream). This creates several problems:

  1. Applications can't adapt at runtime - A program compiled with both WolfSSL and OpenSSL support cannot let users choose which to use via configuration
  2. Library authors face a dilemma - Libraries built on corosio must either pick one TLS implementation or expose the choice to their users via templates
  3. No graceful degradation - Applications cannot fall back to an alternative TLS implementation if the preferred one isn't available

Proposed Solution

1. TLS Provider Service

A service registered with the execution context that manages TLS provider discovery and stream creation:

namespace boost::corosio::tls {

enum class provider
{
    none,       // Use default
    openssl,
    wolfssl,
    // future: boringssl, libressl, schannel, secure_transport
};

class tls_provider_service : public capy::execution_context::service
{
public:
    using key_type = tls_provider_service;

    explicit tls_provider_service(capy::execution_context& ctx);

    // Query available providers on this context
    std::vector<provider> available_providers() const;

    // Check if a specific provider is available
    bool is_available(provider p) const;

    // Get/set the default provider for this context
    provider default_provider() const;
    void set_default_provider(provider p);

    // Factory function - creates a polymorphic TLS stream
    // Uses default_provider() if p == provider::none
    std::unique_ptr<tls_stream> create_stream(
        socket sock,
        context& ctx,
        provider p = provider::none);
};

// Convenience function to acquire the service
tls_provider_service& use_service(capy::execution_context& ctx);

// Free function convenience wrappers
std::vector<provider> available_providers(capy::execution_context& ctx);
bool is_available(capy::execution_context& ctx, provider p);

} // namespace boost::corosio::tls

2. Polymorphic TLS Stream (Already Exists)

The existing tls_stream class is already polymorphic:

class tls_stream : public io_stream
{
public:
    // Async operations (polymorphic via virtual tls_stream_impl)
    auto handshake(handshake_type type);
    auto shutdown();
    auto read_some(mutable_buffer buf);   // inherited from io_stream
    auto write_some(const_buffer buf);    // inherited from io_stream

    io_stream& next_layer() noexcept;

protected:
    struct tls_stream_impl : io_stream_impl
    {
        virtual void handshake(...) = 0;
        virtual void shutdown(...) = 0;
    };
};

Concrete implementations (wolfssl_stream, openssl_stream) derive from tls_stream and provide backend-specific tls_stream_impl.

3. Unified TLS Context

A type-erased context that can be configured for any provider:

namespace boost::corosio::tls {

class context
{
public:
    explicit context(provider p = provider::none);

    // Common configuration (works with any provider)
    void set_verify_mode(verify_mode mode);
    void load_system_certs();
    void load_cert_file(std::string_view path);
    void load_key_file(std::string_view path);
    void set_server_name(std::string_view hostname);

    provider current_provider() const;
};

} // namespace boost::corosio::tls

Example Usage

#include <boost/corosio/tls.hpp>

capy::task<> connect_with_tls(
    corosio::io_context& ioc,
    corosio::socket sock,
    std::string_view host)
{
    using namespace corosio::tls;

    // Get the TLS provider service from the execution context
    auto& tls_svc = use_service(ioc);

    // Check what's available
    auto providers = tls_svc.available_providers();
    if (providers.empty())
        throw std::runtime_error("No TLS providers available");

    // Prefer WolfSSL if available, fall back to default
    provider p = tls_svc.is_available(provider::wolfssl)
        ? provider::wolfssl
        : tls_svc.default_provider();

    // Create context
    context ctx(p);
    ctx.load_system_certs();
    ctx.set_server_name(host);

    // Create polymorphic stream via factory
    auto stream = tls_svc.create_stream(std::move(sock), ctx, p);

    // Use the stream through the base class interface
    auto [ec] = co_await stream->handshake(tls_stream::client);
    if (ec)
        throw std::system_error(ec);

    // Read/write through polymorphic interface
    std::array<char, 1024> buf;
    auto [ec2, n] = co_await stream->read_some(
        capy::mutable_buffer(buf.data(), buf.size()));
    // ...
}

Setting a Context-Wide Default

void configure_tls(corosio::io_context& ioc)
{
    auto& tls_svc = corosio::tls::use_service(ioc);

    // Set WolfSSL as default if available, otherwise OpenSSL
    if (tls_svc.is_available(corosio::tls::provider::wolfssl))
        tls_svc.set_default_provider(corosio::tls::provider::wolfssl);
    else if (tls_svc.is_available(corosio::tls::provider::openssl))
        tls_svc.set_default_provider(corosio::tls::provider::openssl);
}

Design Considerations

Service-Based Architecture

Tying provider discovery to execution_context follows the established Asio service pattern and allows:

  • Per-context default provider configuration
  • Future support for context-specific TLS settings
  • Clean integration with existing service infrastructure

Polymorphic Stream Interface

The existing tls_stream class already provides a polymorphic interface via virtual methods in tls_stream_impl. The factory returns std::unique_ptr<tls_stream>, allowing users to work with any TLS backend through the common interface.

Virtual Dispatch Overhead

The polymorphic stream uses virtual dispatch for I/O operations. For applications where this overhead matters, the concrete types (wolfssl::stream, openssl::stream) remain available for direct use.

Context Lifetime

The tls::context must outlive all streams created from it, matching the existing concrete context semantics.

Provider-Specific Features

Some TLS providers have unique features (e.g., WolfSSL's FIPS mode). These remain accessible via the concrete types. The polymorphic interface covers the common subset.

Alternatives Considered

  1. Global free functions - Rejected: doesn't tie to execution context, can't have per-context defaults
  2. Template-based approach - Rejected: requires all user code to be templated, poor ergonomics
  3. Preprocessor selection only - Rejected: no runtime flexibility, can't respond to configuration

Tasks

  • Define tls::provider enum
  • Implement tls_provider_service with provider registry
  • Implement create_stream() factory method
  • Implement type-erased tls::context
  • Add use_service() and convenience free functions
  • Add documentation and examples
  • Add tests for provider selection and factory

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions