In a previous article, I explained How to Use Secure Sockets in C on Linux. In that tutorial, I showcased the basics of initializing SSL sockets, and how to implement a simple client/server application that securely transmits data back and forth. The scope of that past article was the basic utility of secure sockets and basic certificate validation. This present article extends this scope by introducing the validation of certificate chains. Certificate chains stem from a trusted certificate authority (or CA) and allow derived certificates to be validated for trust. The basis for certificate trust chains is that the root CA is trusted and only signs other trusted entities. In this article, I will show you how to implement chain validation in C on Linux so you can incorporate this functionality into your own applications.
Certificate chain validation is crucial for ensuring the authenticity and integrity of a digital certificate. This is primarily used for:
- Secure Communications: Ensuring that the connection between a client and a server (e.g., HTTPS) is secure and the server is legitimate. One example is securing web communications to ensure data privacy and integrity between users and websites (especially when trying to stay private).
- Email Security: Validating email certificates to ensure the authenticity of the sender. Commonly used for signing and encrypting emails to protect against phishing and eavesdropping.
- Software Integrity: Verifying that software binaries are signed by trusted developers. This includes ensuring that software has not been tampered with and verifying the identity of the publisher.
- Document Signing: Providing authenticity and integrity for digital documents.
We will be focusing on the secure communications aspect. While HTTPS is a major use-case for certificate chains, we will apply it to raw SSL socket communication to understand the implementation side better.
You can find the full source code for this tutorial in this GitHub repository.
Historical Overview
The below gives a brief historical overview related to certificate chain evolution. It is not meant to be exhaustive but to convey an idea of where trusted certificate chains have their origins.
- 1976: Whitfield Diffie and Martin Hellman introduced the concept of public key cryptography, laying the foundation for digital certificates.
- 1988: ITU-T introduced X.509 standard, which became the basis for public key infrastructure (PKI) and digital certificates.
- 1995: Netscape introduced SSL (Secure Sockets Layer), which used digital certificates for secure web communication. This evolved into TLS (Transport Layer Security).
- 2000s: Widespread adoption of SSL/TLS for secure communications, with the introduction of various CAs (Certificate Authorities).
- 2014: Let’s Encrypt launched, providing free SSL/TLS certificates and automating the certificate issuance process, significantly increasing HTTPS adoption.
How Chain Validation Works
Chain validation is a process used to ensure that a given digital certificate can be trusted. This process involves verifying that the certificate is issued and signed by a trusted Certificate Authority (CA) and, if applicable, by one or more intermediate CAs. There are several entities involved in forming a trusted certificate chain, and each one of them should be verified until trust is established. Below shows the components of a chain validation:
Root Certificate Authority (CA)
The root CA is at the top of the certificate hierarch, the trusted anchor so to say. It is highly trusted and used to sign intermediate certificates. Commonly used root certificate authorities include:
- DigiCertSectigo (formerly Comodo)
- GlobalSign
- Let’s Encrypt
- VeriSign (now part of Symantec)
Root certificates are commonly pre-installed in operating systems and browsers, establishing the foundation of trust. This way, software on your local computer does not have to rely on external information for verifying certificate integrity, but can cross-check certificate chains with locally stored root certificates.
Intermediate Certificate Authority
Intermediate CAs are signed by root CAs and can issue certificates to end-users or entities. This adds a layer of security and manages the load of certificate issuance. Therefore, intermediate certificates inherit trust from root CAs.
An intermediate certificate issued by DigiCert might be used to sign certificates for various websites.
End-User Certificate
These are issued to end-users or entities (like websites) and are used to secure communications. For example, SSL/TLS certificates are used for HTTPS websites. End-user certificates inherit trust from intermediate and root CAs.
Chain Validation Process
In order to verify the integrity of a certificate chain, the following steps need to be taken. This is a general step overview, which we will later on follow through in code.
Certificate Chain Construction
The certificate chain is constructed starting from the end-user certificate up to the root certificate. Intermediate certificates form the chain between them.
Signature Verification
Each certificate in the chain is verified to ensure it is correctly signed by the CA immediately above it in the hierarchy. The end-user certificate is verified by an intermediate CA, which in turn is verified by a root CA.
Trust Anchor Verification
The root certificate is checked against the list of trusted root certificates pre-installed in the operating system or browser. If the root certificate is trusted, the entire chain is considered valid.
Generating A Chain Of Trust
Certificate Validation requires an existing Chain of Trust. This means that all entities in a chain from a root entity down to an end-user certificate need to be verifiably trustworthy. This is achieved through virtually unforgeable digital signatures. In this section, you will learn to create a simple but complete certificate chain that we will subsequently verify.
Creating the Server Certificate Chain
To work out an example for chain validation, we will first create a certificate chain ourselves. The chain will be simple, but should include all the entities described above: Root CA, Intermediate CA, and End-User Certificate:
The following steps require the command line tool openssl to be installed on your local system. Assuming you’re using Ubuntu, install it with the following commands:
sudo apt-get update
sudo apt-get install openssl
With openssl available, first generate a Root CA private key:
openssl genpkey -algorithm RSA -out rootCA.key -aes256 -pass pass:rootCAPassword
This key needs to be kept safe. It will only be used to generate the root certificate and should not be accessible by anyone else. This particular command uses RSA + AES 256 for encryption, and sets the password “rootCAPassword” for the private key. Never lose the password if you define one for your private key, or you won’t be able to use it again. The private key will be saved as “rootCA.key“.
Now using that private key, generate the Root CA Certificate:
openssl req -x509 -new -key rootCA.key -sha256 -days 3650 -out rootCA.crt -subj "/C=US/ST=State/L=City/O=Organization/OU=OrgUnit/CN=RootCA" -passin pass:rootCAPassword
This requires the Root CA password defined above, features an expiration date of 3650 days after which it needs to be renewed, and sets some identification data in the subj parameter. The generated certificate follows the X.509 standard as described above and is saved as “rootCA.crt“.
With the Root CA key and certificate at hand, we have all data that defines our Root Certificate Authority. With this, we will generate and sign an Intermediate CA Certificate now.
First, generate private key for the Intermediate CA (setting the password “intermediateCAPassword“):
openssl genpkey -algorithm RSA -out intermediateCA.key -aes256 -pass pass:intermediateCAPassword
The new private key is saved as “intermediateCA.key“. This key should be kept private to administrators of the Intermediate CA and must not be shared. Next, generate a Certificate Signing Request (CSR) for the Intermediate CA Certificate:
openssl req -new -key intermediateCA.key -out intermediateCA.csr -subj "/C=US/ST=State/L=City/O=Organization/OU=OrgUnit/CN=IntermediateCA" -passin pass:intermediateCAPassword
This CSR (stored as intermediateCA.csr) needs to be signed by the Root CA private key to generate the Intermediate CA Certificate:
openssl x509 -req -in intermediateCA.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out intermediateCA.crt -days 1825 -sha256 -passin pass:rootCAPassword -extfile <(printf "basicConstraints=CA:TRUE\nkeyUsage=critical,digitalSignature,cRLSign,keyCertSign\nsubjectKeyIdentifier=hash\nauthorityKeyIdentifier=keyid:always,issuer")
After this step, you now have the Intermediate CA Certificate stored as “intermediateCA.crt“. For the last link in the chain of trust, we generate the End-User Certificate. This may be the HTTPS certificate websites hand out to browsers. For that, start by generating the End-User private key, using “endUserPassword” as the key pass phrase:
openssl genpkey -algorithm RSA -out enduser.key -aes256 -pass pass:endUserPassword
Again, generate a CSR to be signed by the Intermediate CA:
openssl req -new -key enduser.key -out enduser.csr -subj "/C=US/ST=State/L=City/O=Organization/OU=OrgUnit/CN=EndUser" -passin pass:endUserPassword
And finally sign the CSR using the Intermediate CA:
openssl x509 -req -in enduser.csr -CA intermediateCA.crt -CAkey intermediateCA.key -CAcreateserial -out enduser.crt -days 365 -sha256 -passin pass:intermediateCAPassword
This last step results in the file “enduser.crt“, which is the End-User Certificate. This certificate is now trusted by the Intermediate CA, which in turn is trusted by the Root CA. To test this chain of trust, you can verify it using the below command:
openssl verify -CAfile rootCA.crt -untrusted intermediateCA.crt enduser.crt
Here, we assume rootCA.crt as the trusted amchor and include intermediateCA.crt as an untrusted intermediary. The command will reveal if the chain of trust is correct and all certificate signatures match. We will be using these certificates in the next step: How to Implement Chain Validation in C on Linux.
Chain Validation in C
The OpenSSL libraries available in C cover all functionalities we need for validating a certifcate chain. In this section, we will be using the certificates generated in the previous section and require these files:
- rootCA.crt: The Root CA Certificate
- intermediateCA.crt: The Intermediate CA Certificate
- enduser.crt: The End-User Certificate
We will ultimately verify the validity of enduser.crt based on a trusted rootCA.crt with an arbitrary number of Intermediate CA certificates (in this case just one, intermediateCA.crt). This process is important when connecting to servers that claim to be trustworthy to make sure the connection is really safe and private. Not verifying a server’s certificate signature can lead to Man-in-the-Middle attacks or rogue servers imporrsonating legitimate services. Be sure to validate certificates before transmitting sensitive information.
We will be using the setup from my previous tutorial on using secure sockets in C as the basis and will add the chain verification logic. The main function we will be using for verification is:
SSL_CTX_load_verify_locations
We will be extending the client logic from that previous article, since the server logic will stay the same.
The server will present the concatenated certificate chain to the client. For this, we need to make this chain available to the server as fullchain.crt. Generate that file using this command concatenating the Intermediate Certificate and the End-User Certificate:
cat enduser.crt intermediateCA.crt > fullchain.crt
In the following, we will cover the implementation of the server and the client. The general logic will follow the previous tutorial on using secure sockets in C and won’t be explained in detail.
Building the Client Application
The client has relatively simple logic: It sets up the SSL context, loads the Root CA and Intermediate CA certificates, and connects to the server. It will then verify the server’s presented certificate chain and only send data if that is valid. Below you can find the client implementation (also available here):
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#define PORT 4910
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_client_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) {
// Load the root CA certificate
if (SSL_CTX_load_verify_locations(ctx, "rootCA.crt", NULL) <= 0) {
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
// Load the intermediate CA certificate
if (SSL_CTX_load_verify_locations(ctx, "intermediateCA.crt", 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, 2);
}
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();
}
The certificate loading portion happens in configure_context. One important configuration detail here is this block:
// Set the verification mode to verify the server certificate
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
SSL_CTX_set_verify_depth(ctx, 2);
It configures SSL to check chains up to a depth of 2 certificates. Later on, SSL_get_verify_result(ssl) != X509_V_OK will ensure that the chain validation actually succeeded before we start transmitting sensitive information. Next up, we will define the server application.
Building the Server Application
The server application will accept connections from clients and identify itself with the fullchain.crt certificate chain defined above. Again, the code is also available on GitHub here and follows the general logic of the secure sockets tutorial from before:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#define PORT 4910
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 combined certificate chain file (end-user + intermediate)
if (SSL_CTX_use_certificate_file(ctx, "fullchain.crt", SSL_FILETYPE_PEM) <= 0) {
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
// Set the private key file
if (SSL_CTX_use_PrivateKey_file(ctx, "enduser.key", SSL_FILETYPE_PEM) <= 0) {
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
// Load the root CA certificate
if (SSL_CTX_load_verify_locations(ctx, "rootCA.crt", NULL) <= 0) {
ERR_print_errors_fp(stderr);
exit(EXIT_FAILURE);
}
}
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();
}
Now that we have both applications at hand, we need to compile them. We use this simple Makefile configuration to compile the source and link to the correct libraries:
CC=gcc
CFLAGS=-Wall -Wextra -g
LIBS=-lssl -lcrypto
all: server client
server: server.o
$(CC) -o server server.o $(LIBS)
client: client.o
$(CC) -o client client.o $(LIBS)
server.o: server.c
$(CC) $(CFLAGS) -c server.c
client.o: client.c
$(CC) $(CFLAGS) -c client.c
clean:
rm -f *.o server client
If you haven’t generated the certificates using the command above yet, you can also use the gen_certs.sh convenience script in the GitHub repository to do that. Next, let’s run the server and client and test the connection.
Running the Server and Client
Now that both applications are compiled, run the server in one terminal and the client in another on the same host. The client will connect to the server, and will send a “Ping” message, answered by a “Pong” message from the server. This interaction is now encrypted using the fullchain.crt (or enduser.crt specifically) certificate, which was validated by the client upon connect.
If the chain validation fails upon connect, you will see output similar to this:
40B718DC107F0000:error:0A000086:SSL routines:tls_post_process_server_certificate:certificate verify failed:../ssl/statem/statem_clnt.c:1883:
You won’t see this output though if you followed the steps from above. Everything should align properly. You now have a fully working validated certificate chain at hand and two applications that use it to protect their communication.
Conclusion
Congratulations, you just implemented a full chain validation in C using SSL sockets! With this knowledge at hand, you can now implement much safer communication mechanisms for your socket-based projects. The above code is available in its entirety in this GitHub repository as well. If you want to further increase your connection security, be sure to use a VPN like PIA. Should you be planning to host your own website (especially anonymously), find out how you can make sure to stay private, too!
If you liked this article or want to share your own thoughts and experiences, comment below to get the conversation started!