DELIGHT Cybersecurity Workbook Series  ·  Encryption Series

Intermediate
Cryptography Labs

Four self-guided, hands-on exercises covering asymmetric encryption, TLS analysis, password security, and authenticated encryption.

Prerequisites: Comfortable with the Linux terminal and basic scripting (Python or Bash). Complete the beginner cipher, hashing, and OpenSSL labs first.

Kali Linux recommended

This workbook is designed for Kali Linux. Most tools — OpenSSL, Wireshark, Hashcat, rockyou.txt, John the Ripper, Python 3, and the cryptography library — come pre-installed or are one apt install away. If you are on Ubuntu/Debian, a one-line setup command is provided in the Environment Setup section below. Running on Windows or macOS is possible via WSL2 or Docker but is not covered here.

Environment setup — run this first

Open a terminal on your Kali Linux machine and run the commands below before starting any lab. On Kali, most tools are already present; the commands below confirm versions and install anything missing.

# 1. Confirm you are on Kali (optional check)
cat /etc/os-release | grep PRETTY_NAME

# 2. Update package lists
sudo apt update

# 3. Install / confirm all required tools in one shot
sudo apt install -y openssl wireshark hashcat john python3-pip

# 4. rockyou.txt is shipped compressed on Kali — unzip it
sudo gunzip /usr/share/wordlists/rockyou.txt.gz
# If already unzipped you will see "not in gzip format" — that is fine.

# 5. Install Python libraries used in Labs 1, 3 and 4
pip3 install cryptography bcrypt argon2-cffi

# 6. Allow Wireshark captures without root (Lab 2)
sudo usermod -aG wireshark $USER
newgrp wireshark

# 7. Create a dedicated working directory for all labs
mkdir -p ~/crypto_labs && cd ~/crypto_labs
echo "Working directory: $(pwd)"

Every lab in this workbook assumes your terminal is open inside ~/crypto_labs. Keep it as your working directory throughout.

Kali tip If you are running Kali in a VM, make sure to allocate at least 2 GB of RAM and enable 3D acceleration. Hashcat (Lab 3) benefits from the additional memory. For GPU passthrough to work in VirtualBox or VMware, consult your hypervisor's documentation.
01

Intermediate · ~60 min

RSA Key Pair Generation & PKI

In this lab you will generate an RSA key pair, encrypt a short message using a partner's public key, decrypt it with your private key, and then sign a document and verify that signature. By the end you will have a concrete feel for how asymmetric cryptography underlies email security, code signing, and HTTPS certificates.
Tools: openssl python3 / cryptography gpg
kali@kali:~/crypto_labs — how to run this lab

All commands in this lab run directly in your Kali terminal. OpenSSL is pre-installed on Kali — no setup needed. Navigate to your working directory first, then follow each step in order.

# Make sure you are in the lab directory
cd ~/crypto_labs

# Confirm OpenSSL is available
openssl version
# Expected: OpenSSL 3.x.x  ...

# For the Python bonus steps, confirm the library
python3 -c "from cryptography.hazmat.primitives.asymmetric import rsa; print('OK')"
Learning goal: Understand asymmetric encryption, key exchange, and digital signatures — and explain why the private key must never leave your machine.

Core concept — asymmetric cryptography

RSA uses a mathematically linked pair of keys. The public key encrypts data; only the paired private key can decrypt it. The same pairing is used in reverse for signatures: you sign with the private key and anyone can verify with the public key.

  • Encryption direction: public key → encrypted message → private key decrypts
  • Signature direction: private key signs → public key verifies
  • Security assumption: factoring the product of two large primes is computationally infeasible

Alice → Bob ↓

Plaintext
Alice's message
readable text
Bob's public key
🔒
Encrypt
RSA-OAEP
On the wire
🔐
Ciphertext
unreadable binary
Bob's private key
🔑
Decrypt
only Bob can do this
Plaintext
Message recovered
identical to original
Click any card above to learn what happens at that step.

Alice signs, Bob verifies ↓

Document
📄
Document + hash
SHA-256 digest
Alice's private key
🔒
Sign
encrypt the hash
On the wire
Signature
attached to doc
Alice's public key
🔑
Verify
decrypt the hash
Result
Hashes match?
verified OK / failed
Click any card above to learn what happens at that step.

Switch tabs to compare both RSA operations side by side

Lab steps

STEP 01Generate your RSA key pair

Create a 2048-bit private key, then extract the public key from it. Run these commands directly in your terminal from ~/crypto_labs.

# Generate a 2048-bit private key — output saved to my_private.pem
openssl genrsa -out my_private.pem 2048

# Extract the public key from it
openssl rsa -in my_private.pem -pubout -out my_public.pem

# Verify both files exist
ls -lh my_private.pem my_public.pem

# Inspect the key structure (optional — press q to exit)
openssl rsa -in my_private.pem -text -noout | head -20
Kali OpenSSL 3.x is pre-installed on Kali 2023+. If you see a legacy warning about -text output format, it is cosmetic only — the keys are valid.
Tip In production, use 4096-bit keys or switch to Elliptic Curve (EC) keys for equivalent security at smaller size. For this exercise 2048 bits is fine and faster to generate.
STEP 02Exchange public keys with a partner

Share your my_public.pem with a classmate. Save their key as partner_public.pem in the same directory. If working alone, simulate a second participant:

# Simulate a second user's key pair in the same directory
openssl genrsa -out partner_private.pem 2048
openssl rsa -in partner_private.pem -pubout -out partner_public.pem

# Confirm you now have four .pem files
ls *.pem
STEP 03Encrypt a message with the partner's public key

Write a plaintext message and encrypt it so only the holder of partner_private.pem can read it.

# Create a short plaintext file
echo "Meet me at the clock tower at noon." > message.txt

# Encrypt using the partner's public key (OAEP padding)
openssl pkeyutl -encrypt \
  -pubin -inkey partner_public.pem \
  -in message.txt \
  -out message.enc

# Confirm the output is unreadable binary
xxd message.enc | head -4
# You should see hex bytes — no readable text
Note RSA is only suitable for short messages (under ~190 bytes for a 2048-bit key). For larger data, encrypt a random AES key with RSA and use AES for the data — this is hybrid encryption, covered in Lab 4.
STEP 04Decrypt with the private key

Use the partner's private key to recover the original message. In a real exchange your partner would do this with their private key on their machine.

# Decrypt using partner's private key
openssl pkeyutl -decrypt \
  -inkey partner_private.pem \
  -in message.enc \
  -out message_decrypted.txt

# Read the recovered plaintext
cat message_decrypted.txt
# Expected: Meet me at the clock tower at noon.
STEP 05Sign a document and verify the signature

Digital signatures prove authenticity and integrity. OpenSSL hashes the document and encrypts that hash with your private key. Then tamper with the document to watch verification fail.

# Create the document to sign
echo "I, Alice, agree to the terms dated 2024-01-15." > contract.txt

# Sign it with your private key
openssl dgst -sha256 -sign my_private.pem \
  -out contract.sig contract.txt

# Verify the signature using your public key
openssl dgst -sha256 -verify my_public.pem \
  -signature contract.sig contract.txt
# Expected output: Verified OK

# Now tamper: append a dot to the document
echo "." >> contract.txt

# Verify again — it should now FAIL
openssl dgst -sha256 -verify my_public.pem \
  -signature contract.sig contract.txt
# Expected output: Verification Failure
Think about it Even a single extra character breaks the signature. This is the avalanche effect — the same property you saw with hash functions in the beginner lab — now embedded inside the signing process.
BONUSRepeat using Python's cryptography library

Save the script as rsa_demo.py in ~/crypto_labs and run it with python3 rsa_demo.py.

# rsa_demo.py — save this file then run: python3 rsa_demo.py
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes

private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key  = private_key.public_key()

message = b"Meet me at the clock tower."
ciphertext = public_key.encrypt(
    message,
    padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),
                 algorithm=hashes.SHA256(), label=None)
)
print(f"Encrypted (first 20 bytes): {ciphertext[:20].hex()}")

plaintext = private_key.decrypt(
    ciphertext,
    padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),
                 algorithm=hashes.SHA256(), label=None)
)
print(f"Decrypted: {plaintext.decode()}")
# Run it
python3 rsa_demo.py
?Checkpoint questions

1. Alice wants to send Bob an encrypted message. Which key does she use to encrypt it?

A Her own private key
B Bob's public key
C A shared symmetric key
D Bob's private key

2. What does a successful Verified OK tell you about a digitally signed document?

It confirms two things: (1) the document was signed by whoever holds the private key corresponding to the public key used to verify — proving authenticity; and (2) the document has not been altered since it was signed — proving integrity. It does not confirm that the public key genuinely belongs to who you think it does — that requires a certificate issued by a trusted CA, which you will build in the Advanced labs.

02

Intermediate · ~50 min

TLS/SSL Traffic Analysis

You will capture real HTTPS traffic with Wireshark, observe the TLS handshake in detail, and then use the browser's session key log to fully decrypt and read the encrypted conversation. This exercise bridges the theory of asymmetric and symmetric encryption with what actually happens on the wire every time you visit a website.
Tools: Wireshark SSLKEYLOGFILE Firefox curl
kali@kali:~/crypto_labs — how to run this lab

This lab requires your local Kali Linux machine — it cannot be run on Colab or a remote server because it requires access to a real network interface and a locally running browser. Wireshark is pre-installed on Kali. The key step is setting the SSLKEYLOGFILE environment variable in the same terminal session from which you launch Firefox, so both processes share the variable.

# Confirm Wireshark is installed
wireshark --version | head -1

# Confirm you are in the wireshark group (set during environment setup)
groups | grep wireshark
# If not listed: sudo usermod -aG wireshark $USER && newgrp wireshark

# Confirm Firefox is installed
firefox --version
Learning goal: Understand how encryption works inside a real network protocol — specifically how TLS negotiates keys, why forward secrecy matters, and what an attacker actually sees on the wire.

Core concept — the TLS handshake

TLS sets up an encrypted channel in two phases. The handshake uses asymmetric crypto to authenticate the server and agree on a shared secret. The record layer then uses fast symmetric crypto (AES-GCM or ChaCha20-Poly1305) for all actual data.

  • ClientHello — client sends supported cipher suites, TLS version, a random nonce
  • ServerHello + Certificate — server picks cipher suite, sends its certificate
  • Key exchange — ECDHE or DHE to derive session keys without transmitting them
  • Finished — both sides confirm with a MAC; encrypted data flows thereafter
CLIENT
(Browser)
SERVER
(Website)
Client → Server
ClientHello
TLS version, cipher suites, client random
Server → Client
ServerHello + Certificate
Chosen cipher suite, server random, X.509 cert
Client → Server
ClientKeyShare
ECDHE public value, derives shared secret
Server → Client
ServerKeyShare + Finished
Server's ECDHE value, MAC confirming handshake
Client ↔ Server (both directions)
🔒 Application Data
AES-256-GCM encrypted — HTTPS traffic
ClientHello — the handshake begins
Client sendsUnencrypted
The client opens the handshake by announcing what TLS versions and cipher suites it supports, along with a 32-byte random nonce. The nonce is used later to derive the session keys — it ensures keys are unique per connection even between the same client and server. This message is sent in plaintext: an attacker can see what cipher suites the client prefers, but cannot yet do anything harmful. Wireshark's tls.handshake.type == 1 filter isolates this packet.

Click each message to see what it contains and why it matters

STEP 01Set SSLKEYLOGFILE and launch Firefox from the terminal

Both commands must run in the same terminal session. Setting the environment variable and then launching Firefox from that same session ensures Firefox inherits the variable and writes session keys to the log file.

# Set the key log file path (stays active for this terminal session)
export SSLKEYLOGFILE=~/crypto_labs/tls_keys.log

# Confirm the variable is set
echo $SSLKEYLOGFILE
# Expected: /home/kali/crypto_labs/tls_keys.log

# Launch Firefox from this same terminal (& runs it in background)
firefox &

# Visit a simple JSON API in Firefox — easy to spot after decryption
# Browse to: https://httpbin.org/get

# After visiting the site, confirm the key log was written
ls -lh ~/crypto_labs/tls_keys.log
cat ~/crypto_labs/tls_keys.log | head -5
Warning The keylog file contains session secrets. Delete it after the lab with rm ~/crypto_labs/tls_keys.log and never use this technique on a machine handling real sensitive data.
STEP 02Capture HTTPS traffic with Wireshark

You can launch Wireshark from the terminal or from the Kali application menu. Starting from the terminal lets you see any errors.

# Launch Wireshark (GUI will open)
wireshark &

# Alternatively, capture directly to file from terminal (no GUI needed)
sudo tcpdump -i any -w ~/crypto_labs/capture.pcap port 443 &
# Browse to https://httpbin.org/get in Firefox, then stop tcpdump:
sudo pkill tcpdump

# Open the saved capture in Wireshark
wireshark ~/crypto_labs/capture.pcap

In Wireshark, type tcp port 443 in the display filter bar. You will see TLS records with Application Data payloads — unreadable binary at this point.

Kali If Wireshark shows "You don't have permission to capture on that device", run sudo usermod -aG wireshark $USER && newgrp wireshark and restart your terminal.
STEP 03Inspect the TLS handshake packets

In Wireshark's filter bar, type tls.handshake to isolate handshake packets. Click a ClientHello packet, expand the TLS layer in the detail pane, and locate:

  • The TLS version (look for 0x0304 = TLS 1.3)
  • The cipher suites offered (look for TLS_AES_256_GCM_SHA384)
  • The 32-byte client random nonce

Then click the Certificate packet and find the server's domain in the Subject Alternative Name (SAN) field.

STEP 04Load the key log into Wireshark to decrypt traffic

In Wireshark, go to Edit → Preferences → Protocols → TLS. Set the (Pre)-Master-Secret log filename field to the full path of your keylog file, then click OK.

# Get the full path to paste into Wireshark
realpath ~/crypto_labs/tls_keys.log
# e.g. /home/kali/crypto_labs/tls_keys.log
What you should see The Application Data packets will now show the decrypted HTTP/2 response body from httpbin.org — including the JSON payload and your browser's User-Agent header. This is exactly what a network administrator with access to session keys would see.
STEP 05Record your observations

Fill in the table below in your own notes using what you find in Wireshark:

FieldWhere to find it in WiresharkYour observation
Cipher suite chosenServerHello → Cipher Suitewrite here
Key exchange algorithmLook for ECDHE or DHE in the suite namewrite here
Certificate valid untilCertificate → Validity → Not Afterwrite here
Certificate issuer (CA)Certificate → Issuer → CNwrite here
Encrypted record sizeApplication Data packet → Length fieldwrite here
CLEANUPDelete sensitive files after the lab
# Remove the session key log and capture file
rm ~/crypto_labs/tls_keys.log
rm ~/crypto_labs/capture.pcap

# Unset the environment variable for this session
unset SSLKEYLOGFILE
?Checkpoint questions

1. Why can Wireshark decrypt TLS traffic when given the SSLKEYLOGFILE, but a normal network attacker cannot?

The SSLKEYLOGFILE contains session keys derived after the handshake — keys generated fresh for each connection and never transmitted over the network. An attacker intercepting traffic only sees the handshake public-key material and encrypted records. With TLS 1.3 ECDHE, even later stealing the server's private key does not decrypt past sessions. This is called forward secrecy.

2. What is the purpose of the client and server random nonces sent during the handshake?

Nonces ensure derived session keys are unique for every connection, even between the same client and server. Without them, an attacker who recorded two sessions with identical key material could correlate or replay them. Nonces also prevent replay attacks against the handshake itself.

03

Intermediate · ~60 min

Password Hashing & Cracking

You will create password hashes using three algorithms of varying strength — MD5, bcrypt, and Argon2 — then use Hashcat with the rockyou wordlist to crack the weakest ones. Timing the attacks makes the difference concrete: what takes milliseconds against MD5 takes years against a properly configured Argon2 hash.
Tools: python3 bcrypt argon2-cffi hashcat rockyou.txt
kali@kali:~/crypto_labs — how to run this lab

Hashcat and rockyou.txt are pre-installed on Kali Linux. The rockyou wordlist ships compressed — unzip it once if you have not done so already. Python libraries need a one-time install. All commands run in your terminal from ~/crypto_labs.

# Decompress rockyou if not already done
sudo gunzip /usr/share/wordlists/rockyou.txt.gz 2>/dev/null
ls -lh /usr/share/wordlists/rockyou.txt
# Should show ~134 MB uncompressed

# Install Python password hashing libraries
pip3 install bcrypt argon2-cffi

# Confirm Hashcat is available
hashcat --version

# Make sure you are in the working directory
cd ~/crypto_labs
Kali GPU Hashcat uses the GPU by default if one is available. In a VM without GPU passthrough, it falls back to CPU mode. Cracking will be slower but all exercises still complete. Add --force to suppress the CPU-mode warning if needed.
Learning goal: Explain why fast hashing algorithms are inappropriate for passwords, understand how salting defeats rainbow tables, and articulate the role of key stretching in password hashing functions.

Core concept — why MD5 is not enough for passwords

Cryptographic hash functions are designed to be fast. That is exactly the wrong property for password storage. An attacker with a GPU can compute billions of MD5 hashes per second, so a leaked MD5 database can be cracked with a wordlist in minutes.

  • Salt: a random value prepended to the password before hashing — makes identical passwords produce different hashes, defeating precomputed rainbow tables
  • Key stretching: deliberately repeating the hash thousands of times to slow each guess (bcrypt cost factor, Argon2 iterations)
  • Memory hardness: Argon2 forces large RAM usage per guess, killing GPU parallelism

Algorithm comparison at a glance

AlgorithmSpeedSalt?Stretching?Suitable for passwords?
MD5~10 billion/s (GPU)Manual onlyNoNo
SHA-256~4 billion/s (GPU)Manual onlyNoNo
bcrypt~20 k/s (cost 12)Built-in (22 chars)Yes (cost factor)Yes (aging)
Argon2idTunable / slowBuilt-inYes + memory-hardBest current
STEP 01Hash passwords with Python and compare timing

Save the script as hash_compare.py and run it. Observe the timing difference — it tells you directly how hard each algorithm is to crack.

# Save as ~/crypto_labs/hash_compare.py
import hashlib, bcrypt, time
from argon2 import PasswordHasher

password = b"hunter2"

t0 = time.perf_counter()
md5_hash = hashlib.md5(password).hexdigest()
print(f"MD5 : {md5_hash}")
print(f"  time: {(time.perf_counter()-t0)*1000:.3f} ms\n")

t0 = time.perf_counter()
bc_hash = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
print(f"bcrypt: {bc_hash.decode()}")
print(f"  time: {(time.perf_counter()-t0)*1000:.0f} ms\n")

ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=1)
t0 = time.perf_counter()
arg_hash = ph.hash(password.decode())
print(f"Argon2: {arg_hash}")
print(f"  time: {(time.perf_counter()-t0)*1000:.0f} ms")
# Run it
python3 hash_compare.py
Observe MD5 takes under 1 ms; bcrypt around 300 ms; Argon2 around 100–500 ms. Run the script twice — bcrypt and Argon2 produce a different hash string each time because a new random salt is embedded, while MD5 always returns the same hex string for the same input.
STEP 02Generate MD5 hashes and crack them with Hashcat

Create a file of MD5 hashes, then use Hashcat with rockyou.txt to crack them.

# Generate MD5 hashes of common passwords
python3 -c "
import hashlib
passwords = ['password', 'iloveyou', 'monkey', 'qwerty123', 'hunter2']
for p in passwords:
    print(hashlib.md5(p.encode()).hexdigest())
" > md5_hashes.txt

cat md5_hashes.txt

# Run Hashcat: mode 0 = raw MD5, attack mode 0 = dictionary
hashcat -m 0 -a 0 md5_hashes.txt /usr/share/wordlists/rockyou.txt

# If running in a VM without GPU, add --force to suppress the warning
# hashcat -m 0 -a 0 --force md5_hashes.txt /usr/share/wordlists/rockyou.txt

# Show cracked results from the potfile
hashcat -m 0 md5_hashes.txt --show
Ethics Only crack hashes you created yourself or that belong to a lab environment you own. Using these techniques against real systems without permission is illegal. These tools are taught so you understand attacker capabilities and can build better defences.
STEP 03Attempt to crack bcrypt — compare speeds

Generate a bcrypt hash and watch the H/s (hashes per second) metric in Hashcat. The difference in cracking speed compared to MD5 is the core takeaway.

# Generate a bcrypt hash of "monkey"
python3 -c "
import bcrypt
h = bcrypt.hashpw(b'monkey', bcrypt.gensalt(rounds=12))
print(h.decode())
" > bcrypt_hash.txt

cat bcrypt_hash.txt

# Attack it — mode 3200 = bcrypt
hashcat -m 3200 -a 0 bcrypt_hash.txt /usr/share/wordlists/rockyou.txt

# Watch the H/s figure: ~10 billion/s for MD5 vs ~20,000/s for bcrypt
# "monkey" is in rockyou so it should still crack — but compare the ETA
# for a full rockyou scan vs the MD5 run.
STEP 04Demonstrate salting — rainbow table defeat

Save and run this script to see how salting ensures two users with the same password store different hashes.

# Save as ~/crypto_labs/salt_demo.py
import hashlib, os

password = "password123"

# Without salt — same input always gives same output
h1 = hashlib.sha256(password.encode()).hexdigest()
h2 = hashlib.sha256(password.encode()).hexdigest()
print(f"Same hash (no salt): {h1 == h2}")

# With a unique salt per user
salt1 = os.urandom(16).hex()
salt2 = os.urandom(16).hex()
h1 = hashlib.sha256((salt1 + password).encode()).hexdigest()
h2 = hashlib.sha256((salt2 + password).encode()).hexdigest()
print(f"Same hash (with salt): {h1 == h2}")
print(f"User 1 stored: {salt1}:{h1}")
print(f"User 2 stored: {salt2}:{h2}")
# Run it
python3 salt_demo.py
?Checkpoint questions

1. An application stores passwords as unsalted SHA-256 hashes. A database breach exposes these hashes. Why is this serious even though SHA-256 is "cryptographically secure"?

The attacker does not invert the hash — they just hash millions of candidate passwords and compare. SHA-256 runs at ~4 billion hashes per second on a GPU, so the entire rockyou wordlist (14 million entries) is tested in about 3.5 milliseconds. Without salts, identical passwords share an identical hash, so cracking one reveals all accounts with that password simultaneously.

2. What is the purpose of the cost factor in bcrypt, and how should you choose its value?

The cost factor controls the number of iterations: each increment doubles the work. It should be calibrated so that hashing one password takes about 100–300 ms on your server hardware — slow enough to hurt attackers but fast enough for normal logins. The cost is embedded in the hash string itself, so you can increase it over time as hardware improves: re-hash the user's password on their next successful login.

04

Intermediate · ~70 min

AES-GCM File Encryption

You will implement a command-line file encryption tool in Python using AES-256-GCM — an authenticated encryption scheme that simultaneously provides confidentiality and integrity. You will also see what happens when an attacker flips a single bit in the ciphertext, demonstrating why unauthenticated encryption is dangerous in practice.
Tools: python3 cryptography (PyPI) Fernet (comparison)
kali@kali:~/crypto_labs — how to run this lab

This lab is entirely Python — no special system tools beyond what is pre-installed on Kali. Every script is saved as a .py file in ~/crypto_labs and run with python3. The cryptography library may already be present on Kali; the install command below is safe to run either way.

# Install (or confirm) the cryptography library
pip3 install cryptography
python3 -c "import cryptography; print(cryptography.__version__)"

# Make sure you are in the working directory
cd ~/crypto_labs
Learning goal: Implement encryption correctly in code; understand the difference between encryption and authenticated encryption; appreciate the risks of unauthenticated ciphertext.

Core concept — authenticated encryption (AEAD)

Encryption provides confidentiality — attackers cannot read the plaintext. But without authentication, an attacker who can modify ciphertext in transit can cause controlled changes to the decrypted output without knowing the key. AES-GCM solves this with a Galois/Counter Mode authentication tag appended to the ciphertext. Decryption fails immediately if even a single bit has been altered.

  • Nonce (IV): a 96-bit random value; must be unique per encryption with the same key
  • Auth tag: 128-bit MAC over ciphertext + associated data; verifies integrity
  • Associated data (AAD): optional metadata authenticated but not encrypted

Click any segment to learn what it does

Salt
16 bytes
Not secret
Nonce / IV
12 bytes
Not secret
Ciphertext
same length as plaintext
Encrypted
Auth tag
16 bytes
Integrity
AAD
optional
Authenticated
Salt — input to key derivation
Not secretStored in file
The salt is a random 16-byte value generated fresh every time a file is encrypted. It is passed to PBKDF2 alongside the user's passphrase to derive the 256-bit AES key. Because the salt is different each time, two files encrypted with the same passphrase will always produce different keys and therefore completely different ciphertext — preventing anyone from detecting that you encrypted similar content twice. The salt is not secret and must be stored with the file so decryption can re-derive the same key.

Each segment is a distinct part of the encrypted file — click to explore

STEP 01Write and run the encryption script

Save this as encrypt_file.py in ~/crypto_labs. The key is derived from your passphrase using PBKDF2 — never hard-code keys in source files.

# Save as ~/crypto_labs/encrypt_file.py
import os, sys
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
import getpass

def derive_key(passphrase, salt):
    kdf = PBKDF2HMAC(algorithm=hashes.SHA256(), length=32,
                     salt=salt, iterations=390000)
    return kdf.derive(passphrase.encode())

def encrypt_file(input_path, output_path, passphrase):
    salt  = os.urandom(16)
    nonce = os.urandom(12)
    key   = derive_key(passphrase, salt)
    aesgcm = AESGCM(key)
    with open(input_path, "rb") as f:
        plaintext = f.read()
    ciphertext = aesgcm.encrypt(nonce, plaintext, None)
    with open(output_path, "wb") as f:
        f.write(salt + nonce + ciphertext)
    print(f"Encrypted: {input_path} -> {output_path}")
    print(f"  Salt : {salt.hex()}")
    print(f"  Nonce: {nonce.hex()}")

if __name__ == "__main__":
    pw = getpass.getpass("Passphrase: ")
    encrypt_file(sys.argv[1], sys.argv[1] + ".enc", pw)
# Create a test file and encrypt it
echo "TOP SECRET: Project Nightingale details go here." > secret.txt
python3 encrypt_file.py secret.txt
# You will be prompted for a passphrase — remember it for decryption

# Confirm the output is unreadable binary
xxd secret.txt.enc | head -4
STEP 02Write and run the decryption script

Save this as decrypt_file.py. It reads the salt and nonce from the file header — neither is secret; only the passphrase must be protected.

# Save as ~/crypto_labs/decrypt_file.py
import sys, getpass
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.exceptions import InvalidTag
from encrypt_file import derive_key

def decrypt_file(enc_path, out_path, passphrase):
    with open(enc_path, "rb") as f:
        data = f.read()
    salt       = data[:16]
    nonce      = data[16:28]
    ciphertext = data[28:]
    key    = derive_key(passphrase, salt)
    aesgcm = AESGCM(key)
    try:
        plaintext = aesgcm.decrypt(nonce, ciphertext, None)
    except InvalidTag:
        print("Decryption failed: auth tag mismatch.")
        print("The ciphertext was tampered with, or the passphrase is wrong.")
        sys.exit(1)
    with open(out_path, "wb") as f:
        f.write(plaintext)
    print(f"Decrypted: {enc_path} -> {out_path}")

if __name__ == "__main__":
    pw = getpass.getpass("Passphrase: ")
    decrypt_file(sys.argv[1], sys.argv[1].replace(".enc", ".dec"), pw)
# Decrypt the file (enter the same passphrase you used to encrypt)
python3 decrypt_file.py secret.txt.enc

# Compare original and decrypted — should be identical
diff secret.txt secret.txt.dec && echo "Files match!"
STEP 03Demonstrate authentication — tamper with the ciphertext

Flip a single byte in the encrypted file and attempt to decrypt it. AES-GCM's authentication tag will reject it immediately.

# Flip one byte inside the ciphertext (past the 28-byte header)
python3 -c "
with open('secret.txt.enc', 'rb') as f:
    data = bytearray(f.read())
data[30] ^= 0xFF  # flip byte at position 30
with open('secret_tampered.enc', 'wb') as f:
    f.write(data)
print('Tampered file written.')
"

# Attempt decryption — should fail with auth tag mismatch
python3 decrypt_file.py secret_tampered.enc
# Expected: Decryption failed: auth tag mismatch.
Key insight This is exactly why authenticated encryption matters. With plain AES-CBC (no auth tag), the same bit-flip would silently produce garbled but "decryptable" output — and a skilled attacker can exploit this to recover plaintext byte-by-byte. That attack is the padding oracle, covered in the Advanced labs.
STEP 04Add authenticated associated data (AAD)

AAD lets you authenticate metadata alongside the ciphertext without encrypting it. Save and run this standalone demo.

# Save as ~/crypto_labs/aad_demo.py
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.exceptions import InvalidTag

key    = AESGCM.generate_key(bit_length=256)
nonce  = os.urandom(12)
aesgcm = AESGCM(key)

plaintext = b"Confidential payload"
aad       = b"recipient:alice,version:1"

ciphertext = aesgcm.encrypt(nonce, plaintext, aad)
print("Encrypted OK")

# Correct AAD — decrypts fine
msg = aesgcm.decrypt(nonce, ciphertext, aad)
print(f"Decrypted: {msg}")

# Wrong AAD — auth failure
try:
    aesgcm.decrypt(nonce, ciphertext, b"recipient:bob,version:1")
except InvalidTag:
    print("Auth tag mismatch: AAD was changed!")
# Run it
python3 aad_demo.py
BONUSCompare with Fernet — batteries-included AEAD

Fernet is a high-level recipe combining AES-128-CBC + HMAC-SHA256. Simpler to use correctly but less flexible than raw AES-GCM.

# Run inline — no file needed
python3 -c "
from cryptography.fernet import Fernet
key   = Fernet.generate_key()
f     = Fernet(key)
token = f.encrypt(b'Secret message')
print(f'Token: {token[:40]}...')
msg   = f.decrypt(token)
print(f'Decrypted: {msg}')
"
When to use AES-GCM over Fernet Use raw AES-GCM when you need 256-bit keys, streaming encryption for large files, authenticated associated data (AAD), or interoperability with non-Python systems. Fernet is the simpler choice for Python-only projects where 128-bit keys are acceptable.
?Checkpoint questions

1. You encrypt two files using AES-256-GCM with the same key. Why is it critical to use a different nonce for each, and what can an attacker do if you reuse a nonce?

If the same (key, nonce) pair is used twice, an attacker who sees both ciphertexts can XOR them together, cancelling the keystream and leaking information about the relationship between the plaintexts. Worse, nonce reuse in GCM allows recovery of the GHASH authentication key, completely breaking authentication for past and future messages under that key. Always generate a fresh random nonce per encryption.

2. Why is the salt stored unencrypted at the start of the output file, even though it was used to derive the encryption key?

The salt is not secret. Its only purpose is to ensure the same passphrase produces a different derived key each time a file is encrypted, so two files encrypted with the same passphrase look completely different. The recipient needs the salt to re-derive the same key for decryption, and without the passphrase the salt tells an attacker nothing useful.

05

Intermediate · ~75 min

Digital Signatures & Digital Envelopes in Email

Every time you receive a signed email, two cryptographic guarantees are being made: the message came from the claimed sender (authenticity) and has not been altered in transit (integrity). When you receive an encrypted email, a digital envelope is at work — your public key was used to seal a random session key, and that session key encrypted the message body. In this lab you will implement both techniques using GPG (GNU Privacy Guard) on Kali Linux, first on the command line and then by inspecting the raw email structure to understand exactly what travels over the wire.
Tools: gpg gpg2 openssl smime python3 / email mutt (optional)
kali@kali:~/crypto_labs — how to run this lab

GPG is pre-installed on Kali Linux. This lab uses only the terminal — no email client is required. You will simulate two users (Alice and Bob) on the same machine using separate GPG home directories, which is a clean way to practice key exchange and email encryption without needing two physical machines or real email accounts.

# Confirm GPG is installed
gpg --version | head -2
# Expected: gpg (GnuPG) 2.x.x

# Create isolated GPG home directories for Alice and Bob
mkdir -p ~/crypto_labs/alice_gpg ~/crypto_labs/bob_gpg
chmod 700 ~/crypto_labs/alice_gpg ~/crypto_labs/bob_gpg

# Set a shell alias so we can easily switch between them
alias gpg-alice='gpg --homedir ~/crypto_labs/alice_gpg'
alias gpg-bob='gpg --homedir ~/crypto_labs/bob_gpg'

# Make sure you are in the working directory
cd ~/crypto_labs
Kali tip The alias commands above only last for your current terminal session. If you close the terminal and reopen it, re-run those two alias lines before continuing. Alternatively, add them to ~/.bashrc to make them permanent.
Learning goal: Understand and implement both digital signatures (proving authorship and integrity) and digital envelopes (encrypting email so only the intended recipient can read it) using GPG — the standard open-source tool used in real-world secure email workflows.

Core concept 1 — digital signatures in email

A digital signature on an email is created by the sender's private key and verified by anyone who has the sender's public key. It answers two questions: Did this really come from Alice? (authenticity) and Has anything been changed since Alice sent it? (integrity). It does not hide the message content — a signed email is still readable by anyone who intercepts it.

  • The email body is hashed (SHA-256 or SHA-512)
  • That hash is encrypted with the sender's private key → this is the signature
  • The signature is attached to the email (in a separate MIME part for PGP/MIME)
  • The recipient decrypts the signature with the sender's public key and compares hashes

Core concept 2 — digital envelopes in email

A digital envelope is hybrid encryption applied to email. Encrypting the whole message body with RSA is impractical (RSA is slow and has size limits). Instead, a random symmetric session key is generated, the message is encrypted with that fast symmetric key, and the session key itself is encrypted with the recipient's public key. The recipient uses their private key to unwrap the session key, then uses that to decrypt the message.

  • Step 1 — generate a random AES session key (one per message)
  • Step 2 — encrypt the message body with that AES session key
  • Step 3 — encrypt the AES session key with the recipient's RSA public key
  • Step 4 — send both the encrypted body and the encrypted session key together
  • Recipient reverses: RSA-decrypt the session key → AES-decrypt the body

Alice signs ↓

Sender
Email body
Plaintext message
Sender
#
SHA-256 hash
Body fingerprint
Sender
🔒
Sign with private key
Alice's secret key
On the wire
Signature block
Attached to email

Bob verifies ↓

Receiver
🔑
Decrypt with public key
Alice's public key
Receiver
#
Recovered hash
From signature
Receiver
#
Re-hash body
Independently computed
Receiver
Compare hashes
Match = good signature
Click any card above to see a detailed explanation of that step.

Alice seals the envelope ↓

Sender
Email body
Plaintext message
Sender
Random AES key
Session key
Sender
🔒
AES-encrypt body
With session key
Sender
🔒
RSA-seal the key
Bob's public key
On the wire
📨
GPG message
Sealed envelope

Bob opens the envelope ↓

Receiver
🔑
RSA-decrypt key
Bob's private key
Receiver
Recovered AES key
Session key restored
Receiver
🔓
AES-decrypt body
Plaintext recovered
Receiver
👁
Read email
Only Bob can do this
Click any card above to see a detailed explanation of that step.

Switch tabs to compare how signatures and envelopes work

Lab steps

STEP 01Generate GPG key pairs for Alice and Bob

Each participant needs their own key pair. You are simulating two users on one machine using separate --homedir paths so their keyrings never mix.

# Generate Alice's key pair in her isolated keyring
# GPG will open an interactive prompt — fill in the fields below
gpg --homedir ~/crypto_labs/alice_gpg --full-generate-key

# At the prompts, select:
#   Key type    : (1) RSA and RSA
#   Key size    : 4096
#   Expiry      : 0  (does not expire — fine for a lab)
#   Real name   : Alice Lab
#   Email       : alice@lab.local
#   Comment     : (leave blank)
#   Passphrase  : choose something simple, e.g. alicepass
# Generate Bob's key pair in his isolated keyring
gpg --homedir ~/crypto_labs/bob_gpg --full-generate-key

# At the prompts:
#   Real name   : Bob Lab
#   Email       : bob@lab.local
#   Passphrase  : e.g. bobpass
# Confirm both keyrings have their keys
gpg --homedir ~/crypto_labs/alice_gpg --list-keys
gpg --homedir ~/crypto_labs/bob_gpg   --list-keys
What you see Each listing shows a pub (public key) block with the key ID, creation date, and the associated email. The uid line is the identity (name + email). The sub line is a separate encryption subkey — GPG generates a signing master key and an encryption subkey by default. This separation is a security best practice.
STEP 02Export and exchange public keys

In a real email workflow, you publish your public key to a keyserver or attach it to an email. Here you will export both keys to files and import them into each other's keyring — simulating the key exchange step.

# Export Alice's public key to a file
gpg --homedir ~/crypto_labs/alice_gpg \
    --armor --export alice@lab.local \
    > alice_public.asc

# Export Bob's public key to a file
gpg --homedir ~/crypto_labs/bob_gpg \
    --armor --export bob@lab.local \
    > bob_public.asc

# Inspect what the exported key looks like
cat alice_public.asc
# You will see a -----BEGIN PGP PUBLIC KEY BLOCK----- header
# Import Bob's public key into Alice's keyring
# (Alice needs Bob's key to encrypt messages TO him)
gpg --homedir ~/crypto_labs/alice_gpg \
    --import bob_public.asc

# Import Alice's public key into Bob's keyring
# (Bob needs Alice's key to verify her signatures)
gpg --homedir ~/crypto_labs/bob_gpg \
    --import alice_public.asc

# Verify both keyrings now contain two keys
gpg --homedir ~/crypto_labs/alice_gpg --list-keys
gpg --homedir ~/crypto_labs/bob_gpg   --list-keys
Trust model GPG will warn "no indication this key belongs to the named user" because the key is not signed by a trusted third party. In production you would verify the key fingerprint out-of-band (phone call, in person) and then sign it. For this lab you can bypass the warning by marking the key as trusted — shown in Step 3.
STEP 03Sign an email message — digital signature

Alice writes an email and signs it with her private key. The output is a cleartext message with a PGP signature block appended — anyone can read the message, but only Alice's private key could have produced that signature.

# Create the email body file
cat > email_body.txt << 'EOF'
To: bob@lab.local
From: alice@lab.local
Subject: Project update

Hi Bob,

The deployment is scheduled for Friday at 14:00 UTC.
Please confirm receipt.

-- Alice
EOF
# Sign it with Alice's private key (clearsign = readable + signature)
gpg --homedir ~/crypto_labs/alice_gpg \
    --clearsign \
    --local-user alice@lab.local \
    email_body.txt
# Enter Alice's passphrase when prompted
# Output written to: email_body.txt.asc

# Inspect the signed message
cat email_body.txt.asc
# You will see the original text plus a -----BEGIN PGP SIGNATURE----- block
# Bob verifies the signature using Alice's public key (already in his keyring)
gpg --homedir ~/crypto_labs/bob_gpg \
    --verify email_body.txt.asc
# Expected output:
# gpg: Good signature from "Alice Lab <alice@lab.local>"
# gpg: WARNING: This key is not certified ... (trust warning — ignore for lab)
# Now tamper with the message — edit one word — then verify again
sed -i 's/Friday/Saturday/' email_body.txt.asc

gpg --homedir ~/crypto_labs/bob_gpg \
    --verify email_body.txt.asc
# Expected: gpg: BAD signature from "Alice Lab <alice@lab.local>"
Key insight Changing a single word from "Friday" to "Saturday" breaks the signature entirely. This is the integrity guarantee — even a tiny change to the body invalidates the signature, because the hash of the message no longer matches what Alice signed.
STEP 04Encrypt an email — digital envelope

Alice encrypts a message to Bob. GPG generates a random session key, encrypts the message body with it, and then wraps the session key with Bob's public key — a digital envelope that only Bob can open.

# Re-create the email body (undo the tamper from Step 3)
cat > secret_email.txt << 'EOF'
To: bob@lab.local
From: alice@lab.local
Subject: Confidential

Bob, the server credentials are: admin / Tr0ub4dor&3
Do not share. Delete after reading.
-- Alice
EOF
# Encrypt to Bob (uses Bob's public key from Alice's keyring)
# --armor = ASCII output instead of binary (email-safe)
# --recipient = who can decrypt this
gpg --homedir ~/crypto_labs/alice_gpg \
    --armor \
    --encrypt \
    --recipient bob@lab.local \
    secret_email.txt
# Output: secret_email.txt.asc

# Inspect the encrypted output — should be unreadable
cat secret_email.txt.asc
# You will see -----BEGIN PGP MESSAGE----- followed by base64 data
# Bob decrypts using his private key
gpg --homedir ~/crypto_labs/bob_gpg \
    --decrypt secret_email.txt.asc
# Enter Bob's passphrase when prompted
# The plaintext message appears on stdout

# Redirect decrypted output to a file
gpg --homedir ~/crypto_labs/bob_gpg \
    --output secret_email_decrypted.txt \
    --decrypt secret_email.txt.asc

cat secret_email_decrypted.txt
# Try decrypting with Alice's key — it should FAIL
# (only Bob's private key can open this envelope)
gpg --homedir ~/crypto_labs/alice_gpg \
    --decrypt secret_email.txt.asc
# Expected: gpg: decryption failed: No secret key
What GPG is doing under the hood GPG generates a random 256-bit AES session key, encrypts the message with AES-256, then RSA-encrypts that session key with Bob's 4096-bit public key. The final .asc file contains both: the AES-encrypted body and the RSA-encrypted session key packet. Bob's private key unwraps the session key first, then AES decryption recovers the body. This is exactly the digital envelope structure from the diagram above.
STEP 05Sign and encrypt together — the full secure email

In real secure email both operations happen together: Alice signs with her private key (so Bob knows it came from her) and encrypts with Bob's public key (so only Bob can read it). GPG does both in a single command.

# Alice signs AND encrypts in one step
gpg --homedir ~/crypto_labs/alice_gpg \
    --armor \
    --sign \
    --encrypt \
    --local-user alice@lab.local \
    --recipient bob@lab.local \
    secret_email.txt
# Output: secret_email.txt.asc (signed + encrypted)
# Bob decrypts — GPG will automatically verify the signature too
gpg --homedir ~/crypto_labs/bob_gpg \
    --decrypt secret_email.txt.asc
# Expected output includes:
# gpg: Good signature from "Alice Lab <alice@lab.local>"
# followed by the decrypted plaintext
Order matters GPG signs first, then encrypts. This means the signature is protected inside the encryption layer — no one can strip Alice's signature without breaking the encryption. If the order were reversed (encrypt then sign), an attacker could in theory strip the outer signature and re-sign with their own key.
STEP 06Inspect the raw PGP packet structure

GPG's --list-packets flag lets you see the internal structure of a PGP message — the individual packets for the encrypted session key, the encrypted body, the signature, and so on. This is the email equivalent of inspecting a TLS handshake in Wireshark.

# Inspect the signed-and-encrypted message structure
gpg --homedir ~/crypto_labs/bob_gpg \
    --list-packets secret_email.txt.asc

# Expected packet types (from outer to inner):
#   :pubkey enc packet:    — RSA-encrypted session key (for Bob)
#   :encrypted data packet: — the AES-encrypted payload
#     :compressed data packet: — GPG compresses before encrypting
#       :onepass sig packet:   — marker that a signature follows
#       :literal data packet:  — the actual message text
#       :signature packet:     — Alice's RSA signature over the message
# Inspect just the public-key encrypted session key packet
# Note the key ID — it corresponds to Bob's encryption subkey
gpg --homedir ~/crypto_labs/bob_gpg \
    --list-packets secret_email.txt.asc 2>&1 | grep -A5 "pubkey enc"
Connect the dots The :pubkey enc packet: is the locked box containing the session key — this is the envelope. The :encrypted data packet: is the message inside. The :signature packet: is Alice's seal proving she wrote it. Everything you have built across Labs 1–4 is present here: RSA from Lab 1, hybrid encryption from Lab 4, and integrity verification from both.
STEP 07Understand key fingerprints and the Web of Trust

A GPG public key is identified by its fingerprint — a SHA-1 or SHA-256 hash of the key material. Verifying fingerprints out-of-band (phone, in-person) is how you confirm a public key genuinely belongs to who you think it does before trusting it. This is the human-layer solution to the key authentication problem.

# View Alice's key fingerprint
gpg --homedir ~/crypto_labs/alice_gpg \
    --fingerprint alice@lab.local

# View Bob's key fingerprint
gpg --homedir ~/crypto_labs/bob_gpg \
    --fingerprint bob@lab.local

# The fingerprint looks like:
# pub   rsa4096 2024-01-15 [SC]
#       A1B2 C3D4 E5F6 7890 ABCD  EF12 3456 7890 ABCD EF12
# Read it in groups of 4 characters when verifying over the phone
# Mark Bob's key as fully trusted in Alice's keyring
# (required to suppress the trust warning when encrypting)
gpg --homedir ~/crypto_labs/alice_gpg \
    --edit-key bob@lab.local

# At the gpg> prompt, type:
#   trust
#   5   (I trust ultimately)
#   y
#   quit
Web of Trust GPG uses a decentralised Web of Trust instead of a certificate authority hierarchy (as in TLS/HTTPS). If Alice trusts Carol, and Carol has signed Bob's key, Alice can transitively trust Bob's key. In S/MIME (the other email encryption standard), this is replaced by X.509 certificates issued by a CA — the same model used in TLS.
BONUSBuild a digital envelope in pure Python

This script implements the full digital envelope manually — no GPG — using the same cryptography library from Lab 4. It makes the hybrid encryption structure explicit in code.

# Save as ~/crypto_labs/digital_envelope.py
import os
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives import hashes, serialization

# ── Key generation (Bob's key pair) ──────────────────────────────
bob_private = rsa.generate_private_key(public_exponent=65537, key_size=4096)
bob_public  = bob_private.public_key()

# ── ALICE SENDS: seal the digital envelope ───────────────────────
message = b"Bob, the secret credentials are: admin / Tr0ub4dor&3"

# Step 1: generate a random 256-bit AES session key
session_key = os.urandom(32)
nonce       = os.urandom(12)

# Step 2: encrypt the message body with the session key (AES-256-GCM)
aesgcm           = AESGCM(session_key)
encrypted_body   = aesgcm.encrypt(nonce, message, None)

# Step 3: encrypt the session key with Bob's RSA public key (the "envelope seal")
encrypted_session_key = bob_public.encrypt(
    session_key,
    padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),
                 algorithm=hashes.SHA256(), label=None)
)

print(f"[Alice] Envelope sealed.")
print(f"  Session key (encrypted, first 16 bytes): {encrypted_session_key[:16].hex()}")
print(f"  Encrypted body (first 16 bytes)        : {encrypted_body[:16].hex()}\n")

# ── BOB RECEIVES: open the digital envelope ──────────────────────

# Step 4: decrypt the session key with Bob's private key
recovered_session_key = bob_private.decrypt(
    encrypted_session_key,
    padding.OAEP(mgf=padding.MGF1(algorithm=hashes.SHA256()),
                 algorithm=hashes.SHA256(), label=None)
)

# Step 5: use the recovered session key to decrypt the message body
aesgcm2   = AESGCM(recovered_session_key)
plaintext = aesgcm2.decrypt(nonce, encrypted_body, None)

print(f"[Bob]   Envelope opened.")
print(f"  Decrypted message: {plaintext.decode()}")
# Run it
python3 digital_envelope.py
?Checkpoint questions

1. Alice sends Bob a signed email. What two things does Bob learn from a successful "Good signature" verification, and what does he not learn?

Bob learns: (1) Integrity — the email body has not been altered since it was signed; and (2) Authenticity — the signature was produced by whoever holds the private key that corresponds to alice@lab.local's public key in his keyring. What Bob does not automatically learn is whether that public key genuinely belongs to Alice — an attacker could have substituted a different public key labelled with Alice's email (a key substitution attack). This is why fingerprint verification and the Web of Trust exist.

2. Why does GPG encrypt the message body with a random AES session key instead of directly encrypting it with Bob's RSA public key?

Two reasons. First, RSA has a hard size limit — a 4096-bit key can only directly encrypt about 470 bytes before padding overhead fills it; most email messages are far longer. Second, RSA is computationally expensive compared to AES. The hybrid approach (AES for the body, RSA only for the short session key) gives you the best of both: AES handles unlimited message sizes at high speed, while RSA handles the key distribution problem that symmetric encryption cannot solve on its own.

3. Alice signs her email first and then encrypts it. Why is this order more secure than encrypting first and then signing?

When you sign first then encrypt, the signature is sealed inside the encryption layer. Only someone who can decrypt the message (i.e. the intended recipient with the right private key) can even reach the signature. If the order were reversed — encrypt then sign — an attacker who intercepts the encrypted message could strip Alice's outer signature and re-sign the ciphertext with their own key, making it appear to Bob as if the message came from the attacker rather than Alice, even though the encrypted content is unchanged. Sign-then-encrypt prevents this attack.

4. What is the difference between GPG's Web of Trust and the Certificate Authority model used in TLS?

In the Web of Trust (GPG/PGP), trust is decentralised and user-driven. You trust a key because someone you already trust has signed it, creating a network of peer endorsements. There is no central authority — trust spreads organically. In the Certificate Authority (CA) model (TLS/S/MIME), trust is hierarchical and centralised. A root CA vouches for intermediate CAs, which issue end-entity certificates. Your browser/OS ships with a pre-installed list of trusted root CAs. The CA model is simpler for users and scales better for HTTPS, but it introduces a single point of failure: a compromised CA can issue fraudulent certificates for any domain.

Workbook complete

What you have covered

Ready for the Advanced labs? Next topics include implementing Diffie-Hellman key exchange from scratch, exploiting a padding oracle, building a full certificate authority with S/MIME email, and simulating hybrid-encryption ransomware in an isolated VM.