Introduction
Once I needed to create a secure communication channel between my server and my application. I remembered that the documentation for Boost Asio mentioned that it can work with secure connections using OpenSSL. I began to look for information on this topic, but, alas, I did not find much, especially under Windows. So now, having understood this question, I decided to write this instruction, so that it was easier for other people to understand.
The task is to build a server and client for Windows using Boost Asio and OpenSSL so that the client and server exchange information via a secure TLS channel. For example, I decided to take this
client and
server from the official website of Boost.
In order to solve this problem, we need to build OpenSSL, prepare keys and certificates, and collect both examples using Boost Asio, OpenSSL.
Installing 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 in 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 assembly sequence is as follows:
First you need to configure OpenSSL using a Perl script, under Win32. Below, I will assume that OpenSSL is in C: \ Work \ OpenSSL. You should go to 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)
Note the following:
- Here, the --prefix parameter is explicitly specified and the path is specified where the result of the assembly will be located. OpenSSL will be in a separate \ output subdirectory and will not mix with 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 compile 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 therefore I decided to compile with the 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 force the compiler to search for header files there. Instead, you can add -IC: \ Work \ OpenSSL. Another option is that after you call ms \ do_nasm, simply manually edit the ms file \ nt.mak and enter the path to the sources there.
Next you need to prepare assembly source code for the assembly. It is necessary to call the build script from the same directory:
ms\do_nasm
At this point, you need to close the usual command line, and run the MS Visual Studio command line, which defines additional file paths and additional environment variables. You can find the MS Visual Studio command line in the
C: \ Program Files (x86) \ Microsoft Visual Studio 12.0 \ Common7 \ Tools \ Shortcuts directory .
From the MS Visual Studio command line, 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, then you need to run ntdll.mak.
After executing this command, a lengthy build procedure should begin. If the build fails, then here are possible solutions to this problem:
- Make sure you add the path to nasm.exe to the PATH environment variable.
- Make sure you run the build from the C: \ Work \ OpenSSL directory.
- Make sure that you run the assembly not from the usual command line, but from the MS Visual Studio command line.
Another problem is possible during the assembly process. The compiler will complain that it could not find the tmp32 / x86cpuid.obj file or other files that should be compiled from * .asm sources. In my case, the problem was resolved after I added the path to nasm to the PATH environment variable. Another solution is to manually compile nasm all assembly files, there are only 22 of them.
After the build is complete, you need to copy the libraries and source files to the new directory:
nmake -f ms\nt.mak install
This completes the OpenSSL build for Windows.
Build client and server
As I said before, for example, I decided to take this
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 sources.
So:
- On January 27, 2015, OpenSSL made a very large and important commit , which rendered many different structures, declarations and functions from the main header ssl.h to the internal header ssl_locl.h. All these structures are used in Boost Asio, so you need to include this file ssl_locl.h.
- The ssl_locl.h header also refers to the packet_locl.h header, and the implicit conversion from void * to unsigned char * occurs on line 411:
*data = BUF_memdup(pkt->curr, length);
Although this place is declared as extern "C", and from the point of view of C, there are no errors here, but Visual Studio does not give us any opportunity to turn off 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 for some reason it was removed. This constant is used in Boost Asio, and you can simply declare it before connecting the header.
#define SSL_R_SHORT_READ 219 #include "ssl_locl.h" #include <boost/asio/ssl.hpp>
- You need to remember to add preprocessor directives _WIN32_WINNT = 0x0501 - for Boost, OPENSSL_NO_SSL2 - to disable the outdated version of SSL and OPENSSL_USE_DEPRECATED, since we configured OpenSSL with the key enable-deprecated.
- And, finally, you need to add the directories D: \ Work \ OpenSSL and D: \ Work \ OpenSSL \ output \ include to the search paths for header files
After all the above mentioned manipulations, I managed to build and run a project with Boost Asio and OpenSSL under Windows using Visual Studio 2013.
Server source code:
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 source code:
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; }
Creating keys and certificates
At this stage, the client and server are started, now you need to check their work. To do this, create a root certificate and sign a certificate for the server.
After building in the directory C: \ Work \ OpenSSL \ output \ bin will lie openssl.exe, 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, we create a root certificate 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, division, 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 signature request:
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 was different from the Common Name of the root certificate, this is important!
Now we sign this request with a 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 with a 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, for correction, it was only necessary to make the Common Name of both certificates different.
And finally, we still need the DH parameters that 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 is enough to register the path to these files for the client and server, and you can establish a secure connection between them.