Secure TLS connection using Boost.Asio and OpenSSL for Windows
- Tutorial
Introduction
Once I needed to create a secure communication channel between my server and my application. I remembered that the Boost Asio documentation mentioned that it can work with secure connections using OpenSSL. I started looking for information on this topic, but, alas, I did not find much, especially under Windows. So now, having dealt with this issue, I decided to write this instruction so that it would be easier for other people to understand.
The task - you need to build a server and client for Windows using Boost Asio and OpenSSL, so that the client and server exchange information over a secure TLS channel. For example, I decided to take this client and server from the official Boost website.
In order to solve this problem, we need to collect OpenSSL, prepare keys and certificates, and collect both examples using Boost Asio, OpenSSL.
Install OpenSSL under Windows
I took OpenSSL from the official repository: github.com/openssl/openssl
To install OpenSSL we need:
- NASM, to compile the source code in assembler, I took from here: www.nasm.us You also need to add the path to nasm.exe to the PATH environment variable.
- Active Perl, to run the configuration scripts, I took from here: www.activestate.com/activeperl And the path to perl.exe also needs to be added to the PATH environment variable.
To build OpenSSL, I used MS Visual Studio 2013, and I built a static library.
The build sequence is as follows:
First you need to configure OpenSSL using a script in Perl, under Win32. Below, I will assume that you have OpenSSL in C: \ Work \ OpenSSL. You should go into this directory and call the configuration script:
cd C:\Work\OpenSSL
perl Configure VC-WIN32 --prefix=C:\Work\OpenSSL\output enable-deprecated -I$(SRC_D)
Pay attention to the following:
- Here the parameter --prefix is explicitly set and the path where the result of the assembly will lie is indicated. OpenSSL will lie in a separate subdirectory \ output and will not mix with the source files.
- The enable-deprecated parameter is also set here - this means that the deprecated code will be included in the assembly. I tried to build without this parameter, and Boost Asio complained about the lack of CRYPTO_set_id_callback functions from openssl \ crypto.h and DH_free from openssl \ dh.h and so I decided to build with enable-deprecated parameter.
- For reasons unknown to me, the configurator does not add the source directory C: \ Work \ OpenSSL to the list of directories to search for * .h files, so I added -I $ (SRC_D) to make the compiler look for header files there. Instead, you can add -IC: \ Work \ OpenSSL. Another option - after you call ms \ do_nasm, simply edit the ms \ nt.mak file manually and enter the path to the sources there.
Next, you need to prepare the assembler sources for assembly. It is necessary to call the build script from the same directory:
ms\do_nasm
To do this, close the normal command line and run the MS Visual Studio command line, in which additional file paths and additional environment variables are defined. You can find the MS Visual Studio command line in the directory C: \ Program Files (x86) \ Microsoft Visual Studio 12.0 \ Common7 \ Tools \ Shortcuts .
From the MS Visual Studio command prompt, go to the C: \ Work \ OpenSSL directory and start the build using nmake:
nmake -f ms\nt.mak
This is a command to build a static library, if you want to build a dynamic library, you need to run ntdll.mak.
After executing this command, a lengthy build procedure should begin. If the assembly does not work, then here are the possible solutions to this problem:
- Make sure you add the path to nasm.exe to the PATH environment variable
- Make sure you run the assembly from the C: \ Work \ OpenSSL directory
- Make sure that you start the assembly not from the usual command line, but from the MS Visual Studio command line.
During the assembly process, another problem is possible. The compiler will complain that it could not find the tmp32 / x86cpuid.obj file or other files that must be compiled from the * .asm sources. In my case, the problem was solved after I added the path to nasm to the PATH environment variable. Another solution - you can simply manually compile all assembler files with nasm, there are only 22 of them.
After the assembly is complete, you need to copy the libraries and source files to a new directory:
nmake -f ms\nt.mak install
This completes the assembly of OpenSSL for Windows.
Client and server assembly
As I said earlier, for an example I decided to take these client and server from the Boost Asio documentation. However, when I tried to build, I ran into some problems, and as a result I had to modify the source.
So:
- On January 27, 2015, OpenSSL introduced a very large and important commit , which brought many different structures, declarations, and functions from the ssl.h main header to the ssl_locl.h internal header. All these structures are used in Boost Asio, so you need to include this ssl_locl.h file.
- The ssl_locl.h header also refers to the packet_locl.h header, and it implicitly converts from void * to unsigned char * on line 411:
*data = BUF_memdup(pkt->curr, length);
Although this place is declared extern “C”, and from the point of view of C there are no errors, Visual Studio does not give us any way to disable this error. I had to make changes and convert the type explicitly:*data = (unsigned char*)BUF_memdup(pkt->curr, length); - Initially, the SSL_R_SHORT_READ constant was declared in ssl.h, but then it was removed for some reason. This constant is used in Boost Asio, and you can simply declare it before attaching a header.
#define SSL_R_SHORT_READ 219 #include "ssl_locl.h" #include - You must remember to add the preprocessor directives _WIN32_WINNT = 0x0501 - for Boost, OPENSSL_NO_SSL2 - to disable the obsolete version of SSL and OPENSSL_USE_DEPRECATED, since we configured OpenSSL with the enable-deprecated key.
- And finally, you need to add the directories D: \ Work \ OpenSSL and D: \ Work \ OpenSSL \ output \ include in the search path for header files
After all the above manipulations, I managed to build and run the project with Boost Asio and OpenSSL under Windows using Visual Studio 2013.
Server source code:
server.cpp
#include
#include
#include
#include
#define SSL_R_SHORT_READ 219
#include "ssl/ssl_locl.h"
#include
typedef boost::asio::ssl::stream 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 \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 Source Code:
client.cpp
#include
#include
#include
#include
#define SSL_R_SHORT_READ 219
#include "ssl/ssl_locl.h"
#include
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 socket_;
char request_[max_length];
char reply_[max_length];
};
int main(int argc, char* argv[])
{
try
{
if (argc != 3)
{
std::cerr << "Usage: client \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;
}
Creating keys and certificates
At this stage, the client and server are started, now it is necessary to check their work. To do this, create a root certificate and sign a certificate for the server.
After the assembly, openssl.exe will be in the C: \ Work \ OpenSSL \ output \ bin directory, you need to use it to generate keys and certificates.
First, create a private key for the root certificate:
openssl genrsa -out rootca.key 2048
Then, based on this key, create a root certificate that is valid for 20,000 days:
openssl req -x509 -new -nodes -key rootca.key -days 20000 -out rootca.crt
In the interactive menu you will be asked to enter the two-letter country code, province, city, organization, unit, Common Name and e-mail address. You need to fill in all the fields at your discretion.
Now you need to create another certificate signed by the root certificate.
Create another key:
openssl genrsa -out user.key 2048
Create a request for signature:
openssl req -new -key user.key -out user.csr
In the interactive menu, you will need to answer the same questions as when creating the root certificate. It is necessary that the Common Name you entered differs from the Common Name of the root certificate, this is important!
Now sign this request with the root certificate:
openssl x509 -req -in user.csr -CA rootca.crt -CAkey rootca.key -CAcreateserial -out user.crt -days 20000
Just in case, you can check that everything is signed correctly:
openssl verify -CAfile rootca.crt rootca.crt
openssl verify -CAfile rootca.crt user.crt
openssl verify -CAfile user.crt user.crt
The first command should return OK, because the root certificate is self-signed.
The second command should return OK, because user.crt is signed by the root certificate.
The last command should return an error because user.crt is not self-signed. If the last command returns OK, then something went wrong. In my case, to fix it, it was only necessary to make the Common Name for both certificates different.
And finally, we still need DH-parameters, which are needed for the Diffie-Hellman Protocol , we need to generate them. Generation will take some time:
openssl dhparam -out dh2048.pem 2048
That's all, now it’s enough to register the path to these files for the client and server, and you can establish a secure connection between them.