Skip to content

TLS Certificates, Hostnames and Trust

An encrypted connection is not enough if you have connected to the wrong server. Use the previous C++ client to separate two questions: who issued this certificate, and is it for the server you intended?

Course examples on GitHub

Reuse the standalone TLS client, certificate fixture and negative tests. There is no second implementation to keep in sync.

Before you start

Complete Your First Verified TLS Connection, including a successful echo. Run commands on your Pi from the examples repository's network/ directory. This lesson does not install a CA into the OS or browser.

Outcome: explain and reproduce separate trust-chain and hostname failures, and confirm that the C++ client sends no application message when either check fails.

Two checks before application data

flowchart TD accTitle: A trusted issuer does not imply the correct server accDescr: A server certificate must pass chain validation against the configured trust anchors and identity validation against the intended hostname. Failure of either check stops the TLS client before application data. A[Server presents certificate] --> B{Valid chain to a trusted CA?} B -->|No| F[Stop before application data] B -->|Yes| C{Certificate matches intended hostname?} C -->|No| F C -->|Yes| D[Complete verified handshake] D --> E[Send the application message]

This separates the decisions conceptually, not OpenSSL's internal validation order. Chain validation includes more than checking a signature: certificate validity periods, CA constraints and allowed usage matter too. The handshake also proves possession of the server's private key. Merely copying another server's public certificate is not sufficient.

In this exercise the chain is short: a practice CA signs a server certificate. Real deployments can include intermediate CAs. The local ca.pem is our explicitly selected trust anchor, not something the server gets to declare trustworthy.

TLS identity: address, expected name and SNI

Value In this client What it does not establish
Destination address 127.0.0.1 in sockaddr_in Which DNS identity a certificate should contain.
Expected name localhost, passed to SSL_set1_host Whether the issuer is trusted.
SNI localhost, sent with SSL_set_tlsext_host_name Whether the returned certificate is valid.
Trust file Your selected ca.pem Whether a trusted certificate belongs to this hostname.

SNI helps a server hosting multiple names select a certificate. It is not the hostname check. The fixture serves just one certificate, so changing SNI cannot make it produce a certificate for another name.

Our certificate contains DNS:localhost, not IP:127.0.0.1. The client connects to the loopback IP but deliberately verifies the DNS identity. A general IP-identity client should use OpenSSL's IP-address verification API and require an IP Subject Alternative Name; do not replace DNS verification with a string comparison against the certificate's printed subject.

Inspect your practice certificate

If the previous terminal has closed, create a fresh, private practice directory:

1
2
3
practice=$(mktemp -d /tmp/network-trust-practice.XXXXXX)
python3 fixtures/tls_echo.py certs "$practice/certs"
certs="$practice/certs"

Inspect the public server certificate, not its secret key:

openssl x509 -in "$certs/server.pem" -noout \
    -issuer -subject -dates -ext subjectAltName

Look for the practice CA issuer, the two-day validity window and DNS:localhost. A readable certificate is not evidence that verification passed.

Now check it explicitly for the right name:

openssl verify -CAfile "$certs/ca.pem" \
    -purpose sslserver -verify_hostname localhost "$certs/server.pem"

Expected: the certificate path followed by OK, with exit status zero. This is an offline certificate check, not a network handshake or proof that a live peer possesses the private key.

Change only the required identity:

openssl verify -CAfile "$certs/ca.pem" \
    -purpose sslserver -verify_hostname wrong.example "$certs/server.pem"

Expected: hostname mismatch and a nonzero exit status. The issuer is still trusted, but this is not a certificate for wrong.example. Exact diagnostic formatting can vary with OpenSSL.

Reproduce failures through the C++ client

Start a fresh fixture in terminal A before each attempt:

timeout 35s python3 fixtures/tls_echo.py serve 9443 "$certs"

Terminal B needs the same printed certificate-directory path, just as in the previous lesson. First, keep the right CA but request the wrong name:

./build/native/debug/tls_echo_client 9443 "$certs/ca.pem" wrong.example

Expected: exit status 1, a TLS error on stderr and no successful echo on stdout. The fixture can also report the client's fatal TLS alert and exit with an error; this is an intentional failed handshake, not a reason to disable checking.

For an issuer failure, create an independent practice CA in terminal B:

1
2
3
other=$(mktemp -d /tmp/network-other-ca.XXXXXX)
python3 fixtures/tls_echo.py certs "$other/certs"
./build/native/debug/tls_echo_client 9443 "$other/certs/ca.pem" localhost

Restart terminal A first, still using the original $certs server certificate. The independent CA cannot authenticate that server. The two practice CAs happen to share a display name, but have different keys; matching issuer text is not a valid trust check.

Do not replace the trust file with a certificate supplied by an unknown peer merely to get a successful result. Decide trust through an authenticated channel outside the connection being checked.

Prove that failure stops the application

Run the independent peer checks:

1
2
3
ctest --preset native-debug \
    -R 'network.tls_echo.(good|wrong_ca|wrong_host)$' \
    --output-on-failure

The harness records application bytes seen by the Python TLS peer. It requires the exact message on the good path and no application request for the wrong-CA and wrong-host paths. This provides stronger evidence than checking only whether the C++ process printed an error.

In the existing C++ source, locate these four responsibilities:

  1. SSL_CTX_load_verify_locations loads the selected CA file.
  2. SSL_CTX_set_verify(..., SSL_VERIFY_PEER, ...) makes peer verification mandatory.
  3. SSL_set1_host sets the required identity before the handshake.
  4. The SSL_write_ex loop is reached only after SSL_connect succeeds and verification is checked.

Keep the formatted full source in the previous lesson open while tracing that path. There is no insecure fallback or reconnect-without-verification branch.

What this exercise does not cover

This fixture authenticates the server, not the client. A server that also requires client certificates needs a separate mutual-TLS policy and client credentials. Application login and permissions are separate again.

The fixture does not demonstrate revocation checking, public certificate issuance, rotation or browser trust management. OpenSSL verification with this CA file does not automatically configure an application-specific revocation policy. A production client needs a maintained trust store and an explicit operational policy, not the two-day practice CA.

TLS protects application content in transit between its endpoints. It does not hide all network metadata, make the endpoints trustworthy, or prove the truth of a sensor reading.

Check your understanding

  1. Can a certificate from a trusted CA still be wrong for your connection?
  2. Does sending SNI enable certificate verification?
  3. Why do two CA certificates with identical subject text not necessarily establish the same trust?
  4. Does an offline openssl verify success prove that a network server holds the private key?
Answers

Yes: its identity, validity or permitted usage can be unsuitable. SNI requests a server name but does not validate the returned certificate. Trust depends on keys and validated signatures, not display-name equality. Offline verification checks certificate material; the live handshake provides proof of private-key possession.

Continue the course

Previous: Your First Verified TLS Connection · Next: Your First HTTPS Request

References: OpenSSL certificate verification, server identity checking, TLS 1.3 authentication.

Donate