18 ноября 2015 в 21:33

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

C++*

Введение


Однажды мне потребовалось создать защищенный канал связи между своим сервером и своим приложением. Я помнил, что в документации к 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

На этом все, теперь достаточно прописать клиенту и серверу пути к этим файлам, и вы сможете установить между ними защищенное соединение.
Vladislav Khorev @Mephi1984
карма
21,0
рейтинг 0,0
Developer
Самое читаемое Разработка

Комментарии (8)

  • 0
    На этом все, теперь достаточно прописать клиенту и серверу пути к этим файлам, и вы сможете установить между ними защищенное соединение.

    Что-то мне кажется, что этого будет недостаточно, как сервер и клиент проверят подлинность сертификатов друг друга, если корневой не сделать известным (кому, системе)?

    Кроме того, не очень понятно, кому к чему пути прописывать? У вас 3 файла вроде получилось, причем dh2048.pem вообще никак не связан с первыми двумя, судя по коду его генерации.
    • 0
      Привет!
      Я делаю всю эту систему чтобы шифровать траффик между сервером и мобильным приложением, корневой сертификат будет предустановлен в мобильное приложение.

      Всего получилось 5 файлов:
      Ключ для корневого сертификата rootca.key, его нужно убрать подальше, на сервере он не нужен.
      Корневой сертификат rootca.crt, он будет на сервере, а также будет распространятся вместе с приложением.
      Ключ для рабочего сертификата user.key он будет хранится и использоваться на сервере.
      Рабочий сертификат user.crt он будет хранится и использоваться на сервере.

      dh2048.pem никак не связан с сертификатами, но он требуется для работы сервера и загружается при его создании. Может быть возможно настроить контекст boost::asio::ssl::context так, чтобы не использовать dh-параметры, но я не пробовал.
      • 0
        А, ну вот так все понятно. Я бы добавил это в конец статьи, чтобы она была завершенной.
  • 0
    Здравствуйте, у меня не получается собрать OpenSSL, делал все по инструкции, ошибка — на скриншоте или же ниже:

    D:\Lib & src\source\openssl-master\ssl\statem\statem_srvr.c: fatal error C1001: Внутренняя ошибка в файле компилятора.
    (файл компилятора «f:\dd\vctools\compiler\utc\src\p2\main.c», строка 246)
    Чтобы обойти эту неполадку, попытайтесь упростить или изменить программу в строках около указанной позиции.
    Для получения дополнительных сведений выберите команду «Техническая поддержка»
    в меню «Справка» Visual C++ или откройте файл справки технической поддержки.

    D:\Lib & src\source\openssl-master\ssl\statem\statem_srvr.c: fatal error C1001: Внутренняя ошибка в файле компилятора.
    (файл компилятора «f:\dd\vctools\compiler\utc\src\common\error.c», строка 794)
    Чтобы обойти эту неполадку, попытайтесь упростить или изменить программу в строках около указанной позиции.
    Для получения дополнительных сведений выберите команду «Техническая поддержка»
    в меню «Справка» Visual C++ или откройте файл справки технической поддержки.
    NMAKE: fatal error U1077: «C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\cl.EXE»: возвращенный код «0x3»
    Stop.

    D:\Lib & src\source\openssl-master>
    • 0
      Добрый день!
      Я погуглил вашу ошибку, люди жалуются на тот же баг вот тут и вот тут. Обе проблемы свежие, и появились в январе 2016 года.
      Я вижу следующие решения:
      — Собрать проект с помощью Visual Studio 2013
      — Посмотреть какие последние изменения были в проекте OpenSSL в файле ssl\statem\statem_srvr.c и склонировать более раннюю версию. Судя по жалобам, все сломалось совсем недавно, буквально пару недель назад
      — Попробовать обновить Visual Studio 14.0, может быть эта ошибка уже исправлена в обновлениях от Microsoft

      К сожалению, это все что я могу предложить сейчас. Когда у меня будет время, я попробую посмотреть из-за чего возникла проблема и как ее исправить.
      • 0
        Эх. Не получилось у меня собрать по Вашим советам. Спасибо за отклик, я не ожидал, что ответите. Распишу по пунктам.
        Дано:
        хост с 7(64), VS2015 Update 1,
        Виртуалка с 7(32), VS2013,
        набор утилит по ссылкам выше из статьи, и openssl-1.0.0t. Мне не нужна последняя версия, хочу собрать cpp-netlib с ssh, она требует.
        Попытка собрать openssl-1.0.0t, хост, проблемы:

        На этапе компиляции cl, warning C4267: =: преобразование из «size_t» в «short»; возможна потеря данных. Error C2220: предупреждение обработано как ошибка… Исправлял копирование команды и удалением флага -WX. Было пару раз за компиляцию.
        Далее буду честно копировать вывод с консоли, не хочу вставлять скриншоты.

        link /nologo /subsystem:console /opt:ref /debug /out:out32\constant_time
        _test.exe @C:\Users\120D~1\AppData\Local\Temp\nmC912.tmp
        constant_time_test.obj: error LNK2019: ссылка на неразрешенный внешний символ _
        __iob_func в функции _main
        out32\constant_time_test.exe: fatal error LNK1120: неразрешенных внешних элемен
        тов: 1
        NMAKE: fatal error U1077: «C:\Program Files (x86)\Microsoft Visual Studio 14.0\
        VC\BIN\link.EXE»: возвращенный код «0x460»
        Stop.
        Это не смог решить.

        Попытка собрать openssl-1.0.0t, виртуалка, проблемы:

        link /nologo /subsystem:console /opt:ref /debug /out:out32\constant_time
        _test.exe @C:\Users\test\AppData\Local\Temp\nm99AC.tmp
        constant_time_test.obj: error LNK2019: unresolved external symbol ___stdio_comm
        on_vfprintf referenced in function __vfprintf_l
        out32\constant_time_test.exe: fatal error LNK1120: 1 unresolved externals
        NMAKE: fatal error U1077: '«C:\Program Files\Microsoft Visual Studio 12.0\VC\BI
        N\link.EXE»': return code '0x460'
        Stop.
        Честно я не мастер что то собирать, именно сборкой из исходников занимаюсь не давно. Может я что то упускаю, или какие то моменты не освещены в статье, но очень хочется программировать, а не бороться с компилятором и линковщиком.
        • 0
          Обновляю свое сообщение — временным решением скачал собранные библиотеки, с ними собрал cpp-netlib.
  • 0
    Промахнулся.

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