Cryptography
- Cryptography
The mathematics under every other control
Cryptography provides the mathematical foundations for confidentiality, integrity, and authentication — the goals named in the CIA triad and pursued, in one form or another, by nearly every page in this course. It is the machinery underneath the controls you meet elsewhere: the signatures behind SPF/DKIM/DMARC and DANE, the chain of trust in DNSSEC, the key exchange and forward secrecy inside VPNs and IPSec, the password hashing and MFA in identity and access management, and the code signing that lets a host verify what it runs.
This page covers the primitives in the order you’d build them up: symmetric and asymmetric encryption, key exchange and forward secrecy, hashing and MACs, digital signatures, TLS, PKI, and key management — each with runnable Python and command-line examples.
⚠️ Don’t roll your own crypto. Everything here is about correctly using well-vetted primitives, never reimplementing them. The single most common cryptographic vulnerability is a developer building their own scheme. Use the library, use the standard mode, use the recommended parameters.
Symmetric Encryption
Symmetric encryption uses the same key for both encryption and decryption. It is fast and suitable for bulk data encryption.
AES
AES (Advanced Encryption Standard) is the standard symmetric cipher. It operates on 128-bit blocks with key sizes of 128, 192, or 256 bits. The mode of operation matters significantly:
| Mode | IV required | Authenticated | Use case |
|---|---|---|---|
| ECB | No | No | Never use — identical plaintext blocks produce identical ciphertext |
| CBC | Yes | No | Legacy; requires separate HMAC for integrity |
| GCM | Yes (nonce) | Yes (AEAD) | Recommended — provides confidentiality and integrity together |
| CTR | Yes (nonce) | No | Streaming; combine with HMAC |
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
# AES-256-GCM: authenticated encryption (recommended)
key = os.urandom(32) # 256-bit key — generate securely, store in Vault
nonce = os.urandom(12) # 96-bit nonce — must be unique per encryption
aesgcm = AESGCM(key)
# Encrypt
plaintext = b"sensitive configuration value"
aad = b"context-authenticated-not-encrypted" # additional authenticated data
ciphertext = aesgcm.encrypt(nonce, plaintext, aad)
# Decrypt — raises InvalidTag if ciphertext or aad was tampered with
recovered = aesgcm.decrypt(nonce, ciphertext, aad)
assert recovered == plaintext
# Fernet: high-level symmetric encryption (AES-128-CBC + HMAC-SHA256)
# Use when you want a simpler API and don't need raw AES-GCM control
from cryptography.fernet import Fernet
key = Fernet.generate_key() # 32-byte URL-safe base64 key
f = Fernet(key)
token = f.encrypt(b"my secret data")
plaintext = f.decrypt(token)
Key derivation
When you need to derive a key from a password (rather than generating a random key), use a key derivation function (KDF). Never use raw SHA-256 or MD5 to “hash” a password into a key.
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
import base64
import os
password = b"user-entered-passphrase"
salt = os.urandom(16) # store this alongside the derived key/ciphertext
kdf = PBKDF2HMAC(
algorithm=hashes.SHA256(),
length=32, # 256-bit key
salt=salt,
iterations=600_000, # NIST SP 800-132 recommends 600,000 for SHA-256
)
key = kdf.derive(password)
Asymmetric Encryption
Asymmetric (public-key) cryptography uses a mathematically linked key pair: a public key (shareable) and a private key (secret). Data encrypted with the public key can only be decrypted with the private key.
RSA
RSA is the classical asymmetric algorithm. For new deployments, prefer RSA-4096 or elliptic curve algorithms.
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes, serialization
# Generate RSA key pair
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=4096,
)
public_key = private_key.public_key()
# Serialize private key to PEM (for storage)
pem_private = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.BestAvailableEncryption(b"key-passphrase"),
)
# Encrypt with public key (OAEP padding — required; PKCS1v15 is deprecated)
ciphertext = public_key.encrypt(
b"small secret (RSA max payload is limited by key size)",
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
),
)
# Decrypt with private key
plaintext = private_key.decrypt(ciphertext, padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None,
))
Elliptic Curve Cryptography
ECC provides equivalent security to RSA with much smaller key sizes. A 256-bit ECC key is roughly equivalent in security to a 3072-bit RSA key.
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
# Ed25519: fast, secure, recommended for signatures
private_key = Ed25519PrivateKey.generate()
public_key = private_key.public_key()
# Sign a message
message = b"artifact contents to sign"
signature = private_key.sign(message)
# Verify (raises InvalidSignature on failure)
public_key.verify(signature, message)
# OpenSSL: generate and inspect keys from the command line
# Generate an Ed25519 key pair
openssl genpkey -algorithm ed25519 -out private.pem
openssl pkey -in private.pem -pubout -out public.pem
# Generate an ECDSA key (P-256 curve)
openssl ecparam -name prime256v1 -genkey -noout -out ec-private.pem
openssl ec -in ec-private.pem -pubout -out ec-public.pem
# Generate RSA-4096
openssl genrsa -out rsa-private.pem 4096
openssl rsa -in rsa-private.pem -pubout -out rsa-public.pem
Digital Signatures
Digital signatures use the private key to sign, and the public key to verify. They provide authentication (who signed it) and integrity (has it changed).
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import (
decode_dss_signature, encode_dss_signature
)
from cryptography.hazmat.primitives import hashes
from cryptography.exceptions import InvalidSignature
# ECDSA signing
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()
artifact = b"binary content to sign"
signature = private_key.sign(artifact, ec.ECDSA(hashes.SHA256()))
# Verification
try:
public_key.verify(signature, artifact, ec.ECDSA(hashes.SHA256()))
print("Signature valid")
except InvalidSignature:
print("Signature INVALID — artifact may have been tampered with")
Key Exchange and Forward Secrecy
Symmetric encryption is fast, but it has a chicken-and-egg problem: both parties need the same key, and the network between them is exactly the place an eavesdropper is listening. How do two parties agree on a shared secret over a wire someone is watching? This is the key-exchange problem, and its solution is one of the most important ideas in all of computing.
Diffie–Hellman
Diffie–Hellman (DH, 1976) lets two parties derive a shared secret without ever transmitting it. Each side mixes its own secret with a public value; the math (modular exponentiation, or point multiplication on an elliptic curve) is arranged so both arrive at the same result, while an eavesdropper who sees only the public values cannot feasibly compute it. The elliptic-curve variant ECDH — especially over Curve25519 (X25519) — is the modern default.
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes
# Each party generates an ephemeral key pair
alice_priv = X25519PrivateKey.generate()
bob_priv = X25519PrivateKey.generate()
# They exchange ONLY public keys over the (possibly hostile) network
alice_shared = alice_priv.exchange(bob_priv.public_key())
bob_shared = bob_priv.exchange(alice_priv.public_key())
assert alice_shared == bob_shared # same secret, never transmitted
# Never use the raw DH output as a key — run it through a KDF
session_key = HKDF(algorithm=hashes.SHA256(), length=32,
salt=None, info=b"handshake").derive(alice_shared)
# OpenSSL: derive a shared secret from your private key + their public key
openssl genpkey -algorithm x25519 -out alice.pem
openssl pkey -in alice.pem -pubout -out alice.pub
# ... exchange public keys, then each side runs:
openssl pkeyutl -derive -inkey alice.pem -peerkey bob.pub -out shared.bin
Perfect Forward Secrecy
The crucial move is to make the key-exchange keys ephemeral — generated fresh for each session and thrown away afterward (the E in DHE / ECDHE). This gives Perfect Forward Secrecy (PFS): because the session keys were never derived from the server’s long-term private key, an attacker who later steals that long-term key — or who recorded the encrypted traffic months ago — still cannot decrypt past sessions. Each conversation’s keys died with it.
⚠️ PFS defeats “harvest now, decrypt later.” Adversaries record encrypted traffic today hoping to decrypt it after a future key compromise. Without forward secrecy (e.g. old RSA key-transport cipher suites), one stolen server key unlocks years of captured traffic. With ephemeral key exchange, it unlocks nothing. This is why TLS 1.3 removed static RSA key exchange entirely and mandates (EC)DHE, and why IKEv2 in IPSec negotiates fresh Diffie–Hellman keys per session.
Future secrecy and the Signal protocol
Forward secrecy protects the past after a compromise. The Signal protocol — used by Signal, WhatsApp, and others for end-to-end messaging — adds future secrecy (a.k.a. post-compromise security or “self-healing”): even if an attacker steals a device’s current keys, the conversation recovers security for future messages once a fresh exchange happens. It achieves this with two mechanisms:
- X3DH (Extended Triple Diffie–Hellman) — the initial handshake, combining several DH exchanges (identity keys, a signed prekey, and a one-time prekey) so two parties can establish a shared secret even when one is offline.
- The Double Ratchet — for every message, a new key is derived by “ratcheting” forward two chains: a Diffie–Hellman ratchet (new ephemeral DH on each round-trip) and a symmetric-key ratchet (a KDF chain). Each message gets a unique key that is immediately discarded, giving both forward and future secrecy at the granularity of a single message.
This is a sharp contrast with PGP/S-MIME email encryption, which uses long-lived keys and has no forward secrecy — steal the key once and every past message is readable. It is the cryptographic reason secure messengers are preferred over encrypted email for sensitive conversations.
A note on the post-quantum transition
A sufficiently large quantum computer would break the discrete-log and factoring problems that DH, ECDH, and RSA rest on — and “harvest now, decrypt later” means traffic captured today is already at risk. NIST standardized the first replacements in August 2024: ML-KEM (FIPS 203, the key-encapsulation mechanism formerly called CRYSTALS-Kyber) for key exchange, plus ML-DSA (FIPS 204) and SLH-DSA (FIPS 205) for signatures. Deployment is already underway as hybrids that run a classical and a post-quantum exchange together — X25519MLKEM768, on by default in Chrome and Firefox since late 2024 — so the session stays secure as long as either component holds.
🔮 This area is moving fast. Treat specific algorithm names, browser defaults, and standard numbers here as a 2024–2025 snapshot and re-verify before relying on them.
Cryptographic Hashing
Hash functions take arbitrary input and produce a fixed-size digest. They are one-way (you cannot reverse them) and collision-resistant (it is computationally infeasible to find two different inputs with the same hash).
SHA-256 and SHA-3
SHA-256 (from the SHA-2 family) is the current standard for integrity verification. SHA-3 (Keccak) is an alternative with different internal design.
import hashlib
# SHA-256
data = b"file contents or message"
digest = hashlib.sha256(data).hexdigest()
print(digest) # 64-character hex string
# SHA-512 (stronger; often used for password storage and HMAC)
digest512 = hashlib.sha512(data).hexdigest()
# SHA3-256 (SHA-3 family)
digest3 = hashlib.sha3_256(data).hexdigest()
# Streaming hash for large files
def sha256_file(path: str) -> str:
h = hashlib.sha256()
with open(path, 'rb') as f:
for chunk in iter(lambda: f.read(65536), b''):
h.update(chunk)
return h.hexdigest()
print(sha256_file('/etc/os-release'))
# Command-line hash verification
sha256sum myfile.tar.gz # compute hash
sha256sum --check SHA256SUMS # verify against a manifest
# Generate a hash manifest for a directory
find /etc -type f -exec sha256sum {} \; > /root/etc-checksums.txt
# Later: verify
sha256sum --check /root/etc-checksums.txt
HMAC
HMAC (Hash-based Message Authentication Code) combines a hash function with a secret key, providing both integrity and authentication. Unlike a plain hash, an attacker cannot forge an HMAC without knowing the key.
import hmac
import hashlib
import secrets
key = secrets.token_bytes(32) # 256-bit key — keep secret
# Compute HMAC
message = b"API request body or payload"
mac = hmac.new(key, message, hashlib.sha256).hexdigest()
# Verify (use compare_digest to prevent timing attacks)
def verify_hmac(key: bytes, message: bytes, provided_mac: str) -> bool:
expected = hmac.new(key, message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, provided_mac)
Password Hashing
Passwords must never be stored as plain SHA-256 or MD5 hashes — those are too fast and vulnerable to brute-force and rainbow table attacks. Use purpose-built, slow password hashing algorithms.
# bcrypt: the classic recommendation
import bcrypt
password = b"user-password"
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12)) # store this
# Verify
bcrypt.checkpw(password, hashed) # returns True/False
# Argon2id: NIST SP 800-63B and OWASP recommended (stronger than bcrypt)
from argon2 import PasswordHasher
ph = PasswordHasher(
time_cost=3, # iterations
memory_cost=65536, # 64 MB
parallelism=4,
)
hashed = ph.hash("user-password") # store this string
try:
ph.verify(hashed, "user-password") # returns True or raises exception
print("Password correct")
if ph.check_needs_rehash(hashed): # upgrade parameters over time
new_hash = ph.hash("user-password")
except Exception:
print("Password incorrect")
TLS (Transport Layer Security)
TLS is the protocol that secures network communications. It provides authentication (the server presents a certificate), confidentiality (traffic is encrypted), and integrity (MAC prevents tampering). It is the workhorse that ties this whole page together — bulk symmetric encryption keyed by an ephemeral key exchange, the server authenticated by a certificate, and is itself the basis of HTTPS, DoH, and email’s STARTTLS.
TLS handshake overview
Client Server
│ │
│──── ClientHello (TLS 1.3, ciphers) ────▶│
│◀─── ServerHello + Certificate ──────────│
│◀─── CertificateVerify + Finished ───────│
│──── Finished ──────────────────────────▶│
│ │
│═══ Encrypted application data ══════════│
TLS 1.3 (RFC 8446) eliminates weak cipher suites and reduces handshake latency to one round-trip. Critically, it mandates ephemeral (EC)DHE key exchange — so every TLS 1.3 session has Perfect Forward Secrecy — and removes the old static-RSA key transport that lacked it. TLS 1.2 is still in widespread use but TLS 1.0 and 1.1 are deprecated.
Generating and inspecting certificates
# Generate a self-signed certificate (for testing only)
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
-days 365 -nodes \
-subj "/CN=localhost/O=Test/C=US"
# Inspect a certificate
openssl x509 -in cert.pem -text -noout
# Check a remote server's certificate
openssl s_client -connect example.com:443 -showcerts </dev/null 2>/dev/null \
| openssl x509 -text -noout
# Verify certificate chain
openssl verify -CAfile ca-bundle.crt server.crt
# Check certificate expiry
openssl s_client -connect example.com:443 </dev/null 2>/dev/null \
| openssl x509 -noout -dates
# Test TLS configuration (cipher suites, protocol versions)
nmap --script ssl-enum-ciphers -p 443 example.com
# Python: enforce TLS in all HTTP requests
import ssl
import requests
# requests uses certifi by default — always verifies certificates
response = requests.get('https://api.example.com/data') # TLS verified
# Never disable verification in production:
# requests.get(url, verify=False) # INSECURE — disables certificate validation
# Custom CA bundle (for internal PKI)
response = requests.get('https://internal.example.com', verify='/etc/ssl/internal-ca.crt')
# Client certificate authentication (mTLS)
response = requests.get(
'https://mtls-api.example.com',
cert=('/path/to/client.crt', '/path/to/client.key'),
verify='/path/to/ca.crt',
)
TLS hardening in nginx
# /etc/nginx/conf.d/tls.conf
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
# Protocols: TLS 1.2 minimum; prefer TLS 1.3
ssl_protocols TLSv1.2 TLSv1.3;
# Modern cipher suite (Mozilla Intermediate configuration)
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
# HSTS: force HTTPS for 1 year, include subdomains
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
# DH parameters (for DHE cipher suites)
ssl_dhparam /etc/ssl/dhparam.pem;
}
PKI and Certificate Lifecycle
A Public Key Infrastructure (PKI) is the system of certificates, certificate authorities (CAs), and policies that enables trust in public keys. It answers the question key exchange leaves open: how do you know the public key you received actually belongs to the server, and not an attacker performing a machine-in-the-middle? The answer is a chain of digital signatures anchored in CAs your system already trusts.
PKI components
| Component | Role |
|---|---|
| Root CA | Self-signed certificate at the top of the trust chain; kept offline |
| Intermediate CA | Issues end-entity certificates; online; if compromised, only revoke this CA |
| End-entity certificate | Issued to a server, client, or code-signing identity |
| Certificate Revocation List (CRL) | List of revoked certificates published by the CA |
| OCSP | Online Certificate Status Protocol — real-time revocation check |
Certificate lifecycle management
# Generate a certificate signing request (CSR)
openssl req -new -newkey rsa:4096 -keyout server.key -out server.csr -nodes \
-subj "/CN=api.example.com/O=Example Corp/C=US" \
-addext "subjectAltName=DNS:api.example.com,DNS:www.api.example.com"
# Sign the CSR with your intermediate CA
openssl x509 -req -in server.csr -CA intermediate.crt -CAkey intermediate.key \
-CAcreateserial -out server.crt -days 365 \
-extfile <(echo "subjectAltName=DNS:api.example.com")
# Check expiry across a fleet (run before certificates expire)
for host in api.example.com www.example.com db.example.com; do
expiry=$(openssl s_client -connect "${host}:443" </dev/null 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
echo "${host}: expires ${expiry}"
done
cert-manager (Kubernetes)
cert-manager automates certificate issuance and renewal in Kubernetes clusters:
# ClusterIssuer using Let's Encrypt
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: ops@example.com
privateKeySecretRef:
name: letsencrypt-prod-key
solvers:
- http01:
ingress:
class: nginx
---
# Certificate request
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: api-tls
namespace: production
spec:
secretName: api-tls-secret
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- api.example.com
Key Management
Keys are only as secure as the system that stores them. Hardcoding keys in source code, environment variables, or unencrypted files creates serious vulnerabilities — a leaked key is among the most common findings in application security secret-scanning, and it instantly undoes every primitive above.
Key management principles
- Never hardcode keys in source code or configuration files
- Rotate keys on a schedule and immediately after suspected compromise
- Use envelope encryption: encrypt data keys with a master key stored in a KMS
- Separate environments: development, staging, and production must use different keys
- Audit access: every key access should be logged
HashiCorp Vault for key management
See the CI/CD, Secret Management, and GitOps page for a full Vault reference. For cryptographic operations specifically:
# Vault Transit secrets engine: encryption as a service
# Applications encrypt/decrypt through the API — they never see the key
vault secrets enable transit
vault write transit/keys/myapp-key type=aes256-gcm96
# Encrypt data (returns base64 ciphertext)
vault write transit/encrypt/myapp-key \
plaintext=$(echo -n "sensitive value" | base64)
# Decrypt data
vault write transit/decrypt/myapp-key \
ciphertext="vault:v1:..."
# Rotate the key (old versions still decrypt old data)
vault write transit/keys/myapp-key/rotate
import hvac
import base64
import os
client = hvac.Client(url=os.environ['VAULT_ADDR'], token=os.environ['VAULT_TOKEN'])
def vault_encrypt(plaintext: str, key_name: str = 'myapp-key') -> str:
encoded = base64.b64encode(plaintext.encode()).decode()
result = client.secrets.transit.encrypt_data(name=key_name, plaintext=encoded)
return result['data']['ciphertext']
def vault_decrypt(ciphertext: str, key_name: str = 'myapp-key') -> str:
result = client.secrets.transit.decrypt_data(name=key_name, ciphertext=ciphertext)
return base64.b64decode(result['data']['plaintext']).decode()
AWS KMS
import boto3
import base64
kms = boto3.client('kms', region_name='us-east-1')
KEY_ID = 'arn:aws:kms:us-east-1:123456789012:key/abc-123'
# Encrypt
def kms_encrypt(plaintext: str) -> bytes:
response = kms.encrypt(KeyId=KEY_ID, Plaintext=plaintext.encode())
return response['CiphertextBlob']
# Decrypt (KMS looks up the key automatically from the ciphertext metadata)
def kms_decrypt(ciphertext_blob: bytes) -> str:
response = kms.decrypt(CiphertextBlob=ciphertext_blob)
return response['Plaintext'].decode()
# Envelope encryption: generate a data key, use it locally
data_key = kms.generate_data_key(KeyId=KEY_ID, KeySpec='AES_256')
plaintext_key = data_key['Plaintext'] # use this to encrypt data, then discard
encrypted_key = data_key['CiphertextBlob'] # store this alongside the encrypted data
Key takeaways
- Never roll your own crypto — correctly use vetted primitives with standard modes and recommended parameters; don’t reimplement them.
- Symmetric encryption (AES-GCM and other AEAD modes) is fast and does the bulk work; asymmetric (RSA, ECC/Ed25519) is slow but solves key distribution and signatures.
- Key exchange (Diffie–Hellman / ECDH) lets two parties derive a shared secret over a hostile network; making the keys ephemeral ((EC)DHE) gives Perfect Forward Secrecy, which defeats “harvest now, decrypt later.” TLS 1.3 mandates it.
- The Signal protocol (X3DH + Double Ratchet) adds future secrecy — per-message keys that self-heal after a compromise — which is why secure messengers beat PGP email for sensitive chat.
- Hashes are one-way and collision-resistant; a MAC/HMAC adds a key for authenticity; passwords need slow purpose-built KDFs (Argon2id, bcrypt), never raw SHA-256.
- TLS composes all of the above into a secure channel; PKI supplies the trust that the far end’s key is genuine; key management (KMS/Vault, rotation, no hardcoding) protects the keys that everything else depends on.
- The post-quantum transition is underway — ML-KEM (FIPS 203) and hybrid X25519MLKEM768 — because captured traffic is a future-decryption risk today.
References
- NIST SP 800-57 — Recommendation for Key Management. https://csrc.nist.gov/publications/detail/sp/800/57-part-1/rev-5/final
- NIST SP 800-131A Rev. 2 — Transitioning the Use of Cryptographic Algorithms and Key Lengths. https://csrc.nist.gov/publications/detail/sp/800/131a/rev-2/final
- W. Diffie and M. Hellman, New Directions in Cryptography (1976) — the original key-exchange paper. https://ee.stanford.edu/~hellman/publications/24.pdf
- RFC 8446 — TLS 1.3. https://datatracker.ietf.org/doc/html/rfc8446
- Signal — The X3DH Key Agreement Protocol and The Double Ratchet Algorithm. https://signal.org/docs/
- NIST FIPS 203 — Module-Lattice-Based Key-Encapsulation Mechanism (ML-KEM). https://csrc.nist.gov/pubs/fips/203/final
- Python
cryptographylibrary documentation. https://cryptography.io/en/latest/ - Mozilla TLS Configuration Generator. https://ssl-config.mozilla.org/
- OWASP — Cryptographic Storage Cheat Sheet. https://cheatsheetseries.owasp.org/cheatsheets/Cryptographic_Storage_Cheat_Sheet.html
Related course pages: Access Control and Authorization · Identity and Access Management · Email Security · DNS Security and Privacy · VPNs and IPSec · Network Security · Application Security
🛠️ Maintenance note: the cryptographic recommendations here drift — re-verify before each term. PBKDF2 iteration counts and Argon2 parameters rise over time; cipher-suite and TLS-version guidance follows the Mozilla generator; and the post-quantum section (FIPS 203/204/205, hybrid X25519MLKEM768, browser defaults) is a 2024–2025 snapshot of a fast-moving transition. Confirm the Python
cryptographyAPI calls against the version on the course VM.