Net Privacy Pro

Online security and privacy is commonly perceived from the user perspective. Whether you want to host content anonymously or set up your own VPN, you’re using pre-existing services and software. Developers need to build these for you first though. Welcome to a C tutorial on making client/server connections more secure youself! If you have ever used plain C sockets in your programs, you know that they are relatively simple to set up, easy to use, but expose all transmitted information in the plain. Tools like Wireshark allow anyone with access to the same physical network to potentially read every byte you send or receive this way. In this article I will show you how to use secure sockets in C on Linux to upgrade your connection security and encrypt all transmitted data effectively!

The logo of this article, explaining how to use secure sockets in C on Linux

You can find the complete source code for this tutorial, including instructions, in this GitHub repository!

Choosing and Installing a Secure Sockets Library

While you could implement encryption algorithms yourself (which is an interesting learning experience), you should rely on established, well-tested, and well-maintained crypto libraries. The reason is simple: Mistakes happen, bugs exist, and many of them lead to broken encryption. In the best case, this leads to a wrong sense of safety, in the worst case this can cost real money and reputation. In this section, we will lay the foundation for using secure sockets in our own C programs.

There are multiple libraries available to choose from. For the purpose of this tutorial, we will use libssl, the OpenSSL implementation of the secure sockets layer. It enjoys wide-spread use and popularity, is stable, and it supports modern encryption algorithms. For this purpose, and assuming you’re using any Debian derivative (for example Ubuntu), install the library and development headers for libssl:

sudo apt-get install libssl-dev

This should only take a moment. From now on, the libssl headers and binaries are available for you to develop with. Next, we need to generate SSL certificates that make encryption possible.

Creating the SSL Certificates

Certificates are recipes for how to encrypt and decrypt data when communicating. They steer the SSL protocol, and consist of two elements: A Certificate and a Key. In this tutorial on creating your own SSH keys I explained the importance of asymmetric encryption and what to watch out for when using it. For the sake of simplicity for this article, we will generate the required SSL certificates with the following command:

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes

The resulting files cert.pem and key.pem are what the client and server of our SSL socket project will use for communication. The generated certificates are valid for 365 days and will expire afterwards (see the -days 365 argument). Refer to man openssl on your console for more details on the command line options available.

Now that the certificate files are available, let’s dive into coding. We will build two applications: An SSL server that accepts incoming connections on port 4910, and a client that connects to the server on that port on the local host (127.0.0.1). We will cover this in the next section.

Building the Server and Client Applications

Every client/server architecture requires two distinct processes. One listens to incoming connections (the server) and one tries to open connections (the client). Typically, the server serves multiple clients, while clients connect to only one server. There are exceptions to this, but we will stick with this simple case for the sake of this tutorial. Below, I will show you how to use secure sockets in C on Linux in both parts of the connection.

As described above, we will build two binaries: A server and a client. They will communicate over port 4910 (TCP) on the same host. The server will accept client connections and wait for data from them. The client sends “Ping”, and the server sends “Pong” back. The client then closes the connection again and quits, while the server keeps running, waiting for new connections. Let’s code out the server first.

Building the Server Application

The server consists of three parts:

  • Initializing SSL
  • Preparing a Socket
  • Listening for and Serving Connections

The code I’m presenting in this section is also available here if you want the entire file at once. Here, let’s go through the individual parts and see which one does what. To get started, the following code initializes SSL for our purpose:

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;
}

In this code snippet, a function creates an SSL context and returns it upon success. It exits the application if anything goes wrong. This is the base work you need for using SSL. Next, we will configure the context to use the certificates we generated earlier:

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);
    }
}

Here you also see the filenames of the certificate generation mentioned earlier: cert.pem and key.pem. This function assumes that both files are present in the same folder as the program running this function. We will add two neat little functions to simply our lives for initializing and shutting down SSL again:

void init_openssl() {
    SSL_load_error_strings();
    OpenSSL_add_ssl_algorithms();
}

void cleanup_openssl() {
    EVP_cleanup();
}

What’s missing now is the listening and serving part of the server. Below you can see the main function that ties everything together. It initializes SSL (by using the above functions), creates a socket, listens for connections, and serves their requests:

int main() {
    int32_t sock;
    struct sockaddr_in addr;
    SSL_CTX* ctx;

    init_openssl();
    ctx = create_context();

    configure_context(ctx);

    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("Unable to create socket");
        exit(EXIT_FAILURE);
    }

    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("Unable to bind");
        exit(EXIT_FAILURE);
    }

    if (listen(sock, 1) < 0) {
        perror("Unable to listen");
        exit(EXIT_FAILURE);
    }

    while (1) {
        struct sockaddr_in addr;
        uint32_t len = sizeof(addr);
        SSL* ssl;
        const char reply[] = "Pong";

        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 {
            char buf[256] = {0};
            SSL_read(ssl, buf, sizeof(buf));
            printf("Received: %s\n", buf);

            SSL_write(ssl, reply, strlen(reply));
        }

        SSL_free(ssl);
        close(client);
    }

    close(sock);
    SSL_CTX_free(ctx);
    cleanup_openssl();
}

It might look dauntingly complex at first, but it you squit at it hard, you will see that it has a very clear structure. The main part of the program is wrapped in the while(1) loop, and all it does is accepting socket connections, opening them with an SSL context, and transmitting data. This is the first step to learning how to use secure sockets in C on Linux. This code can be found on GitHub as well.

Now that we have the code for the server, let’s write the client code.

Building the Client

The client code performs the opposite of what the server does. What they do have in common is the initialization of the SSL context and the handling of SSL sockets. In short, what it does is this:

  • Initializing SSL
  • Preparding a Socket
  • Connecting to a Server and Transmitting Data

The last part is what distinguishes the client from the server. What the client does in addition to connecting to the server is validating the server certificate. While the server’s key is private, the certificate can be shared with potential clients. We ignore the key distribution aspect for the sake of simplicity and load the same cert.pem file that the server uses directly. In reality, clients have a certificate store available that allow them to draw from publicly known certificates and derivative certificates. The code below demonstrates the principle mechanism of validation. Again, you can find the full code for the client on GitHub.

The client’s initialization of SSL is the same as for the server, so we’re leaving this out here. For validating the server’s certificate, the client does need to load the server’s certificate file:

void configure_context(SSL_CTX *ctx) {
    // Load the server's certificate (acting as the CA certificate here)
    if (SSL_CTX_load_verify_locations(ctx, "cert.pem", NULL) <= 0) {
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }

    // Set the verification mode to verify the server certificate
    SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
    SSL_CTX_set_verify_depth(ctx, 1);
}

To learn how to validate a whole certificate chain, have a look at this article that explains chain verification in C!

This function will be called from the main function after the SSL socket has connected to the server. This is the main function that in the end ties everything together:

int main() {
    int32_t sock;
    struct sockaddr_in addr;
    SSL_CTX* ctx;
    SSL* ssl;
    const char* hostname = "127.0.0.1";
    const char* message = "Ping";
    char buf[256] = {0};

    init_openssl();
    ctx = create_context();

    configure_context(ctx);

    ssl = SSL_new(ctx);

    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("Unable to create socket");
        exit(EXIT_FAILURE);
    }

    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    if (inet_pton(AF_INET, hostname, &addr.sin_addr) <= 0) {
        perror("Invalid address");
        exit(EXIT_FAILURE);
    }

    if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("Unable to connect");
        exit(EXIT_FAILURE);
    }

    SSL_set_fd(ssl, sock);

    if (SSL_connect(ssl) <= 0) {
        ERR_print_errors_fp(stderr);
    } else {
        // Verify the server certificate
        if (SSL_get_verify_result(ssl) != X509_V_OK) {
            fprintf(stderr, "Failed to verify server certificate.\n");
            SSL_free(ssl);
            close(sock);
            SSL_CTX_free(ctx);
            cleanup_openssl();
            return EXIT_FAILURE;
        }

        SSL_write(ssl, message, strlen(message));
        SSL_read(ssl, buf, sizeof(buf));
        printf("Received: %s\n", buf);
    }

    SSL_free(ssl);
    close(sock);
    SSL_CTX_free(ctx);
    cleanup_openssl();
}

What you can see here is that the client initializes SSL like before, prepares a socket, and connects to the server. Upon successful connection, it establishes the SSL context (SSL_connect), verifies the server’s certificate, and writes out the encrypted data to the SSL socket. It then waits for a response and prints it. Afterwards, the client closes the SSL and regular sockets and quits gracefully. The source code for this is avaibable here in its entirety.

Tieing it all Together: Compiling the Server and Client

To make the above programs useful, you need to compile them and link them against the libssl and crypto libraries. You can find the final Makefile here. It automatically compiles all required source files and links them against the correct libraries.

Conclusion

That’s it, you just learned how to use secure sockets in C on Linux! The SSL sockets you’ve learned about above are easy to use and effective in encrypting your data-in-transit. The SSL calls might look complex at first, but you only need to implement this once and can wrap it away in convenience functions. This price is low for what you get: Tools like Wireshark or similar won’t be able to simply dump all cleartext data you send/receive. This raises the bar significantly for snooping eyes or casual hackers trying to steal your information. The next step is to learn about chain validation – you can find an elaborate article on that here!

If you want to add an additional layer of protection, be sure to use a VPN connection like ZeroTier One, set up your own with WireGuard, or use professional VPN providers like Private Internet Access!

If you liked this article and want to share your own thoughts and experiences, comment below to get the conversation started!

6 Responses

  1. Thanks for posting this, however, I feel there is quite a bit left out. Certificate chain validation, key exchange for the symmetric shared secret or ephemeral key etc. Part duex perhaps?

    1. Yes, definitely! You’re absolutely right, thanks! This was just meant as an entry point to this topic, but you’re raising an important point. Without the features you mention the safety of the connection is easily jeopardized. I’ll make sure to cover this in a follow-up!

        1. Thanks for the feedback! You’re right, this is a crucial piece missing here. I updated the article and the code on GitHub. Without server certificate validation, using SSL sockets is moot. The code should now reflect the principle mechanism correctly.

          1. Great ! I was just looking for such simple example in c to build a small secured socket server for an embedded project.

Leave a Reply

Your email address will not be published. Required fields are marked *