Your First Verified TLS Connection in C++¶
TLS adds protected communication and peer authentication to the TCP connection you already know. Build a C++ client that verifies a local server, sends a short message and reads its encrypted echo.
Course examples on GitHub
Standalone C++ client · Fixture and certificate generator · Build instructions.
Before you start¶
Complete HTTP Connection Reuse. Review TCP deadlines if nonblocking sockets and poll() are unfamiliar. Run the following commands on your Pi, from the examples repository's network/ directory.
Outcome: exchange a verified TLS 1.3 echo and reject an untrusted certificate, a wrong hostname or a peer that exceeds the session deadline.
This is TLS, not HTTPS yet. The application message is the fixed 16-byte string Hello over TLS.\n, not an HTTP request.
What TLS changes¶
The figure simplifies a full, certificate-based TLS 1.3 handshake; it does not show every handshake message or TCP packet. There is no resumption, early data or client certificate in this exercise.
TCP still transports bytes. OpenSSL handles the TLS handshake, record protection and certificate processing. Your program decides which authority to trust and which server name to require. We do not implement encryption ourselves.
TLS does not give your application message boundaries. Here, both peers know the one message is 16 bytes long. An arbitrary-length protocol would still need framing.
Install the development dependency¶
Inspect your installation first:
Having the openssl command does not mean that C++ headers and linker files are installed. On Raspberry Pi OS / Debian, install missing development tools yourself:
These are administrative changes. The exercise requires OpenSSL 3.0 or later and a working system clock for certificate validity checks. The server fixture also uses Python's ssl module. CMake does not download a TLS implementation.
Build the client:
Or compile it directly:
Create a disposable local authority¶
In terminal A:
Copy the printed certificate-directory path for terminal B. The generator refuses to overwrite an existing certificate directory. It creates a short-lived practice CA and a separate server certificate with DNS:localhost in its Subject Alternative Name.
The CA certificate is public trust material; the *.key files are secrets. Keep this practice directory private, do not commit it, and never install this CA into the system/browser trust store. These unencrypted practice keys and two-day certificates are not production credentials.
Start a one-connection, loopback-only fixture:
It prints LISTEN 9443, waits for a client, echoes exactly 16 bytes, exchanges TLS close notifications and exits. The outer timeout bounds the whole fixture process.
Run the client¶
In terminal B, replace the path below with the path printed by terminal A:
Expected successful output:
The network destination is always 127.0.0.1. The expected certificate identity defaults to localhost. Connecting to an IP address and verifying a DNS identity are separate decisions.
Read the C++ implementation¶
Follow the calls in this order:
| Step | Visible operation | Responsibility |
|---|---|---|
| Trust policy | SSL_CTX_load_verify_locations, SSL_VERIFY_PEER |
Require a certificate chain rooted in the supplied practice CA. |
| Server identity | SSL_set1_host |
Check the certificate against the expected DNS name. |
| Name indication | SSL_set_tlsext_host_name |
Send SNI so a server can select its certificate. SNI is not verification. |
| TCP | socket, connect, poll, getsockopt |
Establish a nonblocking loopback connection and inspect its result. |
| TLS | SSL_set_fd, SSL_connect |
Attach TLS to that socket and complete the verified handshake. |
| Application I/O | SSL_write_ex, SSL_read_ex |
Send plaintext into TLS and receive decrypted bytes from TLS. |
| Finish | SSL_shutdown, frees, close |
Exchange close notifications, release TLS objects and close the descriptor. |
Do not use raw send() or recv() for application data once TLS owns the connection. That would bypass the TLS layer or consume bytes OpenSSL needs.
Full standalone client: app/tls-echo/client.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | |
Retry the TLS operation, not just the socket¶
A nonblocking TLS call can need either read or write readiness. Check SSL_get_error() immediately after the failed call, with no intervening OpenSSL call. Wait for its requested direction, then retry the same operation.
ERR_clear_error() clears the thread's error queue before each operation. A write retry uses the same unsent buffer and length; it does not advance the offset until OpenSSL reports success. A socket-ready event alone does not mean the handshake or application operation succeeded.
The lambdas keep this retry/deadline logic local; there is no course-specific socket header or wrapper class.
One deadline, including shutdown¶
The default 3000 ms budget starts just before TCP connect and covers connect, handshake, application I/O and TLS shutdown. Local argument parsing, CA-file loading and TLS-object setup happen before that budget. Progress does not extend it.
SSL_shutdown() can return zero after sending our close notification, before receiving the peer's. Continue the shutdown state machine; do not confuse zero here with successful two-way completion. An unexpected TCP EOF is not a verified TLS close.
The process ignores SIGPIPE because OpenSSL's socket BIO writes do not use our earlier MSG_NOSIGNAL flag. Failures instead reach the error path and explicit cleanup. This is a small, single-connection program, not a process-wide signal policy for a larger application.
Check failure paths¶
Run the automated cases without manually starting the fixture:
The checks cover successful/split echoes, wrong CA and hostname, missing/malformed trust files, a TLS 1.2-only peer, plaintext on the TLS port, stalled handshake/read/shutdown, slow progress, short or wrong echoes, abrupt TCP closure, invalid input and connection refusal.
The TLS 1.2 rejection is this exercise's TLS 1.3-only policy, not a claim that every TLS 1.2 deployment is invalid. These checks exercise API behavior; they are not a cryptographic audit.
Troubleshooting¶
- OpenSSL not found / ssl.h missing: install the development package, not just the command-line program. Reconfigure CMake afterward.
- Cannot load CA file: check the printed practice path and that
ca.pemexists. - Certificate verification failure: check hostname, CA file and system time. Regenerate expired two-day practice certificates in a fresh directory; never turn verification off.
- Connection refused: restart terminal A's one-connection fixture before each manual attempt.
- Session deadline expired: distinguish a stuck handshake, stalled echo and absent close notification. Do not solve an untrusted certificate by increasing the budget.
- Port already in use: choose an unused loopback port in both commands; do not terminate an unrelated process.
Check your understanding¶
- Why are both CA trust and hostname verification needed?
- Why can
SSL_write_ex()need read readiness? - Does one successful TLS read correspond to one application message?
- Why is closing the TCP descriptor not the same as a clean TLS shutdown?
Answers
Trust checks the issuer chain; hostname checking ties the certificate to the intended server. TLS protocol work can require input during a write. Reads expose a byte stream, so the application still needs framing. A clean TLS shutdown exchanges close notifications; a raw TCP close can truncate protected communication.
Continue the course¶
Previous: HTTP Connection Reuse · Next: Certificates, Hostnames and Trust
References: TLS 1.3 handshake, OpenSSL identity checking, TLS error handling, TLS shutdown.