diff --git a/include/seastar/net/tls.hh b/include/seastar/net/tls.hh index 491f8c5357..cdef240228 100644 --- a/include/seastar/net/tls.hh +++ b/include/seastar/net/tls.hh @@ -494,6 +494,19 @@ namespace tls { */ future> get_alt_name_information(connected_socket& socket, std::unordered_set types = {}); + using certificate_data = std::vector; + + /** + * Get the raw certificate (chain) that the connected peer is using. + * This function forces the TLS handshake. If the handshake didn't happen before the + * call to 'get_peer_certificate_chain' it will be completed when the returned future + * will become ready. + * The function returns the certificate chain on success. If the peer didn't send the + * certificate during the handshake, the function returns an empty certificate chain. + * If the socket is not connected the system_error exception will be thrown. + */ + future> get_peer_certificate_chain(connected_socket& socket); + /** * Checks if the socket was connected using session resume. * Will force handshake if not already done. diff --git a/src/net/tls.cc b/src/net/tls.cc index 0d99ab7234..8ede74135e 100644 --- a/src/net/tls.cc +++ b/src/net/tls.cc @@ -1790,6 +1790,22 @@ class session : public enable_lw_shared_from_this { }, std::move(types)); } + future> get_peer_certificate_chain() { + return state_checked_access([this] { + unsigned int list_size = 0; + const gnutls_datum_t* client_cert_list = gnutls_certificate_get_peers(*this, &list_size); + auto res = std::vector{}; + res.reserve(list_size); + if (client_cert_list) { + for (auto const& client_cert : std::span{client_cert_list, list_size}) { + res.emplace_back(client_cert.size); + std::copy_n(client_cert.data, client_cert.size, res.back().data()); + } + } + return res; + }); + } + struct session_ref; private: @@ -1922,6 +1938,9 @@ class tls_connected_socket_impl : public net::connected_socket_impl, public sess future> get_alt_name_information(std::unordered_set types) { return _session->get_alt_name_information(std::move(types)); } + future> get_peer_certificate_chain() { + return _session->get_peer_certificate_chain(); + } future<> wait_input_shutdown() override { return _session->socket().wait_input_shutdown(); } @@ -2113,6 +2132,10 @@ future> tls::get_alt_name_information(connect return get_tls_socket(socket)->get_alt_name_information(std::move(types)); } +future> tls::get_peer_certificate_chain(connected_socket& socket) { + return get_tls_socket(socket)->get_peer_certificate_chain(); +} + future tls::check_session_is_resumed(connected_socket& socket) { return get_tls_socket(socket)->check_session_is_resumed(); } diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index be07637c08..c49c29fcac 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -541,6 +541,7 @@ function(seastar_add_certgen name) set(CERT_PRIVKEY ${CERT_NAME}.key) set(CERT_REQ ${CERT_NAME}.csr) set(CERT_CERT ${CERT_NAME}.crt) + set(CERT_CERT_DER ${CERT_NAME}.crt.der) set(CERT_CAPRIVKEY ca${CERT_NAME}.key) set(CERT_CAROOT ca${CERT_NAME}.pem) @@ -581,8 +582,14 @@ function(seastar_add_certgen name) WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} ) - add_custom_target(${name} + add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/${CERT_CERT_DER}" + COMMAND ${OPENSSL} x509 -in ${CERT_CERT} -out ${CERT_CERT_DER} -outform der DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${CERT_CERT}" + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + ) + + add_custom_target(${name} + DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/${CERT_CERT_DER}" ) endfunction() diff --git a/tests/unit/tls_test.cc b/tests/unit/tls_test.cc index 7ed734f858..39b5fe4ef7 100644 --- a/tests/unit/tls_test.cc +++ b/tests/unit/tls_test.cc @@ -1435,6 +1435,66 @@ SEASTAR_THREAD_TEST_CASE(test_alt_names) { } +SEASTAR_THREAD_TEST_CASE(test_peer_certificate_chain_handling) { + tls::credentials_builder b; + + b.set_x509_key_file(certfile("test.crt"), certfile("test.key"), tls::x509_crt_format::PEM).get(); + b.set_x509_trust_file(certfile("catest.pem"), tls::x509_crt_format::PEM).get(); + b.set_client_auth(tls::client_auth::REQUIRE); + + auto creds = b.build_certificate_credentials(); + auto serv = b.build_server_credentials(); + + ::listen_options opts; + opts.reuse_address = true; + opts.set_fixed_cpu(this_shard_id()); + + auto addr = ::make_ipv4_address( {0x7f000001, 4712}); + auto server = tls::listen(serv, addr, opts); + + { + auto sa = server.accept(); + auto c = tls::connect(creds, addr).get(); + auto s = sa.get(); + + auto in = s.connection.input(); + output_stream out(c.output().detach(), 1024); + out.write("nils").get(); + + auto fscrts = tls::get_peer_certificate_chain(s.connection); + auto fccrts = tls::get_peer_certificate_chain(c); + + auto fout = out.flush(); + auto fin = in.read(); + + fout.get(); + + auto scrts = fscrts.get(); + auto ccrts = fccrts.get(); + fin.get(); + + in.close().get(); + out.close().get(); + + s.connection.shutdown_input(); + s.connection.shutdown_output(); + + c.shutdown_input(); + c.shutdown_output(); + + auto read_file = [](std::filesystem::path const& path) { + auto contents = tls::certificate_data(std::filesystem::file_size(path)); + std::ifstream{path, std::ios_base::binary}.read(reinterpret_cast(contents.data()), contents.size()); + return contents; + }; + + auto ders = {read_file(certfile("test.crt.der"))}; + + BOOST_REQUIRE(std::ranges::equal(scrts, ders)); + BOOST_REQUIRE(std::ranges::equal(ccrts, ders)); + } +} + SEASTAR_THREAD_TEST_CASE(test_skip_wait_for_eof) { tls::credentials_builder b;