| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| #include <gtest/gtest.h> |
| #include <openssl/ssl.h> |
| #include <pulsar/Authentication.h> |
| #include <pulsar/Client.h> |
| |
| #include <atomic> |
| #include <future> |
| #include <thread> |
| |
| #include "lib/AsioDefines.h" |
| #include "lib/LogUtils.h" |
| |
| #ifdef USE_ASIO |
| #include <asio.hpp> |
| #include <asio/ssl.hpp> |
| #else |
| #include <boost/asio.hpp> |
| #include <boost/asio/ssl.hpp> |
| #endif |
| |
| DECLARE_LOG_OBJECT() |
| |
| #ifndef TEST_CONF_DIR |
| #error "TEST_CONF_DIR is not specified" |
| #endif |
| |
| static const std::string caPath = TEST_CONF_DIR "/cacert.pem"; |
| static const std::string clientPublicKeyPath = TEST_CONF_DIR "/client-cert.pem"; |
| static const std::string clientPrivateKeyPath = TEST_CONF_DIR "/client-key.pem"; |
| |
| using namespace pulsar; |
| |
| class MockTlsServer { |
| public: |
| MockTlsServer() |
| : acceptor_(io_context_, ASIO::ip::tcp::endpoint(ASIO::ip::tcp::v4(), 0)), |
| ctx_(ASIO::ssl::context::sslv23) { |
| ctx_.set_options(ASIO::ssl::context::default_workarounds | ASIO::ssl::context::no_sslv2 | |
| ASIO::ssl::context::no_sslv3); |
| |
| ctx_.use_certificate_chain_file(clientPublicKeyPath); |
| ctx_.use_private_key_file(clientPrivateKeyPath, ASIO::ssl::context::pem); |
| ctx_.set_verify_mode(ASIO::ssl::context::verify_none); |
| } |
| |
| int getPort() const { return acceptor_.local_endpoint().port(); } |
| |
| void setTls12Only() { |
| SSL_CTX* ssl_ctx = ctx_.native_handle(); |
| #if defined(TLS1_2_VERSION) |
| SSL_CTX_set_min_proto_version(ssl_ctx, TLS1_2_VERSION); |
| SSL_CTX_set_max_proto_version(ssl_ctx, TLS1_2_VERSION); |
| #else |
| LOG_WARN("TLS 1.2 not supported by OpenSSL headers"); |
| #endif |
| } |
| |
| void setTls13Only() { |
| SSL_CTX* ssl_ctx = ctx_.native_handle(); |
| #if defined(TLS1_3_VERSION) |
| SSL_CTX_set_min_proto_version(ssl_ctx, TLS1_3_VERSION); |
| SSL_CTX_set_max_proto_version(ssl_ctx, TLS1_3_VERSION); |
| #else |
| LOG_WARN("TLS 1.3 not supported by OpenSSL headers"); |
| #endif |
| } |
| |
| bool acceptAndHandshake() { |
| auto socket = std::make_shared<ASIO::ip::tcp::socket>(io_context_); |
| acceptor_.accept(*socket); |
| |
| ASIO::ssl::stream<ASIO::ip::tcp::socket&> ssl_stream(*socket, ctx_); |
| |
| ASIO_ERROR error; |
| ssl_stream.handshake(ASIO::ssl::stream_base::server, error); |
| |
| if (error) { |
| LOG_ERROR("Handshake failed: " << error.message()); |
| return false; |
| } |
| LOG_INFO("Handshake success!"); |
| return true; |
| } |
| |
| private: |
| ASIO::io_context io_context_; |
| ASIO::ip::tcp::acceptor acceptor_; |
| ASIO::ssl::context ctx_; |
| }; |
| |
| TEST(TlsNegotiationTest, testTls12) { |
| #if !defined(TLS1_2_VERSION) |
| return; // Skip if TLS 1.2 is not available |
| #endif |
| |
| MockTlsServer server; |
| server.setTls12Only(); |
| int port = server.getPort(); |
| |
| std::promise<bool> handshakePromise; |
| auto handshakeFuture = handshakePromise.get_future(); |
| |
| std::thread serverThread([&server, &handshakePromise]() { |
| bool result = server.acceptAndHandshake(); |
| handshakePromise.set_value(result); |
| }); |
| |
| std::string serviceUrl = "pulsar+ssl://localhost:" + std::to_string(port); |
| ClientConfiguration config; |
| config.setTlsTrustCertsFilePath(caPath); |
| config.setTlsAllowInsecureConnection(true); // Self-signed certs match |
| config.setValidateHostName(false); |
| |
| Client client(serviceUrl, config); |
| |
| // Trigger connection by creating a producer. |
| // It will fail to create producer because mock server doesn't speak Pulsar, |
| // but we only care about the handshake. |
| Producer producer; |
| client.createProducerAsync("topic", [](auto, const auto&) {}); |
| |
| // Wait for handshake |
| ASSERT_TRUE(handshakeFuture.get()); |
| |
| serverThread.join(); |
| client.close(); |
| } |
| |
| TEST(TlsNegotiationTest, testTls13) { |
| #if !defined(TLS1_3_VERSION) |
| LOG_INFO("Skipping TLS 1.3 test because OpenSSL does not support it"); |
| return; |
| #endif |
| |
| MockTlsServer server; |
| server.setTls13Only(); |
| int port = server.getPort(); |
| |
| std::promise<bool> handshakePromise; |
| auto handshakeFuture = handshakePromise.get_future(); |
| |
| std::thread serverThread([&server, &handshakePromise]() { |
| bool result = server.acceptAndHandshake(); |
| handshakePromise.set_value(result); |
| }); |
| |
| std::string serviceUrl = "pulsar+ssl://localhost:" + std::to_string(port); |
| ClientConfiguration config; |
| config.setTlsTrustCertsFilePath(caPath); |
| config.setTlsAllowInsecureConnection(true); |
| config.setValidateHostName(false); |
| |
| Client client(serviceUrl, config); |
| |
| client.createProducerAsync("topic", [](auto, const auto&) {}); |
| |
| ASSERT_TRUE(handshakeFuture.get()); |
| |
| serverThread.join(); |
| client.close(); |
| } |