diff --git a/CMakeLists.txt b/CMakeLists.txt index f60785f..d3424f3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,9 +69,21 @@ add_compile_options(-Wno-inconsistent-missing-override) # https://github.com/madmongo1/blog-december-2020/blob/master/CMakeLists.txt # find_package(Boost 1.88 COMPONENTS thread atomic url filesystem process program_options OPTIONAL_COMPONENTS system REQUIRED) -find_package(OpenSSL) find_package(Threads) +# +# TLS: BoringSSL -- or rather AWS-LC, which is API compatible and what the devcontainer builds into +# /opt/boringssl, deliberately away from the system OpenSSL. It only comes as static libraries. +# +# Everything in the process has to use the very same TLS library: Boost.Asio's SSL streams, +# ngtcp2_crypto_boringssl and our own code. Linking the system OpenSSL on top would silently mix +# up two libraries exporting the same symbols. +# +set(BORINGSSL_ROOT /opt/boringssl CACHE PATH "Installation prefix of BoringSSL / AWS-LC") +list(PREPEND CMAKE_PREFIX_PATH ${BORINGSSL_ROOT}) # ssl-config.cmake finds 'crypto' through it +find_package(ssl CONFIG REQUIRED) +message(STATUS "Using TLS library: ${ssl_DIR}") + # # nghttp2 # https://github.com/curl/curl/blob/master/CMake/FindNGHTTP2.cmake diff --git a/include/anyhttp/h3_session.hpp b/include/anyhttp/h3_session.hpp index 580cb8e..c702155 100644 --- a/include/anyhttp/h3_session.hpp +++ b/include/anyhttp/h3_session.hpp @@ -8,7 +8,6 @@ #include #include #include -#include #include @@ -112,7 +111,7 @@ class Http3Session : public Session::Impl int handle_expiry(); // - // ngtcp2 <-> ngtcp2_crypto_ossl bridge. + // ngtcp2 <-> ngtcp2_crypto_boringssl bridge, reached through SSL_get_app_data(). // static ngtcp2_conn* get_conn(ngtcp2_crypto_conn_ref* ref) { @@ -219,7 +218,7 @@ class Http3Session : public Session::Impl asio::any_io_executor executor_; ngtcp2_conn* conn_ = nullptr; - ngtcp2_crypto_ossl_ctx* ossl_ctx_ = nullptr; + SSL* ssl_ = nullptr; // also ngtcp2's TLS native handle ngtcp2_crypto_conn_ref conn_ref_{}; nghttp3_conn* h3_ = nullptr; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e7414a2..c0d2629 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,7 +8,7 @@ include_directories(${CMAKE_SOURCE_DIR}/include) # pkg_check_modules(NGTCP2 REQUIRED IMPORTED_TARGET libngtcp2) pkg_check_modules(NGHTTP3 REQUIRED IMPORTED_TARGET libnghttp3) -pkg_check_modules(NGTCP2_CRYPTO_OSSL REQUIRED IMPORTED_TARGET libngtcp2_crypto_ossl) +pkg_check_modules(NGTCP2_CRYPTO_BORINGSSL REQUIRED IMPORTED_TARGET libngtcp2_crypto_boringssl) # # lib -- the protocol backends are the h1_*, h2_* and h3_* sources; only the h2_* ones use @@ -23,11 +23,15 @@ target_include_directories(anyhttp PUBLIC ../include) target_include_directories(anyhttp PRIVATE "/opt/nghttp3/build/include") target_link_libraries(anyhttp PRIVATE Threads::Threads) -target_link_libraries(anyhttp PRIVATE OpenSSL::SSL) target_link_libraries(anyhttp PRIVATE Boost::thread Boost::atomic Boost::url Boost::filesystem) target_link_libraries(anyhttp PRIVATE spdlog::spdlog_header_only) target_link_libraries(anyhttp PRIVATE PkgConfig::NGHTTP2) -target_link_libraries(anyhttp PRIVATE PkgConfig::NGTCP2 PkgConfig::NGHTTP3 PkgConfig::NGTCP2_CRYPTO_OSSL) +target_link_libraries(anyhttp PRIVATE PkgConfig::NGTCP2_CRYPTO_BORINGSSL PkgConfig::NGTCP2 PkgConfig::NGHTTP3) + +# PUBLIC, because the backend headers include Boost.Asio's SSL streams: whoever includes them must +# see the BoringSSL headers, not the system OpenSSL ones in /usr/include. Static libraries, so this +# comes after ngtcp2_crypto_boringssl, which needs it. +target_link_libraries(anyhttp PUBLIC AWS::ssl) # # server diff --git a/src/h3_client.cpp b/src/h3_client.cpp index 68d7719..bff6a0a 100644 --- a/src/h3_client.cpp +++ b/src/h3_client.cpp @@ -48,7 +48,7 @@ #include #include #include -#include +#include #include #include @@ -83,25 +83,19 @@ namespace { // -// One-shot process-wide initialization of ngtcp2_crypto_ossl and the client-role OpenSSL SSL_CTX -// used for every outgoing QUIC connection. +// The process-wide client-role BoringSSL SSL_CTX used for every outgoing QUIC connection. // struct TlsClientContext { TlsClientContext() { - static const int init_once = [] - { - if (ngtcp2_crypto_ossl_init() != 0) - throw std::runtime_error("ngtcp2_crypto_ossl_init"); - return 0; - }(); - (void)init_once; - ctx = SSL_CTX_new(TLS_client_method()); if (!ctx) throw std::runtime_error("SSL_CTX_new"); + if (ngtcp2_crypto_boringssl_configure_client_context(ctx) != 0) + throw std::runtime_error("ngtcp2_crypto_boringssl_configure_client_context"); + static constexpr unsigned char alpn[] = "\x02h3"; SSL_CTX_set_alpn_protos(ctx, alpn, sizeof(alpn) - 1); diff --git a/src/h3_server.cpp b/src/h3_server.cpp index 57ff096..8659da3 100644 --- a/src/h3_server.cpp +++ b/src/h3_server.cpp @@ -65,7 +65,7 @@ #include #include #include -#include +#include #include #include @@ -118,29 +118,25 @@ namespace { // -// One-shot process-wide initialization of ngtcp2_crypto_ossl and the OpenSSL SSL_CTX -// used for every QUIC connection. +// The process-wide BoringSSL SSL_CTX used for every QUIC connection. // struct TlsServerContext { TlsServerContext() { - static const int init_once = [] - { - if (ngtcp2_crypto_ossl_init() != 0) - throw std::runtime_error("ngtcp2_crypto_ossl_init"); - return 0; - }(); - (void)init_once; - ctx = SSL_CTX_new(TLS_server_method()); if (!ctx) throw std::runtime_error("SSL_CTX_new"); - SSL_CTX_set_options(ctx, (SSL_OP_ALL & ~SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS) | - SSL_OP_SINGLE_ECDH_USE | SSL_OP_CIPHER_SERVER_PREFERENCE | - SSL_OP_NO_ANTI_REPLAY); - SSL_CTX_set_mode(ctx, SSL_MODE_RELEASE_BUFFERS); + if (ngtcp2_crypto_boringssl_configure_server_context(ctx) != 0) + throw std::runtime_error("ngtcp2_crypto_boringssl_configure_server_context"); + + // + // What OpenSSL needed SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS, SSL_OP_SINGLE_ECDH_USE and + // SSL_MODE_RELEASE_BUFFERS for is the default in BoringSSL. SSL_OP_NO_ANTI_REPLAY does not + // exist, but it would only matter for 0-RTT, which we don't enable. + // + SSL_CTX_set_options(ctx, SSL_OP_CIPHER_SERVER_PREFERENCE); SSL_CTX_set_alpn_select_cb(ctx, &TlsServerContext::alpn_select_cb, nullptr); diff --git a/src/h3_session.cpp b/src/h3_session.cpp index bba8b1e..a19472e 100644 --- a/src/h3_session.cpp +++ b/src/h3_session.cpp @@ -40,14 +40,10 @@ Http3Session::~Http3Session() nghttp3_conn_del(h3_); if (conn_) ngtcp2_conn_del(conn_); - if (ossl_ctx_) + if (ssl_) { - if (auto ssl = ngtcp2_crypto_ossl_ctx_get_ssl(ossl_ctx_)) - { - SSL_set_app_data(ssl, nullptr); - SSL_free(ssl); - } - ngtcp2_crypto_ossl_ctx_del(ossl_ctx_); + SSL_set_app_data(ssl_, nullptr); + SSL_free(ssl_); } } @@ -453,10 +449,14 @@ void Http3Session::fill_settings(ngtcp2_settings& settings, ngtcp2_transport_par params.max_idle_timeout = static_cast(idle_timeout.count()); } +// +// The QUIC specifics were configured on the SSL_CTX already (see the roles' TlsServerContext and +// TlsClientContext), so with BoringSSL, the session is a plain SSL whose app data leads back to us. +// int Http3Session::setup_tls(SSL_CTX* ssl_ctx, bool is_server) { - auto* ssl = SSL_new(ssl_ctx); - if (!ssl) + ssl_ = SSL_new(ssl_ctx); + if (!ssl_) { mloge("SSL_new failed"); return -1; @@ -464,30 +464,14 @@ int Http3Session::setup_tls(SSL_CTX* ssl_ctx, bool is_server) conn_ref_.get_conn = &Http3Session::get_conn; conn_ref_.user_data = this; - SSL_set_app_data(ssl, &conn_ref_); + SSL_set_app_data(ssl_, &conn_ref_); if (is_server) - SSL_set_accept_state(ssl); + SSL_set_accept_state(ssl_); else - SSL_set_connect_state(ssl); - - auto configure = is_server ? &ngtcp2_crypto_ossl_configure_server_session - : &ngtcp2_crypto_ossl_configure_client_session; - if (configure(ssl) != 0) - { - mloge("ngtcp2_crypto_ossl_configure_{}_session failed", is_server ? "server" : "client"); - SSL_free(ssl); - return -1; - } - - if (ngtcp2_crypto_ossl_ctx_new(&ossl_ctx_, ssl) != 0) - { - mloge("ngtcp2_crypto_ossl_ctx_new failed"); - SSL_free(ssl); - return -1; - } + SSL_set_connect_state(ssl_); - ngtcp2_conn_set_tls_native_handle(conn_, ossl_ctx_); + ngtcp2_conn_set_tls_native_handle(conn_, ssl_); return 0; } @@ -573,7 +557,7 @@ int Http3Session::cb_handshake_completed(ngtcp2_conn*, void* user) { auto self = static_cast(user); logi("[{}] TLS handshake completed: {}", self->log_prefix_, - tls_handshake_info(ngtcp2_crypto_ossl_ctx_get_ssl(self->ossl_ctx_))); + tls_handshake_info(self->ssl_)); if (self->setup_http3() != 0) return NGTCP2_ERR_CALLBACK_FAILURE; return 0; diff --git a/src/tls.cpp b/src/tls.cpp index fb42530..85a782d 100644 --- a/src/tls.cpp +++ b/src/tls.cpp @@ -1,7 +1,6 @@ #include #include -#include #include #include @@ -16,17 +15,16 @@ namespace /** * Key exchange group of the handshake, like the "Server Temp Key" line of h2load, e.g. - * "X25519 (253 bits)" or "prime256v1 (256 bits)". + * "X25519 (253 bits)" or "P-256 (256 bits)". */ std::string key_exchange(SSL* ssl) { // - // The negotiated group is known even for groups OpenSSL has no EVP_PKEY name for, like the - // post-quantum hybrids ("X25519MLKEM768") that are the default in OpenSSL 3.5. + // The negotiated group is known even for groups that have no EVP_PKEY, like the post-quantum + // hybrids ("X25519MLKEM768"). // - std::string name; - if (const char* group = SSL_get0_group_name(ssl)) - name = group; + const char* group = SSL_get_group_name(SSL_get_group_id(ssl)); + std::string name = group ? group : "unknown"; // // On the client this is the server's key share, on the server the client's one. Either way, @@ -34,21 +32,9 @@ std::string key_exchange(SSL* ssl) // EVP_PKEY* key = nullptr; if (SSL_get_peer_tmp_key(ssl, &key) != 1 || !key) - return name.empty() ? "unknown" : name; - - if (name.empty()) - { - char group[80]; - size_t len = 0; - if (EVP_PKEY_get_group_name(key, group, sizeof(group), &len) == 1 && len) - name.assign(group, len); - else if (const char* sn = OBJ_nid2sn(EVP_PKEY_get_id(key))) - name = sn; - else - name = "unknown"; - } - - auto bits = EVP_PKEY_get_bits(key); + return name; + + auto bits = EVP_PKEY_bits(key); EVP_PKEY_free(key); return std::format("{} ({} bits)", name, bits);