Server Name Indication (SNI) with Qt and OpenSSL (Not QSslSocket)

If you are reading this post, you are probably looking for a way to implement Server Name Indication (SNI) with Qt. I would even go further and assume that you have been using the QTcpSocket and QSslSocket classes for unencrypted and encrypted socket communications respectively in your program. You have also probably looked around and cannot find a way to implement SNI in a server program with Qt while using the QSslSocket class.

Let’s stop for a minute and look closely at exactly what we are talking about here. What is SNI, and why is it important? Simply put, Server Name Indication is a TLS protocol extension that allows a client to indicate which host name it is requesting. Without SNI, an HTTPS server set up to serve content for multiple host names (virtual hosts) over a unique IP address would not be able to differentiate which client requests are meant for each host name respectively.

During an HTTP communication (request and response), which is not encrypted, the host name can easily be read by a receiving server simply by parsing the request headers. The server then knows whether it needs to respond to, let’s say, a request for thatPicture.jpg on the website with the host name nachega.com instead of thatPicture.jpg on the website with the host name nicholasnachega.com. The server here is obviously set up to serve content for both of those websites. Otherwise, it would simply return a response saying that the content is not available.

But what happens if the request is encrypted and sent over an HTTPS communication? The client and the server then have to do what is called a TLS/SSL handshake first, in which they will exchange a certificate and key and agree on how their communication is going to be encrypted. As during the HTTP communication, the server still needs to know which host name is being requested by the client during the TLS/SSL handshake in order to send the certificate that corresponds to that host name.

But, unlike during an HTTP communication, the server cannot read the HTTPS request headers before the handshake is complete in order to find out which host name is being requested by the client. Otherwise, reading the request headers before the handshake, if it was at all possible, would be done insecurely since the client and the server would not yet have agreed on how to secure their communication. But, even during an HTTPS communication, the server can still find out which host name is being requested thanks to Server Name Indication.

The SNI extension to the TLS protocol adds a way for a client to send the host name to the server during the handshake. This way, the server can know which exact certificate it needs to present to the client in order to complete a successful TLS/SSL handshake. By the time the HTTPS request headers are received by the server, the TLS/SSL handshake will have been successfully completed, and the server will then be able to read the encrypted HTTPS headers and respond to the request.

The server is able to find out which host name is being requested because the ClientHello message sent by the client to the server at the start of the TLS handshake contains the host name being requested. Adding the host name to the ClientHello message is a requirement of the TLS protocol since the SNI extension was standardized in 2003. The ClientHello message is sent unencrypted before the server and the client agree on how to secure their communication, which means that even if the handshake is not successfully completed, the server might still be able to know which host name was requested.

It is important to note that knowing the right host name during the TLS /SSL handshake is only important when a server is serving content for multiple host names such as nachega.com and nicholasnachega.com (virtual hosts). If a server is only set up to serve content for one host name, let’s say nachega.com, then it would not necessarily have to worry about identifying which host name a client is requesting. It could just assume that all requests are meant for nachega.com.

So, now let’s go back to Qt and SNI. As I wrote earlier, you are probably looking for a way to implement SNI using the QSslSocket class. I am afraid to inform you that, as of the current available version of Qt (5.14.0), you cannot do it using the QSslSocket class.

“Why not?”, you might ask. Well, it is simply not implemented yet. QSslSocket does currently allow you to set one certificate that will be used during the TLS/SSL handshake, but for either a predefined host name or multiple host names in case the certificate is a Unified Communications certificate (UCC) or a Subject Alternative Names (SAN) certificate. That’s it. As of the day of publication of this post, you do not have access, at least through QSslSocket, to a way that will allow you to read the SNI data sent by the client at the beginning of the TLS/SSL handshake. And without access to the SNI data at the beginning of the TLS/SSL handshake, you simply cannot securely complete a handshake using SNI and QSslSocket.

Obviously, you can successfully complete a TLS/SSL handshake by using QSslSocket alone without relying on SNI. That’s not the issue. You will be limited to setting your server to use the one certificate you provided using QSslSocket. The issue arises when you want to be able to set up your server to respond to requests for multiple host names while using different certificates depending on which host name is requested. You cannot yet set up different certificates to respond to requests for multiple host names using QSslSocket. That’s the issue.

“Why not just use a SAN certificate then?”, you might ask. Using a UCC or SAN certificate might allow you to circumvent the fact that QSslSocket doesn’t yet support SNI, but using one certificate for multiple host names is not always a practical solution. Not everyone wants their certificate to include multiple host names, which for some does lower the authentication trust level. Also, if you need to add a host name to a SAN certificate, then you need to get a new certificate including all the other host names. That might be a costly approach unless you are using a free certificate issuer such as Let’s Encrypt, since SAN certificates cost much more than single host name certificates.

Now, hold on! You are not completely out of luck. You can still use Qt and SNI in a server program. If you want to continue using QSslSocket, you will have to rewrite the underlying code of how Qt handles the start of TLS/SSL connections. At the moment, Qt uses the OpenSSL library to handle TLS/SSL connections. If you have enough time and know-how, you can rewrite the QSslSocket class, or extend it, or do whatever you feel like, to support SNI. Not exactly a small task, especially if all you expected was to browse the Internet for an easy solution.

Another way to support SNI in your Qt server program would be to simply not use QSslSocket when you write the part of your program that receives and responds to HTTPS requests. You can use OpenSSL directly to receive requests, decide what happens at the beginning of the TLS/SSL handshake, and then respond. Here is the code for a simple TLS server found on the OpenSSL wiki.

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
int create_socket(int port)
{
    int s;
    struct sockaddr_in addr;
    
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);
    
    s = socket(AF_INET, SOCK_STREAM, 0);
    if (s < 0) {
        perror("Unable to create socket");
        exit(EXIT_FAILURE);
    }
    
    if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("Unable to bind");
        exit(EXIT_FAILURE);
    }
    
    if (listen(s, 1) < 0) {
        perror("Unable to listen");
        exit(EXIT_FAILURE);
    }
    
    return s;
}
void init_openssl()
{ 
    SSL_load_error_strings();   
    OpenSSL_add_ssl_algorithms();
}
void cleanup_openssl()
{
    EVP_cleanup();
}
SSL_CTX *create_context()
{
    const SSL_METHOD *method;
    SSL_CTX *ctx;
    
    method = SSLv23_server_method();
    
    ctx = SSL_CTX_new(method);
    if (!ctx) {
        perror("Unable to create SSL context");
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }
    
    return ctx;
}
void configure_context(SSL_CTX *ctx)
{
    SSL_CTX_set_ecdh_auto(ctx, 1);
    
    /* Set the key and cert */
    if (SSL_CTX_use_certificate_file(ctx, "cert.pem", SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }
    
    if (SSL_CTX_use_PrivateKey_file(ctx, "key.pem", SSL_FILETYPE_PEM) <= 0 ) {
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }
}
int main(int argc, char **argv)
{
    int sock;
    SSL_CTX *ctx;
    
    init_openssl();
    ctx = create_context();
    
    configure_context(ctx);
    
    sock = create_socket(4433);
    
    /* Handle connections */
    while(1) {
        struct sockaddr_in addr;
        uint len = sizeof(addr);
        SSL *ssl;
        const char reply[] = "test\n";
        
        int client = accept(sock, (struct sockaddr*)&addr, &len);
        if (client < 0) {
            perror("Unable to accept");
            exit(EXIT_FAILURE);
        }
        
        ssl = SSL_new(ctx);
        SSL_set_fd(ssl, client);
        
        if (SSL_accept(ssl) <= 0) {
            ERR_print_errors_fp(stderr);
        }
        else {
            SSL_write(ssl, reply, strlen(reply));
        }
        
        SSL_shutdown(ssl);
        SSL_free(ssl);
        close(client);
    }
    
    close(sock);
    SSL_CTX_free(ctx);
    cleanup_openssl();
}


You can integrate and adapt this code into your Qt server program. “But, where is the SNI part?”, you ask. Well, hold on! We’re getting there. OpenSSL does support SNI. As you can read on this thread on StackOverflow, after integrating some of the code above into your program, you’d also need to:

  • Set up an additional SSL_CTX() for each different certificate;
  • Add a servername callback to each SSL_CTX() using SSL_CTX_set_tlsext_servername_callback();
  • In the callback, retrieve the client-supplied servername with SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name). Figure out the right SSL_CTX to go with that host name, then switch the SSL object to that SSL_CTX with SSL_set_SSL_CTX().

Here is an example of a callback found on this StackOverflow thread that retrieves the SNI host name data and sets the appropriate certificate.

static int ServerNameCallback(SSL *ssl, int *ad, void *arg) { UNUSED(ad); UNUSED(arg); ASSERT(ssl); if (ssl == NULL) return SSL_TLSEXT_ERR_NOACK; const char* servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); ASSERT(servername && servername[0]); if (!servername || servername[0] == '\0') return SSL_TLSEXT_ERR_NOACK; /* Does the default cert already handle this domain? */ if (IsDomainInDefCert(servername)) return SSL_TLSEXT_ERR_OK; /* Need a new certificate for this domain */ SSL_CTX* ctx = GetServerContext(servername); ASSERT(ctx != NULL); if (ctx == NULL) return SSL_TLSEXT_ERR_NOACK; /* Useless return value */ SSL_CTX* v = SSL_set_SSL_CTX(ssl, ctx); ASSERT(v == ctx); if (v != ctx) return SSL_TLSEXT_ERR_NOACK; return SSL_TLSEXT_ERR_OK; }

And that’s it! By integrating these two examples into your Qt server program, you should be able to get the SNI host name data sent during the TLS/SSL handshake, set the certificate that corresponds to the host name, complete a successful handshake, and securely read and respond to the encrypted request.

Contact: nick@nachega.com
© Nachega.com