Introduction

Once I needed to create secured channel between my server and my application. As I remember, Boost.Asio could work with secured channels using OpenSSL. I tried to search information and code samples for it, but unfortunately, could not find much, especially for Windows. So now, after I figured out the solution, I decided to write this instruction manual for future use.
The task is – I need to make client and server software for Windows, using Boost.Asio and OpenSSL, so that client and and server could exchange data via encrypted TLS channel. As a simple example, I decided to take these two projects:

A simple client: https://www.boost.org/doc/libs/1_69_0/doc/html/boost_asio/example/cpp11/ssl/client.cpp

A simple server: https://www.boost.org/doc/libs/1_69_0/doc/html/boost_asio/example/cpp11/ssl/server.cpp

To solve this task, we need to build OpenSSL for Windows, prepare keys and certificates, and build both samples (client and server) in Visual Studio.

Install OpenSSL for Windows

Prepare

I cloned OpenSSL from master branch of the official github repository: https://github.com/openssl/openssl

To build OpenSSL, we need:

NASM, to build assembler sources. I downloaded it here: https://www.nasm.us/ Also, after installing NASM, you may need to add path to nasm.exe to your system’s the PATH environment variable.
Active Perl, to run configuration scripts. I downloaded it here: https://www.activestate.com/products/activeperl/ same as above, you need to add path to perl.exe to the PATH environment variable.

In addition, after installing Perl you need to use PPM (Perl packet manager) to install dmake. Open command line and run:

ppm install dmake

Build

To build OpenSSL, I uses MS Visual Studio 2017. I am building x64 dynamic library (DLL).
At first, you need to configure OpenSSL using Perl script for Windows x64. For now, I assume that you cloned OpenSSL to D:\Work\TorchProjects\openssl, if you installed it to a different location, just replace this path to yours in all commands below.
Don’t use regular Windows command line utility. You need to open Visual Studio 2017 command line tools to do that:

To configure OpenSSL, you need to enter this catalog and run configuration script:

cd D:\Work\TorchProjects\openssl

perl Configure no-asm VC-WIN64A 
--prefix=D:\Work\TorchProjects\openssl\output 
--openssldir=D:\Work\TorchProjects\openssl\output\openssl

Explanation:

  • no-asm and VC-WIN64A are required to build for x64 architecture
  • prefix defines path where OpenSSL binaries and header files will be deployed
  • openssldir defines where OpenSSL user files will be located – such as, configuration, keys, certificates, etc.

In case if configuration fails, make sure that Perl and NASM are installed and added to PATH environment variable. After configuration is completed, you can start building:

nmake
nmake test
nmake install

After installation is finished, make sure you have catalogs D:\Work\TorchProjects\openssl\output and D:\Work\TorchProjects\openssl\output\openssl. Now you can start generating certificates and keys for testing.

Generate certificates and keys

Intro

After OpenSSL is built, catalog D:\Work\TorchProjects\openssl\output\bin would contain openssl.exe file. You can use it for generation.

Root certificate

At first, let’s create private key for root certificate:

openssl genrsa -out rootca.key 2048

Then, let’s make a root certificate based on this key, and set its validity as 20000 days:

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

Interactive certificate generation wizard will ask you to fill a form. You need to enter your country code, province, city, organization, department, Common Name and email address. You can fill all fields at your discretion.

Here is an example:

User certificate

Now you need to create another certificate, signed by root certificate. Let’s call it user certificate.

Generate a new key:

openssl genrsa -out user.key 2048

Then make a certificate signing request:

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

You need to fill the form again, similar to root certificate form. The Common Name of user certificate field must be different than the Common Name of root certificate, this is important!

Example:

Now, we sign this request by root certificate:

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

Output should looks like this:

Verify

User certificate is ready. Just in case, let’s make a simple check:

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

First command should return OK, because root certificate is self-signed.

Second command should return OK, because user.crt is signed by root certificate.

Last command should return error, because user.crt is not self-signed, it is signed by root certificate. If last command returns “OK”, that means something went wrong. In my case, the problem was that Common Name for both certificates matched. I successfully solved the problem by writing different Common Name for user certificate.

Example output:

DH Params

And last, we also need to generate DH parameters for Diffie–Hellman key exchange protocol:

openssl dhparam -out dh2048.pem 2048

Generation would take some time.

DH parameters generating example:

Keys and certificates are ready, now everything is ready for the client and the server.

Building Visual Studio 2017 projects with OpenSSL and Boost.Asio

Client

Create a new empty Visual Studio x64 project for client. Add file named client.cpp with following source code:

• client.cpp:
//
// client.cpp
// ~~~~~~~~~~
//
// Copyright (c) 2003-2018 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//

#include <cstdlib>
#include <cstring>
#include <functional>
#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>

using boost::asio::ip::tcp;
using std::placeholders::_1;
using std::placeholders::_2;

enum { max_length = 1024 };

class client
{
public:
client(boost::asio::io_context& io_context,
boost::asio::ssl::context& context,
const tcp::resolver::results_type& endpoints)
: socket_(io_context, context)
{
socket_.set_verify_mode(boost::asio::ssl::verify_peer);
socket_.set_verify_callback(
std::bind(&client::verify_certificate, this, _1, _2));

connect(endpoints);
}

private:
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 connect(const tcp::resolver::results_type& endpoints)
{
boost::asio::async_connect(socket_.lowest_layer(), endpoints,
[this](const boost::system::error_code& error,
const tcp::endpoint& /*endpoint*/)
{
if (!error)
{
handshake();
}
else
{
std::cout << "Connect failed: " << error.message() << "\n";
}
});
}

void handshake()
{
socket_.async_handshake(boost::asio::ssl::stream_base::client,
[this](const boost::system::error_code& error)
{
if (!error)
{
send_request();
}
else
{
std::cout << "Handshake failed: " << error.message() << "\n";
}
});
}

void send_request()
{
std::cout << "Enter message: ";
std::cin.getline(request_, max_length);
size_t request_length = std::strlen(request_);

boost::asio::async_write(socket_,
boost::asio::buffer(request_, request_length),
[this](const boost::system::error_code& error, std::size_t length)
{
if (!error)
{
receive_response(length);
}
else
{
std::cout << "Write failed: " << error.message() << "\n";
}
});
}

void receive_response(std::size_t length)
{
boost::asio::async_read(socket_,
boost::asio::buffer(reply_, length),
[this](const boost::system::error_code& error, std::size_t length)
{
if (!error)
{
std::cout << "Reply: ";
std::cout.write(reply_, length);
std::cout << "\n";
}
else
{
std::cout << "Read failed: " << error.message() << "\n";
}
});
}

boost::asio::ssl::stream<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_context io_context;

tcp::resolver resolver(io_context);
auto endpoints = resolver.resolve(argv[1], argv[2]);

boost::asio::ssl::context ctx(boost::asio::ssl::context::sslv23);
ctx.load_verify_file("D:\\Work\\TorchProjects\\openssl\\output\\bin\\rootca.crt");

client c(io_context, ctx, endpoints);

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

system("pause");

return 0;
}

In project C++ compiler properties, add Boost folder and D:\Work\TorchProjects\openssl\output\include folder to project include path.

In project linker properties, add path to D:\Work\TorchProjects\openssl\output\lib to library search folder. In library dependencies section, add both libcrypto.lib and libssl.lib. Don’t need to add path to Boost libraries, because Boost.Asio is header-only library.
Also, don’t forget to copy libcrypto.dll and libssl.dll dynamic libraries from D:\Work\TorchProjects\openssl\output\bin to the project folder.
Here are screenshots with explanations:

• How to set include path:

• How to set additional libraries path:

• How to add libraries:

In the source code, you need to set path to root certificate:

ctx.load_verify_file("D:\\Work\\TorchProjects\\openssl\\output\\bin\\rootca.crt");

Change this value to match path to your root certificate file location.
Now build project, it should generate exe file successfully.

Server

Create a new empty Visual Studio x64 project for server. Add file named server.cpp with following source code:

• server.cpp:
//
// server.cpp
// ~~~~~~~~~~
//
// Copyright (c) 2003-2018 Christopher M. Kohlhoff (chris at kohlhoff dot com)
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//

#include <cstdlib>
#include <functional>
#include <iostream>
#include <boost/asio.hpp>
#include <boost/asio/ssl.hpp>

using boost::asio::ip::tcp;

class session : public std::enable_shared_from_this<session>
{
public:
session(tcp::socket socket, boost::asio::ssl::context& context)
: socket_(std::move(socket), context)
{
}

void start()
{
do_handshake();
}

private:
void do_handshake()
{
auto self(shared_from_this());
socket_.async_handshake(boost::asio::ssl::stream_base::server,
[this, self](const boost::system::error_code& error)
{
if (!error)
{
do_read();
}
});
}

void do_read()
{
auto self(shared_from_this());
socket_.async_read_some(boost::asio::buffer(data_),
[this, self](const boost::system::error_code& ec, std::size_t length)
{
if (!ec)
{
std::cout << "Received: ";
std::cout.write(data_, length);
std::cout << "\n";
std::cout << "Sending back again\n";
do_write(length);
}
});
}

void do_write(std::size_t length)
{
auto self(shared_from_this());
boost::asio::async_write(socket_, boost::asio::buffer(data_, length),
[this, self](const boost::system::error_code& ec,
std::size_t /*length*/)
{
if (!ec)
{
do_read();
}
});
}

boost::asio::ssl::stream<tcp::socket> socket_;
char data_[1024];
};

class server
{
public:
server(boost::asio::io_context& io_context, unsigned short port)
: acceptor_(io_context, tcp::endpoint(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(std::bind(&server::get_password, this));
context_.use_certificate_chain_file("D:\\Work\\TorchProjects\\openssl\\output\\bin\\user.crt");
context_.use_private_key_file("D:\\Work\\TorchProjects\\openssl\\output\\bin\\user.key", boost::asio::ssl::context::pem);
context_.use_tmp_dh_file("D:\\Work\\TorchProjects\\openssl\\output\\bin\\dh2048.pem");

do_accept();
}

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

void do_accept()
{
acceptor_.async_accept(
[this](const boost::system::error_code& error, tcp::socket socket)
{
if (!error)
{
std::make_shared<session>(std::move(socket), context_)->start();
}

do_accept();
});
}

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_context io_context;

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

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

return 0;
}

Set all properties same as for the client – include path, library search path, additional libraries. Same as for the client, copy dll files to the project folder.

You also need to set up path to user private key, user certificate and DH parameters file:

context_.use_certificate_chain_file("D:\\Work\\TorchProjects\\openssl\\output\\bin\\user.crt");
context_.use_private_key_file("D:\\Work\\TorchProjects\\openssl\\output\\bin\\user.key", boost::asio::ssl::context::pem);
context_.use_tmp_dh_file("D:\\Work\\TorchProjects\\openssl\\output\\bin\\dh2048.pem");

Now build project, it should generate exe file successfully.

Running client and server

To test connection, you need to run server first, client later. Start server and pass port number to arguments, for example 8080. Then start client and pass to parameters 127.0.0.1 as IP address and port 8080. Client should connect successfully. Now type something on the client window, hit “Enter” and make sure same message successfully appears in the server window and is returned back to client.

Here is video explanation about how to do it: