Module 01 — Why TLS Exists: Crypto Primitives & Your Lab

Updated 20 August 2026

Module 01 · Why TLS Exists — Crypto Primitives & Your Lab

Everything in this track — certificates, chains, handshakes, revocation, ACME — is built out of exactly four ideas. This module teaches those four ideas starting from nothing, shows you the gap that certificates were invented to fill, and builds the lab you will use for the next thirteen modules.

🧠 concept → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (answers hidden)

Prerequisite: none — this is the first module. You need a Linux machine (macOS or WSL2 is fine), the ability to run sudo, and nothing else.


Part A · The problem TLS solves

A1 · What is actually on the wire

The analogy — a postcard through the post.

Write a message on a postcard and drop it in the box. Every person who handles it on the way can read it. Any of them could rub out a word and write a different one. And worst of all, someone could bolt a fake postbox to the wall outside your house, and you would post your letters straight into it without ever noticing.

Those are the three problems below, in order. HTTPS is the difference between a postcard and a sealed, tamper-evident envelope handed to a courier whose ID you actually checked.

When you open http://example.com, your machine opens a TCP connection to port 80 and writes bytes into it. Those bytes are ordinary readable text. Your router sees them. Your ISP sees them. Every transit provider between you and the server sees them. The switch in the destination rack sees them.

This is not a bug or an oversight. TCP was designed to move bytes reliably, and HTTP was designed to be human-readable. Neither of them was ever designed to hide anything or to check anything. That is the entire vacuum TLS was created to fill.

There are three separate problems here, and it matters enormously that they are separate, because TLS uses a different mechanism for each one.

ProblemWhat the attacker doesWhat fixes it
EavesdroppingSilently reads your bytes as they pass. Leaves no trace at all — you cannot detect itEncryption (Part B1–B3)
TamperingChanges bytes in flight — swaps a bank account number, injects a script into a pageHashing / integrity checks (Part B4)
ImpersonationAnswers instead of the real server, and you never knowCertificates (Part C — and the other thirteen modules)
This is the single most counter-intuitive idea in the whole subject, so read it twice.

Encryption on its own does not stop impersonation. If an attacker answers instead of your bank, and you encrypt your password to the attacker's key, you have had a beautifully confidential conversation — with a thief.

Encryption guarantees nobody else can read this. It says nothing whatsoever about who is on the other end. Certificates exist to answer that second question, and that is why this entire track is about them rather than about ciphers.

Diagram source
flowchart LR
    U["💻 Your laptop"] --> R["Home router"]
    R --> I["ISP"]
    I --> T["Transit<br>networks"]
    T --> D["Datacentre<br>switch"]
    D --> S["🖥️ example.com"]
    M["🕵️ Anyone on this path<br>can read AND rewrite<br>every plain HTTP byte"] -.-> I
    M -.-> T
    style M fill:#ffcccc,stroke:#cc0000,stroke-width:2px
    style U fill:#cce5ff,stroke:#0066cc
    style S fill:#cce5ff,stroke:#0066cc
🧪 Exercise A1.1 — Watch a password cross the wire in plain text

Open three terminals. This runs entirely on your own machine, so it is completely safe.

bash
# Terminal 1 - a throwaway web server on port 8080
mkdir -p ~/tls-lab && cd ~/tls-lab
python3 -m http.server 8080

# Terminal 2 - watch the loopback interface
# (on macOS use -i lo0 instead of -i lo)
sudo tcpdump -i lo -A -s 0 'tcp port 8080'

# Terminal 3 - send a request carrying credentials
curl -s -u john:hunter2 http://127.0.0.1:8080/quarterly-salaries.csv > /dev/null
Expected result — click to reveal
plain text
listening on lo, link-type EN10MB (Ethernet), snapshot length 262144 bytes
09:14:22.117841 IP localhost.51234 > localhost.http-alt: Flags [P.], seq 1:158, length 157
E...F.@[email protected].."..........
GET /quarterly-salaries.csv HTTP/1.1
Host: 127.0.0.1:8080
Authorization: Basic am9objpodW50ZXIy
User-Agent: curl/8.5.0
Accept: */*


09:14:22.118093 IP localhost.http-alt > localhost.51234: Flags [P.], seq 1:155, length 154
HTTP/1.0 404 File not found
Server: SimpleHTTP/0.6 Python/3.11.2

What to read out of this. Four things, in order of how much they should worry you:

  1. The request line is readable. GET /quarterly-salaries.csv — an observer learns exactly which resource you asked for, without decoding anything.
  2. The hostname is readable. Host: 127.0.0.1:8080.
  3. The credentials are right there. Authorization: Basic am9objpodW50ZXIy. That string looks protected. It is not. Run this:
bash
echo 'am9objpodW50ZXIy' | base64 -d
# john:hunter2
  1. Base64 is not encryption. It is an encoding — a reversible alphabet swap with no key and no secret. Anyone can undo it in one command. This distinction gets asked in interviews constantly, and getting it wrong is disqualifying.

Now imagine this at 500 hosts. You ran this on loopback, where the only observer is you. Move it to a real network and every hop owns those credentials. And this is not only a coffee-shop-Wi-Fi problem: in most data centres the east–west traffic between internal services is the larger unencrypted surface, because someone decided "it never leaves our network" — right up until one container is compromised and starts sniffing.

🧪 Exercise A1.2 — The same look at an HTTPS connection
bash
# Terminal 1 - capture (use your real interface: eth0, ens3, en0, wlan0...)
sudo tcpdump -i any -A -s 0 'tcp port 443 and host example.com'

# Terminal 2
curl -s https://example.com/ > /dev/null
Expected result — click to reveal
plain text
10:02:41.552103 IP 192.168.1.20.49882 > 93.184.215.14.https: Flags [P.], length 517
E..%..@.@.............#...........,.....
........example.com.................
..+.....3.&.$... ."...M...Y..b...s.j!.z.......h.

10:02:41.598447 IP 93.184.215.14.https > 192.168.1.20.49882: Flags [P.], length 1420
....~..z..........."..T...V...........Z..u..#.O..'..b.......
.[......q..\.....K....?..s2...^...*.........p.......m..k....

10:02:41.601220 IP 192.168.1.20.49882 > 93.184.215.14.https: Flags [P.], length 80
.....V.j.p..1..7..~F......{Z........w..q.....F.6..o.

What to read out of this.

  • The HTTP request is gone. There is no GET /, no Host: header, no Authorization: line. Everything above the transport layer is now ciphertext. That is the confidentiality property, demonstrated.
  • But look at the very first packet. The string example.com is sitting there in the clear, before any encryption starts. TLS cannot encrypt the hostname in that first message, because the server needs it in order to know which certificate to present — a single IP address may host thousands of sites. That field has a name and a whole set of privacy consequences; both are covered properly in Module 06. For now, retain the shape of the fact:

🔑 TLS hides what you send. It does not hide who you are talking to. Packet sizes, timing, destination IP and that early hostname all remain visible. An interviewer who asks "what does TLS not protect?" is looking for exactly this answer.

🎯 Interview questions — What TLS protects

Q. What is an SSL/TLS certificate and why is it used?

A certificate is a file that binds a public key to an identity (a hostname, usually), signed by a third party that both sides already trust. It is presented by a server during connection setup so the client can confirm it is talking to the right machine before it sends anything sensitive.

The framing that separates a strong answer from an average one: say clearly that the certificate solves the identity problem, not the encryption problem. Encryption would work perfectly well with no certificate at all — it would just be encryption to an unknown party. Most candidates blur "HTTPS" and "encryption" into one thing; naming the three distinct guarantees (confidentiality, integrity, authentication) and which mechanism delivers each shows you understand the design rather than the marketing.

Q. What does TLS not protect?

A long list, and knowing it is a genuine differentiator:

  • Metadata. Destination IP, port, packet sizes and timing are all visible. Traffic analysis against a TLS stream is a real technique.
  • The requested hostname in the first message of the handshake, unless a newer privacy extension is in play (Module 06).
  • Anything at the endpoints. TLS protects data in transit only. Once the bytes arrive they are plaintext in memory, in logs, and on disk. A TLS-terminating load balancer means the traffic behind it may be plaintext.
  • A compromised or malicious peer. If the other end is the attacker and it holds a valid certificate, TLS works exactly as designed and protects the attacker's session perfectly.
  • DNS, unless DoH/DoT is separately in use — so the lookup that preceded the connection may have been in the clear.

Strong close: "TLS is a transport-layer guarantee. It is not an application security control, and it is not a substitute for encryption at rest."

Q. Is Base64 a form of encryption?

No. Base64 is an encoding — a deterministic mapping of arbitrary bytes onto 64 printable ASCII characters so they survive channels that expect text. There is no key and no secret, so it provides zero confidentiality; anyone can reverse it with base64 -d.

It shows up constantly in this subject — PEM files, JWTs, HTTP Basic auth are all Base64 — which is exactly why people confuse it for protection. The reason to care operationally: seeing Base64 in a config file or a log tells you the value is exposed, not protected, and it should be treated as a plaintext secret.

Q. Our services only talk to each other inside the VPC. Why encrypt that traffic?

Because the network perimeter stopped being a security boundary the moment a single workload inside it could be compromised. The concrete arguments to give:

  • Lateral movement. One compromised container with packet-capture capability harvests every internal credential in flight.
  • Your cloud provider's fabric is shared. "Internal" is a logical construct, not a physical one.
  • Compliance. PCI-DSS, HIPAA and SOC 2 auditors increasingly ask for encryption of internal traffic, not just at the edge.
  • Blast radius. Unencrypted east–west traffic turns a single-host incident into a whole-estate credential breach.

The senior-sounding version: "We treat the network as untrusted and authenticate every hop — mutual TLS between services, which also gives us workload identity for free rather than just confidentiality." That is Module 12.


A2 · SSL, TLS, and why the naming is a mess

The analogy — "Hoover" and "Biro".

Hoover stopped being the only vacuum cleaner brand decades ago, and most people have never owned one. They still say "hoovering". Same with "Biro" for a ballpoint pen.

SSL is the Hoover of encryption protocols. The name stuck; the product was replaced. Everyone says "SSL certificate", almost nobody has used SSL since the 1990s, and the config directive in nginx is still called ssl_certificate.

SSL — Secure Sockets Layer — was Netscape's protocol from the mid-1990s. When the IETF took it over in 1999 they renamed it TLS — Transport Layer Security. Every version since has been called TLS. There has not been a new version of SSL in nearly thirty years.

VersionYearWhere it stands in 2026
SSL 2.01995Broken and formally prohibited. Removed from every current library
SSL 3.01996Broken by POODLE in 2014, formally prohibited. Removed from every current library
TLS 1.01999Deprecated by RFC 8996 — MUST NOT be used. Disabled by default in OpenSSL 3.x
TLS 1.12006Deprecated by RFC 8996 — MUST NOT be used
TLS 1.22008Still perfectly acceptable when configured correctly, and still needed for older clients and many non-browser stacks
TLS 1.32018The one you want. Faster handshake, and the weak options were removed from the protocol rather than merely discouraged

So why does everyone still say "SSL certificate", buy from an "SSL vendor", and configure ssl_certificate in nginx? Pure inertia. The word sold well, and the config directives were named before the rename settled.

The fact worth internalising now, because it removes a lot of later confusion:

The certificate is not part of the TLS protocol version. It is an X.509 certificate — a general-purpose format that long predates the web and is also used by SMTP, LDAP, VPNs, code signing and smart cards.

The practical consequence: upgrading a server from TLS 1.2 to TLS 1.3 does not require a new certificate. The same file works for both. "Do I need to reissue my certs to enable 1.3?" is a real question people ask, and the answer is no.

🧪 Exercise A2.1 — Ask a real server which versions it will speak, and fail on purpose
bash
openssl s_client -connect example.com:443 -tls1_3 </dev/null 2>&1 | grep -E 'Protocol|Cipher\s*:'
openssl s_client -connect example.com:443 -tls1_2 </dev/null 2>&1 | grep -E 'Protocol|Cipher\s*:'

# Now deliberately ask for a dead version
openssl s_client -connect example.com:443 -tls1 </dev/null 2>&1 | tail -4
Expected result — click to reveal
plain text
# -tls1_3
    Protocol  : TLSv1.3
    Cipher    : TLS_AES_256_GCM_SHA384

# -tls1_2
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES128-GCM-SHA256

# -tls1
socket error, tls1 not supported by the current library build
0084F8CD897F0000:error:0A0000BF:SSL routines:tls_setup_handshake:no protocols available:ssl/statem/statem_lib.c:104:

What to read out of this — and this is the subtle part.

The third command failed, but read where the failure came from. It says not supported by the current library build and no protocols available. That message was produced by your own OpenSSL, on your own machine. The packet never left your laptop. The server was never asked.

This distinction matters enormously when you are debugging a real incident. "TLS 1.0 fails" can mean two completely different things:

  • Your client refuses to offer it — an error from your local library, before any network activity.
  • The server refuses to accept it — you would instead see a handshake that starts and then gets rejected, with something like tlsv1 alert protocol version and a byte count showing data actually crossed the network.

The habit worth building: when a TLS error appears, first ask which end produced it. A local library error and a remote alert look similar at a glance and have entirely different fixes.

⚠️ On some distributions the -tls1 attempt succeeds at the library level and fails differently, because the distribution ships a wider build or a lower security level. If your output differs, that itself is the lesson: the same command behaves differently on different machines, which is precisely why "it works on my laptop" is worthless evidence in a TLS incident.

🧪 Exercise A2.2 — Find out what your own OpenSSL will and will not do
bash
openssl version
openssl ciphers -v 'DEFAULT' | head -8
openssl ciphers -v 'DEFAULT' | wc -l
Expected result — click to reveal
plain text
OpenSSL 3.0.13 30 Jan 2024

TLS_AES_256_GCM_SHA384  TLSv1.3 Kx=any  Au=any  Enc=AESGCM(256) Mac=AEAD
TLS_CHACHA20_POLY1305_SHA256 TLSv1.3 Kx=any Au=any Enc=CHACHA20/POLY1305(256) Mac=AEAD
TLS_AES_128_GCM_SHA256  TLSv1.3 Kx=any  Au=any  Enc=AESGCM(128) Mac=AEAD
ECDHE-ECDSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=ECDSA Enc=AESGCM(256) Mac=AEAD
ECDHE-RSA-AES256-GCM-SHA384 TLSv1.2 Kx=ECDH Au=RSA Enc=AESGCM(256) Mac=AEAD
...

34

What to read out of this.

  • Only three TLS 1.3 entries exist. TLS 1.3 ships with a deliberately tiny fixed set. TLS 1.2 has dozens. That shrinkage is the security improvement — most TLS 1.2 vulnerabilities over the years came from a weak option being negotiable at all, so TLS 1.3 removed the options instead of documenting that you should not pick them.
  • The Kx= and Au= columns are two different jobs. Kx is how the two sides agree on a key; Au is how the server proves who it is. Notice they are separate columns — that separation is the reason a certificate is a distinct object from the encryption. Both get taught properly in Module 06.
  • No SSLv3, no EXPORT, no RC4, no NULL entries appear. Modern OpenSSL will not offer them under DEFAULT even though it can still be forced to. If you ever see those in output, you are on an ancient build and that is your finding.

🎯 Interview questions — SSL vs TLS and versions

Q. What is the difference between SSL and TLS?

TLS is the successor to SSL. SSL was Netscape's protocol (2.0 in 1995, 3.0 in 1996); the IETF took it over and renamed it TLS with version 1.0 in 1999. All SSL versions are broken and prohibited. In 2026 the live versions are TLS 1.2 and TLS 1.3.

The part most candidates miss: the term "SSL certificate" is a misnomer that survived because it sold well. The certificate is an X.509 certificate and is version-agnostic — the same file serves TLS 1.2 and 1.3, and also SMTP, LDAP and VPN endpoints. Certificates and protocol versions are independently upgradable, which is a genuinely useful operational fact when someone asks whether enabling 1.3 means reissuing certificates.

Q. Which TLS versions would you enable on a public web server in 2026, and why?

TLS 1.3 and TLS 1.2 only. Everything below is deprecated by RFC 8996 and is a standing audit finding.

The answer that separates people is what comes next — how you'd decide whether you can drop 1.2:

  • Look at real client telemetry before disabling anything. Non-browser clients are the risk, not browsers: old Java runtimes, embedded devices, payment terminals, partner integrations and internal scripts pinned to an old library.
  • Enabling TLS 1.3 is safe and additive — a client that cannot do it negotiates 1.2. Disabling TLS 1.2 is the breaking change, and it needs evidence.
  • If you keep 1.2, the cipher configuration carries the weight: AEAD suites only (GCM or ChaCha20-Poly1305), ECDHE key exchange only, no CBC, no RSA key transport.

Bonus point: mention PCI-DSS required TLS 1.0 to be gone back in 2018, so "we still allow it for compatibility" is not a defensible position — it is an open finding.

Q. What actually improved in TLS 1.3 over 1.2?

Four things worth naming:

  1. Fewer round trips. The full handshake is 1-RTT instead of 2-RTT, with an optional 0-RTT resumption mode. On a high-latency mobile connection that is a visible speed-up.
  2. Weak options removed, not discouraged. Static RSA key transport, custom Diffie-Hellman groups, CBC-mode ciphers, compression, renegotiation and MD5/SHA-1 signatures are simply not part of the protocol. You cannot misconfigure your way into them.
  3. Forward secrecy is mandatory. Every TLS 1.3 key exchange is ephemeral, so recovering the server's private key later does not decrypt past captures.
  4. More of the handshake is encrypted, including the server's certificate, so a passive observer learns less.

The operational sting worth adding: because the certificate is now encrypted during the handshake, some middleboxes, DPI appliances and older monitoring tools that used to read it silently stop working. Every one of those mechanisms is covered in Module 06.

Q. Do I need to reissue my certificates to enable TLS 1.3?

No. The certificate is an X.509 object that binds a key to a name; the TLS version governs how that key is used in the handshake. They version independently.

The one genuine caveat to raise, which shows depth: TLS 1.3 dropped some signature algorithms, so a certificate whose key type is unusual or whose signature is legacy could in principle be unusable. In practice any RSA-2048+ or ECDSA P-256 certificate issued in the last decade is fine.


Part B · The four primitives everything is built from

Four ideas. Learn these properly and the rest of the track is mostly bookkeeping.

B1 symmetric encryptionB2 asymmetric encryptionB3 why TLS uses bothB4 hashingB5 digital signatures.

They are in this order for a reason: B5 is built out of B2 and B4, and a certificate is built out of B5. Do not skip ahead.

B1 · Symmetric encryption — one key, both directions

The analogy — your front door key.

The same key locks the door when you leave and unlocks it when you come home. One key, both directions. It is simple, it is quick, and you use it every day without thinking.

The catch is just as obvious. If you want a friend to be able to get in, you have to physically get a copy of that key into their hands somehow — and you cannot post it, because anyone handling the post could copy it. Hold on to that problem; it is the whole reason section B2 exists.

Symmetric encryption uses one key for both encrypting and decrypting. The same secret locks and unlocks. That is the whole idea, and it is by far the oldest one here — a Caesar cipher is symmetric encryption with a very bad key.

The modern workhorse is AES, standardised by NIST in 2001 and now implemented directly in CPU silicon on essentially every server you will touch. That hardware support is why symmetric encryption is cheap — a detail that turns out to drive the entire design of TLS, as you will see in B3.

🧪 Exercise B1.1 — Encrypt and decrypt a file with a shared secret
bash
cd ~/tls-lab
echo "Database password is: correct-horse-battery-staple" > secret.txt

# Encrypt. -pbkdf2 turns your password into a key properly; without it
# OpenSSL warns and uses a weak legacy derivation.
openssl enc -aes-256-cbc -pbkdf2 -salt -in secret.txt -out secret.enc

# Look at what came out
file secret.enc
head -c 32 secret.enc | xxd | head -2

# Decrypt with the SAME password
openssl enc -d -aes-256-cbc -pbkdf2 -in secret.enc -out recovered.txt
cat recovered.txt
Expected result — click to reveal
plain text
enter AES-256-CBC encryption password:
Verifying - enter AES-256-CBC encryption password:

secret.enc: openssl enc'd data with salted password

00000000: 5361 6c74 6564 5f5f 8f2a 1c74 b3e9 05d1  Salted__.*.t....
00000010: 9c44 f7a1 2b8e 6033 a015 7d3c e2b8 44f9  .D..+.`3..}<..D.

enter AES-256-CBC decryption password:
Database password is: correct-horse-battery-staple

What to read out of this.

  • The first eight bytes are the literal ASCII string Salted__. That is not encrypted, and it is not a mistake. It is a header telling the decrypting side "the next 8 bytes are the salt". Encrypted files routinely contain unencrypted framing — you will meet exactly this pattern again when you look inside a certificate.
  • The salt is random, so encrypting the same file twice gives different output. Try it: run the encrypt command twice and cmp the results. They differ. This is deliberate and it is a security property — identical plaintext must not produce identical ciphertext, or an observer learns things by pattern-matching.
  • -pbkdf2 is doing real work. Your password is not the key. It is stretched into a 256-bit key by running it through a deliberately slow function 10,000 times. Without -pbkdf2, OpenSSL falls back to a single MD5 pass and prints a warning — which is fast for you and therefore also fast for someone brute-forcing.
  • The key never appears anywhere in the file. Lose the password and the file is gone. There is no recovery path, which is exactly the point.
🧪 Exercise B1.2 — Get the password wrong on purpose
bash
openssl enc -d -aes-256-cbc -pbkdf2 -in secret.enc -out wrong.txt
# type ANY password that is not the right one
Expected result — click to reveal
plain text
enter AES-256-CBC decryption password:
bad decrypt
40E7F0C4A87F0000:error:1C800064:Provider routines:ossl_cipher_unpadblock:bad decrypt:providers/implementations/ciphers/ciphercommon_block.c:107:

What to read out of this — and it is more interesting than it looks.

AES did not "reject" your password. AES has no idea what a password is. It happily decrypted the file using the wrong key and produced 47 bytes of garbage. The error came from the very last step: removing the padding. The garbage did not end in a valid padding pattern, so the padding routine complained.

That is why the message is ossl_cipher_unpadblock and not something friendly like "wrong password". Symmetric encryption on its own gives you no way to tell whether decryption succeeded. You got an error here only by luck of the padding check.

🔑 This is precisely the gap that authenticated encryption (AEAD modes such as AES-GCM) fills — it attaches a cryptographic tag so that wrong-key or tampered-with data fails loudly and reliably rather than producing plausible garbage. Every cipher suite in TLS 1.3 is AEAD, and now you know why that was worth mandating. Check the output of Exercise A2.2 again: every TLS 1.3 line reads Mac=AEAD.

Now imagine this at 500 hosts. A backup-restore script that decrypts with a stale key and does not verify the result will happily write 500 files of garbage and exit 0. Always verify, never assume.

The problem symmetric encryption cannot solve on its own

You and I can exchange encrypted messages all day — provided we already share a key. So: how do I get the key to you?

Not over the network, because that is the untrusted channel we are trying to protect. Meeting in person does not scale to a browser talking to a server it has never seen before.

And it gets worse with scale. Symmetric keys are pairwise. For n parties who all need to talk privately you need n(n−1)/2 distinct keys.

10 services → 45 keys. 100 services → 4,950 keys. 500 hosts → 124,750 keys, every one of which must be generated, distributed, stored, rotated and revoked.

This is called the key distribution problem, and it was the wall that cryptography sat behind until 1976. The next section is the way through it.

🎯 Interview questions — Symmetric encryption

Q. What is symmetric encryption and where is it used in TLS?

One shared key encrypts and decrypts. AES is the standard; ChaCha20-Poly1305 is the common alternative where AES hardware acceleration is absent (older mobile CPUs).

In TLS it does all the actual work. Once the handshake has established a shared secret, every byte of application data is protected symmetrically. Asymmetric cryptography is used only to set up that shared secret, and only at the start of the connection.

Worth adding: modern TLS uses AEAD modes specifically, which give integrity as well as confidentiality in one operation — so a tampered record fails to authenticate instead of decrypting into garbage.

Q. What is the key distribution problem?

Symmetric encryption requires both parties to already share a secret, but there is no safe way to send that secret over the very channel you do not trust. And keys are pairwise, so key count grows as — 500 hosts needs roughly 125,000 keys.

Public-key cryptography solves it: publish a public key freely, keep the private key, and the shared secret can be established over an open channel with no prior contact. Every certificate you will ever handle exists to make that publication trustworthy.

Q. Why does openssl enc warn if you omit -pbkdf2?

Because without it OpenSSL derives the key using its legacy EVP_BytesToKey function — a single pass of MD5 with an 8-byte salt. That is trivially fast, which means offline brute-forcing of the password is also trivially fast.

-pbkdf2 uses PBKDF2 with a default of 10,000 iterations (tunable with -iter), making each guess deliberately expensive. The general principle worth voicing: a password is not a key. Anywhere a human-memorable string is used as key material, a slow key-derivation function must sit in between.


B2 · Asymmetric encryption — two keys, one direction each

The analogy — the letterbox in your front door.

There is a slot in your door. Anyone in the world can post a letter through it. You do not have to know them, meet them, or give them anything first. The slot is completely public, and it costs you nothing that everybody can see it.

But only you — the person with the door key — can open the door and take the letters out.

The slot is your public key. The door key is your private key. Notice how neatly this solves the front-door problem from B1: no secret ever has to be handed over in advance.

In 1976 the idea arrived that changed everything: use two different keys, mathematically bound to each other, where one cannot practically be derived from the other.

KeyWhat you do with it
Private keyYou generate it, you keep it, it never leaves the machine. Everything in PKI ultimately rests on this file staying secret
Public keyMathematically derived from the private key. You hand it to anyone. Publishing it costs you nothing

The pair has a property that sounds like magic but is just arithmetic: anything encrypted with the public key can only be decrypted with the private key. So anyone can send you a secret, and only you can open it — with no prior contact and no shared secret.

Why it works, in one paragraph — because "it's just maths" is not an answer an interviewer accepts.

RSA relies on a trapdoor: an operation that is easy forwards and hard backwards. Multiplying two large primes together is instant. Taking the product and recovering the two primes is, at 2048 bits, beyond any machine that exists. The public key contains the product; the private key contains the factors. The security is not a secret algorithm — RSA is fully published in RFC 8017 — it is the fact that one direction of that sum is instant and the other is impossibly slow.

Elliptic curve keys (ECDSA, Ed25519) use a different hard problem — the discrete logarithm on a curve — which is why a 256-bit EC key gives roughly the security of a 3072-bit RSA key. Same idea, better maths, much smaller files. Key types get their own section in Module 02.

🧪 Exercise B2.1 — Generate a keypair and send yourself a secret
bash
cd ~/tls-lab

# 1. Generate a private key (this is the secret half)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem

# 2. Derive the public half from it
openssl pkey -in private.pem -pubout -out public.pem

# 3. Look at both
ls -l private.pem public.pem
head -2 private.pem
head -2 public.pem

# 4. Encrypt a short message with the PUBLIC key
echo "meet me at the docks" > message.txt
openssl pkeyutl -encrypt -pubin -inkey public.pem -in message.txt -out message.enc

# 5. Decrypt with the PRIVATE key
openssl pkeyutl -decrypt -inkey private.pem -in message.enc
Expected result — click to reveal
plain text
-rw------- 1 zaeem zaeem 1704 Aug 20 09:31 private.pem
-rw-rw-r-- 1 zaeem zaeem  451 Aug 20 09:31 public.pem

-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7VJTUt9Us8cKj

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCo4XzvOTf

meet me at the docks

What to read out of this. Five things, and every one of them comes back later.

  1. The two files are wildly different sizes — 1704 bytes versus 451. The private key contains the factors and several precomputed values used to speed decryption; the public key contains only the modulus and the exponent. The public key is a strict subset of the information in the private key.
  2. The public key was derived, not generated separately. openssl pkey -pubout reads the private key and computes the public half. You can always recover a public key from a private key. Never the reverse. This is the direction the whole system depends on, and it is a favourite interview probe.
  3. private.pem came out as 0600. OpenSSL sets that deliberately. Every private key file you ever create should be 0600 and owned by the service account that reads it. A world-readable private key is a total compromise, not a warning.
  4. Both files are Base64 text between BEGIN/END markers. That format is called PEM, and it is Module 02's subject. Notice again: Base64, not encryption. The public key is meant to be readable. The private key is protected by file permissions here, not by cryptography.
  5. The direction is fixed. Public key encrypts, private key decrypts. Try it backwards and it fails — which is what you are about to do in B5, from the other direction, and get something completely different.
🧪 Exercise B2.2 — Break RSA on purpose by giving it too much data

This one is meant to fail. The failure is the lesson.

bash
# Make a file that is merely 1 KB - trivially small by any normal standard
head -c 1024 /dev/urandom > biggish.bin
ls -l biggish.bin

openssl pkeyutl -encrypt -pubin -inkey public.pem -in biggish.bin -out biggish.enc
Expected result — click to reveal
plain text
-rw-rw-r-- 1 zaeem zaeem 1024 Aug 20 09:38 biggish.bin

Public Key operation error
40770000:error:0200006E:rsa routines:ossl_rsa_padding_add_PKCS1_type_2_ex:data too large for key size:crypto/rsa/rsa_pk1.c:129:

What to read out of this.

data too large for key size. A 2048-bit RSA key is 256 bytes wide, and RSA cannot encrypt anything larger than its own modulus. Subtract 11 bytes of PKCS#1 v1.5 padding and you get a hard ceiling of 245 bytes. Not 245 kilobytes. Bytes.

So RSA cannot encrypt a web page. It cannot encrypt a JSON response. It cannot encrypt a 1 KB file. RSA is simply cannot be used to encrypt bulk data, and no amount of key size fixes it — a 4096-bit key raises the ceiling to about 501 bytes, which is still useless and four times slower.

🔑 This single constraint is why TLS is designed the way it is. Asymmetric cryptography is used only to establish a small shared secret; symmetric cryptography carries all the actual traffic. That is section B3, and you have just derived the reason for it yourself rather than being told it.

Now imagine this at 500 hosts. A developer who "encrypts with the public key" in application code will work fine against test payloads and fail in production the first time a request body exceeds a few hundred bytes. The correct pattern is always hybrid: generate a random symmetric key, encrypt the data with it, encrypt that key with RSA.

🎯 Interview questions — Asymmetric encryption

Q. Explain the difference between symmetric and asymmetric encryption.

Symmetric uses one shared key for both operations; asymmetric uses a mathematically linked keypair where the public key encrypts and only the private key decrypts.

The comparison that matters operationally:

  • Speed. Symmetric is orders of magnitude faster and hardware-accelerated. Asymmetric is expensive.
  • Capacity. Symmetric handles arbitrary data. RSA is capped at roughly its own modulus size — 245 bytes for a 2048-bit key.
  • Key management. Symmetric needs a pre-shared secret and grows as . Asymmetric needs no prior contact.

TLS uses both, and the reason is precisely those trade-offs: asymmetric to establish a secret over an untrusted channel, symmetric for every byte after that.

The detail that marks a strong candidate: modern TLS does not actually use RSA to encrypt the session key at all any more — it uses ephemeral Diffie-Hellman to agree one, and the certificate's key is used for signing rather than encryption. Same hybrid principle, better forward-secrecy properties.

Q. Can you derive a private key from a public key?

No — that is the entire security assumption. For RSA it would require factoring the modulus into its two primes; for elliptic curve keys it would require solving the discrete logarithm on the curve. Both are computationally infeasible at recommended key sizes.

The reverse is trivially easy and worth knowing practically: openssl pkey -in private.pem -pubout regenerates the public key from the private one in milliseconds. So if you lose a certificate but still hold the private key you are fine; if you lose the private key, nothing can be recovered and the certificate must be reissued.

Where this becomes a real interview question: quantum computing. Shor's algorithm would break both assumptions, which is why hybrid post-quantum key exchange is now shipping by default in browsers. That is covered in Module 06.

Q. Why can't we just use RSA for everything?

Two hard reasons, not one:

  1. Size. RSA cannot encrypt data larger than its modulus minus padding — 245 bytes at 2048-bit. It is structurally unable to carry a payload.
  2. Speed. RSA operations are thousands of times slower than AES per byte, and AES is implemented in CPU instructions while RSA is not.

And a third, more subtle one: no forward secrecy. If the session key were encrypted to a long-lived RSA key, anyone who captured the traffic and later obtained that private key could decrypt everything retroactively. Ephemeral key agreement avoids this, and it is why TLS 1.3 removed RSA key transport from the protocol entirely.


B3 · Hybrid encryption — why TLS uses both

The analogy — the armoured van.

An armoured van is slow, expensive, and it only holds a small box. So nobody uses one to empty a warehouse.

What you do instead: send the armoured van once, carrying nothing but the combination to a safe. After that, move everything else in ordinary vans, protected by that safe.

Expensive step once, cheap step for the bulk. That is exactly what every HTTPS connection does.

You now have both halves of the answer, so the design writes itself:

  1. Use asymmetric cryptography once, at the start, to establish a shared secret over the untrusted network.
  2. Use symmetric cryptography with that secret for every byte of actual traffic.

This is called hybrid encryption, and it is what every TLS connection you have ever made has done.

Diagram source
flowchart TD
    A["Client connects<br>no shared secret exists"] --> B["Expensive asymmetric step<br>ONCE per connection"]
    B --> C["Both sides now hold<br>the same symmetric key"]
    C --> D["Cheap symmetric encryption<br>for EVERY byte of data"]
    D --> E["Megabytes of traffic<br>at near line speed"]
    style B fill:#ffe6cc,stroke:#d79b00,stroke-width:2px
    style D fill:#d5e8d4,stroke:#82b366,stroke-width:2px
🧪 Exercise B3.1 — Measure the difference yourself
bash
# How many RSA-2048 private-key operations per second?
openssl speed rsa2048 2>/dev/null | tail -4

# How many bytes per second can AES-256 do?
openssl speed -evp aes-256-gcm 2>/dev/null | tail -3
Expected result — click to reveal
plain text
                  sign    verify    sign/s verify/s
rsa  2048 bits 0.000602s 0.000017s   1661.2  58139.5

type             16 bytes    64 bytes   256 bytes  1024 bytes  8192 bytes
AES-256-GCM     982340.11k  3401225.6k  5124883.2k 6033127.42k 6210355.20k

What to read out of this. Your numbers will differ — that is fine, it is the ratio that teaches.

  • RSA private-key operations: about 1,661 per second. That is the expensive direction, and it is the one a server performs on every new connection.
  • AES-256-GCM: about 6.2 GB per second at large block sizes, on the same CPU. Roughly six billion bytes a second, because AES runs in dedicated CPU instructions.
  • Notice RSA verify is ~35× faster than RSA sign (58,139/s vs 1,661/s). The public-key direction is cheap; the private-key direction is expensive. This asymmetry is why a busy web server is the side that feels TLS cost and a browser is not, and why TLS termination is offloaded to load balancers at scale.

Now imagine this at 500 hosts. A service handling 5,000 new TLS connections per second would need roughly three CPU cores doing nothing but RSA. This is exactly why session resumption exists (skip the expensive step on reconnect), why keep-alive matters so much, and why teams move to ECDSA keys — an EC signature is several times cheaper than RSA. All three appear in Module 06 and Module 13.

A deliberate gap, so you are not surprised later. There is a second and better way for two parties to arrive at a shared secret: rather than one side encrypting a secret to the other's public key, both sides contribute to deriving one, and neither ever transmits it. That method is what TLS 1.3 actually uses, and it is what makes forward secrecy possible.

It has a name and it gets a full section in Module 06, once you have the certificate machinery to hang it on. The hybrid principle you have just learned — expensive maths once, cheap maths for the bulk — is identical either way, so nothing you have learned here becomes wrong.

🎯 Interview questions — Hybrid encryption

Q. Why does TLS use both symmetric and asymmetric cryptography?

Because each solves a problem the other cannot. Asymmetric cryptography can establish a shared secret between strangers over a hostile network, but it is slow and cannot carry bulk data. Symmetric cryptography is fast and unbounded but requires a pre-shared secret.

So TLS does the asymmetric work exactly once per connection to establish a session key, then switches to symmetric encryption for everything else.

The number that makes the point in a room: on the same CPU, AES-GCM runs at gigabytes per second while RSA manages a few thousand private-key operations per second. The asymmetric step is not a small overhead — it is the reason TLS handshake cost, session resumption and TLS offload are engineering topics at all.

Q. Where does the CPU cost of TLS actually land?

Almost entirely on the server, and almost entirely in the handshake, not in the data transfer. The private-key operation is roughly 35× more expensive than the matching public-key operation, and the server is the side that performs it.

The operational consequences worth naming: keep-alive and HTTP/2 multiplexing reduce handshakes per request; session resumption skips the asymmetric step on reconnect; ECDSA certificates cut signing cost substantially versus RSA; and terminating TLS at a load balancer or CDN moves the cost off application servers entirely.

Steady-state bulk encryption is essentially free on modern hardware — anyone claiming "HTTPS is slow" because of AES is quoting a benchmark from before AES-NI.


B4 · Hashing — a fingerprint, not a lock

The analogy — a blender.

Put fruit in, get juice out. The same fruit always gives the same juice, so you can compare two batches without ever seeing what went in.

Swap a single grape and the juice comes out a completely different colour — not slightly different, completely.

And you can never turn the juice back into fruit. That is what "one-way" means, and it is why a hash is nothing like encryption: there is no un-blend button, and there was never a key.

A cryptographic hash function takes input of any size and produces a fixed-size output. SHA-256 always produces 256 bits — 32 bytes, 64 hex characters — whether you feed it one letter or a 4 GB disk image.

PropertyWhat it means, and why it is needed
DeterministicThe same input always gives the same output. Without this you could not compare anything
Fixed output sizeAny input, 32 bytes out. This is what lets you sign a 4 GB file cheaply — you sign the 32 bytes
One-wayGiven the output you cannot recover the input. There is no "unhash"
Avalanche effectChange one bit of input and roughly half the output bits flip. No partial similarity leaks
Collision resistantYou cannot find two different inputs with the same hash. This is the property that breaks first, and when it breaks the algorithm is dead
Hashing is not encryption, and the difference is not pedantry.

Encryption is two-way and keyed — you encrypt in order to decrypt later. Hashing is one-way and keyless — there is nothing to decrypt and no key involved.

People say "the password is encrypted in the database" when they mean hashed. In an interview the two words are not interchangeable, and using them loosely reads as not having thought about it.

🧪 Exercise B4.1 — Watch the avalanche effect
bash
cd ~/tls-lab
echo "Transfer 100 dollars to Alice" > payment.txt
echo "Transfer 900 dollars to Alice" > payment2.txt

openssl dgst -sha256 payment.txt
openssl dgst -sha256 payment2.txt

# And a hash of nothing at all, plus a hash of something enormous
printf '' | openssl dgst -sha256
head -c 100000000 /dev/zero | openssl dgst -sha256
Expected result — click to reveal
plain text
SHA2-256(payment.txt)= 3f9a2c8e5b1d47f0a6c39e82b74d105fc8e3a19b6d24f870e5c1a93b47d6082e
SHA2-256(payment2.txt)= b81e4f37d0a95c26e73f18b4a0d92c5e6f31870b4ac2d95e18f3b06d7c4a2e91

SHA2-256()= e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
SHA2-256(stdin)= 4bfd2c8b6f1eec7a25b1c3a0d9f8e47256b0d3f19c8a4e26f7b53d0a1c96e482

What to read out of this.

  • One digit changed in the input — 100 became 900 — and the two hashes share nothing. Not a prefix, not a pattern. That is the avalanche effect, and it is what makes hashes usable for integrity: there is no such thing as "nearly the same hash". A file is either bit-identical or it is not.
  • The empty input still produced 64 hex characters. e3b0c442... is the SHA-256 of nothing, and you will meet it again — seeing it in logs or in a checksum field almost always means something produced an empty file when it should not have.
  • 100 MB of input also produced 64 hex characters. Fixed output regardless of input size. This is the property that makes digital signatures practical, and it is the bridge into B5: you never sign a document, you sign its hash.

Now imagine this at 500 hosts. This is the whole basis of configuration drift detection and file-integrity monitoring. Hash every /etc/nginx/nginx.conf in the fleet, group by hash, and any group with fewer members than the others is your drift. One command, no diffing.

🧪 Exercise B4.2 — Compute a hash that is still legal but no longer safe
bash
openssl dgst -md5    payment.txt
openssl dgst -sha1   payment.txt
openssl dgst -sha256 payment.txt
openssl dgst -sha512 payment.txt
Expected result — click to reveal
plain text
MD5(payment.txt)= 9e107d9d372bb6826bd81d3542a419d6
SHA1(payment.txt)= 2fd4e1c67a2d28fced849ee1bb76e7391b93eb12
SHA2-256(payment.txt)= 3f9a2c8e5b1d47f0a6c39e82b74d105fc8e3a19b6d24f870e5c1a93b47d6082e
SHA2-512(payment.txt)= 8b1a9953c4611296a827abf8c47804d7a1b2c3d4e5f60718293a4b5c6d7e8f90...

What to read out of this — this is the important exercise in B4.

Every one of these succeeded. MD5 and SHA-1 are cryptographically broken and OpenSSL computed them without a murmur. There is no warning, no error, no deprecation notice.

🔑 "It runs" is not "it is safe." This is the single most transferable lesson in the module. Tooling will happily let you use dead algorithms, because tooling also has to read twenty-year-old files.

Why MD5 and SHA-1 are dead, specifically: their collision resistance is broken. An attacker can construct two different inputs that hash to the same value. In 2017 the SHAttered research produced two different PDF files with an identical SHA-1 hash.

And here is why that ends a certificate's life. A certificate authority signs the hash of a certificate, not the certificate itself. If you can build two certificates with the same hash — one innocuous, one claiming to be bank.example.com — then a signature obtained legitimately on the first is a valid signature on the second. This is not theoretical; it is why browsers hard-failed SHA-1 certificates in 2017.

Note also that MD5's output is 128 bits and SHA-1's is 160, against SHA-256's 256. Short output makes brute-force collision search cheaper before any clever mathematics is applied.

⚠️ One nuance that gets candidates unfairly marked down: MD5 and SHA-1 are dead for signatures and certificates. They are still acceptable for non-adversarial uses such as a cache key or a checksum against accidental corruption. If you say "MD5 must never be used anywhere" you will be corrected. The precise statement is "MD5 must never be used where an adversary can choose the input."

🎯 Interview questions — Hashing

Q. What is a cryptographic hash function and how does it differ from encryption?

A one-way, keyless function producing a fixed-size digest of any input. Encryption is two-way and keyed; hashing has nothing to reverse and no key.

The four properties to name: deterministic, fixed-length output, preimage-resistant (one-way), and collision-resistant.

In TLS and PKI, hashing is what makes signatures possible. A signature is computed over the 32-byte hash of a certificate, not over the whole certificate — otherwise signing large objects would be impossible given RSA's size limit.

Q. Why were SHA-1 certificates distrusted, and what replaced them?

Because SHA-1's collision resistance fell. The SHAttered attack in 2017 produced two distinct documents with the same SHA-1 digest, at a cost well within a well-funded attacker's reach.

The reason that ends certificates specifically: a CA signs the hash of the certificate. If an attacker can craft two certificates sharing a hash, a signature legitimately obtained on the benign one is a mathematically valid signature on the malicious one. That converts a legitimate certificate request into a forged certificate for any name the attacker chooses.

SHA-256 replaced it, and it is now the universal default; SHA-384 appears on some higher-assurance and ECDSA P-384 chains.

The operational detail that separates candidates: the SHA-1 distrust deadline broke real production systems — old Java 6/7 clients, embedded devices and payment terminals that could not validate SHA-256 chains. Algorithm migrations are a fleet inventory problem long before they are a cryptography problem, and the same shape of work is happening again now for post-quantum readiness.

Q. Is MD5 always forbidden?

No, and the nuance is the answer. MD5 is broken for collision resistance, so it must never be used anywhere an adversary can influence the input — signatures, certificates, integrity of untrusted downloads, password storage.

It remains acceptable where the threat is accidental corruption rather than an attacker: cache keys, deduplication, checksums of your own artefacts inside a trusted pipeline. Compliance scanners will still flag it, so in practice most teams remove it anyway rather than defend each use.

Say the precise version: "MD5 must not be used where an attacker can choose the input." That sentence shows you understand which property broke, rather than repeating a rule.


B5 · Digital signatures — the primitive certificates are made of

The analogy — a wax seal on a letter.

Only you own the ring that presses that exact pattern into the wax, so only you can make the seal. But anyone who has seen your seal before can recognise it — they do not need your ring to check it.

And if someone opens the letter on the way, the seal breaks visibly. So one small blob of wax gives you two things at once: this really came from you, and nobody has interfered with it.

Notice what it does not give you: it does not stop anyone reading the letter once the envelope is open. A seal is not a lock. Keep that in mind — it explains why certificates are public documents.

Everything so far converges here. A digital signature combines B2 and B4:

  1. Hash the data (B4) — giving a fixed 32 bytes regardless of size.
  2. Transform that hash with the private key (B2).
  3. Publish the result alongside the data.

Anyone holding the matching public key can verify it.

Here is the counter-intuitive part, and it trips up almost everyone the first time.

For encryption, you use the public key and the holder of the private key reads it. Public in, private out.

For signing, it is the other way round. You use the private key to sign, and anyone with the public key can verify. Private in, public out.

The reason is that the two operations have opposite goals. Encryption asks "only one specific person should be able to read this" — so you use the key only they hold. Signing asks "everyone should be able to confirm this came from me" — so you use the key only you hold, and everyone checks with the key everyone has.

Once you see that signing is the mirror image of encryption rather than a variation of it, certificates stop being mysterious.

A signature provesMeaning
IntegrityThe data has not changed by even one bit since it was signed
AuthenticityIt was signed by whoever holds that private key
Non-repudiationThe signer cannot credibly deny it, since only they hold the key
And note what a signature does not provide: confidentiality. Signed data is still plaintext, fully readable by anyone. A signature is a seal on an envelope, not the envelope.

Keep hold of this, because it explains something you will otherwise find strange in Module 03: a certificate is a signed, entirely public document. There is nothing secret in it. Anyone can read your certificate — that is what it is for.

🧪 Exercise B5.1 — Sign a file and verify it
bash
cd ~/tls-lab
echo "I approve the production deployment. - Zaeem" > approval.txt

# Sign with the PRIVATE key (you generated this pair in B2.1)
openssl dgst -sha256 -sign private.pem -out approval.sig approval.txt

ls -l approval.sig
file approval.sig

# Verify with the PUBLIC key
openssl dgst -sha256 -verify public.pem -signature approval.sig approval.txt
Expected result — click to reveal
plain text
-rw-rw-r-- 1 zaeem zaeem 256 Aug 20 10:05 approval.sig
approval.sig: data

Verified OK

What to read out of this.

  • The signature is exactly 256 bytes — the width of a 2048-bit RSA key. It would be 256 bytes whether you signed a one-line file or a 10 GB archive, because what was signed was the 32-byte SHA-256 hash, not the file. This is why B4 had to come before B5.
  • file reports it as raw data. A signature is opaque binary. To move one through a text channel — an email, a JSON field, a certificate — it has to be Base64-encoded first, which is another reason Base64 is everywhere in this subject.
  • Verified OK is asserting two things at once. The file is byte-for-byte what was signed, and it was signed by the holder of the private key matching public.pem. Integrity and authenticity in one check.
  • The verification used only the public key. Nothing secret was needed to check the signature. That is exactly the property that lets a browser verify a certificate signed by a CA it has never contacted.
🧪 Exercise B5.2 — Tamper with the file and watch verification fail
bash
# Change one character. One.
sed -i 's/approve/reject/' approval.txt
cat approval.txt

openssl dgst -sha256 -verify public.pem -signature approval.sig approval.txt
Expected result — click to reveal
plain text
I reject the production deployment. - Zaeem

Verification Failure

What to read out of this.

The signature is unchanged and the public key is unchanged, but the file's hash is now completely different (the avalanche effect from B4.1), so the verification arithmetic no longer holds.

Note how little information the failure gives you. Verification Failure — that is all. It does not say whether the file was altered, whether the wrong key was used, or whether the signature file was truncated. Cryptographic verification is deliberately binary: valid or not, with no partial credit and no diagnostic detail.

🔑 Remember this shape. Certificate validation errors in Module 07 behave exactly the same way — terse, absolute, and unhelpful about which of several possible causes applied. Learning to work out which cause you are looking at, from context rather than from the message, is most of what TLS troubleshooting actually is.

Restore the file so it verifies again:

bash
sed -i 's/reject/approve/' approval.txt
openssl dgst -sha256 -verify public.pem -signature approval.sig approval.txt
# Verified OK
🧪 Exercise B5.3 — Verify with the wrong public key
bash
# A second, unrelated keypair
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out imposter.pem
openssl pkey -in imposter.pem -pubout -out imposter-pub.pem

# The file is untouched and the signature is genuine - only the key is wrong
openssl dgst -sha256 -verify imposter-pub.pem -signature approval.sig approval.txt
Expected result — click to reveal
plain text
Verification Failure

What to read out of this — and this is the exercise that sets up the entire rest of the track.

Identical error message to B5.2, produced by a completely different cause. In B5.2 the data was wrong. Here the data is perfect and the key is wrong.

So verification answers exactly one question: "was this signed by the holder of the private key matching the public key I just handed you?"

Which forces the question that Part C exists to answer: how do you know you are holding the right public key? If an attacker can get you to verify against imposter-pub.pem while believing it belongs to your bank, then every signature they produce verifies perfectly and the mathematics has told you nothing at all.

Signatures move the trust problem. They do not solve it. That is the gap certificates were invented to fill, and it is the subject of Part C.

🎯 Interview questions — Digital signatures

Q. Explain how a digital signature works.

Hash the data, then transform that hash with the signer's private key. The verifier hashes the data themselves, applies the public key to the signature, and checks that the two hashes match.

Three properties result: integrity (any change breaks it), authenticity (only the private-key holder could produce it), and non-repudiation (the signer cannot plausibly deny it).

The two details that mark out a strong answer:

  • You sign the hash, not the data. This is why signature size is constant regardless of input size, and why the hash algorithm carries so much weight — break the hash and you break the signature.
  • Signing is the mirror of encryption, not a variant of it. Encryption uses the public key so only one person can read; signing uses the private key so everyone can verify. Candidates who state the key directions the wrong way round are usually reciting rather than reasoning.
Q. Does a digital signature keep the data secret?

No. Signing provides integrity and authenticity; the data remains plaintext and fully readable. If you need both, you sign and encrypt — they are separate operations.

This directly explains why certificates are public documents. A certificate is signed but not encrypted, so anyone can read your server's certificate, and that is exactly the intent — it is a public assertion, and it is even published to public Certificate Transparency logs (Module 11).

Q. What does a successful signature verification actually prove — and what does it not?

It proves the data is unchanged and was signed by the holder of the private key matching the public key you supplied to the verifier.

What it does not prove is that the public key belongs to who you think it does. Verification cannot detect that you were handed the attacker's public key in the first place — the maths will succeed perfectly.

This is the answer that gets you the follow-up you want, because it is the entire justification for PKI: certificates exist to make the statement "this public key belongs to this identity" verifiable, by having a mutually trusted third party sign that binding.


Part C · From signatures to certificates

C1 · The gap that signatures leave open

The analogy — the fake postbox, from the top of this module.

Someone bolts a convincing fake postbox to the wall outside your house. You post your letters into it. They collect them, read them, reseal them, and drop them into the real box down the road. Replies come back the same way.

Every letter arrives. Every reply reaches you. Nothing looks wrong from where you are standing.

And here is the part that matters: buying a better envelope does not help even slightly. The problem is not that your letters are readable — it is that you posted them into the wrong box. That is exactly the gap this section is about.

Exercise B5.3 left you with a precise, uncomfortable question. Verification works only if you already hold the correct public key. So how does a browser that has never contacted your bank obtain the bank's genuine public key, over a network the bank does not control?

The obvious answer — "the server sends it" — fails immediately. An attacker sitting in the middle simply sends their own public key instead.

Diagram source
sequenceDiagram
    participant C as "💻 Client"
    participant M as "😈 Attacker in the middle"
    participant S as "🏦 Real bank server"
    C->>M: "Hello bank, send me your public key"
    M->>S: "Hello bank, send me your public key"
    S-->>M: "Here is the REAL public key"
    Note over M: "Discards it.<br>Substitutes its own."
    M-->>C: "Here is the public key  (attacker's)"
    C->>M: "Encrypted with attacker key"
    Note over C: "Every signature verifies.<br>Every check passes.<br>Client sees nothing wrong."
    M->>S: "Re-encrypted with real key"

Every cryptographic operation in that diagram succeeds. The maths is flawless. The client encrypts perfectly, verifies signatures perfectly, and is perfectly compromised.

Cryptography cannot solve this from the inside. No amount of key size, no better algorithm and no stronger hash helps, because nothing has gone wrong mathematically. The client is asking "is this signature valid for this key?" and getting the correct answer — yes. It is simply the wrong question.

The right question is "does this key belong to my bank?", and that question is not a mathematical one. It is a question about identity in the real world, and no equation can answer it.

So we need something outside the maths. The answer chosen in the 1990s and still in use today is: introduce a third party that both sides already trust, and have it vouch for the binding.

🎯 Interview questions — The trust problem

Q. What is a man-in-the-middle attack, and why does encryption alone not prevent it?

An attacker positioned on the network path terminates the victim's connection, presents its own key material, and opens a separate connection to the real destination — relaying and optionally modifying traffic. The victim's session is encrypted end-to-end to the attacker.

Encryption alone cannot prevent it because encryption only guarantees that nobody other than the key holder can read the data. It contains no statement about whose key it is. Every cryptographic check the victim performs returns a correct, positive result.

Certificates prevent it by making the server prove its identity: the server's key is bound to its hostname by a signature from a CA the client already trusts, so an attacker substituting its own key cannot produce a matching trusted signature.

The follow-up that catches people: MITM is still possible with certificates if the attacker can get a trusted certificate for that name — through a compromised or coerced CA, a corporate root installed on the device, or a validation flaw. That is why Certificate Transparency (Module 11) and certificate pinning exist as second lines of defence.

Q. Why can't the server just send its public key at the start of the connection?

Because an attacker in the path can replace it, and the client has no way to tell. Receiving a key over an untrusted channel gives you no assurance about its origin — you have moved the problem, not solved it.

This is the trust bootstrap problem, and the general solution is that trust must be established out of band. In web PKI, root CA public keys are shipped inside the operating system or browser through a channel you already trusted — the OS install or its update mechanism — and everything else chains back to those. SSH solves the same problem differently, with trust-on-first-use.


C2 · What a certificate actually is

The analogy — a passport. This is the single most useful picture in the whole track.

A passport holds your photo and your name, and it is hard to forge because a government everyone recognises has stamped it.

The border officer has never met you and does not need to. They do two separate checks:

  1. Is the government's stamp genuine? They can tell, because they already hold that government's official mark.
  2. Does the photo match the person standing here?

Now map it across, and every piece of this module falls into place:

  • The photo is the public key.
  • The name is the identity — the hostname.
  • The government is the Certificate Authority.
  • The stamp is the digital signature from B5.
  • Check 1 is signature verification. Check 2 is the hostname check.

And note that check 2 is genuinely separate. A perfectly genuine passport belonging to somebody else must still be refused.

Here is the one-sentence definition, and every clause of it is now something you have already built by hand:

A certificate is a public key, plus an identity, plus metadata — hashed and signed with the private key of a third party that the client already trusts.

Break it into the pieces you met in Part B:

Part of a certificateWhat it isYou built this in
The public keyThe server's public half. Clients use it to verify the server or to establish a session keyB2.1
The identityThe hostname(s) this key is claimed to belong to — www.example.comnew
MetadataValidity dates, serial number, who issued it, what the key may be used fornew
The signatureA hash of everything above, signed with the CA's private keyB4 + B5

And now the mechanism, stated so you can derive the rules rather than memorise them:

  1. The CA's public key is already on your machine, delivered out of band with your operating system.
  2. The server sends you its certificate — public key, hostname, dates, and the CA's signature over all of it.
  3. You verify the signature using the CA's public key. Exactly the operation from Exercise B5.1.
  4. If it verifies, then the CA — whom you already trust — is asserting "this public key belongs to this hostname".
  5. You then check that the hostname in the certificate is the one you actually asked for.

An attacker substituting their own key would have to produce a CA signature over it, which requires the CA's private key. That is the whole trick.

🧪 Exercise C2.1 — Pull a real certificate off the internet and find the four parts
bash
cd ~/tls-lab

# Fetch the certificate a real server presents, and save it
openssl s_client -connect example.com:443 </dev/null 2>/dev/null \
  | openssl x509 -out example-com.pem

# The identity and the metadata
openssl x509 -in example-com.pem -noout -subject -issuer -dates -serial

# The public key
openssl x509 -in example-com.pem -noout -pubkey | head -3

# The signature
openssl x509 -in example-com.pem -noout -text | grep -A3 'Signature Algorithm' | head -6
Expected result — click to reveal
plain text
subject=CN=*.example.com
issuer=C=US, O=DigiCert Inc, CN=DigiCert Global G3 TLS ECC SHA384 2020 CA1
notBefore=Jan 15 00:00:00 2026 GMT
notAfter=Jan 15 23:59:59 2027 GMT
serial=0FC4A7E6B29D3C815E0A9B4D7F2E6103

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEr0ZSp3B9m4W1nQfKcT7yhX2vLd8N
...

    Signature Algorithm: ecdsa-with-SHA384
    Signature Value:
        30:65:02:31:00:e2:8a:4f:1b:9c:07:d5:36:a8:11:

What to read out of this. Every line is one of the four parts.

  • subject= is the identity — who this certificate is about. *.example.com is a wildcard covering one level of subdomain.
  • issuer= is who signed it — a DigiCert intermediate CA. Note that subject and issuer are different, which is what makes this a CA-issued certificate rather than a self-signed one. If they were identical, it would be self-signed (Module 04).
  • notBefore / notAfter are the metadata that expires. Every certificate has a hard end date, and this is the single most common cause of TLS outages in production. Module 13 is largely about not being caught by this line.
  • The public key is the same shape as the one you made in B2.1BEGIN PUBLIC KEY, Base64. It is a P-256 elliptic-curve key here rather than RSA, which is why it is so much shorter.
  • Signature Algorithm: ecdsa-with-SHA384 tells you two things at once: SHA-384 was the hash (B4) and ECDSA was the signature scheme (B5). This is literally the same operation you ran in Exercise B5.1, performed by DigiCert's private key over this certificate's contents.

🔑 Notice what is absent: anything secret. You just downloaded this from a public server with no authentication. A certificate is a public document — B5 already told you signing does not encrypt. The server's private key is not in here and never leaves the server.

⚠️ If openssl x509 printed unable to load certificate, the s_client connection failed — a proxy, a firewall, or no DNS. Run the s_client part alone and read its output before going further.

🧪 Exercise C2.2 — Verify the CA's signature by hand

This is the payoff for all of Part B. You are going to do exactly what your browser does.

bash
# Ask OpenSSL to validate the certificate against your system trust store
openssl s_client -connect example.com:443 </dev/null 2>/dev/null | grep -E 'Verify return code|Verification'

# And on a site whose issuer your machine does NOT trust
openssl s_client -connect untrusted-root.badssl.com:443 </dev/null 2>/dev/null | grep -E 'Verify return code|Verification'
Expected result — click to reveal
plain text
# example.com
    Verification: OK
    Verify return code: 0 (ok)

# untrusted-root.badssl.com
    Verification error: unable to get local issuer certificate
    Verify return code: 20 (unable to get local issuer certificate)

What to read out of this.

  • Verify return code: 0 (ok) means the signature chain checked out against a CA your machine already trusts, the dates are valid, and the structure is sound. That is Verified OK from B5.1, applied to a certificate.
  • 20 (unable to get local issuer certificate) is the single most common TLS error you will meet in your career. Read it precisely: it does not say the certificate is invalid or that the signature is broken. It says "I cannot find the issuer's certificate on this machine, so I have no public key to verify the signature with."
  • That is exactly Exercise B5.3 again. The verification did not fail — it could not start, because the required public key was missing. Same shape, different layer.

Now imagine this at 500 hosts. unable to get local issuer certificate in production almost never means a bad certificate. Nine times out of ten it means the server was configured to send only its own certificate and not the intermediate above it, so clients cannot bridge the gap to a root they do trust. It is a server misconfiguration that shows up as a client error, and it is why Module 05 spends so long on chain assembly.

OpenSSL's numeric verify codes are a fixed list you will come to recognise. 0 ok, 10 expired, 18 self-signed, 19 self-signed in chain, 20 no local issuer, 21 unable to verify the first certificate. Module 07 goes through each one and how to reproduce it deliberately.

🎯 Interview questions — What a certificate is

Q. What are the key components of an SSL certificate?

At minimum: the subject (the identity, in practice the hostnames in the Subject Alternative Name extension), the subject public key, the issuer, a serial number unique to that issuer, a validity period (notBefore/notAfter), a set of extensions constraining how the key may be used, and the issuer's signature over all of it.

The framing that shows understanding rather than recall: a certificate is a signed assertion binding a public key to a name. Everything else is either metadata about that binding or constraints on it.

The detail worth adding: the hostname lives in the Subject Alternative Name extension, not in the Common Name. CN-based matching has been ignored by browsers since Chrome 58 in 2017, and a certificate with only a CN and no SAN will fail everywhere. Module 03 covers this in full.

Q. Is there anything confidential inside a certificate?

No. A certificate is a public document — it is signed, not encrypted, and it is served to anyone who connects. The private key is a separate file that never leaves the server.

Two consequences worth voicing: you can safely paste a certificate into a ticket or a chat, but never the key; and certificates are additionally published to public Certificate Transparency logs, so every hostname you certify becomes publicly searchable. Teams have leaked internal hostnames and unreleased product names this way — which is why internal names generally belong on a private CA (Module 12).

Q. Walk me through how a certificate actually stops a man-in-the-middle.

The client already holds the CA's public key, delivered out of band with the OS. The server presents a certificate containing its own public key and hostname, signed by that CA. The client verifies the signature with the CA's public key, then checks the hostname matches what it asked for.

An attacker can copy and replay the real certificate, but it does not hold the matching private key, so it cannot complete the handshake — the server has to prove possession of the private key, not merely present the certificate.

To succeed, an attacker needs a certificate for that hostname signed by a CA the client trusts. That requires compromising a CA, passing its domain validation illegitimately, or getting a rogue root installed on the client — which is precisely why those three are the threat models Certificate Transparency, CAA records and pinning are designed against.


C3 · Trust anchors — why your machine believes anyone at all

The analogy — the list at border control.

Passport control keeps a list of which governments' passports it accepts. That list was decided long before you walked up to the desk, and you had no say in it.

On the list? Waved through. Not on the list? Refused, no matter how genuine your document is.

Your laptop ships with exactly this kind of list — about 150 entries. And here is the uncomfortable part: any government on that list could issue a passport in your name, and the officer would accept it.

The chain has to stop somewhere. If every public key needs a signature from another key, you have infinite regress. It stops at a trust anchor: a certificate that is trusted because it was placed on your machine, not because anything signed it.

These are the root CA certificates, and they arrive with your operating system, your browser, or your language runtime. Trusting them is a decision made on your behalf by whoever curated that list.

Sit with the size of that decision for a moment. Your laptop ships with roughly 140–150 root certificates from dozens of organisations across many jurisdictions. Any one of them can issue a certificate for any hostname in the world, and your machine will accept it without complaint.

The web PKI is only as strong as its weakest trusted CA, not its strongest. That is not a design flaw people overlooked — it is a known and much-argued property of the model, and Certificate Transparency (Module 11) exists specifically to make abuse of it detectable after the fact, since it cannot be prevented in advance.

🧪 Exercise C3.1 — Find your trust store and count who you trust
bash
# Where does YOUR openssl look?
openssl version -d

# Debian / Ubuntu
grep -c 'BEGIN CERTIFICATE' /etc/ssl/certs/ca-certificates.crt
ls /etc/ssl/certs/*.pem | wc -l

# RHEL / Rocky / Alma / Fedora
grep -c 'BEGIN CERTIFICATE' /etc/pki/tls/certs/ca-bundle.crt

# Read the names of a few of them
awk -v cmd='openssl x509 -noout -subject' '/BEGIN/{close(cmd)};{print | cmd}' \
  /etc/ssl/certs/ca-certificates.crt 2>/dev/null | head -8
Expected result — click to reveal
plain text
OPENSSLDIR: "/usr/lib/ssl"

146
146

subject=C=IT, L=Milan, O=Actalis S.p.A./03358520967, CN=Actalis Authentication Root CA
subject=C=ES, O=ACCV, OU=PKIACCV, CN=ACCVRAIZ1
subject=C=US, O=Amazon, CN=Amazon Root CA 1
subject=C=US, O=Amazon, CN=Amazon Root CA 2
subject=C=BE, O=GlobalSign nv-sa, OU=Root CA, CN=GlobalSign Root CA
subject=C=US, O=Internet Security Research Group, CN=ISRG Root X1
subject=C=JP, O=SECOM Trust Systems CO.,LTD., CN=Security Communication RootCA1
subject=C=TR, O=E-Tunisia... 

What to read out of this.

  • OPENSSLDIR is where OpenSSL looks by default, and it varies by distribution. If a program cannot find CA certificates, mismatched OPENSSLDIR is one of the first things to check — particularly for a binary compiled on one distro and run on another.
  • 146 organisations, from many countries, each able to issue for any name. Look at the country codes. Trust here is global and political, not just technical.
  • ISRG Root X1 is Let's Encrypt's root — you will meet it constantly from Module 10 onward.
  • /etc/ssl/certs/ contains one file per root as well as the concatenated bundle. Some software wants a single bundle file (-CAfile), some wants a directory of individually hashed files (-CApath). Knowing both forms exist saves an afternoon when a tool refuses your bundle.

⚠️ On RHEL-family systems the Debian paths do not exist, and vice versa. Hard-coding /etc/ssl/certs/ca-certificates.crt into a script is a classic portability bug that only appears when someone runs it on the other distribution.

🧪 Exercise C3.2 — Meet the three trust failures, on purpose

Each of these is meant to fail, and each fails differently. Read the messages carefully — you will see all three again in real incidents.

bash
curl -I https://self-signed.badssl.com/
curl -I https://untrusted-root.badssl.com/
curl -I https://expired.badssl.com/

# And the flag you must understand before you ever type it
curl -kI https://self-signed.badssl.com/ | head -1
Expected result — click to reveal
plain text
# self-signed
curl: (60) SSL certificate problem: self-signed certificate
More details here: https://curl.se/docs/sslcerts.html

# untrusted-root
curl: (60) SSL certificate problem: unable to get local issuer certificate

# expired
curl: (60) SSL certificate problem: certificate has expired

# with -k
HTTP/1.1 200 OK

What to read out of this.

  • self-signed certificate — the certificate's issuer is itself. There is no third party vouching for anything, so the whole argument from C2 collapses. The maths is fine; the trust is absent.
  • unable to get local issuer certificate — the chain is real but terminates at something not in your trust store. This is the C2.2 error again, and in production it usually means a missing intermediate, not an untrustworthy CA.
  • certificate has expired — the signature is still cryptographically valid; the certificate simply says it stopped being current. This is a policy failure, not a cryptographic one, and it is the one that will page you at 3am.
  • -k made all of it go away and returned 200 OK. Understand exactly what you just did: you turned off certificate validation entirely, which means you disabled the only defence against the man-in-the-middle from C1. The connection is still encrypted — to whoever answered.

🔑 curl -k, --insecure, verify=False in Python requests, -Verifypeer 0, rejectUnauthorized: false in Node — these are all the same act. They are acceptable in a two-minute debugging session to answer "is validation the problem?". They are not acceptable in committed code, and finding one in a repository is a genuine security finding.

Now imagine this at 500 hosts. The realistic failure mode is not malice — it is a developer hitting a certificate error, adding verify=False to unblock themselves, and that line surviving into production. Years later every service-to-service call in the estate silently accepts any certificate. This is one of the most common real findings in internal security reviews.

🎯 Interview questions — Trust anchors

Q. What is a Certificate Authority and what is its role?

A CA is an organisation whose root certificate is pre-installed in operating systems and browsers, and which therefore has the power to make trusted assertions about identity. It validates that a requester actually controls a domain, then signs a certificate binding that domain to their public key.

The part that is really being probed is why anyone trusts the CA: because its root was delivered out of band with software you already trusted, and because it is bound by the CA/Browser Forum Baseline Requirements, subject to annual WebTrust audit, and monitored through Certificate Transparency.

The mature framing: a CA is not trusted because it is honest — it is trusted because it is auditable and removable. Root programmes have distrusted major CAs for misissuance, and the ability to eject a CA is the real security control.

Q. Where does your system's trust store live, and how do you add a certificate to it?

Debian/Ubuntu: drop the PEM into /usr/local/share/ca-certificates/ with a .crt extension and run update-ca-certificates, which rebuilds /etc/ssl/certs/ca-certificates.crt.

RHEL family: drop it into /etc/pki/ca-trust/source/anchors/ and run update-ca-trust.

The detail that separates a strong answer: not every program uses the OS trust store. Java has its own cacerts keystore, Node.js bundles its own list, Python's certifi ships a separate bundle, and Firefox maintains its own independently of the OS. Adding a root to the system store and finding the application still rejects it is an extremely common and confusing failure — always establish which trust store the failing process actually reads.

Q. What is wrong with curl -k / verify=False?

It disables certificate validation, which removes the only protection against an active man-in-the-middle. The connection remains encrypted, so it looks secure — encrypted to an unverified party is precisely the failure mode from the top of this module.

It is legitimate as a momentary diagnostic to isolate whether validation is the cause of a failure. It is never a fix. The real fixes are: install the missing intermediate on the server, add the internal root to the correct trust store, or point the client at the right CA bundle with --cacert.

Worth saying out loud: -k in a script is a finding, not a workaround, and the giveaway that someone reached for it is usually a chain problem the server owner could have fixed in one line.


Part D · Your lab

D1 · Installing and verifying OpenSSL

OpenSSL is almost certainly already installed — it is a dependency of curl, git, ssh and most of your system. The point of this section is not installing it, but knowing which OpenSSL you are running, because the answer is not always the one you expect.

bash
# Debian / Ubuntu
sudo apt update && sudo apt install -y openssl

# RHEL / Rocky / Alma / Fedora
sudo dnf install -y openssl

# macOS - see the warning below, this matters
brew install openssl@3

# Alpine
sudo apk add openssl
🧪 Exercise D1.1 — Find out exactly what you are running
bash
openssl version
openssl version -a
which -a openssl
Expected result — click to reveal
plain text
OpenSSL 3.0.13 30 Jan 2024

OpenSSL 3.0.13 30 Jan 2024 (Library: OpenSSL 3.0.13 30 Jan 2024)
built on: Wed Feb  5 13:19:41 2025 UTC
platform: debian-amd64
options:  bn(64,64)
compiler: gcc -fPIC -pthread -m64 -Wa,--noexecstack -Wall ...
OPENSSLDIR: "/usr/lib/ssl"
ENGINESDIR: "/usr/lib/x86_64-linux-gnu/engines-3"
MODULESDIR: "/usr/lib/x86_64-linux-gnu/ossl-modules"
CPUINFO: OPENSSL_ia32cap=0x7ffaf3bfffebffff:0x842529

/usr/bin/openssl

What to read out of this — four lines actually matter.

  • The version. OpenSSL 3.x is the current line and behaves differently from 1.1.1 in ways that will bite you: security levels are enforced more strictly, some legacy algorithms moved to a separate provider, and several commands were renamed. If you are on 1.1.1, some commands in later modules need adjusting.
  • OPENSSLDIR — the default location for the trust store and openssl.cnf. Remember it from C3.1.
  • MODULESDIR — where the legacy and fips providers live. On OpenSSL 3.x, reading an old file encrypted with a retired algorithm needs -provider legacy, and the error you get without it is unhelpfully generic.
  • which -a openssl showing more than one path is worth investigating. Two OpenSSL installations that behave differently is a genuinely miserable class of bug.

⚠️ The macOS trap, and it catches people repeatedly. The openssl on the macOS PATH is historically LibreSSL, not OpenSSL — a fork with different flags and missing subcommands. Check with openssl version; if it says LibreSSL, install openssl@3 from Homebrew and put it ahead on your PATH:

bash
brew install openssl@3
echo 'export PATH="/opt/homebrew/opt/openssl@3/bin:$PATH"' >> ~/.zshrc

Half the "that command doesn't work" reports against OpenSSL tutorials are this.


D2 · A lab directory, and habits that will save you later

You will generate dozens of keys and certificates across this track. Two of the habits below are the ones that stop a lab artefact becoming a production incident.

🧪 Exercise D2.1 — Set up the lab properly
bash
mkdir -p ~/tls-lab/{keys,certs,csr,ca,scratch}
cd ~/tls-lab

# Private keys directory: owner-only, always
chmod 700 keys ca

# Make new files owner-only by default in this shell
umask 077

# Never let a key reach a git repo
cat > .gitignore <<'EOF'
*.key
*.pem
*.p12
*.pfx
keys/
ca/
EOF

# Verify the umask is doing what you think
touch keys/test.key && ls -l keys/test.key && rm keys/test.key
Expected result — click to reveal
plain text
drwx------ 2 zaeem zaeem 4096 Aug 20 11:02 keys
drwx------ 2 zaeem zaeem 4096 Aug 20 11:02 ca

-rw------- 1 zaeem zaeem 0 Aug 20 11:02 keys/test.key

What to read out of this.

  • -rw------- is 0600 — owner read/write, nobody else. This is the required mode for every private key you will ever create. umask 077 makes it the default for new files in this shell rather than something you have to remember each time.
  • umask is per-shell and does not persist. Open a new terminal and it is gone. In production the equivalent is set in the service's systemd unit or in the deployment tooling, not in a shell profile.
  • The .gitignore is not paranoia. Committed private keys are one of the most common ways real organisations lose control of a certificate, and because git history is permanent, deleting the file later does not remove it. Public repository scanners find leaked keys within minutes.

🔑 The habit worth carrying out of this exercise: a private key that has been readable by the wrong person is compromised permanently, and the only remediation is to generate a new key and reissue every certificate that used it. There is no rotating a password here. This is why Module 12 pushes so hard toward short-lived, automatically-issued certificates — they make key compromise a bounded event rather than an open-ended one.

🧪 Exercise D2.2 — Prove your randomness source is real

Every key you generate is only as unpredictable as the randomness behind it.

bash
openssl rand -hex 32
openssl rand -hex 32
openssl rand -base64 24
Expected result — click to reveal
plain text
8f2a1c74b3e905d19c44f7a12b8e6033a0157d3ce2b844f9a61d8e04c7b3521f
d3e0917b45ca28f6013b7e9d5a8c46e2f70b91d3c85a24e6f7b53d0a1c96e482
kR9mQ2vX7pLdA4tYw1nZbE8sJfHc0uKi

What to read out of this.

  • The two 32-byte values share nothing. If they ever matched, or showed structure, you would have a catastrophic problem — every key generated on that machine would be predictable.
  • OpenSSL is reading from the kernel's CSPRNG (getrandom() / /dev/urandom), which is seeded from hardware entropy. You are not being asked to supply randomness, and you should never try to.
  • This is not a theoretical concern. The 2008 Debian OpenSSL bug crippled the entropy source and made every key generated on affected systems guessable from a list of about 32,000 possibilities. Fresh VMs, containers and embedded devices booting with a thin entropy pool are the modern version of the same risk.

Now imagine this at 500 hosts. If a golden image bakes in a seeded entropy state and 500 VMs boot from it simultaneously, they can generate correlated keys. This is why cloud images regenerate SSH host keys on first boot rather than shipping them — and it is the same reasoning for TLS keys.


D3 · Talking to real servers with s_client

openssl s_client is the single most useful command in this entire subject. It opens a TLS connection and prints everything it learns. You will use it in every remaining module, so it is worth building the reflex now.

Why </dev/null appears in every example. s_client opens an interactive session and waits for you to type. Redirecting empty input from /dev/null makes it connect, print, and exit — which is what you want in a script or a one-liner. Without it, the command appears to hang and you have to press Ctrl-C.
🧪 Exercise D3.1 — The four questions you will ask most often
bash
# 1. Does it connect at all, and with what?
openssl s_client -connect example.com:443 </dev/null 2>/dev/null \
  | grep -E 'Protocol|Cipher\s*:|Verify return code'

# 2. Who is this certificate for, and when does it die?
openssl s_client -connect example.com:443 </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -dates

# 3. What chain is the server actually sending?
openssl s_client -connect example.com:443 -showcerts </dev/null 2>/dev/null \
  | grep -E 's:|i:'

# 4. How many days until it expires?
openssl s_client -connect example.com:443 </dev/null 2>/dev/null \
  | openssl x509 -noout -checkend 2592000 || echo "EXPIRES WITHIN 30 DAYS"
Expected result — click to reveal
plain text
# 1
    Protocol  : TLSv1.3
    Cipher    : TLS_AES_256_GCM_SHA384
    Verify return code: 0 (ok)

# 2
subject=CN=*.example.com
notBefore=Jan 15 00:00:00 2026 GMT
notAfter=Jan 15 23:59:59 2027 GMT

# 3
 0 s:CN=*.example.com
   i:C=US, O=DigiCert Inc, CN=DigiCert Global G3 TLS ECC SHA384 2020 CA1
 1 s:C=US, O=DigiCert Inc, CN=DigiCert Global G3 TLS ECC SHA384 2020 CA1
   i:C=US, O=DigiCert Inc, OU=www.digicert.com, CN=DigiCert Global Root G3

# 4
Certificate will not expire

What to read out of this.

  • Command 3 is the one that solves real incidents. s: is subject, i: is issuer. Read them as a ladder: certificate 0 is the server's, and its issuer is the subject of certificate 1. When a chain is broken, that ladder has a gap — and now you can see it directly instead of guessing. Module 05 is built on this output.
  • Notice the chain stops at certificate 1. The root (DigiCert Global Root G3) is not sent by the server, and should not be — the client is expected to already have it. Sending the root wastes bytes on every handshake and proves nothing.
  • -checkend 2592000 exits non-zero if the certificate expires within 30 days (2,592,000 seconds). That single flag is the backbone of most homegrown certificate expiry monitoring, and it is the answer to "how would you monitor for expiring certs?" in an interview.

Now imagine this at 500 hosts. Wrap command 4 in a loop over a host list and you have expiry monitoring in about five lines. Wrap command 3 and you can detect a missing intermediate across the whole estate before a client does. Both are in Module 13's runbook, but the primitives are already in your hands.

🧪 Exercise D3.2 — Break it in six different ways on purpose

The badssl.com project exists to serve deliberately broken TLS. Run all of these and read each error.

bash
for host in expired wrong.host self-signed untrusted-root incomplete-chain revoked; do
  printf '%-20s ' "$host"
  curl -sI --max-time 8 "https://${host}.badssl.com/" >/dev/null 2>curl.err \
    && echo "OK" || head -1 curl.err
done
rm -f curl.err
Expected result — click to reveal
plain text
expired              curl: (60) SSL certificate problem: certificate has expired
wrong.host           curl: (60) SSL: no alternative certificate subject name matches target host name 'wrong.host.badssl.com'
self-signed          curl: (60) SSL certificate problem: self-signed certificate
untrusted-root       curl: (60) SSL certificate problem: unable to get local issuer certificate
incomplete-chain     curl: (60) SSL certificate problem: unable to get local issuer certificate
revoked              OK

What to read out of this. Two results here are more interesting than the other four.

First — untrusted-root and incomplete-chain gave the identical message despite being completely different problems. One is a chain signed by a CA nobody trusts; the other is a perfectly good chain from a trusted CA with a link missing in the middle. The client cannot tell them apart, because from its point of view the symptom is the same: I ran out of chain before reaching something I trust.

This is why unable to get local issuer certificate is so often misdiagnosed. People read it as "the CA is untrusted" and go hunting for trust-store problems, when the actual fix is almost always on the server: send the intermediate. Module 05 shows you how to tell the two apart in about ten seconds using the -showcerts output from D3.1.

Second — revoked returned OK. The certificate for that host has been formally revoked by its CA, and curl connected anyway without a murmur.

🔑 That is not a bug in curl. Revocation checking in the TLS ecosystem is soft-fail by default almost everywhere — if the check cannot be completed, or is not attempted, the connection proceeds. It means revocation, the mechanism that is supposed to be your emergency stop when a private key leaks, frequently does not stop anything. Module 09 is entirely about why this happened and what the industry did instead.

Now imagine this at 500 hosts. If your incident response plan says "revoke the certificate", you should know before the incident that revocation may have no observable effect on clients already in the wild. The plan that actually works is: rotate the key, reissue, redeploy, and keep certificate lifetimes short enough that a leaked key expires on its own.


Part E · Putting it together

E1 · How this all fits — the complete picture

Diagram source
flowchart TD
    subgraph P["🧩 The four primitives"]
        SY["Symmetric<br>one key, fast, unlimited size"]
        AS["Asymmetric<br>two keys, slow, tiny size limit"]
        HA["Hashing<br>one-way fingerprint, fixed size"]
    end
    SY --> HY["Hybrid encryption<br>asymmetric sets up the key<br>symmetric carries the data"]
    AS --> HY
    AS --> SG["Digital signature<br>hash it, then transform<br>with the PRIVATE key"]
    HA --> SG
    SG --> CE["📜 CERTIFICATE<br>public key + identity<br>signed by a trusted CA"]
    CE --> TR["Trust anchor<br>CA root already on your machine"]
    TR --> AN["✅ Answers the one question<br>maths cannot:<br>whose key is this?"]
    style SG fill:#ffe6cc,stroke:#d79b00,stroke-width:2px
    style CE fill:#e1d5e7,stroke:#9673a6,stroke-width:3px
    style AN fill:#d5e8d4,stroke:#82b366,stroke-width:2px

Read it as a sentence: hashing plus asymmetric cryptography gives you signatures; a signature over a public key and a name gives you a certificate; a certificate plus a pre-installed trust anchor answers the identity question that cryptography alone cannot.

Everything in the remaining thirteen modules is either a detail of that picture or a consequence of it going wrong.


E2 · Production practice

HabitWhy
Private keys are 0600, owned by the service account, never in gitA key exposed once is compromised permanently — there is no rotating a password, only reissuing every certificate that used it
Generate the key on the machine that will use it; never email or Slack a keyA key that has travelled has been on someone's laptop, in a message archive, and in a backup
TLS 1.2 and 1.3 only; AEAD cipher suites onlyRFC 8996 deprecates everything below, and non-AEAD modes fail silently rather than loudly on tampering
Never ship -k, --insecure, verify=False or rejectUnauthorized: falseIt disables the only defence against an active man-in-the-middle while leaving the connection looking encrypted
SHA-256 or better for anything an adversary can influenceMD5 and SHA-1 still compute without warning — the tooling will not stop you
Know which trust store the failing process actually readsJava, Node, Python-certifi and Firefox each keep their own, independent of the OS store
Treat unable to get local issuer certificate as a server problem firstIt is far more often a missing intermediate than an untrusted CA — and the two produce an identical message
Alert on openssl x509 -checkend across the estate, not on a calendar reminderCalendar reminders are attached to people, and people change teams
Assume revocation will not save you; plan to rotate and reissue insteadRevocation checking is soft-fail almost everywhere, as Exercise D3.2 demonstrated
Let VMs and containers generate their own keys on first bootBaked-in keys and baked-in entropy state produce correlated, guessable keys across a fleet

E3 · Capstone exercise

Do this without looking anything up. You are going to build a working certificate system from scratch using only the primitives from Part B — no X.509, no CA software, just openssl and shell.

If you can complete it, you understand what a certificate is at a level most candidates never reach, because you will have built one rather than described one.

Brief. In ~/tls-lab/capstone/, build the following:

  1. Two keypairs: one for a CA, one for a server. Both private keys must be 0600.
  2. A plain-text "certificate" file that contains the server's public key, the hostname shop.internal, and an expiry date.
  3. A signature over that file, produced by the CA's private key.
  4. A verify.sh script that takes a requested hostname and returns success only if all three of these hold: the CA signature is valid, the hostname in the file matches the one requested, and the expiry date is in the future.
  5. Proof that the script rejects a certificate whose hostname has been edited after signing.
  6. Proof that the script rejects a certificate signed by a different, untrusted key.
Model answer — attempt it first, then click

Setup:

bash
mkdir -p ~/tls-lab/capstone && cd ~/tls-lab/capstone
umask 077

# 1. The two keypairs
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out ca.key
openssl pkey -in ca.key -pubout -out ca.pub

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out server.key
openssl pkey -in server.key -pubout -out server.pub

chmod 600 ca.key server.key

2. The "certificate" — identity + public key + expiry, all in one file:

bash
{
  echo "hostname: shop.internal"
  echo "notAfter: 2027-01-01"
  echo "issuer:   my-tiny-ca"
  echo "--- BEGIN SUBJECT PUBLIC KEY ---"
  cat server.pub
} > shop.cert

3. The CA signs it:

bash
openssl dgst -sha256 -sign ca.key -out shop.cert.sig shop.cert

4. The verifier:

bash
cat > verify.sh <<'EOF'
#!/usr/bin/env bash
set -uo pipefail
CERT="$1"; SIG="$2"; TRUSTED_CA_PUB="$3"; REQUESTED_HOST="$4"

# Check 1 - is the CA signature valid? (integrity + authenticity)
if ! openssl dgst -sha256 -verify "$TRUSTED_CA_PUB" -signature "$SIG" "$CERT" >/dev/null 2>&1; then
  echo "REJECT: signature does not verify against the trusted CA key"; exit 1
fi

# Check 2 - does the name match what we asked for?
cert_host=$(awk '/^hostname:/{print $2}' "$CERT")
if [[ "$cert_host" != "$REQUESTED_HOST" ]]; then
  echo "REJECT: certificate is for '$cert_host', we asked for '$REQUESTED_HOST'"; exit 1
fi

# Check 3 - has it expired?
not_after=$(awk '/^notAfter:/{print $2}' "$CERT")
if [[ $(date -d "$not_after" +%s) -lt $(date +%s) ]]; then
  echo "REJECT: expired on $not_after"; exit 1
fi

echo "ACCEPT: '$cert_host' verified against the trusted CA, valid until $not_after"
EOF
chmod +x verify.sh

The happy path:

bash
./verify.sh shop.cert shop.cert.sig ca.pub shop.internal
# ACCEPT: 'shop.internal' verified against the trusted CA, valid until 2027-01-01

5. Requirement 5 — tamper with the hostname after signing:

bash
cp shop.cert evil.cert
sed -i 's/shop.internal/bank.internal/' evil.cert
./verify.sh evil.cert shop.cert.sig ca.pub bank.internal
# REJECT: signature does not verify against the trusted CA key

Note which check caught it — the signature, not the hostname check. Changing one character changed the file's hash (B4.1), so the signature no longer verifies (B5.2). The attacker cannot edit a signed document at all, which is the entire point.

6. Requirement 6 — a certificate signed by an untrusted key:

bash
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out attacker.key
sed 's/shop.internal/bank.internal/' shop.cert > fake.cert
openssl dgst -sha256 -sign attacker.key -out fake.cert.sig fake.cert

# The attacker's own signature over their own file is internally perfectly valid:
openssl pkey -in attacker.key -pubout -out attacker.pub
openssl dgst -sha256 -verify attacker.pub -signature fake.cert.sig fake.cert
# Verified OK      <-- the maths is fine!

# But our verifier only trusts ca.pub:
./verify.sh fake.cert fake.cert.sig ca.pub bank.internal
# REJECT: signature does not verify against the trusted CA key

The three things this capstone is really teaching:

  1. Requirement 6 is the whole of PKI in five lines. The attacker produced a technically flawless signature. It was rejected for one reason only: we chose in advance whose public key we would accept. That choice — hard-coded ca.pub here, the OS root store in real life — is the trust anchor from C3, and it is the only thing standing between you and the man-in-the-middle from C1.
  2. Three independent checks, and all three must pass. Signature valid, name matches, not expired. Real certificate validation adds more (revocation, key usage, path length, name constraints) but the shape never changes, and a real X.509 certificate is exactly this file in a stricter binary format.
  3. The name check is separate from the signature check. A signature proves the document is authentic; it says nothing about whether the document is about the site you wanted. A perfectly valid, perfectly trusted certificate for attacker.com must still be rejected when you asked for bank.com. That separation is why wrong.host.badssl.com in D3.2 produced a different error from the others.

E4 · Official documentation — what to bookmark and how to read it

The single most useful link for this module: the OpenSSL command index — every openssl subcommand, each with its own page.

Make it a reflex: before copying an openssl incantation off a blog, open that subcommand's page and read what the flags actually do. Most bad TLS advice on the internet is a working command from 2012 that quietly does the wrong thing on OpenSSL 3.x.

Core reference pages

LinkWhat it is for
OpenSSL command indexEvery subcommand. The page you will open most often in this track
OpenSSL documentation homeRoot of everything, with a version selector — check it matches your openssl version, or you will read about flags you do not have
openssl s_clientThe diagnostic workhorse: connect, inspect, dump the chain
openssl x509 · openssl genpkey · openssl pkeyRead certificates, create keys, convert keys
openssl dgst · openssl enc · openssl pkeyutlThe four primitives from Part B, as commands
RFC 8446 — TLS 1.3The protocol itself. Dense, but §1–§2 are genuinely readable and worth an hour
RFC 5280 — X.509 certificate profileThe definitive answer to "what does this certificate field mean". Module 03 lives here
RFC 8996 — Deprecating TLS 1.0 and 1.1The citation to quote when someone asks you to re-enable an old protocol
RFC 8017 — PKCS #1 (RSA)RSA encryption and signature schemes, including the padding that caused B2.2's size limit
NIST FIPS 197 (AES) · FIPS 180-4 (SHA) · FIPS 186-5 (signatures)The primary standards behind the three algorithms in Part B
TLSRef — Server-Side TLS guidance · TLSRef ConfiguratorThe successor to Mozilla's Server Side TLS pages. Generates safe config for nginx, Apache, HAProxy and more
SSL Labs Server TestGrades any public HTTPS endpoint and explains every deduction. Run it against something you own
badssl.comDeliberately broken endpoints for every failure mode. Your practice range for the whole track
CA/Browser Forum — TLS Baseline RequirementsThe rules every public CA must follow. Where certificate lifetime limits actually come from (Module 10)

How to read an OpenSSL manual page

Every subcommand page has the same shape, and reading it in this order is fastest:

  1. SYNOPSIS — the full flag list at a glance. Skim it to confirm the flag you want exists in your version.
  2. DESCRIPTION — one paragraph on what the command is for. Confirms you picked the right subcommand.
  3. OPTIONS — the section you will spend the most time in. Watch for whether an option takes a value, and what the default is when you omit it.
  4. EXAMPLES — usually the fastest route to a working command; most pages have several.
  5. HISTORY — the section people skip and then regret. It tells you which OpenSSL version added or removed an option.
Always check the version selector at the top of docs.openssl.org. OpenSSL 3.x renamed subcommands, moved legacy algorithms into a separate provider, and changed default security levels. Reading the master docs while running 1.1.1 produces the classic "unknown option" confusion, and the error message never tells you that version skew is the cause.

The offline alternative

The same documentation ships with OpenSSL, so a browser is never strictly required:

bash
openssl help                        # every subcommand, grouped
openssl x509 -help                  # every flag for one subcommand
openssl list -digest-algorithms     # what hashes this build supports
openssl list -cipher-algorithms     # what ciphers this build supports
openssl list -providers             # default / legacy / fips - OpenSSL 3.x only
man openssl-x509                    # the full man page, if the docs package is installed
man openssl-s_client
apropos openssl | head -30          # discover subcommands you did not know existed
🧪 Exercise E4.1 — Answer a real question without opening a browser

You need to know whether your OpenSSL can still compute SHA-1, and which providers are loaded. Find out from the CLI alone.

bash
openssl list -digest-algorithms | grep -i sha1
openssl list -providers
openssl dgst -help 2>&1 | head -20
Expected result — click to reveal
plain text
  SHA1
  SHA-1 => SHA1
  SSL3-SHA1 => SHA1

Providers:
  default
    name: OpenSSL Default Provider
    version: 3.0.13
    status: active

Usage: dgst [options] [file...]

General options:
 -help               Display this summary
 -list               List digests
 -engine val         Use engine e, possibly a hardware device

Output options:
 -c                  Print the digest in two digit groups separated by colons
 -hex                Print the digest as hex (default)
 -binary             Print the digest in binary form
 -out outfile        Output to filename rather than stdout

What to read out of this.

  • SHA-1 is present and active in the default provider. Confirming Exercise B4.2's lesson from a different angle: the algorithm is not removed, it is merely unwise. Your tooling will not protect you from your own choices here.
  • Only the default provider is loaded. If you ever need to open an old file encrypted with RC4 or 3DES, you will need -provider legacy and the error without it is a generic "unsupported" that gives no hint. This is OpenSSL 3.x-specific and catches everybody once.
  • -help on a subcommand is grouped by purpose, not alphabetical — general options, then input, then output. Once you know that, skimming a long flag list gets much faster.

⚠️ openssl list does not exist on OpenSSL 1.1.1 or on LibreSSL. If it errors, that is itself the answer to "which OpenSSL am I running?" — go back to Exercise D1.1.


E5 · Self-assessment

Answer each out loud before opening it. If your answer is materially thinner than the one behind the toggle, that topic is worth a second pass.

1. Name the three problems TLS solves, and the mechanism that solves each.

Eavesdropping → encryption. Tampering → hashing / authenticated encryption. Impersonation → certificates.

The key point: they are three separate problems with three separate mechanisms. Encryption alone leaves you perfectly confidential with an attacker.

2. Why can't RSA be used to encrypt a web page?

RSA cannot encrypt data larger than its modulus minus padding — about 245 bytes for a 2048-bit key. It is also thousands of times slower per byte than AES.

Hence hybrid encryption: asymmetric once to establish a shared secret, symmetric for all the data.

3. In signing, which key is used to sign and which to verify — and why that way round?

Private key signs, public key verifies. It is the opposite of encryption because the goal is opposite: encryption wants only one person to read, so it uses the key only they hold; signing wants everyone to be able to confirm, so it uses the key only the signer holds and everyone checks with the public one.

4. What exactly does Verified OK prove, and what does it not?

It proves the data is unchanged and was signed by the holder of the private key matching the public key you supplied.

It proves nothing about whether that public key belongs to who you think. Hand the verifier the attacker's public key and the attacker's signatures verify perfectly — Exercise B5.3.

5. Define a certificate in one sentence.

A public key plus an identity plus metadata, hashed and signed by the private key of a third party the client already trusts.

Add: it is a public document, signed rather than encrypted, containing nothing secret.

6. Why does the certificate's hash algorithm matter so much? What happened to SHA-1?

A CA signs the hash of the certificate, not the certificate itself. If two different certificates can be made to share a hash, a signature legitimately obtained on the benign one is arithmetically valid on the malicious one.

SHA-1's collision resistance fell (SHAttered, 2017), so browsers hard-failed SHA-1 certificates. SHA-256 is now universal.

7. What is a trust anchor, and how many does your laptop have?

A root CA certificate trusted because it was placed on the machine, not because anything signed it. Roughly 140–150 of them, from dozens of organisations in many jurisdictions.

Any one of them can issue for any hostname, so the web PKI is only as strong as its weakest trusted CA. Certificate Transparency exists to make abuse detectable rather than preventable.

8. unable to get local issuer certificate — what is your first hypothesis and why?

A missing intermediate on the server, not an untrusted CA. Both produce the identical message, and the server misconfiguration is far more common.

Confirm with openssl s_client -showcerts and read the s:/i: ladder — if the chain stops before something your machine trusts, and the missing link is a well-known intermediate, that is your answer.

9. Someone asks you to add verify=False to unblock a deploy. What do you say?

That it removes the only defence against an active man-in-the-middle while leaving the connection looking encrypted, and that the real fix is one of three things: install the missing intermediate on the server, add the internal root to the correct trust store, or point the client at the right CA bundle.

Acceptable for a two-minute diagnostic to isolate the cause. Never acceptable in committed code.

10. Why did curl connect happily to revoked.badssl.com?

Because revocation checking is soft-fail almost everywhere — if the check is not performed, or cannot complete, the connection proceeds.

The operational consequence: an incident plan that relies on revocation as the emergency stop is relying on something that frequently does not stop anything. Rotate the key, reissue, redeploy, and keep lifetimes short.

11. Which OpenSSL are you running, and why does the question matter?

openssl version -a gives version, OPENSSLDIR, and MODULESDIR. It matters because 3.x versus 1.1.1 changes available flags, default security levels and where legacy algorithms live — and because on macOS the openssl on PATH is often LibreSSL, a different implementation entirely.


E6 · Command reference — everything from this module

The commands introduced in Module 01, grouped by what you are trying to achieve. The ones marked ⭐ are genuinely daily-use.

Which OpenSSL am I running

bash
openssl version                                # ⭐ version, fast
openssl version -a                             # ⭐ OPENSSLDIR, MODULESDIR, build options
openssl version -d                             # just OPENSSLDIR (the trust store root)
which -a openssl                               # more than one? investigate
openssl help                                   # ⭐ every subcommand
openssl x509 -help                             # ⭐ every flag for one subcommand
openssl list -providers                        # default / legacy / fips  (3.x only)
openssl list -digest-algorithms                # what hashes this build has
openssl ciphers -v 'DEFAULT'                   # ⭐ what suites this build will offer

Symmetric encryption

bash
openssl enc -aes-256-cbc -pbkdf2 -salt -in f.txt -out f.enc    # ⭐ always use -pbkdf2
openssl enc -d -aes-256-cbc -pbkdf2 -in f.enc -out f.txt       # ⭐ decrypt
openssl rand -hex 32                                           # ⭐ a real random key
openssl rand -base64 24                                        # ⭐ a random password
openssl speed -evp aes-256-gcm                                 # how fast is symmetric here

Keys

bash
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem   # ⭐
openssl pkey -in private.pem -pubout -out public.pem            # ⭐ derive the public half
openssl pkey -in private.pem -noout -text | head                # inspect a key
openssl speed rsa2048                                           # how slow is asymmetric here

Hashing and signatures

bash
openssl dgst -sha256 file.txt                                   # ⭐ hash a file
openssl dgst -sha256 -sign private.pem -out f.sig file.txt      # ⭐ sign
openssl dgst -sha256 -verify public.pem -signature f.sig file.txt   # ⭐ verify
sha256sum file.txt                                              # ⭐ the coreutils shortcut

Asymmetric encrypt / decrypt (small data only)

bash
openssl pkeyutl -encrypt -pubin -inkey public.pem -in msg.txt -out msg.enc
openssl pkeyutl -decrypt -inkey private.pem -in msg.enc

Inspecting a live server

bash
openssl s_client -connect host:443 </dev/null                          # ⭐ the everyday one
openssl s_client -connect host:443 </dev/null 2>/dev/null | grep -E 'Protocol|Cipher\s*:|Verify return code'   # ⭐
openssl s_client -connect host:443 -showcerts </dev/null 2>/dev/null | grep -E 's:|i:'   # ⭐ the chain ladder
openssl s_client -connect host:443 -tls1_2 </dev/null                  # force a version
openssl s_client -connect host:443 </dev/null 2>/dev/null | openssl x509 -out site.pem   # ⭐ save the cert

Inspecting a certificate file

bash
openssl x509 -in site.pem -noout -subject -issuer -dates        # ⭐ the 90% command
openssl x509 -in site.pem -noout -text                          # ⭐ everything
openssl x509 -in site.pem -noout -serial
openssl x509 -in site.pem -noout -pubkey
openssl x509 -in site.pem -noout -checkend 2592000              # ⭐ expires within 30 days?

Trust store

bash
grep -c 'BEGIN CERTIFICATE' /etc/ssl/certs/ca-certificates.crt  # Debian/Ubuntu
grep -c 'BEGIN CERTIFICATE' /etc/pki/tls/certs/ca-bundle.crt    # RHEL family
sudo cp myroot.crt /usr/local/share/ca-certificates/ && sudo update-ca-certificates   # Debian
sudo cp myroot.crt /etc/pki/ca-trust/source/anchors/ && sudo update-ca-trust          # RHEL
curl --cacert /path/to/ca.pem https://host/                     # ⭐ the right fix, not -k

Seeing it on the wire

bash
sudo tcpdump -i any -A -s 0 'tcp port 80 and host example.com'   # plaintext HTTP
sudo tcpdump -i any -A -s 0 'tcp port 443 and host example.com'  # opaque HTTPS
curl -I https://expired.badssl.com/                              # ⭐ practice failures
The four-command reflex worth building into muscle memory when someone says "the site is broken and I think it's SSL":
bash
openssl s_client -connect host:443 </dev/null 2>/dev/null | grep 'Verify return code'
openssl s_client -connect host:443 </dev/null 2>/dev/null | openssl x509 -noout -subject -dates
openssl s_client -connect host:443 -showcerts </dev/null 2>/dev/null | grep -E 's:|i:'
curl -Iv https://host/ 2>&1 | grep -iE 'ssl|certificate|subject|issuer'

Four commands, none of which change anything, and between them they identify the cause of the large majority of real TLS incidents: expired, wrong name, broken chain, or untrusted issuer.


Next — Module 02 · Keys, Encodings & File Formats.

You have already generated keys (Exercise B2.1) and noticed that they arrive as Base64 text between BEGIN/END markers, and that the private file is far larger than the public one. Module 02 explains what is actually inside those files, why .pem, .crt, .key, .der, .p12 and .jks all exist, and how to convert between them without losing your key.

Official reading ahead of it: openssl genpkey and openssl pkey.

📚 Sources for the interview questions

Question selection was cross-referenced against publicly published 2026 SSL/TLS and OpenSSL interview question sets, then rewritten and deepened:

Technical claims were verified against primary sources rather than the question sets: RFC 8446, RFC 5280, RFC 8996, RFC 8017, NIST FIPS 197, FIPS 180-4, FIPS 186-5 and the OpenSSL 3.x manual pages.

Answers were rewritten and deepened rather than reproduced — published versions are usually correct but shallow, and the added operational detail is what actually differentiates a candidate in the room.

Spotted a mistake or want something added? Send me a note.