Commit 23f32ba3 authored by louiz’'s avatar louiz’

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
parent fa071309
...@@ -21,6 +21,7 @@ find_package(Iconv REQUIRED) ...@@ -21,6 +21,7 @@ find_package(Iconv REQUIRED)
find_package(Libuuid REQUIRED) find_package(Libuuid REQUIRED)
find_package(Libidn) find_package(Libidn)
find_package(SystemdDaemon) find_package(SystemdDaemon)
find_package(Botan)
# To be able to include the config.h file generated by cmake # To be able to include the config.h file generated by cmake
include_directories("${CMAKE_CURRENT_SOURCE_DIR}/src/") include_directories("${CMAKE_CURRENT_SOURCE_DIR}/src/")
...@@ -37,6 +38,10 @@ if(SYSTEMDDAEMON_FOUND) ...@@ -37,6 +38,10 @@ if(SYSTEMDDAEMON_FOUND)
include_directories(${SYSTEMDDAEMON_INCLUDE_DIRS}) include_directories(${SYSTEMDDAEMON_INCLUDE_DIRS})
endif() endif()
if(BOTAN_FOUND)
include_directories(${BOTAN_INCLUDE_DIRS})
endif()
set(POLLER_DOCSTRING "Choose the poller between POLL and EPOLL (Linux-only)") set(POLLER_DOCSTRING "Choose the poller between POLL and EPOLL (Linux-only)")
if(${CMAKE_SYSTEM_NAME} MATCHES "Linux") if(${CMAKE_SYSTEM_NAME} MATCHES "Linux")
set(POLLER "EPOLL" CACHE STRING ${POLLER_DOCSTRING}) set(POLLER "EPOLL" CACHE STRING ${POLLER_DOCSTRING})
...@@ -95,6 +100,9 @@ file(GLOB source_network ...@@ -95,6 +100,9 @@ file(GLOB source_network
src/network/*.[hc]pp) src/network/*.[hc]pp)
add_library(network STATIC ${source_network}) add_library(network STATIC ${source_network})
target_link_libraries(network logger) target_link_libraries(network logger)
if(BOTAN_FOUND)
target_link_libraries(network ${BOTAN_LIBRARIES})
endif()
# #
## irclib ## irclib
......
# - 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)
...@@ -2,3 +2,4 @@ ...@@ -2,3 +2,4 @@
#cmakedefine LIBIDN_FOUND #cmakedefine LIBIDN_FOUND
#cmakedefine SYSTEMDDAEMON_FOUND #cmakedefine SYSTEMDDAEMON_FOUND
#cmakedefine POLLER ${POLLER} #cmakedefine POLLER ${POLLER}
#cmakedefine BOTAN_FOUND
\ No newline at end of file
...@@ -14,6 +14,8 @@ ...@@ -14,6 +14,8 @@
#include <chrono> #include <chrono>
#include <string> #include <string>
#include "config.h"
using namespace std::string_literals; using namespace std::string_literals;
using namespace std::chrono_literals; using namespace std::chrono_literals;
...@@ -35,6 +37,13 @@ IrcClient::IrcClient(std::shared_ptr<Poller> poller, const std::string& hostname ...@@ -35,6 +37,13 @@ IrcClient::IrcClient(std::shared_ptr<Poller> poller, const std::string& hostname
"alive without having to join a real channel of that server. " "alive without having to join a real channel of that server. "
"To disconnect from the IRC server, leave this room and all " "To disconnect from the IRC server, leave this room and all "
"other IRC channels of that server."; "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() IrcClient::~IrcClient()
...@@ -48,29 +57,39 @@ void IrcClient::start() ...@@ -48,29 +57,39 @@ void IrcClient::start()
{ {
if (this->connected || this->connecting) if (this->connected || this->connecting)
return ; 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->bridge->send_xmpp_message(this->hostname, "", "Connecting to "s +
this->hostname + ":" + "6667"); this->hostname + ":" + port + " (" +
this->connect(this->hostname, "6667"); (tls ? "encrypted" : "not encrypted") + ")");
this->connect(this->hostname, port, tls);
} }
void IrcClient::on_connection_failed(const std::string& reason) void IrcClient::on_connection_failed(const std::string& reason)
{ {
this->bridge->send_xmpp_message(this->hostname, "", this->bridge->send_xmpp_message(this->hostname, "",
"Connection failed: "s + reason); "Connection failed: "s + reason);
// Send an error message for all room that the user wanted to join if (this->ports_to_try.empty())
for (const std::string& channel: this->channels_to_join)
{ {
Iid iid(channel + "%" + this->hostname); // Send an error message for all room that the user wanted to join
this->bridge->send_join_failed(iid, this->current_nick, for (const std::string& channel: this->channels_to_join)
"cancel", "item-not-found", reason); {
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() void IrcClient::on_connected()
{ {
this->send_nick_command(this->username); this->send_nick_command(this->username);
this->send_user_command(this->username, 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(); this->send_pending_data();
} }
...@@ -326,7 +345,6 @@ void IrcClient::set_and_forward_user_list(const IrcMessage& message) ...@@ -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); const IrcUser* user = channel->add_user(nick, this->prefix_to_mode);
if (user->nick != channel->get_self()->nick) 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, this->bridge->send_user_join(this->hostname, chan_name, user,
user->get_most_significant_mode(this->sorted_user_modes), user->get_most_significant_mode(this->sorted_user_modes),
false); false);
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <string> #include <string>
#include <stack>
#include <map> #include <map>
#include <set> #include <set>
...@@ -19,8 +20,6 @@ class Bridge; ...@@ -19,8 +20,6 @@ class Bridge;
/** /**
* Represent one IRC client, i.e. an endpoint connected to a single IRC * Represent one IRC client, i.e. an endpoint connected to a single IRC
* server, through a TCP socket, receiving and sending commands to it. * 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 class IrcClient: public SocketHandler
{ {
...@@ -280,6 +279,12 @@ private: ...@@ -280,6 +279,12 @@ private:
* (for example 'ahov' is a common order). * (for example 'ahov' is a common order).
*/ */
std::vector<char> sorted_user_modes; std::vector<char> 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<std::pair<std::string, bool>> ports_to_try;
IrcClient(const IrcClient&) = delete; IrcClient(const IrcClient&) = delete;
IrcClient(IrcClient&&) = delete; IrcClient(IrcClient&&) = delete;
......
...@@ -10,26 +10,39 @@ ...@@ -10,26 +10,39 @@
#include <unistd.h> #include <unistd.h>
#include <stdlib.h> #include <stdlib.h>
#include <errno.h> #include <errno.h>
#include <netdb.h>
#include <cstring> #include <cstring>
#include <fcntl.h> #include <fcntl.h>
#include <netdb.h>
#include <stdio.h> #include <stdio.h>
#include <iostream> #include <iostream>
using namespace std::string_literals; #ifdef BOTAN_FOUND
# include <botan/hex.h>
#endif
#ifndef UIO_FASTIOV #ifndef UIO_FASTIOV
# define UIO_FASTIOV 8 # define UIO_FASTIOV 8
#endif #endif
using namespace std::string_literals;
namespace ph = std::placeholders;
SocketHandler::SocketHandler(std::shared_ptr<Poller> poller): SocketHandler::SocketHandler(std::shared_ptr<Poller> poller):
socket(-1), socket(-1),
poller(poller), poller(poller),
use_tls(false),
connected(false), connected(false),
connecting(false) connecting(false)
{ #ifdef BOTAN_FOUND
} ,
rng(),
credential_manager(),
policy(),
session_manager(rng)
#endif
{}
void SocketHandler::init_socket(const struct addrinfo* rp) void SocketHandler::init_socket(const struct addrinfo* rp)
{ {
...@@ -47,10 +60,11 @@ 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)); 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->address = address;
this->port = port; this->port = port;
this->use_tls = tls;
utils::ScopeGuard sg; utils::ScopeGuard sg;
...@@ -106,6 +120,10 @@ void SocketHandler::connect(const std::string& address, const std::string& port) ...@@ -106,6 +120,10 @@ void SocketHandler::connect(const std::string& address, const std::string& port)
this->poller->add_socket_handler(this); this->poller->add_socket_handler(this);
this->connected = true; this->connected = true;
this->connecting = false; this->connecting = false;
#ifdef BOTAN_FOUND
if (this->use_tls)
this->start_tls();
#endif
this->on_connected(); this->on_connected();
return ; return ;
} }
...@@ -133,10 +151,20 @@ void SocketHandler::connect(const std::string& address, const std::string& port) ...@@ -133,10 +151,20 @@ void SocketHandler::connect(const std::string& address, const std::string& port)
void SocketHandler::connect() void SocketHandler::connect()
{ {
this->connect(this->address, this->port); this->connect(this->address, this->port, this->use_tls);
} }
void SocketHandler::on_recv() 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; static constexpr size_t buf_size = 4096;
char buf[buf_size]; char buf[buf_size];
...@@ -145,6 +173,23 @@ void SocketHandler::on_recv() ...@@ -145,6 +173,23 @@ void SocketHandler::on_recv()
if (recv_buf == nullptr) if (recv_buf == nullptr)
recv_buf = buf; 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); ssize_t size = ::recv(this->socket, recv_buf, buf_size, 0);
if (0 == size) if (0 == size)
{ {
...@@ -155,22 +200,17 @@ void SocketHandler::on_recv() ...@@ -155,22 +200,17 @@ void SocketHandler::on_recv()
{ {
log_warning("Error while reading from socket: " << strerror(errno)); log_warning("Error while reading from socket: " << strerror(errno));
if (this->connecting) if (this->connecting)
this->on_connection_failed(strerror(errno)); {
this->close();
this->on_connection_failed(strerror(errno));
}
else 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 this->close();
// was provided to receive that data directly. The in_buf buffer this->on_connection_close();
// will be handled in parse_in_buffer()
this->in_buf += std::string(buf, size);
} }
this->parse_in_buffer(size);
} }
return size;
} }
void SocketHandler::on_send() void SocketHandler::on_send()
...@@ -241,6 +281,16 @@ socket_t SocketHandler::get_socket() const ...@@ -241,6 +281,16 @@ socket_t SocketHandler::get_socket() const
} }
void SocketHandler::send_data(std::string&& data) 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()) if (data.empty())
return ; return ;
...@@ -269,3 +319,89 @@ void* SocketHandler::get_receive_buffer(const size_t) const ...@@ -269,3 +319,89 @@ void* SocketHandler::get_receive_buffer(const size_t) const
{ {
return nullptr; 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<Botan::TLS::Client>(
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<const Botan::byte*>(recv_buf),
static_cast<size_t>(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<const Botan::byte*>(this->pre_buf.data()),
this->pre_buf.size());
this->pre_buf = "";
}
if (!data.empty())
this->tls->send(reinterpret_cast<const Botan::byte*>(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<const char*>(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<const char*>(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
#ifndef SOCKET_HANDLER_INCLUDED #ifndef SOCKET_HANDLER_INCLUDED
# define SOCKET_HANDLER_INCLUDED # define SOCKET_HANDLER_INCLUDED
#include <logger/logger.hpp>
#include <sys/types.h> #include <sys/types.h>
#include <sys/socket.h> #include <sys/socket.h>
#include <netdb.h> #include <netdb.h>
...@@ -10,6 +12,26 @@ ...@@ -10,6 +12,26 @@
#include <string> #include <string>
#include <list> #include <list>
#include "config.h"
#ifdef BOTAN_FOUND
# include <botan/botan.h>
# include <botan/tls_client.h>
/**
* 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<Botan::X509_Certificate>&)
{ // 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; typedef int socket_t;
class Poller; class Poller;
...@@ -24,21 +46,19 @@ class SocketHandler ...@@ -24,21 +46,19 @@ class SocketHandler
{ {
protected: protected:
~SocketHandler() {} ~SocketHandler() {}
public: public:
explicit SocketHandler(std::shared_ptr<Poller> poller); explicit SocketHandler(std::shared_ptr<Poller> poller);
/** /**
* Initialize the socket with the parameters contained in the given * Connect to the remote server, and call on_connected() if this
* addrinfo structure. * succeeds. If tls is true, we set use_tls to true and will also call
*/ * start_tls() when the connection succeeds.
void init_socket(const struct addrinfo* rp);
/**
* Connect to the remote server, and call on_connected() if this 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(); void connect();
/** /**
* Reads data in our in_buf and the call parse_in_buf, for the implementor * Reads raw data from the socket. And pass it to parse_in_buffer()
* to handle the data received so far. * If we are using TLS on this connection, we call tls_recv()
*/ */
void on_recv(); void on_recv();
/** /**
...@@ -48,6 +68,9 @@ public: ...@@ -48,6 +68,9 @@ public:
/** /**
* Add the given data to out_buf and tell our poller that we want to be * Add the given data to out_buf and tell our poller that we want to be
* notified when a send event is ready. * 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); void send_data(std::string&& data);
/** /**
...@@ -87,29 +110,94 @@ public: ...@@ -87,29 +110,94 @@ public:
bool is_connected() const; bool is_connected() const;
bool is_connecting() const; bool is_connecting() const;
protected: private:
/**