From 23f32ba39ebe5e9bbdfc4dd00d9914c0f0447ef4 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Sun, 18 May 2014 20:23:08 +0200 Subject: Implement TLS support using Botan For now, it tries two TLS ports and then connects to the non-tls port. In the future we would like the user to be able to configure that. fix #2435 --- CMakeLists.txt | 8 ++ cmake/Modules/FindBotan.cmake | 34 ++++++++ src/config.h.cmake | 1 + src/irc/irc_client.cpp | 36 ++++++-- src/irc/irc_client.hpp | 9 +- src/network/socket_handler.cpp | 172 +++++++++++++++++++++++++++++++++---- src/network/socket_handler.hpp | 186 ++++++++++++++++++++++++++++++++++------- src/xmpp/xmpp_component.cpp | 2 +- 8 files changed, 388 insertions(+), 60 deletions(-) create mode 100644 cmake/Modules/FindBotan.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index 90b5432..a71df57 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,7 @@ find_package(Iconv REQUIRED) find_package(Libuuid REQUIRED) find_package(Libidn) find_package(SystemdDaemon) +find_package(Botan) # To be able to include the config.h file generated by cmake include_directories("${CMAKE_CURRENT_SOURCE_DIR}/src/") @@ -37,6 +38,10 @@ if(SYSTEMDDAEMON_FOUND) include_directories(${SYSTEMDDAEMON_INCLUDE_DIRS}) endif() +if(BOTAN_FOUND) + include_directories(${BOTAN_INCLUDE_DIRS}) +endif() + set(POLLER_DOCSTRING "Choose the poller between POLL and EPOLL (Linux-only)") if(${CMAKE_SYSTEM_NAME} MATCHES "Linux") set(POLLER "EPOLL" CACHE STRING ${POLLER_DOCSTRING}) @@ -95,6 +100,9 @@ file(GLOB source_network src/network/*.[hc]pp) add_library(network STATIC ${source_network}) target_link_libraries(network logger) +if(BOTAN_FOUND) + target_link_libraries(network ${BOTAN_LIBRARIES}) +endif() # ## irclib diff --git a/cmake/Modules/FindBotan.cmake b/cmake/Modules/FindBotan.cmake new file mode 100644 index 0000000..f53680d --- /dev/null +++ b/cmake/Modules/FindBotan.cmake @@ -0,0 +1,34 @@ +# - Find botan +# Find the botan cryptographic library +# +# This module defines the following variables: +# BOTAN_FOUND - True if library and include directory are found +# If set to TRUE, the following are also defined: +# BOTAN_INCLUDE_DIRS - The directory where to find the header file +# BOTAN_LIBRARIES - Where to find the library file +# +# For conveniance, these variables are also set. They have the same values +# than the variables above. The user can thus choose his/her prefered way +# to write them. +# BOTAN_LIBRARY +# BOTAN_INCLUDE_DIR +# +# This file is in the public domain + +find_path(BOTAN_INCLUDE_DIRS NAMES botan/botan.h + DOC "The botan include directory") + +find_library(BOTAN_LIBRARIES NAMES botan + DOC "The botan library") + +# Use some standard module to handle the QUIETLY and REQUIRED arguments, and +# set BOTAN_FOUND to TRUE if these two variables are set. +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Botan REQUIRED_VARS BOTAN_LIBRARIES BOTAN_INCLUDE_DIRS) + +if(BOTAN_FOUND) + set(BOTAN_LIBRARY ${BOTAN_LIBRARIES}) + set(BOTAN_INCLUDE_DIR ${BOTAN_INCLUDE_DIRS}) +endif() + +mark_as_advanced(BOTAN_INCLUDE_DIRS BOTAN_LIBRARIES) diff --git a/src/config.h.cmake b/src/config.h.cmake index 62186cc..66ea967 100644 --- a/src/config.h.cmake +++ b/src/config.h.cmake @@ -2,3 +2,4 @@ #cmakedefine LIBIDN_FOUND #cmakedefine SYSTEMDDAEMON_FOUND #cmakedefine POLLER ${POLLER} +#cmakedefine BOTAN_FOUND \ No newline at end of file diff --git a/src/irc/irc_client.cpp b/src/irc/irc_client.cpp index 01bf569..c3765a6 100644 --- a/src/irc/irc_client.cpp +++ b/src/irc/irc_client.cpp @@ -14,6 +14,8 @@ #include #include +#include "config.h" + using namespace std::string_literals; using namespace std::chrono_literals; @@ -35,6 +37,13 @@ IrcClient::IrcClient(std::shared_ptr poller, const std::string& hostname "alive without having to join a real channel of that server. " "To disconnect from the IRC server, leave this room and all " "other IRC channels of that server."; + // TODO: get the values from the preferences of the user, and only use the + // list of default ports if the user didn't specify anything + this->ports_to_try.emplace("6667", false); // standard non-encrypted port +#ifdef BOTAN_FOUND + this->ports_to_try.emplace("6670", true); // non-standard but I want it for some servers + this->ports_to_try.emplace("6697", true); // standard encrypted port +#endif // BOTAN_FOUND } IrcClient::~IrcClient() @@ -48,29 +57,39 @@ void IrcClient::start() { if (this->connected || this->connecting) return ; + std::string port; + bool tls; + std::tie(port, tls) = this->ports_to_try.top(); + this->ports_to_try.pop(); this->bridge->send_xmpp_message(this->hostname, "", "Connecting to "s + - this->hostname + ":" + "6667"); - this->connect(this->hostname, "6667"); + this->hostname + ":" + port + " (" + + (tls ? "encrypted" : "not encrypted") + ")"); + this->connect(this->hostname, port, tls); } void IrcClient::on_connection_failed(const std::string& reason) { this->bridge->send_xmpp_message(this->hostname, "", "Connection failed: "s + reason); - // Send an error message for all room that the user wanted to join - for (const std::string& channel: this->channels_to_join) + if (this->ports_to_try.empty()) { - Iid iid(channel + "%" + this->hostname); - this->bridge->send_join_failed(iid, this->current_nick, - "cancel", "item-not-found", reason); + // Send an error message for all room that the user wanted to join + for (const std::string& channel: this->channels_to_join) + { + Iid iid(channel + "%" + this->hostname); + this->bridge->send_join_failed(iid, this->current_nick, + "cancel", "item-not-found", reason); + } } + else // try the next port + this->start(); } void IrcClient::on_connected() { this->send_nick_command(this->username); this->send_user_command(this->username, this->username); - this->send_gateway_message("Connected to IRC server."); + this->send_gateway_message("Connected to IRC server"s + (this->use_tls ? " (encrypted)": "") + "."); this->send_pending_data(); } @@ -326,7 +345,6 @@ void IrcClient::set_and_forward_user_list(const IrcMessage& message) const IrcUser* user = channel->add_user(nick, this->prefix_to_mode); if (user->nick != channel->get_self()->nick) { - log_debug("Adding user [" << nick << "] to chan " << chan_name); this->bridge->send_user_join(this->hostname, chan_name, user, user->get_most_significant_mode(this->sorted_user_modes), false); diff --git a/src/irc/irc_client.hpp b/src/irc/irc_client.hpp index 2f28c95..7dff1db 100644 --- a/src/irc/irc_client.hpp +++ b/src/irc/irc_client.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -19,8 +20,6 @@ class Bridge; /** * Represent one IRC client, i.e. an endpoint connected to a single IRC * server, through a TCP socket, receiving and sending commands to it. - * - * TODO: TLS support, maybe, but that's not high priority */ class IrcClient: public SocketHandler { @@ -280,6 +279,12 @@ private: * (for example 'ahov' is a common order). */ std::vector sorted_user_modes; + /** + * A list of ports to which we will try to connect, in reverse. Each port + * is associated with a boolean telling if we should use TLS or not if the + * connection succeeds on that port. + */ + std::stack> ports_to_try; IrcClient(const IrcClient&) = delete; IrcClient(IrcClient&&) = delete; diff --git a/src/network/socket_handler.cpp b/src/network/socket_handler.cpp index d509513..d989623 100644 --- a/src/network/socket_handler.cpp +++ b/src/network/socket_handler.cpp @@ -10,26 +10,39 @@ #include #include #include +#include #include #include -#include #include #include -using namespace std::string_literals; +#ifdef BOTAN_FOUND +# include +#endif #ifndef UIO_FASTIOV # define UIO_FASTIOV 8 #endif +using namespace std::string_literals; + +namespace ph = std::placeholders; + SocketHandler::SocketHandler(std::shared_ptr poller): socket(-1), poller(poller), + use_tls(false), connected(false), connecting(false) -{ -} +#ifdef BOTAN_FOUND + , + rng(), + credential_manager(), + policy(), + session_manager(rng) +#endif +{} void SocketHandler::init_socket(const struct addrinfo* rp) { @@ -47,10 +60,11 @@ void SocketHandler::init_socket(const struct addrinfo* rp) throw std::runtime_error("Could not initialize socket: "s + strerror(errno)); } -void SocketHandler::connect(const std::string& address, const std::string& port) +void SocketHandler::connect(const std::string& address, const std::string& port, const bool tls) { this->address = address; this->port = port; + this->use_tls = tls; utils::ScopeGuard sg; @@ -106,6 +120,10 @@ void SocketHandler::connect(const std::string& address, const std::string& port) this->poller->add_socket_handler(this); this->connected = true; this->connecting = false; +#ifdef BOTAN_FOUND + if (this->use_tls) + this->start_tls(); +#endif this->on_connected(); return ; } @@ -133,10 +151,20 @@ void SocketHandler::connect(const std::string& address, const std::string& port) void SocketHandler::connect() { - this->connect(this->address, this->port); + this->connect(this->address, this->port, this->use_tls); } void SocketHandler::on_recv() +{ +#ifdef BOTAN_FOUND + if (this->use_tls) + this->tls_recv(); + else +#endif + this->plain_recv(); +} + +void SocketHandler::plain_recv() { static constexpr size_t buf_size = 4096; char buf[buf_size]; @@ -145,6 +173,23 @@ void SocketHandler::on_recv() if (recv_buf == nullptr) recv_buf = buf; + const ssize_t size = this->do_recv(recv_buf, buf_size); + + if (size > 0) + { + if (buf == recv_buf) + { + // data needs to be placed in the in_buf string, because no buffer + // was provided to receive that data directly. The in_buf buffer + // will be handled in parse_in_buffer() + this->in_buf += std::string(buf, size); + } + this->parse_in_buffer(size); + } +} + +ssize_t SocketHandler::do_recv(void* recv_buf, const size_t buf_size) +{ ssize_t size = ::recv(this->socket, recv_buf, buf_size, 0); if (0 == size) { @@ -155,22 +200,17 @@ void SocketHandler::on_recv() { log_warning("Error while reading from socket: " << strerror(errno)); if (this->connecting) - this->on_connection_failed(strerror(errno)); + { + this->close(); + this->on_connection_failed(strerror(errno)); + } else - this->on_connection_close(); - this->close(); - } - else - { - if (buf == recv_buf) { - // data needs to be placed in the in_buf string, because no buffer - // was provided to receive that data directly. The in_buf buffer - // will be handled in parse_in_buffer() - this->in_buf += std::string(buf, size); + this->close(); + this->on_connection_close(); } - this->parse_in_buffer(size); } + return size; } void SocketHandler::on_send() @@ -241,6 +281,16 @@ socket_t SocketHandler::get_socket() const } void SocketHandler::send_data(std::string&& data) +{ +#ifdef BOTAN_FOUND + if (this->use_tls) + this->tls_send(std::move(data)); + else +#endif + this->raw_send(std::move(data)); +} + +void SocketHandler::raw_send(std::string&& data) { if (data.empty()) return ; @@ -269,3 +319,89 @@ void* SocketHandler::get_receive_buffer(const size_t) const { return nullptr; } + +#ifdef BOTAN_FOUND +void SocketHandler::start_tls() +{ + Botan::TLS::Server_Information server_info(this->address, "irc", std::stoul(this->port)); + this->tls = std::make_unique( + std::bind(&SocketHandler::tls_output_fn, this, ph::_1, ph::_2), + std::bind(&SocketHandler::tls_data_cb, this, ph::_1, ph::_2), + std::bind(&SocketHandler::tls_alert_cb, this, ph::_1, ph::_2, ph::_3), + std::bind(&SocketHandler::tls_handshake_cb, this, ph::_1), + session_manager, credential_manager, policy, + rng, server_info, Botan::TLS::Protocol_Version::latest_tls_version()); +} + +void SocketHandler::tls_recv() +{ + static constexpr size_t buf_size = 4096; + char recv_buf[buf_size]; + + const ssize_t size = this->do_recv(recv_buf, buf_size); + if (size > 0) + { + const bool was_active = this->tls->is_active(); + this->tls->received_data(reinterpret_cast(recv_buf), + static_cast(size)); + if (!was_active && this->tls->is_active()) + this->on_tls_activated(); + } +} + +void SocketHandler::tls_send(std::string&& data) +{ + if (this->tls->is_active()) + { + const bool was_active = this->tls->is_active(); + if (!this->pre_buf.empty()) + { + this->tls->send(reinterpret_cast(this->pre_buf.data()), + this->pre_buf.size()); + this->pre_buf = ""; + } + if (!data.empty()) + this->tls->send(reinterpret_cast(data.data()), + data.size()); + if (!was_active && this->tls->is_active()) + this->on_tls_activated(); + } + else + this->pre_buf += data; +} + +void SocketHandler::tls_data_cb(const Botan::byte* data, size_t size) +{ + this->in_buf += std::string(reinterpret_cast(data), + size); + if (!this->in_buf.empty()) + this->parse_in_buffer(size); +} + +void SocketHandler::tls_output_fn(const Botan::byte* data, size_t size) +{ + this->raw_send(std::string(reinterpret_cast(data), size)); +} + +void SocketHandler::tls_alert_cb(Botan::TLS::Alert alert, const Botan::byte*, size_t) +{ + log_debug("tls_alert: " << alert.type_string()); +} + +bool SocketHandler::tls_handshake_cb(const Botan::TLS::Session& session) +{ + log_debug("Handshake with " << session.server_info().hostname() << " complete." + << " Version: " << session.version().to_string() + << " using " << session.ciphersuite().to_string()); + if (!session.session_id().empty()) + log_debug("Session ID " << Botan::hex_encode(session.session_id())); + if (!session.session_ticket().empty()) + log_debug("Session ticket " << Botan::hex_encode(session.session_ticket())); + return true; +} + +void SocketHandler::on_tls_activated() +{ + this->send_data(""); +} +#endif // BOTAN_FOUND diff --git a/src/network/socket_handler.hpp b/src/network/socket_handler.hpp index 1be3db1..02311c4 100644 --- a/src/network/socket_handler.hpp +++ b/src/network/socket_handler.hpp @@ -1,6 +1,8 @@ #ifndef SOCKET_HANDLER_INCLUDED # define SOCKET_HANDLER_INCLUDED +#include + #include #include #include @@ -10,6 +12,26 @@ #include #include +#include "config.h" + +#ifdef BOTAN_FOUND +# include +# include + +/** + * A very simple credential manager that accepts any certificate. + */ +class Permissive_Credentials_Manager: public Botan::Credentials_Manager +{ +public: + void verify_certificate_chain(const std::string& type, const std::string& purported_hostname, const std::vector&) + { // TODO: Offer the admin to disallow connection on untrusted + // certificates + log_debug("Checking remote certificate (" << type << ") for hostname " << purported_hostname); + } +}; +#endif // BOTAN_FOUND + typedef int socket_t; class Poller; @@ -24,21 +46,19 @@ class SocketHandler { protected: ~SocketHandler() {} + public: explicit SocketHandler(std::shared_ptr poller); /** - * Initialize the socket with the parameters contained in the given - * addrinfo structure. - */ - void init_socket(const struct addrinfo* rp); - /** - * Connect to the remote server, and call on_connected() if this succeeds + * Connect to the remote server, and call on_connected() if this + * succeeds. If tls is true, we set use_tls to true and will also call + * start_tls() when the connection succeeds. */ - void connect(const std::string& address, const std::string& port); + void connect(const std::string& address, const std::string& port, const bool tls); void connect(); /** - * Reads data in our in_buf and the call parse_in_buf, for the implementor - * to handle the data received so far. + * Reads raw data from the socket. And pass it to parse_in_buffer() + * If we are using TLS on this connection, we call tls_recv() */ void on_recv(); /** @@ -48,6 +68,9 @@ public: /** * Add the given data to out_buf and tell our poller that we want to be * notified when a send event is ready. + * + * This can be overriden if we want to modify the data before sending + * it. For example if we want to encrypt it. */ void send_data(std::string&& data); /** @@ -87,29 +110,94 @@ public: bool is_connected() const; bool is_connecting() const; -protected: +private: /** - * Provide a buffer in which data can be directly received. This can be - * used to avoid copying data into in_buf before using it. If no buffer - * can provided, nullptr is returned (the default implementation does - * that). + * Initialize the socket with the parameters contained in the given + * addrinfo structure. */ - virtual void* get_receive_buffer(const size_t size) const; + void init_socket(const struct addrinfo* rp); /** - * The handled socket. + * Reads from the socket into the provided buffer. If an error occurs + * (read returns <= 0), the handling of the error is done here (close the + * connection, log a message, etc). + * + * Returns the value returned by ::recv(), so the buffer should not be + * used if it’s not positive. */ - socket_t socket; + ssize_t do_recv(void* recv_buf, const size_t buf_size); /** - * Where data read from the socket is added until we can extract a full - * and meaningful “message” from it. - * - * TODO: something more efficient than a string. + * Reads data from the socket and calls parse_in_buffer with it. */ - std::string in_buf; + void plain_recv(); + /** + * Mark the given data as ready to be sent, as-is, on the socket, as soon + * as we can. + */ + void raw_send(std::string&& data); + +#ifdef BOTAN_FOUND + /** + * Create the TLS::Client object, with all the callbacks etc. This must be + * called only when we know we are able to send TLS-encrypted data over + * the socket. + */ + void start_tls(); + /** + * An additional step to pass the data into our tls object to decrypt it + * before passing it to parse_in_buffer. + */ + void tls_recv(); + /** + * Pass the data to the tls object in order to encrypt it. The tls object + * will then call raw_send as a callback whenever data as been encrypted + * and can be sent on the socket. + */ + void tls_send(std::string&& data); + /** + * Called by the tls object that some data has been decrypt. We call + * parse_in_buffer() to handle that unencrypted data. + */ + void tls_data_cb(const Botan::byte* data, size_t size); + /** + * Called by the tls object to indicate that some data has been encrypted + * and is now ready to be sent on the socket as is. + */ + void tls_output_fn(const Botan::byte* data, size_t size); + /** + * Called by the tls object to indicate that a TLS alert has been + * received. We don’t use it, we just log some message, at the moment. + */ + void tls_alert_cb(Botan::TLS::Alert alert, const Botan::byte*, size_t); + /** + * Called by the tls object at the end of the TLS handshake. We don't do + * anything here appart from logging the TLS session information. + */ + bool tls_handshake_cb(const Botan::TLS::Session& session); + /** + * Called whenever the tls session goes from inactive to active. This + * means that the handshake has just been successfully done, and we can + * now proceed to send any available data into our tls object. + */ + void on_tls_activated(); +#endif // BOTAN_FOUND + /** + * The handled socket. + */ + socket_t socket; /** * Where data is added, when we want to send something to the client. */ std::list out_buf; + /** + * Keep the details of the addrinfo the triggered a EINPROGRESS error when + * connect()ing to it, to reuse it directly when connect() is called + * again. + */ + struct addrinfo addrinfo; + struct sockaddr ai_addr; + socklen_t ai_addrlen; + +protected: /** * A pointer to the poller that manages us, because we need to communicate * with it, sometimes (for example to tell it that he now needs to watch @@ -119,6 +207,25 @@ protected: * (actually it is sharing our ownership with a Bridge). */ std::shared_ptr poller; + /** + * Where data read from the socket is added until we can extract a full + * and meaningful “message” from it. + * + * TODO: something more efficient than a string. + */ + std::string in_buf; + /** + * Whether we are using TLS on this connection or not. + */ + bool use_tls; + /** + * Provide a buffer in which data can be directly received. This can be + * used to avoid copying data into in_buf before using it. If no buffer + * needs to be provided, nullptr is returned (the default implementation + * does that), in that case our internal in_buf will be used to save the + * data until it can be used by parse_in_buffer(). + */ + virtual void* get_receive_buffer(const size_t size) const; /** * Hostname we are connected/connecting to */ @@ -127,14 +234,6 @@ protected: * Port we are connected/connecting to */ std::string port; - /** - * Keep the details of the addrinfo the triggered a EINPROGRESS error when - * connect()ing to it, to reuse it directly when connect() is called - * again. - */ - struct addrinfo addrinfo; - struct sockaddr ai_addr; - socklen_t ai_addrlen; bool connected; bool connecting; @@ -144,6 +243,33 @@ private: SocketHandler(SocketHandler&&) = delete; SocketHandler& operator=(const SocketHandler&) = delete; SocketHandler& operator=(SocketHandler&&) = delete; + +#ifdef BOTAN_FOUND + /** + * Botan stuff to manipulate a TLS session. + */ + Botan::AutoSeeded_RNG rng; + Permissive_Credentials_Manager credential_manager; + Botan::TLS::Policy policy; + Botan::TLS::Session_Manager_In_Memory session_manager; + /** + * We use a unique_ptr because we may not want to create the object at + * all. The Botan::TLS::Client object generates a handshake message as + * soon and calls the output_fn callback with it as soon as it is + * created. Therefore, we do not want to create it if do not intend to do + * send any TLS-encrypted message. We create the object only when needed + * (for example after we have negociated a TLS session using a STARTTLS + * message, or stuf like that). + * + * See start_tls for the method where this object is created. + */ + std::unique_ptr tls; + /** + * An additional buffer to keep data that the user wants to send, but + * cannot because the handshake is not done. + */ + std::string pre_buf; +#endif // BOTAN_FOUND }; #endif // SOCKET_HANDLER_INCLUDED diff --git a/src/xmpp/xmpp_component.cpp b/src/xmpp/xmpp_component.cpp index 3ec043c..06db085 100644 --- a/src/xmpp/xmpp_component.cpp +++ b/src/xmpp/xmpp_component.cpp @@ -68,7 +68,7 @@ XmppComponent::~XmppComponent() void XmppComponent::start() { - this->connect("127.0.0.1", "5347"); + this->connect("127.0.0.1", "5347", false); } bool XmppComponent::is_document_open() const -- cgit v1.2.3