GnuTLS: TLS guide

This guide describes the implementation of a TLS client in GnuTLS.

The guide covers basic aspects of initiating a secure TLS connection, including certificate validation and hostname verification. When various alternative approaches are possible, the guide presents each of them and specifies their use cases to help you choose which approach suits your needs best.

  • We work with the API in C of GnuTLS, version 3.7.1.
  • We assume the server to communicate with is at x509errors.org and accepts TLS connections on a standard port 443.

In addition to this guide, we have also implemented OpenSSL guides for the following topics:

Certificate Transparency CRL revocation OCSP Stapling OCSP revocation

The source code from all guides is also available as a stand-alone CLI client with options to test multiple revocation schemes:

TLS client source code

Establishing an underlying TCP/IP connection

First, we need to establish an insecure TCP/IP connection with the server. For the most simple connection, a standard set of POSIX functions will suffice.

#include <sys/socket.h>
#include <unistd.h>

/* TCP/IP socket descriptor. */
int sockfd = -1;

/* We will send the server our connection preferences in the form of hints. */
struct addrinfo hints = {0};

/* We allow both IPv4 and IPv6. */
hints.ai_family = AF_UNSPEC;
/* We want a stream socket, not a datagram one. */
hints.ai_socktype = SOCK_STREAM;
/* We know the numeric port number beforehand. */
hints.ai_flags = AI_ADDRCONFIG | AI_NUMERICSERV;
/* We want to use TCP. */
hints.ai_protocol = IPPROTO_TCP;

struct addrinfo *result = NULL;

/* We query a list of addresses for the given hostname. */
if (getaddrinfo("x509errors.org", "443", &hints, &result) != 0 || result == NULL) {
    exit(EXIT_FAILURE);
}

/* Try to connect to each address from the server list until successful. */
struct addrinfo *rr;
for (rr = result; rr != NULL, rr = rr->ai_next) {
    sockfd = socket(rr->ai_family, rr->ai_socktype, rr->ai_protocol);
    if (sockfd == -1)
        continue;
    if (connect(sockfd, rr->ai_addr, rr->ai_addrlen) != -1)
        break;
    close(sockfd);
}

/* We don't need the server info anymore. */
freeaddrinfo(result);

/* We must fail if we didn't manage to connect to any server address. */
if (rr == NULL) {
    exit(EXIT_FAILURE);
}

If everything went well, sockfd is now a descriptor of a valid, connected socket. We can proceed to establish the TLS connection on top of the TCP/IP connection.

Creating a session context structure

Before we connect, a session context structure has to be created and initialized. It will store all the necessary configurations and settings.

#include <gnutls/gnutls.h>

/* Create a TLS session context. */
gnutls_session_t session = NULL;

/* Initialize the TLS session context. */
if (gnutls_init(&session, GNUTLS_CLIENT) < 0) {
    exit(EXIT_FAILURE);
}

/* Create a credentials structure. This is required to set a trusted root. */
gnutls_certificate_credentials_t creds = NULL;

/* Initialize the credentials structure. */
if (gnutls_certificate_allocate_credentials(&creds) < 0) {
    exit(EXIT_FAILURE)
}

Preparing the necessary session settings

For the connection to be functional and secure, we must set multiple options beforehand.

/* Set default cipher suite priorities. These are the recommended option. */
if (gnutls_set_default_priority(session) < 0) {
    exit(EXIT_FAILURE)
}

/* Enable server certificate validation, together with a hostname check.
** Not setting the hostname would mean that we would accept a certificate of any trusted server. */
gnutls_session_set_verify_cert(session, "x509errors.org", 0);

/* In certificate validation, we usually want to trust the system default certificate authorities. */
if (gnutls_certificate_set_x509_system_trust(creds) < 0) {
    exit(EXIT_FAILURE);
}

/* Associate the credentials structure with the session structure. */
if (gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, creds) < 0) {
    exit(EXIT_FAILURE);
}

/* Set the Server Name Indication TLS extension to specify the name of the server. */
/* This is required when multiple servers are running at the same IP address (virtual hosting). */
if (r = gnutls_server_name_set(session, GNUTLS_NAME_DNS, "x509errors.org", strlen("x509errors.org")) < 0) {
    exit(EXIT_FAILURE);
}

Alternative: Setting a custom trust anchor

In some cases, it might be useful to trust an arbitrary certificate authority. This could be the case during testing or within company intranets. If we trust a CA located in trusted_ca.pem and other authorities located in trusted_dir, we can easily change the trust setting as follows (any of the two procedures can be skipped). This must be done before we link the credentials structure to the session context.

/* Set a custom trusted CA for certificate validation from file. The certificate must be in PEM format. */
if (gnutls_certificate_set_x509_trust_file(creds, "trusted_ca.pem", GNUTLS_X509_FMT_PEM) < 0) {
    exit(EXIT_FAILURE);
}

/* Set a custom trusted CA directory. All certificates in the directory must be in the PEM format. */
if (gnutls_certificate_set_x509_trust_dir(creds, "trusted_dir", GNUTLS_X509_FMT_PEM) < 0) {
    exit(EXIT_FAILURE);
}

/* Associate the credentials structure with the session structure. */
if (gnutls_credentials_set(session, GNUTLS_CRD_CERTIFICATE, creds) < 0) {
    exit(EXIT_FAILURE);
}

Optional: Checking revocation using local CRLs

In case when the file containing the certificate revocation list (CRL) is stored locally, it is possible to add this file to the GnuTLS credentials structure. GnuTLS will use this CRL to validate the certificates during the TLS handshake. It is also possible to add multiple CRL files by calling the appropriate API call multiple times. Supported formats are PEM and DER.

In our example, we assume that the CRL file is stored in a file called crl.pem.

/* Validate the certificates against supplied CRL during the TLS handshake. */
/* Function returns number of processed CRLs or negative error code. */
if (gnutls_certificate_set_x509_crl_file(creds, "crl.pem", GNUTLS_X509_FMT_PEM) <= 0) {
    exit(EXIT_FAILURE);
}

Optional: Sending an OCSP status request to the server

One of the modern methods of revocation checking is via OCSP stapling when the server sends revocation information “stapled” in the TLS handshake. GnuTLS checks such revocation information by default, but the server will not send it unless we explicitly tell it to do so.

/* Send the status request extension to the server during the TLS handshake. */
if (gnutls_ocsp_status_request_enable_client(session, NULL, 0, NULL) < 0) {
    exit(EXIT_FAILURE);
}

Alternative: Setting custom verification callback

By default, GnuTLS validates the peer’s certificate but does not check the revocation status. If a custom certificate validation logic or checking the revocation status of the certificates during the TLS handshake is required, it is possible to set a custom verification function. The prototype of this callback function is int (*callback)(gnutls_session_t). The callback function should return 0 for the handshake to continue or non-zero to terminate. An official example can also be found here.

/* Set the hostname to the session structure, so it will be accessible during our custom verification callback. */
gnutls_session_set_ptr(session, (void *) hostname);

/* Set our custom verification function. */
/* Callback will be executed right after the certificate chain has been received, during the TLS handshake. */
int (*f)(gnutls_session_t) = &custom_callback_function;
gnutls_session_set_verify_function(session, f);

We also provide a simple example of such a function. When using a custom verification callback, it is always necessary to validate the certificates manually. After successful validation, the revocation check of all certificates in the chain should be performed.

We have covered guides for checking the revocation status for all certificates in the chain using CRL, OCSP and OCSP-Stapling schemes. We have also covered a partial guide for checking the Certificate Transparency criteria for each certificate in the chain.

int custom_callback_function(gnutls_session_t session) {

    /* Retrieve the result of peer's certificate validation. */
    /* Set to 0 if certificate is trusted, non-zero if problem. */
    unsigned int certificate_validation_status;

    /* Retrieve the previosly set hostname from the session. */
    char *hostname = gnutls_session_get_ptr(session);

    /* Validate the peer's certificate with its hostname. */
    if (gnutls_certificate_verify_peers3(session, hostname, &certificate_validation_status) < 0) {
        exit(EXIT_FAILURE);
    }

    /* Retrieve the certificate type (X509 in most cases). */
    gnutls_certificate_type_t cert_type;
    if ((cert_type = gnutls_certificate_type_get(session)) != GNUTLS_CRT_X509) {
        exit(EXIT_FAILURE);
    }

    /* Check the result of validation. */
    /* If validation failed, we want to immediately terminate the TLS connection by returning non-zero value. */
    if (certificate_validation_status != 0) {
        return GNUTLS_E_CERTIFICATE_VERIFICATION_ERROR;
    }

    /* After performing certificate verification check, revocation check should be performed! */
}

Initializing a TLS connection

At this point, we can link the open socket descriptor to our session context and perform the TLS handshake.

/* Bind the open socket to the TLS session. */
gnutls_transport_set_int(session, sockfd);

/* Set default timeout for the handshake. */
gnutls_handshake_set_timeout(session, GNUTLS_DEFAULT_HANDSHAKE_TIMEOUT);

/* Try to perform handshake until successful (this is the standard way). */
do {
    r = gnutls_handshake(session);
} while (r < 0 && gnutls_error_is_fatal(r) == 0);

/* Fail if the handshake was not succesful. */
if (r < 0) {
    exit(EXIT_FAILURE);
}

Optional: Checking the result of peer certificate validation

If certificate validation fails, gnutls_handshake() will always fail with the same error message. In that case, it is often useful to examine the specific certificate validation error as follows. You can find explanations of certificate validation messages in the official documentation or on our page.

/* Retrieve the certificate validation status. */
unsigned status = gnutls_session_get_verify_cert_status(session);

/* Retrieve the certificate type. */
gnutls_certificate_type_t cert_type = gnutls_certificate_type_get(session);

/* Prepare a buffer for the error message, fill it, and print the message to the standard error output. */
gnutls_datum_t out = {0};
gnutls_certificate_verification_status_print(status, cert_type, &out, 0);
fprintf(stderr, "%s", out.data);
gnutls_free(out.data);

Sending and receiving data using the TLS connection

When the connection is successfully established, we can share application data with the server. These two functions provide the basic interface.

/* Prepare a message and send it to the server. */
char *message = "Hello server";
if (gnutls_record_send(session, message, strlen(message)) < 0) {
    exit(EXIT_FAILURE);
}

/* Prepare a static buffer for the response and read the response into that buffer. */
char buffer[4096];
if (gnutls_record_recv(session, buffer, 4096) < 0) {
    exit(EXIT_FAILURE);
}

Closing the connection

The client is usually the one to indicate that the connection is finished. When we want the connection closed, the following steps are performed.

/* Send the "close notify" message to the server, alerting it that we are closing the connection. */
if (gnutls_bye(session, GNUTLS_SHUT_RDWR) < 0) {
    exit(EXIT_FAILURE);
}

/* Free the credentials structure. */
if (creds != NULL) {
    gnutls_certificate_free_credentials(creds);
}

/* Free the session context structure. */
if (session != NULL) {
    gnutls_deinit(session);
}

/* Close the underlying TCP socket. */
if (sockfd >= 0) {
    close(sockfd);
}