Pull to refresh

Защищенное TLS-соединение с использованием Boost.Asio и OpenSSL под Windows

Reading time 10 min
Views 52K

Введение


Однажды мне потребовалось создать защищенный канал связи между своим сервером и своим приложением. Я помнил, что в документации к Boost Asio упоминалось, что он может работать с защищенными соединениями, используя OpenSSL. Я начал искать информацию по этой теме, но, увы, мало что нашел, тем более под Windows. Так что теперь, разобравшись с этим вопросом, я решил написать эту инструкцию, чтобы другим людям было проще разобраться.

Задача — нужно собрать под Windows сервер и клиент, используя Boost Asio и OpenSSL, чтобы клиент и сервер обменивались информацией по защищенному TLS-каналу. Для пример, я решил взять вот этот клиент и сервер с официального сайта Boost.

Для того чтобы решить эту задачу, нам нужно собрать OpenSSL, подготовить ключи и сертификаты, и собрать оба примера с использованием Boost Asio, OpenSSL.

Установка OpenSSL под Windows


Я взял OpenSSL из официального репозитория: github.com/openssl/openssl

Для установки OpenSSL нам понадобятся:
  • NASM, чтобы компилировать исходники на ассемблере, я взял отсюда: www.nasm.us Также нужно добавить путь к nasm.exe в переменную среды PATH.
  • Active Perl, чтобы запускать скрипты конфигурирования, я взял отсюда: www.activestate.com/activeperl И путь к perl.exe также нужно добавить в переменную среды PATH.


Для сборки OpenSSL я использовал MS Visual Studio 2013, и я собирал статическую библиотеку.

Последовательность сборки следующая:
Сначала нужно сконфигурировать OpenSSL с помощью скрипта на Perl, под Win32. Ниже по тексту я буду считать, что OpenSSL у вас находится в C:\Work\OpenSSL. Вам следует зайти в этот каталог и вызвать скрипт конфигурации:

cd C:\Work\OpenSSL
perl Configure VC-WIN32 --prefix=C:\Work\OpenSSL\output enable-deprecated -I$(SRC_D)

Обратите внимание вот на что:
  • Здесь явно задан параметр --prefix и указан путь, где будет лежать результат сборки. OpenSSL будет лежать в отдельном подкаталоге \output и не будет смешиваться с исходными файлами.
  • Здесь же задан параметр enable-deprecated — это означает, что в сборку будут включен deprecated-код. Я пробовал собрать без этого параметра, и Boost Asio жаловался на отсутствие функций CRYPTO_set_id_callback из openssl\crypto.h и DH_free из openssl\dh.h и поэтому я решил собрать с параметром enable-deprecated.
  • По непонятным мне причинам, конфигуратор не добавляет исходную директорию C:\Work\OpenSSL в список директорий для поиска *.h файлов, поэтому я добавил -I$(SRC_D), чтобы заставить компилятор искать там заголовочные файлы. Вместо этого вы можете добавить -IC:\Work\OpenSSL. Другой вариант — после того, как вы вызовете ms\do_nasm, просто отредактируйте вручную файл ms\nt.mak и впишите туда путь к исходникам.

Дальше нужно подготовить ассемблерные исходники для сборки. Нужно из той же директории вызвать скрипт сборки:

ms\do_nasm

На этом нужно закрыть обычную командную строку, и запустить командную строку MS Visual Studio, в которой определены дополнительные пути к файлам и дополнительные переменные окружения. Вы можете найти командную строку MS Visual Studio в каталоге C:\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\Tools\Shortcuts.

Из командной строки MS Visual Studio нужно перейти в каталог C:\Work\OpenSSL и запустить сборку с помощью nmake:

nmake -f ms\nt.mak

Это команда для сборки статической библиотеки, если вы хотите собрать динамическую библиотеку, то нужно запускать ntdll.mak.

После выполнения этой команды должна начаться длительная процедура сборки. Если сборка не идет, то вот возможные решения этой проблемы:
  • Убедитесь, что вы добавили путь к nasm.exe в переменную среды PATH
  • Убедитесь, что вы запускаете сборку из каталога C:\Work\OpenSSL
  • Убедитесь, что вы запускаете сборку не из обычной командной строки, а из командной строки MS Visual Studio.

В процессе сборки возможна еще одна проблема. Компилятор будет жаловаться на то, что ему не удалось найти файл tmp32/x86cpuid.obj или другие файлы, которые должны быть собраны из исходников *.asm. В моем случае проблема решилась после того, как я добавил путь к nasm в переменную окружения PATH. Другой вариант решения — можно просто вручную скомпилировать nasm-ом все ассемблерные файлы, их там всего 22.

После того, как сборка завершилась, нужно скопировать библиотеки и исходные файлы в новый каталог:

nmake -f ms\nt.mak install

На этом сборка OpenSSL под Windows завершена.

Сборка клиента и сервера


Как я и сказал раньше, для примера я решил взять вот эти клиент и сервер из документации по Boost Asio. Однако при попытке сборки я столкнулся с некоторыми проблемами, и в результате мне пришлось модифицировать исходники.

Итак:
  • 27 января 2015 года в OpenSSL внесли очень большой и важный коммит, который вынес много разных структур, объявлений и функций из главного заголовка ssl.h в внутренний заголовок ssl_locl.h. Эти все структуры используются в Boost Asio, поэтому нужно подключить этот файл ssl_locl.h.
  • Заголовок ssl_locl.h также ссылается на заголовок packet_locl.h, а в нем на 411 строчке происходит неявное преобразование из void* в unsigned char*:
    *data = BUF_memdup(pkt->curr, length);
    

    Хотя это место и объявлено как extern «C», и с точки зрения С тут нет никаких ошибок, но Visual Studio не дает нам никакой возможности отключить эту ошибку. Мне пришлось вносить изменения и преобразовывать тип явно:
    *data = (unsigned char*)BUF_memdup(pkt->curr, length);
    

  • Изначально в ssl.h была объявлена константа SSL_R_SHORT_READ, но потом ее зачем-то убрали. Эта константа используется в Boost Asio, и ее можно просто объявить перед подключением заголовка.
    #define SSL_R_SHORT_READ 219
    #include "ssl_locl.h"
    #include <boost/asio/ssl.hpp>
    

  • Нужно не забыть добавить директивы препроцессора _WIN32_WINNT=0x0501 — для Boost, OPENSSL_NO_SSL2 — чтобы отключить устаревшую версию SSL и OPENSSL_USE_DEPRECATED, поскольку мы конфигурировали OpenSSL с ключом enable-deprecated.
  • И, наконец, нужно добавить в пути поиска заголовочных файлов каталоги D:\Work\OpenSSL и D:\Work\OpenSSL\output\include

После всех вышеупомянутых манипуляций, мне удалось собрать и запустить проект с Boost Asio и OpenSSL под Windows с использованием Visual Studio 2013.

Исходный код сервера:
server.cpp
#include <cstdlib>
#include <iostream>

#include <boost/bind.hpp>
#include <boost/asio.hpp>

#define SSL_R_SHORT_READ 219
#include "ssl/ssl_locl.h"
#include <boost/asio/ssl.hpp>



typedef boost::asio::ssl::stream<boost::asio::ip::tcp::socket> ssl_socket;

class session
{
public:
	session(boost::asio::io_service& io_service,
		boost::asio::ssl::context& context)
		: socket_(io_service, context)
	{
	}

	ssl_socket::lowest_layer_type& socket()
	{
		return socket_.lowest_layer();
	}

	void start()
	{
		socket_.async_handshake(boost::asio::ssl::stream_base::server,
			boost::bind(&session::handle_handshake, this,
			boost::asio::placeholders::error));
	}

	void handle_handshake(const boost::system::error_code& error)
	{
		if (!error)
		{
			socket_.async_read_some(boost::asio::buffer(data_, max_length),
				boost::bind(&session::handle_read, this,
				boost::asio::placeholders::error,
				boost::asio::placeholders::bytes_transferred));
		}
		else
		{
			delete this;
		}
	}

	void handle_read(const boost::system::error_code& error,
		size_t bytes_transferred)
	{
		if (!error)
		{
			boost::asio::async_write(socket_,
				boost::asio::buffer(data_, bytes_transferred),
				boost::bind(&session::handle_write, this,
				boost::asio::placeholders::error));
		}
		else
		{
			delete this;
		}
	}

	void handle_write(const boost::system::error_code& error)
	{
		if (!error)
		{
			socket_.async_read_some(boost::asio::buffer(data_, max_length),
				boost::bind(&session::handle_read, this,
				boost::asio::placeholders::error,
				boost::asio::placeholders::bytes_transferred));
		}
		else
		{
			delete this;
		}
	}

private:
	ssl_socket socket_;
	enum { max_length = 1024 };
	char data_[max_length];
};

class server
{
public:
	server(boost::asio::io_service& io_service, unsigned short port)
		: io_service_(io_service),
		acceptor_(io_service,
		boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)),
		context_(boost::asio::ssl::context::sslv23)
	{
		context_.set_options(
			boost::asio::ssl::context::default_workarounds
			| boost::asio::ssl::context::no_sslv2
			| boost::asio::ssl::context::single_dh_use);
		context_.set_password_callback(boost::bind(&server::get_password, this));
		context_.use_certificate_chain_file("user.crt");
		context_.use_private_key_file("user.key", boost::asio::ssl::context::pem);
		context_.use_tmp_dh_file("dh2048.pem");

		start_accept();
	}

	std::string get_password() const
	{
		return "";
	}

	void start_accept()
	{
		session* new_session = new session(io_service_, context_);
		acceptor_.async_accept(new_session->socket(),
					boost::bind(&server::handle_accept, this, new_session,
					boost::asio::placeholders::error));
	}

	void handle_accept(session* new_session,
		const boost::system::error_code& error)
	{
		if (!error)
		{
			new_session->start();
		}
		else
		{
			delete new_session;
		}

		start_accept();
	}

private:
	boost::asio::io_service& io_service_;
	boost::asio::ip::tcp::acceptor acceptor_;
	boost::asio::ssl::context context_;
};


int main(int argc, char* argv[])
{



	try
	{
		if (argc != 2)
		{
			std::cerr << "Usage: server <port>\n";
			return 1;
		}


		boost::asio::io_service io_service;

		using namespace std; // For atoi.
		server s(io_service, atoi(argv[1]));

		io_service.run();
	}
	catch (std::exception& e)
	{
		std::cerr << "Exception: " << e.what() << "\n";
	}


	return 0;
}



Исходный код клиента:
client.cpp
#include <cstdlib>
#include <iostream>

#include <boost/bind.hpp>
#include <boost/asio.hpp>

#define SSL_R_SHORT_READ 219
#include "ssl/ssl_locl.h"
#include <boost/asio/ssl.hpp>


enum { max_length = 1024 };

class client
{
public:
	client(boost::asio::io_service& io_service,
			boost::asio::ssl::context& context,
			boost::asio::ip::tcp::resolver::iterator endpoint_iterator)
		: socket_(io_service, context)
	{
		socket_.set_verify_mode(boost::asio::ssl::verify_peer);
		socket_.set_verify_callback(
			boost::bind(&client::verify_certificate, this, _1, _2));

		boost::asio::async_connect(socket_.lowest_layer(), endpoint_iterator,
			boost::bind(&client::handle_connect, this,
			boost::asio::placeholders::error));
	}

	bool verify_certificate(bool preverified,
		boost::asio::ssl::verify_context& ctx)
	{
		// The verify callback can be used to check whether the certificate that is
		// being presented is valid for the peer. For example, RFC 2818 describes
		// the steps involved in doing this for HTTPS. Consult the OpenSSL
		// documentation for more details. Note that the callback is called once
		// for each certificate in the certificate chain, starting from the root
		// certificate authority.

		// In this example we will simply print the certificate's subject name.
		char subject_name[256];
		X509* cert = X509_STORE_CTX_get_current_cert(ctx.native_handle());
		X509_NAME_oneline(X509_get_subject_name(cert), subject_name, 256);
		std::cout << "Verifying " << subject_name << "\n";

		return preverified;
	}

	void handle_connect(const boost::system::error_code& error)
	{
		if (!error)
		{
			socket_.async_handshake(boost::asio::ssl::stream_base::client,
				boost::bind(&client::handle_handshake, this,
				boost::asio::placeholders::error));
		}
		else
		{
			std::cout << "Connect failed: " << error.message() << "\n";
		}
	}

	void handle_handshake(const boost::system::error_code& error)
	{
		if (!error)
		{
			std::cout << "Enter message: ";
			std::cin.getline(request_, max_length);
			size_t request_length = strlen(request_);

			boost::asio::async_write(socket_,
				boost::asio::buffer(request_, request_length),
				boost::bind(&client::handle_write, this,
				boost::asio::placeholders::error,
				boost::asio::placeholders::bytes_transferred));
		}
		else
		{
			std::cout << "Handshake failed: " << error.message() << "\n";
		}
	}

	void handle_write(const boost::system::error_code& error,
		size_t bytes_transferred)
	{
		if (!error)
		{
			boost::asio::async_read(socket_,
				boost::asio::buffer(reply_, bytes_transferred),
				boost::bind(&client::handle_read, this,
				boost::asio::placeholders::error,
				boost::asio::placeholders::bytes_transferred));
		}
		else
		{
			std::cout << "Write failed: " << error.message() << "\n";
		}
	}

	void handle_read(const boost::system::error_code& error,
		size_t bytes_transferred)
	{
		if (!error)
		{
			std::cout << "Reply: ";
			std::cout.write(reply_, bytes_transferred);
			std::cout << "\n";
		}
		else
		{
			std::cout << "Read failed: " << error.message() << "\n";
		}
	}

private:
	boost::asio::ssl::stream<boost::asio::ip::tcp::socket> socket_;
	char request_[max_length];
	char reply_[max_length];
};

int main(int argc, char* argv[])
{
	try
	{
		if (argc != 3)
		{
			std::cerr << "Usage: client <host> <port>\n";
			return 1;
		}


		boost::asio::io_service io_service;

		boost::asio::ip::tcp::resolver resolver(io_service);
		boost::asio::ip::tcp::resolver::query query(argv[1], argv[2]);
		boost::asio::ip::tcp::resolver::iterator iterator = resolver.resolve(query);

		boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
		ctx.load_verify_file("rootca.crt");



		client c(io_service, ctx, iterator);

		io_service.run();
	}
	catch (std::exception& e)
	{
		std::cerr << "Exception: " << e.what() << "\n";
	}

	return 0;
}



Создание ключей и сертификатов


На этом этапе клиент и сервер запускаются, теперь необходимо проверить их работу. Для этого нужно создать корневой сертификат и подписать им сертификат для сервера.

После сборки в каталоге C:\Work\OpenSSL\output\bin будет лежать openssl.exe, нужно воспользоваться им, чтобы сгенерировать ключи и сертификаты.

Для начала создаем приватный ключ для корневого сертификата:

openssl genrsa -out rootca.key 2048

Потом на основе этого ключа создаем корневой сертификат, действующий 20000 дней:

openssl req -x509 -new -nodes -key rootca.key -days 20000 -out rootca.crt

В интерактивном меню вас попросят ввести двухбуквенный код страны, провинцию, город, организацию, подразделение, Common Name и e-mail адрес. Нужно заполнить все поля на свое усмотрение.

Теперь нужно создать другой сертификат, подписанный корневым сертификатом.

Создаем еще один ключ:

openssl genrsa -out user.key 2048

Создаем запрос на подпись:

openssl req -new -key user.key -out user.csr

В интерактивном меню вам потребуется ответить на те же вопросы, что и при создании корневого сертификата. Нужно, чтобы введенный вами Common Name отличался от Common Name у корневого сертификата, это важно!

Теперь подписываем этот запрос корневым сертификатом:

openssl x509 -req -in user.csr -CA rootca.crt -CAkey rootca.key -CAcreateserial -out user.crt -days 20000

На всякий случай можно проверить, что подписано все правильно:

openssl verify -CAfile rootca.crt rootca.crt
openssl verify -CAfile rootca.crt user.crt
openssl verify -CAfile user.crt user.crt

Первая команда должна вернуть OK, потому что корневой сертификат — самоподписанный.

Вторая команда должна вернуть ОК, потому что user.crt подписан корневым сертификатом.

Последняя команда должна вернуть ошибку, потому что user.crt не является самоподписанным. Если последняя команда возвращает OK, значит что-то пошло не так. В моем случае для исправления нужно было всего лишь сделать Common Name у обоих сертификатов различающимся.

И напоследок, нам еще понадобится DH-параметры, которые нужны для Протокола Диффи — Хеллмана, нужно их сгенерировать. Генерация займет некоторое время:

openssl dhparam -out dh2048.pem 2048

На этом все, теперь достаточно прописать клиенту и серверу пути к этим файлам, и вы сможете установить между ними защищенное соединение.
Tags:
Hubs:
+7
Comments 8
Comments Comments 8

Articles