Password Attacks & Cryptography · intermediate · ~12 min

TLS, certificates, and digital signatures

**What you will learn** - Explain what a *digital signature* is and how a private key signs while the matching public key verifies, proving both authenticity and integrity — and why that is the opposite direction from encryption-for-secrecy. - Read a real X.509 certificate and identify its key fields: subject, issuer, validity dates, Subject Alternative Names (SANs), public key, and the CA signature. - Trace a certificate *chain of trust* from a leaf certificate up to a trusted root CA, and perform the four checks a client makes before trusting a server. - Walk through a simplified TLS 1.3 handshake and explain how it combines asymmetric authentication with a fast symmetric session key, and what *forward secrecy* buys you. - Detect and clearly report common TLS/PKI weaknesses (expired, self-signed, or hostname-mismatched certificates; weak protocol versions; weak ciphers; missing HSTS) — and, for each, apply a secure fix and *verify* it. - Inspect a certificate safely with `openssl` against an authorized localhost lab and interpret the output without falling for the myth that a lock icon means "safe."

Overview

Security objective. The asset this lesson protects is data in transit — passwords, session cookies, tokens, and payment or health data traveling across a network you do not control. The threat is anyone sitting on the network path (a hostile Wi-Fi access point, a compromised router, a malicious ISP) who wants to read that data (breaking confidentiality), change it silently (breaking integrity), or impersonate the server (breaking authentication). By the end you will be able to detect and prevent the everyday TLS misconfigurations that hand those capabilities to an attacker.

Every time you see the lock icon in a browser, TLS (Transport Layer Security, the modern successor to SSL) is doing three jobs at once: confidentiality (nobody on the network can read your traffic), integrity (nobody can silently modify it), and server authentication (you are really talking to the host you typed, not an impostor). TLS is the backbone of HTTPS, secure email, VPNs, and most machine-to-machine APIs.

This lesson builds directly on the prerequisite, Symmetric and asymmetric encryption. There you learned that symmetric crypto is fast but needs a shared secret, while asymmetric crypto (public/private key pairs) solves key distribution but is slow. TLS is exactly where those two ideas finally cooperate: asymmetric keys authenticate the server and agree on a secret, then symmetric keys encrypt the bulk traffic at high speed. TLS is the real-world payoff for everything in that prerequisite.

To get there we layer three concepts:

  • Digital signatures are the trust primitive. A signature is a hash of some data, transformed with a private key, that anyone can verify with the matching public key.
  • Certificates wrap a public key together with an identity (a hostname) and a CA's signature, so you can trust a stranger's key without ever meeting them.
  • The handshake ties it together: present the certificate, verify it, agree on a session key, switch to symmetric encryption.

Once these click, the security work becomes concrete: read a certificate, judge whether a TLS configuration is safe, and explain why a finding like "expired certificate" or "TLS 1.0 enabled" matters to an engineer who has to fix it.

Authorization and ethics. Everything here is defensive. Only inspect or test TLS on systems you own or are explicitly authorized to assess — your own server, a localhost service, a lab VM, or a CTF target. Reading a certificate a server already presents publicly is fine; probing third-party hosts you do not own is not.

Why it matters

TLS protects the data that matters most: passwords, session cookies, payment details, health records, and API tokens. If TLS is misconfigured, all of that can be read or tampered with by anyone positioned on the network path.

The same building blocks reappear everywhere once you know them:

  • Code signing uses digital signatures so your OS can verify a software update really came from the vendor and was not modified.
  • JWTs (JSON Web Tokens) are signed the same way, so a server can trust a token it issued earlier without storing session state. (Note for later: decoding a JWT to read its claims is not the same as verifying its signature — more on that misconception below.)
  • Certificate Transparency logs every certificate a CA issues, publicly and permanently, so mis-issuance gets caught.

For defenders and testers, TLS and certificate misconfigurations are among the most routine findings in any authorized assessment: expired or self-signed certificates, hostnames that do not match, downgraded protocol versions, weak ciphers, and missing HTTP Strict Transport Security (HSTS). These are not exotic — they appear constantly, they are easy to detect, and they directly weaken confidentiality and authentication. Understanding why each is a problem, and being able to prove the fix works, is what separates copy-pasting a scanner result from writing a finding an engineer can act on. And remember: a clean scanner run does not prove a system is secure — it only proves the scanner did not flag anything it knows how to test.

Core concepts

1. Digital signatures

Definition. A digital signature is a value computed from (a) a hash of the data and (b) a private key, such that anyone holding the matching public key can confirm the data is authentic and unmodified.

Plain language. Hashing turns any message into a short, fixed-size fingerprint. Signing scrambles that fingerprint with a key only the signer has. Verifying re-hashes the message and checks that the signature matches the fingerprint using the public key. Because only the private-key holder could have produced a signature that verifies, a valid signature proves who signed (authenticity) and that the data did not change (integrity).

How it works internally. Sign: signature = sign(privkey, hash(message)). Verify: recompute hash(message) and check verify(pubkey, signature, hash) == true. Note the direction: the private key signs, the public key verifies. This is the opposite of encryption-for-secrecy (where the public key encrypts and the private key decrypts), which is a frequent point of confusion.

When to use / not use. Use signatures whenever you need to prove origin and integrity but do not need secrecy — software updates, certificates, tokens, signed logs. Do not confuse a signature with encryption: a signed message is still readable by everyone. And a signature only proves the data matches a key; it says nothing about whether you should trust that key — that is what certificates are for.

Pitfall. A valid signature on the wrong key is worthless. If an attacker can get you to accept their public key as legitimate, every signature they make will verify. Trust in the key is the hard part.

SIGN (private key)                    VERIFY (public key)
-----------------                     -------------------
message --hash--> H                   message --hash--> H'
  H --sign(priv)--> signature         verify(pub, signature) --> H
                                      H == H'  ?  -> trusted / rejected

Knowledge check. Which key signs and which key verifies? Why does that direction give you authenticity rather than secrecy?

2. X.509 certificates

Definition. A certificate is a signed document (format: X.509) that binds a public key to an identity (one or more hostnames) and is signed by a Certificate Authority (CA).

Plain language. A bare public key is anonymous — you have no idea whose it is. A certificate is a public key plus a vouching statement: "This key belongs to example.com, and I, the CA, attest to that." Because the CA signs the certificate, anyone who trusts the CA can trust the binding.

Structure. Key fields you will actually read:

Field Meaning
Subject Who the certificate is for (the hostname / organization)
Subject Alternative Names (SANs) The list of hostnames the cert is valid for (browsers use this, not the legacy CN)
Issuer Who signed it (the CA)
Validity (Not Before / Not After) The date window the cert is valid in
Public Key The server's public key
Signature The CA's signature over all of the above

When to use / not use. Public-facing servers should use CA-issued certificates so any client trusts them automatically. Self-signed certificates are fine for internal/lab use where you control the clients, but a public site with a self-signed cert will (correctly) trigger a browser warning.

Pitfall. Relying on the legacy Common Name (CN) field for the hostname. Modern clients match the hostname against the SAN list. A cert with the right CN but missing SAN will be rejected by current browsers.

Knowledge check. A certificate's Subject says example.com but its SAN list contains only www.example.com. You visit https://example.com. Will it validate? Which of the four checks fails, and what asset does that check protect?

3. The chain of trust (PKI)

Definition. A chain of trust is the sequence of certificates linking the server's leaf certificate to a root CA your system already trusts, where each certificate is signed by the one above it.

Plain language. CAs do not sign every website with their precious root key. The root signs intermediate CAs, and intermediates sign the leaf certificates websites use. Your operating system or browser ships with a built-in trust store of root certificates. Validation means: follow the signatures up the chain until you reach a root you already trust.

Trust store (preinstalled roots)
        |
   [ Root CA ]                     <- self-signed, trusted by your OS
        | signs
   [ Intermediate CA ]             <- signed by Root
        | signs
   [ Leaf: CN/SAN = example.com ]  <- the server presents this (+ intermediate)
        |
   public key for example.com

The four validation checks. Before a client trusts the server, it confirms all of:

  1. Signature chain — every certificate is correctly signed by the next one up, ending at a trusted root.
  2. Validity dates — the current time is within every certificate's Not Before / Not After window.
  3. Hostname match — the host you connected to appears in the leaf's SAN list.
  4. Not revoked — the certificate has not been revoked (via CRL or OCSP) before its expiry.

When to use / not use. Servers must send the leaf and any intermediate certificates so the client can build the chain. A common production outage is forgetting to bundle the intermediate: it works in your browser (which cached the intermediate) but fails for fresh clients.

Pitfall. Treating a trusted chain as proof the site is safe. It only proves you are talking to the holder of that hostname's private key over an encrypted channel. A phishing site can have a perfectly valid certificate for its own look-alike domain.

Knowledge check (find-the-bug). A server admin says "the cert works fine in my browser but mobile apps and curl say untrusted." The chain validates locally. What configuration mistake is most likely, and which log or command would confirm it?

4. The TLS handshake

Definition. The handshake is the opening exchange of a TLS connection that authenticates the server, agrees on cryptographic parameters, and derives a shared symmetric session key.

Plain language (TLS 1.3, simplified). The client offers the versions and ciphers it supports plus its key-exchange material. The server replies with its choice, its certificate, a signature proving it holds the matching private key, and its own key-exchange material. Both sides combine the exchanged material (an ephemeral Diffie-Hellman exchange) into the same secret, derive a symmetric session key, and from then on encrypt everything symmetrically.

Client                                              Server
  | --- ClientHello (versions, ciphers, key share) ---> |
  | <-- ServerHello (chosen params, key share) -------- |
  | <-- Certificate (+ chain) ------------------------- |
  | <-- CertificateVerify (signature) ----------------- |
  | <-- Finished -------------------------------------- |
  | --- Finished (now both derive session key) -------> |
  | ===== application data, symmetric encryption ====== |

How it works internally. Asymmetric crypto does the authentication (validate the certificate) and the key agreement (Diffie-Hellman). Once both sides share a secret, symmetric crypto (e.g. AES-GCM or ChaCha20-Poly1305) encrypts the bulk traffic because it is far faster. This is exactly the symmetric/asymmetric trade-off from the prerequisite, resolved in one protocol.

Forward secrecy. Because TLS 1.3 uses ephemeral Diffie-Hellman keys discarded after the session, recording the traffic today and stealing the server's private key tomorrow does not let an attacker decrypt past sessions. That property is forward secrecy, and it is a major reason older RSA key-exchange modes were removed.

When to use / not use. Always negotiate the highest mutually supported version (prefer TLS 1.3, accept TLS 1.2). Disable SSLv3, TLS 1.0, and TLS 1.1 — they have known weaknesses and are deprecated.

Pitfall. Assuming "TLS is on" equals "TLS is safe." A server can speak TLS while still allowing a downgrade to a weak version or cipher; an active attacker may push the connection toward the weakest option both sides accept.

Knowledge check (explain in your own words). Why does TLS bother with asymmetric crypto at all if symmetric encryption is faster — why not use symmetric for everything? And why must this experimentation only happen against a host you are authorized to test?

Syntax notes

TLS itself is a wire protocol, not source syntax, so the practical "syntax" you work with is the openssl command line for inspecting certificates and connections. The two commands you will use most:

# Show the certificate a server presents, then print the parsed certificate.
# -servername sends SNI so virtual hosts return the right cert.
openssl s_client -connect localhost:8443 -servername localhost </dev/null \
  | openssl x509 -noout -text

# Read a certificate file directly and show only the fields that matter most.
openssl x509 -in server.crt -noout -subject -issuer -dates -ext subjectAltName

Annotated breakdown of the key flags:

s_client          act as a TLS client and connect
  -connect H:P    target host and port
  -servername H   send SNI (Server Name Indication) so the server picks the right cert
  </dev/null      close stdin immediately so the command exits after the handshake
x509              X.509 certificate tool
  -noout          do not re-print the raw base64 certificate
  -text           print all fields in human-readable form
  -subject/-issuer/-dates   print just those fields
  -ext subjectAltName       print just the SAN extension (the hostnames)

One more you will reach for when checking the chain and the verify result:

openssl s_client -connect localhost:8443 -servername localhost </dev/null 2>/dev/null \
  | grep -E "Verify return code|Protocol|Cipher"
# 'Verify return code: 0 (ok)' means the chain validated against your trust store.

Lesson

TLS (Transport Layer Security) secures most network traffic. It combines several cryptographic primitives into one trusted channel.

Digital signatures (the trust primitive)

Signing means encrypting a hash of the data with a private key. Anyone can then verify it with the matching public key.

A valid signature proves two things:

  • Authenticity — it came from the holder of the private key.
  • Integrity — the data was not changed.

This is how software updates, JWTs, and certificates are trusted.

Certificates and the PKI chain

How do you trust a server's public key? With a certificate.

A certificate binds a public key to an identity (a hostname). It is signed by a Certificate Authority (CA) — a trusted third party.

Your system trusts a set of root CAs. A server's certificate chains up to one of them.

The browser checks that:

  • the signature is valid,
  • the certificate is not expired,
  • the hostname matches,
  • the certificate is not revoked.

Certificate Transparency logs every issued certificate publicly. This is useful for recon, as covered earlier.

The TLS handshake (simplified)

  1. Negotiate parameters; the server presents its certificate.
  2. The client validates the certificate chain (authenticity).
  3. A key exchange (asymmetric, such as Diffie-Hellman) derives a shared session key.
  4. Bulk traffic then uses fast symmetric encryption with that key.

So TLS gives you three guarantees from the networking track: confidentiality, integrity, and server authentication.

What testers check

  • Weak protocol versions (SSLv3, TLS 1.0).
  • Weak ciphers.
  • Expired, self-signed, or hostname-mismatched certificates.
  • Missing HSTS.

These are standard, real findings.

Code examples

Below is a complete, reproducible local lab that generates a self-signed certificate, starts a local HTTPS server, and inspects the certificate with openssl. Everything runs on localhost only — no external hosts are touched. Treat this as the INSECURE-to-SECURE-to-VERIFY shape: the self-signed cert plus any weak config is the insecure baseline; the hardening block is the fix; the checks at the end are the verification.

Authorization checklist before you run any lab:

  • The target is your own machine / VM / container (here: localhost), or a system you have written permission to test.
  • You are on an isolated network or loopback only — no third-party hosts are involved.
  • You will clean up keys, certs, and the running server afterward (the script's trap does this).
  • No real secrets are used; the throwaway key never leaves the lab.
#!/usr/bin/env bash
# tls-lab.sh - generate a local cert, serve HTTPS on localhost, inspect the cert.
# Authorized-lab use only: this binds to localhost and uses a throwaway self-signed cert.
set -euo pipefail

WORKDIR="$(mktemp -d)"            # isolated temp dir; cleaned up at the end
trap 'rm -rf "$WORKDIR"' EXIT     # ensure cleanup even on error
cd "$WORKDIR"

# 1) Create a private key + self-signed cert valid for 1 day, with a SAN for localhost.
#    -nodes: do not encrypt the key file (fine for a throwaway lab key, NEVER in prod).
openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout server.key -out server.crt \
  -days 1 -subj "/CN=localhost" \
  -addext "subjectAltName=DNS:localhost"
chmod 600 server.key              # least privilege: only the owner may read the key

# 2) Inspect the certificate we just made: identity, issuer, validity, and SANs.
echo "=== Certificate summary ==="
openssl x509 -in server.crt -noout -subject -issuer -dates -ext subjectAltName

# 3) Start a local HTTPS server in the background using the cert.
#    -no_ssl3 -no_tls1 -no_tls1_1 hardens it to TLS 1.2+ (the SECURE baseline).
openssl s_server -accept 8443 -cert server.crt -key server.key -www \
  -no_ssl3 -no_tls1 -no_tls1_1 >/dev/null 2>&1 &
SERVER_PID=$!
trap 'kill "$SERVER_PID" 2>/dev/null; rm -rf "$WORKDIR"' EXIT
sleep 1   # give the server a moment to bind

# 4) VERIFY (accepts good): a normal client negotiates a modern protocol + cipher.
echo "=== Negotiated connection (expect TLS 1.3 + AEAD) ==="
openssl s_client -connect localhost:8443 -servername localhost </dev/null 2>/dev/null \
  | grep -E "Protocol|Cipher"

# 5) VERIFY (rejects bad): forcing a deprecated version must fail the handshake.
echo "=== Forced TLS 1.1 (expect handshake failure) ==="
if openssl s_client -connect localhost:8443 -tls1_1 </dev/null >/dev/null 2>&1; then
  echo "UNEXPECTED: weak TLS 1.1 was accepted"
else
  echo "OK: TLS 1.1 rejected as intended"
fi

What it does. It builds a fresh RSA key and a self-signed certificate whose SAN is localhost, prints the certificate's identity fields, starts an openssl HTTPS server on port 8443 restricted to TLS 1.2+, connects as a client to confirm a modern protocol/cipher is negotiated (good input accepted), and finally forces TLS 1.1 to confirm the server refuses it (bad input rejected).

Expected output (values vary by OpenSSL version).

=== Certificate summary ===
subject=CN = localhost
issuer=CN = localhost
notBefore=...
notAfter=...
X509v3 Subject Alternative Name:
    DNS:localhost
=== Negotiated connection (expect TLS 1.3 + AEAD) ===
    Protocol  : TLSv1.3
    Cipher    : TLS_AES_256_GCM_SHA384
=== Forced TLS 1.1 (expect handshake failure) ===
OK: TLS 1.1 rejected as intended

subject and issuer are identical — the signature of a self-signed certificate: nobody else vouches for it, so a normal browser would warn. The negotiated cipher is a modern AEAD suite, which is what you want to see, and the forced TLS 1.1 handshake fails, proving the hardening works.

Lab cleanup / reset. The two trap ... EXIT lines kill the background server and delete the temp directory (key + cert included) automatically when the script ends, even on error. If you ran the openssl s_server command by hand instead, stop it with kill %1 (or find it via pgrep -f 's_server') and delete the working directory. Verify nothing is left listening with lsof -i :8443 (should return nothing).

Key edge cases. If port 8443 is already in use the server fails to bind (pick another port). If you connect with a hostname not in the SAN list (say 127.0.0.1 instead of localhost), strict clients reject the certificate even though the connection is otherwise valid — that is the hostname-match check doing its job.

Line by line

Walking through tls-lab.sh in execution order:

Step Line What happens State after
1 set -euo pipefail Abort on any error, unset variable, or failed pipe Script is fail-fast
2 WORKDIR=$(mktemp -d) + trap ... EXIT Make an isolated temp directory and schedule its deletion All artifacts live in one disposable dir
3 openssl req -x509 -newkey rsa:2048 -nodes ... Generate a 2048-bit RSA key (server.key) and a self-signed X.509 cert (server.crt) with CN=localhost and a localhost SAN A usable key/cert pair exists on disk
4 chmod 600 server.key Restrict the private key to owner-read/write only Key is not world-readable (least privilege)
5 openssl x509 -in server.crt -noout ... Parse the cert and print subject, issuer, dates, and SAN You can see subject == issuer (self-signed)
6 openssl s_server -accept 8443 ... -no_tls1 -no_tls1_1 & Start a TLS 1.2+ server in the background on port 8443 A live HTTPS endpoint on localhost; SERVER_PID holds its PID
7 sleep 1 Wait for the socket to bind Server is ready to accept
8 `s_client ... grep -E "Protocol Cipher"`
9 s_client -tls1_1 ... inside if Force a deprecated version; expect failure Non-zero exit -> prints "OK: TLS 1.1 rejected"
10 trap fires on EXIT Kill the server and remove the temp dir No leftover process or files

What is happening cryptographically during step 8: the client sends a ClientHello with its supported versions and a Diffie-Hellman key share; the server replies with its choice, sends server.crt, and signs the handshake transcript with server.key to prove it owns the certificate's private key; both sides combine their DH shares into the same secret and derive the symmetric session key. The Protocol/Cipher lines you see are the result of that negotiation. The client does not reject the self-signed cert here because s_client does not enforce the trust store by default — a real browser would, which is exactly why self-signed certs warn.

Step 9 is the mitigation-verification half: by forcing -tls1_1, you ask the server to speak a protocol you deliberately disabled. A failed handshake is the success signal — it proves an attacker cannot downgrade the connection to a weak version even if they wanted to.

Common mistakes

Mistake 1 - Confusing signing with encryption.

Wrong mental model: "Sign with the public key, verify with the private key."

Why it is wrong: that is backwards. The private key signs (only the owner can do it); the public key verifies (anyone can). Getting this backwards makes every later concept — certificates, JWTs, the handshake — incoherent.

Corrected: signature = sign(PRIVATE, hash) and verify(PUBLIC, signature). To remember it: the secret thing (private key) does the secret-only action (creating a signature). Recognize/prevent: if your explanation lets anyone create a valid signature, you have swapped the keys.

Mistake 2 - "There's a lock icon, so the site is safe."

Wrong: assuming a valid certificate means the site is trustworthy.

Why it is wrong: a valid certificate only proves you have an encrypted channel to the holder of that hostname's private key. A phishing site paypa1-secure.com can get a perfectly valid certificate for its own domain. TLS authenticates the server, not the intent.

Corrected: treat the certificate as proof of who you are talking to and that nobody can eavesdrop, then separately verify the hostname is the one you actually meant. Recognize/prevent: always read the actual domain, not the padlock.

Mistake 3 - "I decoded the JWT / the scanner passed, so it's verified/secure."

Wrong: pasting a token into a decoder to read its claims and calling it "verified," or treating a clean automated scan as proof of security.

Why it is wrong: decoding a JWT is just base64 — it does not check the signature at all; a forged token decodes identically to a real one. Likewise, a scanner only reports what it knows how to test; passing it does not prove the system is secure, and nothing is ever "completely secure."

Corrected: verify the JWT signature with the issuer's public key (and check exp, iss, aud); treat scanner output as a starting point, then manually confirm. Recognize/prevent: if no public key or secret was involved in your check, you did not verify anything.

Mistake 4 - Forgetting the intermediate certificate.

Wrong (server config): install only the leaf certificate.

Why it is wrong: clients need the full chain to reach a trusted root. Your browser may have cached the intermediate from another site and validate anyway, hiding the bug — but fresh clients (mobile apps, curl, other machines) fail with "unable to get local issuer certificate."

Corrected: configure the server to send the leaf plus the intermediate bundle. Recognize/prevent: test from a clean client — openssl s_client -connect host:443 and confirm Verify return code: 0 (ok).

Mistake 5 - Leaving weak protocols/ciphers enabled to "support old clients."

Wrong: enabling TLS 1.0/1.1 or export/RC4 ciphers for compatibility.

Why it is wrong: an active attacker can negotiate the connection down to the weakest option both sides accept, defeating the strong options entirely.

Corrected: restrict to TLS 1.2+ (prefer 1.3) and modern AEAD ciphers. Recognize/prevent: verify with openssl s_client -tls1_1 ... returning a handshake failure.

How to recognize these overall. Browser warnings ("Not secure", "NET::ERR_CERT_*"), a non-zero Verify return code from s_client, and scanner findings like "TLS 1.0 supported" or "missing HSTS header" all point at one of the above.

Debugging tips

Common openssl s_client results and what they mean:

  • Verify return code: 0 (ok) — chain validated successfully.
  • Verify return code: 21 (unable to verify the first certificate) or 20 (unable to get local issuer certificate) — the server is not sending the intermediate certificate, or the root is not in your trust store. Bundle the intermediate on the server.
  • Verify return code: 10 (certificate has expired) — the current time is outside the validity window. Check notAfter and the server clock.
  • handshake failure / no protocols available — you forced a version (e.g. -tls1_1) the server rejects. That is often the desired result when confirming weak protocols are disabled.

Concrete debugging steps when HTTPS "doesn't work":

  1. Reproduce with openssl s_client -connect host:port -servername host </dev/null and read the Verify return code line.
  2. Dump the presented cert: pipe into openssl x509 -noout -subject -issuer -dates -ext subjectAltName. Check the hostname is in the SAN and the dates are current.
  3. Confirm the chain: look at the Certificate chain block at the top of s_client output — you should see leaf -> intermediate -> (root).
  4. Check the negotiated version/cipher: grep -E "Protocol|Cipher". Unexpectedly old? The server allows downgrades.
  5. Verify SNI: if the server hosts multiple sites, omit -servername and watch it return the wrong (default) certificate — proof that SNI matters.

Questions to ask when it fails:

  • Is the hostname I connected to actually in the SAN list?
  • Is the system clock correct on both ends? (Expired/not-yet-valid errors are often clock skew.)
  • Is the server sending the full chain, or only the leaf?
  • Did I force a protocol/cipher the server intentionally disabled?
  • Is the root CA in this client's trust store, or only my browser's?
  • Am I actually verifying, or only decoding? (Reading cert/token fields is not the same as validating signatures.)

Memory safety

Security and safety

This topic is itself a security control, so the "safety notes" are about deploying and assessing TLS defensively, and about what to log when you do.

Threat model (what TLS defends, and where it does not).

         entry point: the network path (Wi-Fi, ISP, routers)
              |
  Client  <==[ TLS channel ]==>  Server
   ^  trust boundary 1:                ^ trust boundary 2:
   |  client trust store               | server private key + cert config
   |
  ASSETS protected: confidentiality, integrity, and server identity of
  data in transit (passwords, cookies, tokens, payment data)

  NOT protected by TLS: data at rest, application bugs (XSS/SQLi),
  a malicious/look-alike server with its own valid cert, a stolen
  private key (though forward secrecy limits damage to future sessions).

Defensive practices (beginner rules first):

  • Prefer TLS 1.3, accept TLS 1.2, and disable SSLv3 / TLS 1.0 / TLS 1.1.
  • Use modern AEAD ciphers (AES-GCM, ChaCha20-Poly1305); disable RC4, 3DES, and export ciphers.
  • Serve the full chain (leaf + intermediates) and keep certificates renewed before expiry (automate with ACME / Let's Encrypt).
  • Send HSTS (Strict-Transport-Security) so browsers refuse to downgrade to HTTP.
  • Protect the private key: restrictive file permissions (chmod 600), never commit it to source control, rotate if exposed.
  • Rely on forward secrecy (ephemeral DH) so a future key compromise cannot decrypt recorded past traffic.

Insecure vs secure config.

Insecure config:

WARNING: intentionally vulnerable — use only in a local, isolated, authorized lab. Do not deploy.
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;         # TLS 1.0/1.1 are deprecated and downgradable
ssl_ciphers RC4:HIGH:!aNULL;                  # RC4 is broken
# (no Strict-Transport-Security header)

Why unsafe: an active network attacker can force the connection down to TLS 1.0 + RC4, weakening confidentiality; without HSTS, a victim's first HTTP request can be stripped to plaintext before any redirect.

Secure fix:

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

Mitigation verification (prove the fix rejects bad and accepts good):

openssl s_client -connect localhost:8443 -tls1_1 </dev/null   # expect: handshake failure (bad rejected)
openssl s_client -connect localhost:8443 -tls1_3 </dev/null   # expect: Protocol TLSv1.3 (good accepted)
curl -sI https://localhost:8443 | grep -i strict-transport     # expect: HSTS header present

Detection and logging. For each TLS connection or verification event, log: a timestamp, the source (client IP / identifier), the resource or hostname (SNI) requested, the result (handshake success/failure), the security decision (negotiated version + cipher, and the certificate verify code), and a correlation id to tie related events together. Alert on unusual downgrades (a client that suddenly negotiates TLS 1.0), spikes in expired-cert or verify-failure errors, and unexpected certificate changes for monitored hosts. Watch Certificate Transparency logs for certificates issued for your domains that you did not request.

Never log private keys, session keys, full request/response bodies (which may hold passwords or tokens), session cookies, or full PANs. Logging those turns your log store into the very secret an attacker wants.

False positives arise naturally: a legitimate client behind an old corporate proxy may negotiate a lower version; a monitoring probe may trip "repeated connection" alerts; a planned certificate rotation looks like an "unexpected certificate change." Tune thresholds and allowlist known scanners so real abuse is not buried in noise.

Real-world uses

Concrete authorized real-world uses:

  • HTTPS — every banking site, login page, and API uses TLS to protect credentials and session cookies in transit. This is the most common deployment by far.
  • Mutual TLS (mTLS) — internal microservices and Kubernetes meshes use certificates on both ends so services authenticate each other, not just the client trusting the server.
  • Email transport — SMTP, IMAP, and POP3 use TLS (STARTTLS or implicit) to encrypt mail in transit.
  • VPNs and device provisioning — OpenVPN and many IoT fleets use client certificates to authenticate devices to a gateway.
  • Code signing and update channels — the same signature primitive verifies OS and app updates came from the real vendor.

A typical authorized engagement: you are asked to assess your employer's public web endpoints. You inspect each certificate (SAN, dates, issuer, chain), check the negotiated protocols and ciphers against the company baseline, confirm HSTS is present, and write findings for anything weak — always with a fix and a retest step.

Professional best-practice habits.

Beginner rules:

  • Always use HTTPS, never plain HTTP, for anything carrying credentials or personal data.
  • Let an ACME client (Let's Encrypt / certbot) issue and auto-renew certificates so they never silently expire.
  • Read the certificate before trusting it: check SAN, dates, and issuer.
  • Validate input to your own TLS tooling and fail closed on verification errors.

Advanced habits:

  • Enforce HSTS with includeSubDomains and consider preloading.
  • Monitor Certificate Transparency logs for mis-issuance against your domains.
  • Maintain forward secrecy by using only ephemeral key exchange, and apply least privilege to key files and issuance permissions.
  • Plan key rotation and have a tested incident process for a compromised private key (revoke, reissue, redeploy).
  • Review protocol/cipher baselines periodically as standards evolve — and never describe a config as "completely secure," only as meeting the current baseline.

Reporting a TLS finding. When you report one, use a finding template: title (e.g. "TLS 1.0 enabled on login endpoint"), severity (justified by exploitability, access required, and impact — not everything is critical; a downgrade needs an active on-path attacker, so it is usually medium, while a private-key exposure is critical), affected component, preconditions, safe reproduction (the openssl s_client command against the authorized host), evidence (the negotiated version/cipher output), impact, likelihood, remediation (the secure config), and retest (the verification commands that must now fail for weak options and succeed for strong ones).

Practice tasks

Beginner 1 — Read a self-signed certificate. Objective: generate a self-signed certificate for localhost and print its key fields. Requirements: use openssl req -x509 -newkey rsa:2048 -nodes ... to create server.crt, then print subject, issuer, validity dates, and SAN. Expected observation: subject and issuer are identical (self-signed). Constraints: localhost/lab only; delete the key and cert when done. Hints: add the SAN with -addext "subjectAltName=DNS:localhost"; inspect with openssl x509 -noout -text. Concepts: certificate fields, SAN, self-signed certs.

Beginner 2 — Label the four validation checks. Objective: for your certificate, list each of the four checks a client performs and state, for that cert, whether it passes. Requirements: use your cert from task 1; reason about signature chain, validity dates, hostname match (for localhost), and revocation. Expected output: a short table (check / pass-fail / why). Hints: a self-signed cert chains only to itself, so it fails the "trusted root" check on a normal client. Concepts: chain of trust, the four checks.

Intermediate 1 — Inspect a live, authorized endpoint. Objective: run the lab server from the lesson and capture the negotiated protocol and cipher. Requirements: start openssl s_server on localhost, connect with s_client -servername localhost, and extract the Protocol and Cipher lines. Expected output: a TLS 1.3 protocol line and an AEAD cipher. Constraints: localhost only; stop the server and clean up afterward. Hints: pipe s_client through grep -E "Protocol|Cipher". Concepts: handshake, version/cipher negotiation.

Intermediate 2 — Trigger and explain a hostname mismatch. Objective: connect to your localhost cert using 127.0.0.1 as the name and observe/explain the verification difference. Requirements: compare -servername localhost vs connecting by IP; describe why strict clients would reject the IP case and which of the four checks fails. Expected output: an explanation tied to the SAN list. Constraints: localhost/lab only. Hints: the SAN only contains DNS:localhost, not the IP. Concepts: hostname-match check, SAN.

Challenge — Build a weak-then-hardened config, verify, and report. Objective: write two server configs — one intentionally weak (label it with the exact required WARNING line), one hardened — a verification script that proves the hardened one rejects weak protocols and the weak one accepts them, and a one-paragraph finding using the reporting template. Requirements: hardened config restricts to TLS 1.2/1.3 + AEAD ciphers and sets HSTS; the script uses openssl s_client -tls1_1 (expect failure on hardened) and curl -sI ... | grep -i strict-transport (expect header). The finding must include a justified severity (not automatically critical) and a retest step. Constraints: localhost/lab only; never deploy the weak config; clean up both configs and any running server. Defensive conclusion: end by remediating (apply the hardened config) and verifying (the forced-weak handshake now fails). Hints: a forced-version handshake that fails is the success signal for the hardened case. Concepts: protocol/cipher hardening, downgrade resistance, HSTS, mitigation verification, finding template.

Summary

  • TLS delivers three guarantees over an untrusted network: confidentiality, integrity, and server authentication. The protected asset is data in transit; the threat is anyone on the network path.
  • Digital signatures are the trust primitive: the private key signs a hash, the public key verifies it — proving authenticity and integrity, not secrecy. Reversing the keys breaks every later concept.
  • Certificates bind a public key to a hostname and are signed by a CA; clients validate the chain of trust up to a preinstalled root.
  • The four validation checks every client performs: valid signature chain, current validity dates, hostname in the SAN, and not revoked.
  • The handshake uses asymmetric crypto to authenticate and agree a key, then switches to fast symmetric encryption — resolving the trade-off from Symmetric and asymmetric encryption. Ephemeral key exchange gives forward secrecy.
  • Most useful command: openssl s_client -connect host:port -servername host </dev/null | openssl x509 -noout -text; read the Verify return code to judge the chain.
  • Common mistakes: reversing sign/verify keys; equating "valid cert" or "lock icon" with "safe site"; confusing decoding a JWT with verifying it, or a clean scanner run with proof of security; omitting the intermediate certificate; leaving weak protocols/ciphers enabled.
  • Standard, reportable findings: expired/self-signed/mismatched certificates, SSLv3/TLS 1.0/1.1, weak ciphers, missing HSTS — always pair the finding with a secure fix, a mitigation-verification step, and detection/logging (log version, cipher, verify result; never log keys or decrypted secrets).
  • Nothing is ever "completely secure"; report severity by exploitability, access, and impact — not everything is critical.
  • Defensive, authorized-lab-only: inspect and test TLS on systems you own or are permitted to assess; run labs on localhost, and clean up keys, certs, and servers afterward.

Practice with these exercises