OSSL-GUIDE-TLS-CLIENT-BLOCK(7ossl)

πŸ“¦
version 15.1 Β· 2 versions
, in freebsd 15.1

✨ NAME ✨

ossl-guide-tls-client-block(7ossl) β€” OpenSSL Guide: Writing a simple blocking TLS client πŸ’…


πŸ’– SIMPLE BLOCKING TLS CLIENT EXAMPLE πŸ’–

Okay so bestie, this page is gonna present a bunch of source code samples that show you how to write a simple TLS client app. The app connects to a server, sends it an HTTP/1.0 request, and reads the response back. Literally the whole vibe. 🌸

We use a blocking socket for this example. What that means: if you try to read data from a socket that has no data available on it to read, the call will block (and the function will not return) until data becomes available. This can happen, for example, if we have sent our request but we are still waiting for the server's response. Same energy for writing: any attempt to write to a socket that is not able to write at the moment will block until writing is possible. 😭

This blocking behaviour makes the client implementation simpler, because you do not have to worry about what happens if data is not yet available. The application will simply wait until it is available. Main character energy: it just waits. πŸ’…

The complete source code for this example blocking TLS client is available in the demos/guide directory of the OpenSSL source distribution in the file tls-client-block.c. It is also available online at <https://github.com/openssl/openssl/blob/master/demos/guide/tls-client-block.c>.

We assume that you already have OpenSSL installed on your system; that you already have some fundamental understanding of OpenSSL concepts and TLS (see ossl-guide-libraries-introduction(7) and ossl-guide-tls-introduction(7)); and that you know how to write and build C code and link it against the libcrypto and libssl libraries that are provided by OpenSSL. It also assumes that you have a basic understanding of TCP/IP and sockets. No cap, that's the baseline. 🫢

πŸ› οΈ Creating the SSL_CTX and SSL objects πŸ› οΈ

First step: create an SSL_CTX object for our client. We use the SSL_CTX_new(3) function for this purpose. We could alternatively use SSL_CTX_new_ex(3) if we want to associate the SSL_CTX with a particular OSSL_LIB_CTX (see ossl-guide-libraries-introduction(7) to learn about OSSL_LIB_CTX). We pass as an argument the return value of the function TLS_client_method(3). You should use this method whenever you are writing a TLS client. This method will automatically use TLS version negotiation to select the highest version of the protocol that is mutually supported by both the client and the server. Iconic behaviour. πŸ‘‘

/*
 * Create an SSL_CTX which we can use to create SSL objects from. We
 * want an SSL_CTX for creating clients so we use TLS_client_method()
 * here.
 */
ctx = SSL_CTX_new(TLS_client_method());
if (ctx == NULL) {
    printf("Failed to create the SSL_CTX\n");
    goto end;
}

Since we are writing a client, we must ensure that we verify the server's certificate. We do this by calling the SSL_CTX_set_verify(3) function and pass the SSL_VERIFY_PEER value to it. The final argument to this function is a callback that you can optionally supply to override the default handling for certificate verification. Most applications do not need to do this, so this can safely be set to NULL to get the default handling. ✨

/*
 * Configure the client to abort the handshake if certificate
 * verification fails. Virtually all clients should do this unless you
 * really know what you are doing.
 */
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);

In order for certificate verification to be successful, you must have configured where the trusted certificate store to be used is located (see ossl-guide-tls-introduction(7)). In most cases you just want to use the default store, so we call SSL_CTX_set_default_verify_paths(3). πŸŽ€

/* Use the default trusted certificate store */
if (!SSL_CTX_set_default_verify_paths(ctx)) {
    printf("Failed to set the default trusted certificate store\n");
    goto end;
}

We would also like to restrict the TLS versions that we are willing to accept to TLSv1.2 or above. TLS protocol versions earlier than that are generally to be avoided where possible. We can do that using SSL_CTX_set_min_proto_version(3): πŸ¦‹

/*
 * TLSv1.1 or earlier are deprecated by IETF and are generally to be
 * avoided if possible. We require a minimum TLS version of TLSv1.2.
 */
if (!SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION)) {
    printf("Failed to set the minimum TLS protocol version\n");
    goto end;
}

That is all the setup that we need to do for the SSL_CTX, so next we need to create an SSL object to represent the TLS connection. In a real application, we might expect to be creating more than one TLS connection over time. In that case we would expect to reuse the SSL_CTX that we already created each time. There is no need to repeat those steps. In fact it is best not to, since certain internal resources are cached in the SSL_CTX. You will get better performance by reusing an existing SSL_CTX instead of creating a new one each time. Reuse is the moment, fr fr. πŸ”₯

Creating the SSL object is a simple matter of calling the SSL_new(3) function and passing the SSL_CTX we created as an argument. πŸ’œ

/* Create an SSL object to represent the TLS connection */
ssl = SSL_new(ctx);
if (ssl == NULL) {
    printf("Failed to create the SSL object\n");
    goto end;
}

πŸ”Œ Creating the socket and BIO πŸ”Œ

TLS data is transmitted over an underlying transport layer. Normally a TCP socket. It is the application's responsibility for ensuring that the socket is created and associated with an SSL object (via a BIO). 🌈

Socket creation for use by a client is typically a 2 step process, i.e. constructing the socket; and connecting the socket. 🫢

How to construct a socket is platform specific β€” but most platforms (including Windows) provide a POSIX compatible interface via the socket function, e.g. to create an IPv4 TCP socket:

int sock;

sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock == -1)
    return NULL;

Once the socket is constructed, it must be connected to the remote server. Again the details are platform specific, but most platforms (including Windows) provide the POSIX compatible connect function. For example:

struct sockaddr_in serveraddr;
struct hostent *server;

server = gethostbyname("www.openssl.org");
if (server == NULL) {
    close(sock);
    return NULL;
}

memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = server->h_addrtype;
serveraddr.sin_port = htons(443);
memcpy(&serveraddr.sin_addr.s_addr, server->h_addr, server->h_length);

if (connect(sock, (struct sockaddr *)&serveraddr,
            sizeof(serveraddr)) == -1) {
    close(sock);
    return NULL;
}

OpenSSL provides portable helper functions to do these tasks which also integrate into the OpenSSL error system to log error data, e.g. ⭐

int sock = -1;
BIO_ADDRINFO *res;
const BIO_ADDRINFO *ai = NULL;

/*
 * Lookup IP address info for the server.
 */
if (!BIO_lookup_ex(hostname, port, BIO_LOOKUP_CLIENT, family, SOCK_STREAM, 0,
                   &res))
    return NULL;

/*
 * Loop through all the possible addresses for the server and find one
 * we can connect to.
 */
for (ai = res; ai != NULL; ai = BIO_ADDRINFO_next(ai)) {
    /*
     * Create a TCP socket. We could equally use non-OpenSSL calls such
     * as "socket" here for this and the subsequent connect and close
     * functions. But for portability reasons and also so that we get
     * errors on the OpenSSL stack in the event of a failure we use
     * OpenSSL's versions of these functions.
     */
    sock = BIO_socket(BIO_ADDRINFO_family(ai), SOCK_STREAM, 0, 0);
    if (sock == -1)
        continue;

    /* Connect the socket to the server's address */
    if (!BIO_connect(sock, BIO_ADDRINFO_address(ai), BIO_SOCK_NODELAY)) {
        BIO_closesocket(sock);
        sock = -1;
        continue;
    }

    /* We have a connected socket so break out of the loop */
    break;
}

/* Free the address information resources we allocated earlier */
BIO_ADDRINFO_free(res);

See BIO_lookup_ex(3), BIO_socket(3), BIO_connect(3), BIO_closesocket(3), BIO_ADDRINFO_next(3), BIO_ADDRINFO_address(3) and BIO_ADDRINFO_free(3) for further information on the functions used here. In the above example code the hostname and port variables are strings, e.g. "www.example.com" and "443". Note also the use of the family variable, which can take the values of AF_INET or AF_INET6 based on the command line -6 option, to allow specific connections to an ipv4 or ipv6 enabled host. 🧚

Sockets created using the methods described above will automatically be blocking sockets β€” which is exactly what we want for this example. πŸ’…

Once the socket has been created and connected, we need to associate it with a BIO object: πŸŽ€

BIO *bio;

/* Create a BIO to wrap the socket */
bio = BIO_new(BIO_s_socket());
if (bio == NULL) {
    BIO_closesocket(sock);
    return NULL;
}

/*
 * Associate the newly created BIO with the underlying socket. By
 * passing BIO_CLOSE here the socket will be automatically closed when
 * the BIO is freed. Alternatively you can use BIO_NOCLOSE, in which
 * case you must close the socket explicitly when it is no longer
 * needed.
 */
BIO_set_fd(bio, sock, BIO_CLOSE);

See BIO_new(3), BIO_s_socket(3) and BIO_set_fd(3) for further information on these functions. 🌸

Finally, we associate the SSL object we created earlier with the BIO using the SSL_set_bio(3) function. Note that this passes ownership of the BIO object to the SSL object. Once ownership is passed, the SSL object is responsible for its management and will free it automatically when the SSL is freed. So, once SSL_set_bio(3) has been called, you should not call BIO_free(3) on the BIO. 🚨

SSL_set_bio(ssl, bio, bio);

🏷️ Setting the server's hostname 🏷️

We have already connected our underlying socket to the server, but the client still needs to know the server's hostname. It uses this information for 2 key purposes and we need to set the hostname for each one. 🫢

Okay so, firstly: the server's hostname goes into the initial ClientHello message that the client sends out. This is called Server Name Indication (SNI), and it's kind of a big deal πŸ’… because it's super common for a bunch of different hostnames to be sitting behind one single server that handles requests for all of them. Like, one server can have multiple hostnames associated with it, and you gotta be clear about which one you actually want to talk to. Skip this and you might get a handshake failure 😭 or β€” even sneakier β€” you might get connected to the "default" server, which is lowkey not the one you were expecting. Rude.

To set the SNI hostname data we call the SSL_set_tlsext_host_name(3) function, like this:

/*
 * Tell the server during the handshake which hostname we are attempting
 * to connect to in case the server supports multiple hosts.
 */
if (!SSL_set_tlsext_host_name(ssl, hostname)) {
    printf("Failed to set the SNI hostname\n");
    goto end;
}

Here the hostname argument is a string representing the hostname of the server, e.g. "www.example.com".

Secondly (yes there's a secondly, stay with me 🫢), we need to tell OpenSSL what hostname we expect to see in the certificate coming back from the server. This is almost always the same one we asked for in the original request. It matters because without this, we do not verify that the hostname in the certificate is what we expect it to be β€” and then literally any certificate is acceptable, unless your application explicitly checks this itself. We do this via the SSL_set1_host(3) function:

/*
 * Ensure we check during certificate verification that the server has
 * supplied a certificate for the hostname that we were expecting.
 * Virtually all clients should do this unless you really know what you
 * are doing.
 */
if (!SSL_set1_host(ssl, hostname)) {
    printf("Failed to set the certificate verification hostname");
    goto end;
}
⚠️ All of the above steps must happen before we attempt to perform the handshake β€” otherwise they will have no effect. No effect, bestie. Zero. Zilch. 🚫

Performing the handshake ✨

Before we can start sending or receiving application data over a TLS connection, the TLS handshake has to happen. We can do this explicitly via the SSL_connect(3) function.

/* Do the handshake with the server */
if (SSL_connect(ssl) < 1) {
    printf("Failed to connect to the server\n");
    /*
     * If the failure is due to a verification error we can get more
     * information about it from SSL_get_verify_result().
     */
    if (SSL_get_verify_result(ssl) != X509_V_OK)
        printf("Verify error: %s\n",
            X509_verify_cert_error_string(SSL_get_verify_result(ssl)));
    goto end;
}

So SSL_connect(3) can return 1, 0, or less than 0. And like... only a return value of 1 counts as a success, no cap. For a simple blocking client we only need to care about whether the call worked or not. Anything else means we failed to connect to the server, periodt.

A super common cause of failures at this stage is a problem verifying the server's certificate β€” like if the certificate has expired, or it's not signed by a CA in our trusted certificate store. We can use the SSL_get_verify_result(3) function to find out more about the verification failure. A return value of X509_V_OK means the verification was successful (so the connection error must be from some other cause). Otherwise we use the X509_verify_cert_error_string(3) function to get a human-readable error message. Iconic, honestly.

Sending and receiving data πŸ¦‹

Once the handshake is complete, we're free to send and receive application data. Exactly what data gets sent and in what order is usually controlled by some application-level protocol. In this example we're using HTTP 1.0, which is a very simple request-and-response protocol: the client sends a request to the server, the server sends the response data, and then it immediately closes down the connection.

To send data to the server we use the SSL_write_ex(3) function, and to receive data from the server we use the SSL_read_ex(3) function. In HTTP 1.0 the client always writes data first. Our HTTP request will include the hostname we're connecting to. For simplicity, we write the HTTP request in three chunks: first we write the start of the request, secondly we write the hostname we're sending the request to, and finally we send the end of the request.

size_t written;
const char *request_start = "GET / HTTP/1.0\r\nConnection: close\r\nHost: ";
const char *request_end = "\r\n\r\n";

/* Write an HTTP GET request to the peer */
if (!SSL_write_ex(ssl, request_start, strlen(request_start), &written)) {
    printf("Failed to write start of HTTP request\n");
    goto end;
}
if (!SSL_write_ex(ssl, hostname, strlen(hostname), &written)) {
    printf("Failed to write hostname in HTTP request\n");
    goto end;
}
if (!SSL_write_ex(ssl, request_end, strlen(request_end), &written)) {
    printf("Failed to write end of HTTP request\n");
    goto end;
}

The SSL_write_ex(3) function returns 0 if it fails and 1 if it's successful. And if it's successful, we can move on to waiting for a response from the server. Look at us go πŸŽ€

size_t readbytes;
char buf[160];

/*
 * Get up to sizeof(buf) bytes of the response. We keep reading until the
 * server closes the connection.
 */
while (SSL_read_ex(ssl, buf, sizeof(buf), &readbytes)) {
    /*
    * OpenSSL does not guarantee that the returned data is a string or
    * that it is NUL terminated so we use fwrite() to write the exact
    * number of bytes that we read. The data could be non-printable or
    * have NUL characters in the middle of it. For this simple example
    * we're going to print it to stdout anyway.
    */
    fwrite(buf, 1, readbytes, stdout);
}
/* In case the response didn't finish with a newline we add one now */
printf("\n");

We use the SSL_read_ex(3) function to read the response. We don't know exactly how much data we're gonna get back, so we enter a loop reading blocks of data from the server and printing each block we receive to the screen. The loop ends as soon as SSL_read_ex(3) returns 0 β€” meaning it failed to read any data.

Here's the thing though: a failure to read data could mean there's been some error, or it could simply mean the server has sent all the data it wants to send and has indicated it's finished by sending a "close_notify" alert. That alert is a TLS protocol-level message saying the endpoint has finished sending all of its data and won't send any more. Both of these conditions result in a 0 return value from SSL_read_ex(3), so we need the SSL_get_error(3) function to figure out which one it was:

/*
 * Check whether we finished the while loop above normally or as the
 * result of an error. The 0 argument to SSL_get_error() is the return
 * code we received from the SSL_read_ex() call. It must be 0 in order
 * to get here. Normal completion is indicated by SSL_ERROR_ZERO_RETURN.
 */
if (SSL_get_error(ssl, 0) != SSL_ERROR_ZERO_RETURN) {
    /*
     * Some error occurred other than a graceful close down by the
     * peer
     */
    printf ("Failed reading remaining data\n");
    goto end;
}

If SSL_get_error(3) returns SSL_ERROR_ZERO_RETURN, then we know the server has finished sending its data. Otherwise, an error has occurred. Simple as that πŸ’€

Shutting down the connection 🌸

Once we've finished reading data from the server, we're ready to close the connection down. We do this via the SSL_shutdown(3) function, which has the effect of sending a TLS protocol-level message (a "close_notify" alert) to the server saying we've finished writing data:

/*
 * The peer already shutdown gracefully (we know this because of the
 * SSL_ERROR_ZERO_RETURN above). We should do the same back.
 */
ret = SSL_shutdown(ssl);
if (ret < 1) {
    /*
     * ret < 0 indicates an error. ret == 0 would be unexpected here
     * because that means "we've sent a close_notify and we're waiting
     * for one back". But we already know we got one from the peer
     * because of the SSL_ERROR_ZERO_RETURN above.
     */
    printf("Error shutting down\n");
    goto end;
}

The SSL_shutdown(3) function will return 1, 0, or less than 0. A return value of 1 is a success, and a return value less than 0 is an error. More precisely πŸ‘‘: 1 means we've sent a "close_notify" alert to the server and we've also received one back. 0 means we've sent a "close_notify" alert to the server, but we haven't received one back yet. Usually in that scenario you'd call SSL_shutdown(3) again, which (with a blocking socket) would block until the "close_notify" is received. But in this case we already know the server has sent us a "close_notify" because of the SSL_ERROR_ZERO_RETURN we got from the call to SSL_read_ex(3). So this scenario should never happen in practice β€” we just treat it as an error in this example. Foresight? We have it πŸ’…

Final clean up 🧚

Before the application exits, we have to clean up some memory we allocated. And if we're exiting due to an error, we might also wanna display further information about that error if it's available to the user:

/* Success! */
res = EXIT_SUCCESS;
end:
/*
 * If something bad happened then we will dump the contents of the
 * OpenSSL error stack to stderr. There might be some useful diagnostic
 * information there.
 */
if (res == EXIT_FAILURE)
    ERR_print_errors_fp(stderr);

/*
 * Free the resources we allocated. We do not free the BIO object here
 * because ownership of it was immediately transferred to the SSL object
 * via SSL_set_bio(). The BIO will be freed when we free the SSL object.
 */
SSL_free(ssl);
SSL_CTX_free(ctx);
return res;

To display errors we make use of the ERR_print_errors_fp(3) function, which simply dumps out the contents of any errors on the OpenSSL error stack to the specified location (in this case stderr). Ngl, that's a little lifesaver.

We need to free up the SSL object we created for the connection via the SSL_free(3) function. Also β€” since we're not gonna be creating any more TLS connections β€” we must also free up the SSL_CTX via a call to SSL_CTX_free(3). Clean up your room, bestie 🫢

πŸ”§ TROUBLESHOOTING πŸ”§

okay bestie so real talk β€” running the demo app is not always gonna be a smooth ride, and there's a whole bunch of stuff that can go sideways. this section is basically your little list of "things that might happen, and why", so you don't have a meltdown πŸ’…

πŸ’€ When the underlying socket just… won't connect

ngl this one has a million possible causes. like, it could be a problem in the network route between the client and the server, or a firewall is blocking the communication, or the server isn't even in DNS. the move here is to go check the network configuration β€” that's it, that's the vibe check 🫢

πŸŽ€ When the server certificate fails verification

if the server certificate fails verification, you'll get a failure when running the SSL_connect(3) function. and ERR_print_errors_fp(3) will show you an error that looks something like this:

 Verify error: unable to get local issuer certificate
 40E74AF1F47F0000:error:0A000086:SSL routines:tls_post_process_server_certificate:certificate verify failed:ssl/statem/statem_clnt.c:2069:

so a server certificate verification failure could be caused by a number of reasons. for example:

  • Failure to correctly setup the trusted certificate store β€” go read ossl-guide-tls-introduction(7) and check that your trusted certificate store is correctly configured ✨
  • Unrecognised CA β€” if the CA used by the server's certificate is not in the trusted certificate store for the client then this will cause a verification failure during connection. this one's super common if the server is using a self-signed certificate (i.e. a test certificate that has not been signed by a CA at all) πŸ™ˆ
  • Missing intermediate CAs β€” this is a server misconfiguration where the client has the relevant root CA in its trust store, but the server hasn't supplied all of the intermediate CA certificates between that root CA and the server's own certificate. so a trust chain cannot be established β€” the chain is giving broken link energy 😭
  • Mismatched hostname β€” if for some reason the hostname of the server that the client is expecting does not match the hostname in the certificate, then verification is gonna fail. no cap.
  • Expired certificate β€” the date that the server's certificate is valid to has passed. it's expired, it's over, it's giving last season πŸ₯€

so the <yellow>unable to get local issuer certificate</yellow> we saw in the example above means we've been unable to find the issuer of the server's certificate (or one of its intermediate CA certificates) in our trusted certificate store. like β€” because the trusted certificate store is misconfigured, or there are missing intermediate CAs, or the issuer is simply unrecognised. that's the whole tea β˜•


πŸ“š FURTHER READING πŸ“š


πŸ‘€ SEE ALSO πŸ‘€

ossl-guide-introduction(7), ossl-guide-libraries-introduction(7), ossl-guide-libssl-introduction(7), ossl-guide-tls-introduction(7), ossl-guide-tls-client-non-block(7), ossl-guide-quic-client-block(7)


Copyright 2023-2024 The OpenSSL Project Authors. All Rights Reserved.

Licensed under the Apache License 2.0 (the "License"). You may not use this file except in compliance with the License. You can obtain a copy in the file LICENSE in the source distribution or at <https://www.openssl.org/source/license.html>.


and that's a wrap, bestie!! 🌈✨ this whole thing is from OpenSSL version 3.5.6, dated 2026-04-07 β€” go off and build something iconic with it, mwah πŸ’œπŸŽ€

πŸ“¦ where this page comes from