Sunday 20 September 2015

HTTPS Support for a Casablanca Server and Certificate Error 1312


In the previous post about Casablanca C++ REST framework I said that our next task would be adding HTTPS support to our solution. So let's do it.

1. The principle

We start with the server side. On the surface it is rather simple, you just specify the protocol the server is using to be "https://" and voila, everything is done. This is done for example like that:
  _restListener = http_listener(U("https://localhost:8111/test"));
But if we peek into the code, we'll see that server-side HTTPS is only supported for Windows as for now (Casablanca 2.5.0). OK, let's test it! We start the server, let Chrome get some data from the newly created SSL-URL*, and... it won't work, apparently there's no such URL! What? We already started our server there, and it's running.

After a second peek into Casablanca's code, we learn that it's leveraging Windows HTTP Server API, but does not configure any HTTPS support by itself. Then we learn from MSN docs, that this will be usually done in the configuration step, before the HTTP server is started. Apparently a programmatic solution is possible too, bu we start with the simple one (or so we think...)**.

In short, we have to attach a server certificate to a port on the machine where our server runs (i.e. our developer machine, at least in this post). For that, we first have to create a new certificate.

2. First try

For the sake of our tests we want to create a self-signed certificate. On Windows we can use the MakeCert.exe tool for that. So I opened the VisualStudio developer console and typed***:

  makecert.exe -sr LocalMachine -ss Root -a sha512 -n "CN=my_machine_name" -sky exchange -pe -r

as to create a self-signed (-r) cert for local machine (required for the add sslcert step later), export it's private key (-pe), use if for exchange (-sky) and store it straight away into the trusted Root Certification Authorities store. OK, it worked.

Next we open the MMC, check that the certificate is there and it has no errors. Don't know how? It's explained several times on the Web, for example here.

Now we need only to set the certificate for server's IP address. The command for that is :

  netsh http add sslcert ipport=0.0.0.0:8111 certhash=XXXX appid={yyyyy}

For the certhash we can use the thumbprint from certificate's Properties window in MMC, for the application ID the most frequent advice is to use the ID of the IIS Server, but as we don not even have it installed on our machine, we prefer to use an empty GUID: {00000000-0000-0000-0000-000000000000}, it's allowed too! Should be working OK!

Alas the dreaded Error 1312 showed it's ugly head:

  Certificate add failed, Error: 1312 A specified logon session does not exist. It may already have been terminated.

WTF? What is that supposed to mean? There are many reasons, like bad certificate hash (nope, double-checked it), a missing Windows hot-fix (nope, it's 2010, already installed), not exported private key (nope, we did it), some mysterious cases when certificates get renewed (nope, there are fresh here), etc, etc. But definitely not a problem with a logon session, that much is true!

3. The second try

What should we do? As always the advice is: go back to the last known working setup. As to do this, I followed advice in this blogpost quite meticulously, and created two certificates:

  makecert -sk testRootCA -sky signature -sr localmachine -n "CN=RootTrustedCA" -ss TRUST -r RootTrustedCA.cer

  makecert -sk testServer -ss MY -sky exchange -sr localmachine -n "CN=Server" -ic RootTrustedCA.cer -is TRUST Server.cer -pe


The first one is supposed to be the base and trusted one, the second to reference the first. That didn't cut it either, because the RootTrustedCA still wasn't trusted - I had to add it to the Root store using MMC (don't know how? - look here).

This time, I didn't want to type the certificate hash and used this nice tool to apply the certificate to the desired IP address, and guess what - it worked! The last problem was, that the domain and certificate's names didn't match, so I created a cert with my machine's name:

  makecert -sk -ss MY -sky exchange -sr localmachine -n "CN=my_machine_name" -ic RootTrustedCA.cer -is TRUST my_machine_name.cer -pe

 applied it to 0.0.0.0:8111 and, at last, everything worked like a charm:




4. Conclusion

As it seems, add sslcert (an by extension, Windows' HTTP Server API) only supports certificates which reference a base trusted one, and doesn't like the ones from the Root store. Remember: the certificate has to be in the User store and has to be in the computer account (i.e. for the local machine)!

Update: The above conclusion is in principle correct, but only when we the add two small words: "by default"! As explained in a friendly comment, add sslcert accepts a command line switch where the desired certificate store can be specified!

OK, that was a little tiring, we'll add support for the client side in another post.

PS: Later I had to add configuration of the SSL certificates to our server configuration code, lots of Windows  system programming :-/.

--
* I mean TSL, but for historical reasons....

** Of course in the real server we'd will implement programmatic solution for configuration of the SSL options for a given URL. You may look up the HTTP Server API's functions HttpSetServiceConfiguration and HTTPQueryServiceConfiguration

*** not quite, first I tried to create a private certificate referencing an already existing, trusted one with:

  makecert.exe -sr LocalMachine -ss MY -a sha512 -n "CN=my_machine_name" my_machine_name -sky exchange -pe -eka 1.3.6.1.5.5.7.3.1

here the implicit Root Agency certificate is used. The problem here is, that the created certificate was broken, MMC said: "This certificate has an invalid digital signature". After much ado it turned out, that the referenced  Root Agency certificate uses a public key which is too short (only 512 bits!!!) and is thus considered not secure. Thanks for a very informative error message!


15 comments:

Unknown said...

You've overcomplicated things here. Your first attempt of a certificate looks fine. The only thing you needed to do to avoid the "dreaded Error 1312" was to append "certstorename=Root" to the netsh command to tell it to look for the certificate in the Root store rather than the User store.

Marek Krj said...

@Unknown
Thanks a lot, basically RTFM, but I was somehow shocked by the "dreaded Error 1312" :).

Anonymous said...

HTTPS on Linux is now supported (not my commit): https://github.com/Microsoft/cpprestsdk/commit/2c11a50f994f75a8ca2aad6006279498ac187294

Phew! There's not many options when using C++.

Anonymous said...

Hi @Anonymous,

I am new to this casablanca and c++. From your post i came to know that casablanca supports https server for linux platform, installed the latest version of casablanca, I need an example of https server program where it will use certificates. Can any one please help me ...!

Marek Krj said...

@Anonymous

Look at the test cases of the abovementioned HTTPS pull request: TEST_FIXTURE(uri_address, create_https_listener_get).

Seems you can do it using an instance of set_ssl_context_configurer:

boost::asio::const_buffer cert(self_signed_cert, std::strlen(self_signed_cert));
boost::asio::const_buffer key(private_key, std::strlen(private_key));

http_listener_config server_config;

server_config.set_ssl_context_configurer(
[&](boost::asio::ssl::context& ctx)
{
ctx.set_options(boost::asio::ssl::context::default_workarounds);
ctx.use_certificate_chain(cert);
ctx.use_private_key(key, boost::asio::ssl::context::pem);
});

http_listener listener(m_secure_uri, server_config);

Anonymous said...

Hi Marek,

Can you please help me to know whether Casablanca supports server validating client certificate ?

Marek Krj said...

@Anonymous
As far as I remember, my customer didn't want client side certificates, because they weren't supported in Casablanca on client side, and I I'd have to implement this (as of 2.5.0).
For the server side, I can speak only for the Windows as I didn't need to make it run on Mac. Because WinHttp is used under the covers, it will handle client side certificates if requested (AFAIK).

Anonymous said...

Note that it should be server_config.set_ssl_context_callback instead of server_config.set_ssl_context_configurer.

Thanks for the example code, anyway!

Anonymous said...

Mhh, tried all that code. Even analyzed the test code of casablanca.
But as far as I got until now using https is, that the ssl context was started up.
But the callback functions are not triggered. Quite helpless now. Anyone?

Marek Krj said...

@Anonymous You mean that the HTTPS tests are not working? Then just issue a Bug Report, the Casablanca team is (or was as I needed it) quite helpful! Sorry, but I run my HTTPS client only on Windows for now, cannot help here... :-(

Markus Warg said...

@Marek Krj... Well the tests don't complete with 100% success. make test in the build folder returns httpclient_test failed.
The test_runner returns an multiple_https_requests failure with some other tests.
But what I wanted to say was that I copied the code from the casablanca test source code and put it into my program to get the http_server working with ssl.

When I run the code without the ssl context, everything is fine. When I run it with ssl_context I can see that the lambda from starting the ssl context gets fired. All code within the lambda seems to return success. But then nothing more happens, I can trigger successive https queries, ssl context always gets initialized. But none of the support functions gets executed.

So, in the end there are some errors when running the casablanca test suite. But I really don't know if these are related to my problem.

Here are some snippets, given that the callback functions are regular public members of the same class.

server_config = new http_listener_config();
server_config->set_ssl_context_callback(
[&](boost::asio::ssl::context & ctx) {
boost::system::error_code ec;

ctx.set_options(boost::asio::ssl::context::default_workarounds, ec);
std::cout << "lambda::set_options " << ec.value() << " " << ec.message() << std::endl;

ctx.use_certificate_chain_file("ssl/cert.pem", ec);
std::cout << "lambda::use_certificate_chain_file " << ec.value() << " " << ec.message() << std::endl;

ctx.use_private_key_file("ssl/key.pem", boost::asio::ssl::context::pem, ec);
std::cout << "lambda::use_private_key " << ec.value() << " " << ec.message() << std::endl;

// note that without pinning the port, the http_lister will chose a random port
// which also seems strange for me
uri_builder uri("https://192.168.1.1/api/client/foo/bar/");
uri.set_scheme("https");
uri.set_port(443);

listener->support(web::http::methods::GET, std::bind(&Listener::handle_get,
this,
std::placeholders::_1));
listener->support(web::http::methods::POST, std::bind(&Listener::handle_post,
this,
std::placeholders::_1));

listener->open().then([&]() {
std::cout << "Listener::run starting to listen" << std::endl;
}).wait();

pause();

------

void handle_post(http_request request) {
request.reply(status_codes::OK, "text");
}

Markus Warg said...

Sorry, was too late yesterday, I left out some quite important lines of code...

server_config = new http_listener_config();
server_config->set_ssl_context_callback(
[&](boost::asio::ssl::context & ctx) {
boost::system::error_code ec;

ctx.set_options(boost::asio::ssl::context::default_workarounds, ec);
std::cout << "lambda::set_options " << ec.value() << " " << ec.message() << std::endl;

ctx.use_certificate_chain_file("ssl/cert.pem", ec);
std::cout << "lambda::use_certificate_chain_file " << ec.value() << " " << ec.message() << std::endl;

ctx.use_private_key_file("ssl/key.pem", boost::asio::ssl::context::pem, ec);
std::cout << "lambda::use_private_key " << ec.value() << " " << ec.message() << std::endl;
});

// note that without pinning the port, the http_lister will chose a random port
// which also seems strange for me
uri_builder uri("https://192.168.1.1/api/client/foo/bar/");
uri.set_scheme("https");
uri.set_port(443);

listener = new http_listener(uri, server_config);

listener->support(web::http::methods::GET, std::bind(&Listener::handle_get,
this,
std::placeholders::_1));
listener->support(web::http::methods::POST, std::bind(&Listener::handle_post,
this,
std::placeholders::_1));

listener->open().then([&]() {
std::cout << "Listener::run starting to listen" << std::endl;
}).wait();

pause();

------

void handle_post(http_request request) {
request.reply(status_codes::OK, "text");
}

Marek Krj said...

@Markus Warg As I said, I don't have a Casablanca set up on a Mac (I even don't own any, but that's a different story) thus I cannot debug that stuff. Sorry :(.
Failing tests: I meant HTTPS test failing...
Casablanca community: There is a developer who added this stuff to the main branch, just ask him. I found that they respon rather quickly.
If you have more questions just DM me on Twitter. I'll be happy to help :).

ritu said...

VS2017 will not compile set_ssl_context_callback. How can I access this function?

Marek Krj said...

@ritu
set_ssl_context_callback is used with ASIO, which provides the Transport layer on non-Windows platforms. Because you are using VS2017 I suppose you are on Windows and there the natine WinHttp API is used for that thus ASIO Symbols won't be present.