Applied Cryptography and Key Management Questions
Selecting and applying cryptographic primitives correctly: symmetric and asymmetric encryption, hashing, digital signatures, key derivation, secure random number generation, and public key infrastructure. Covers key lifecycle management, key exchange and distribution, choosing appropriate algorithms for a given constraint set including resource-constrained environments, and the forward-looking side of algorithm lifecycle: cryptographic agility and algorithm-migration strategy, forward secrecy, and the post-quantum cryptography transition and planning upgrades without breaking existing data or interoperability. The applied-crypto engineering layer, distinct from compliance-driven crypto standards.
Why can't you just hash a password with SHA-256 and call it done? Walk through what a key derivation function actually is (how it differs from a plain hash or a PRF) and compare PBKDF2, bcrypt, scrypt, and Argon2 as password-storage choices: the core mechanism each relies on (iteration count vs memory hardness), typical parameter knobs, and strengths/weaknesses. Then explain how the calculus changes when you're deriving a session key from an already-random shared secret instead of a low-entropy password, and where HKDF fits that case.
Sample Answer
Direct answer
A plain hash like SHA-256 is deliberately fast and has no built-in salt, so two users with the same password get identical hashes, and an attacker holding stolen hashes can try billions of guesses per second on off-the-shelf hardware. A key derivation function (KDF) exists to do the opposite: turn a secret (a password, or an existing high-entropy secret) into cryptographic key material through a process that is either deliberately expensive, for password-based KDFs, to slow down guessing, or that safely spreads entropy into independent-looking keys, for a secret that is already random. PBKDF2, bcrypt, scrypt, and Argon2 are password-hashing KDFs; HKDF is built for the second case.
KDF vs hash vs pseudorandom function (PRF)
- A cryptographic hash function (SHA-256) maps arbitrary input to a fixed-size output, deterministically and quickly, with no secret key. Good for integrity checks; bad for password storage precisely because it is fast.
- A pseudorandom function (PRF), commonly HMAC-SHA256, is a keyed function whose output is computationally indistinguishable from a truly random function's output to anyone without the key. PRFs are building blocks, not complete answers: they say nothing about work factor or memory cost.
- A KDF composes a hash or PRF with an explicit strategy (iteration, memory hardness, or entropy extraction/expansion) to solve "slow this down for guessing resistance" or "expand this safely."
Comparing the four password-hashing KDFs
| Algorithm | Core mechanism | Typical knobs | Strength | Weakness |
|---|---|---|---|---|
| PBKDF2 | Iterates a PRF (usually HMAC-SHA256) c times | Iteration count only | Simple, standardized in NIST SP 800-132, widely available in FIPS-validated libraries | Needs almost no memory, so custom hardware (ASICs, GPUs) can run millions of cheap parallel guesses |
| bcrypt | Repeated Blowfish key schedule | Cost factor (rounds = 2^cost) | Decades of scrutiny, resists naive GPU ports better than PBKDF2 | Fixed, small (roughly 4 KB) memory footprint; silently truncates passwords past 72 bytes |
| scrypt | Sequential memory-hard mixing | Memory/CPU cost N, block size r, parallelism p | True memory hardness (RFC 7914), first widely deployed | More complex to tune correctly; mostly superseded by Argon2 for new work |
| Argon2 | Memory-hard, tunable in three dimensions | Memory m, iterations t, parallelism p | Winner of the 2015 Password Hashing Competition, documented in RFC 9106, three variants for different threat models | Needs careful parameter tuning per target hardware |
The line that matters most: PBKDF2 and bcrypt scale cost mainly by adding sequential compute, which cheap, highly parallel hardware absorbs well. scrypt and Argon2 scale cost by requiring a large memory footprint per guess, and memory is expensive to duplicate across thousands of parallel attempts, which is why they resist GPU/ASIC cracking far better per unit of legitimate server CPU time spent.
When the input is already random: HKDF
Once you have a high-entropy shared secret, for example the output of an ECDHE key exchange, you have no guessing problem to defend against; you have an "I need several independent-looking keys from one secret" problem (a separate encryption key and a MAC key, or per-direction keys). Deliberately slowing that down the way PBKDF2/Argon2 do would only add latency with no security benefit, since there is no offline dictionary attack to resist. HMAC-based key derivation function (HKDF, RFC 5869) is built for exactly this: an "extract" step concentrates the input's entropy into a fixed-length pseudorandom key using HMAC, then an "expand" step derives as much labeled output keying material as needed, bound to a context string so an encryption key and a MAC key derived from the same secret cannot be confused with each other.
Worked example
Deriving 64 bytes of key material (a 32-byte AES key plus a 32-byte HMAC key) from a 32-byte ECDHE shared secret with HKDF-SHA256 is one Expand call producing two concatenated 32-byte outputs. HKDF-SHA256 has a hard ceiling from its own specification: the expand step can produce at most 255 times the hash length, which for SHA-256 (32-byte output) is
255×32=8160 bytes
Deriving a handful of session keys never comes close to that ceiling, which underlines the point: HKDF is built to expand cheaply, not to resist guessing.
Trade-offs and pitfalls
- Argon2id is the current default recommendation over PBKDF2 or bcrypt precisely because of the memory-hardness gap above.
- Using a password KDF where you actually have a random shared secret wastes CPU and adds latency for no security gain; using HKDF where you actually have a low-entropy password gives an attacker no additional cost at all, since HKDF has no configurable work factor.
- Never build a KDF out of raw hash calls yourself; use the vetted PBKDF2/bcrypt/scrypt/Argon2/HKDF implementation in your language's standard cryptography library.
Describe the core components and trust model of an enterprise Public Key Infrastructure: root CA, intermediate CAs, issuing CAs, certificate profiles and validity periods, registration authorities, and the strategies (offline root, short-lived certs, cross-certification) used to limit blast radius if a CA is compromised. Then walk through what actually happens end to end for one certificate: how it's requested and issued (CSR, CA validation), how a client validates the resulting chain (chain verification, hostname checks), and how automated issuance (e.g. Let's Encrypt / ACME) changes that story at scale.
Sample Answer
Direct answer
A production public key infrastructure (PKI) is a tree of trust: an offline root certificate authority (CA) signs one or a few intermediate CAs, which in turn sign the issuing CAs that actually hand out end-entity certificates. Nobody trusts an end-entity certificate directly; they trust it because it chains up to a root their software already trusts.
Trust model and components
- Root CA: the ultimate trust anchor, pre-installed in operating systems' and browsers' trust stores. Because compromising it would let an attacker mint a trusted certificate for anything, it is kept offline (air-gapped, powered on only to sign new intermediates) and used as rarely as possible.
- Intermediate CA: signed by the root, does the actual day-to-day signing directly or through issuing CAs beneath it. Splitting root from intermediate means a compromised or misused intermediate can be revoked without touching the root already baked into every trust store, avoiding a mass client update.
- Issuing CA: the online intermediate (or sub-CA) that actually processes certificate requests. Large organizations often run separate issuing CAs per purpose (TLS server certs, code-signing certs, client-auth certs), so a problem in one issuing path does not force revoking every certificate type at once.
- Registration authority (RA): the component, sometimes separate, sometimes folded into the issuing CA, that verifies a requester actually controls the domain or identity being certified before the CA signs anything.
- Certificate profiles and validity periods: a profile defines the fields and extensions a given certificate type must carry (key usage, extended key usage, subject alternative names). Shorter validity periods shrink the window a compromised or mis-issued certificate stays dangerous: the CA/Browser Forum's baseline requirements capped public web TLS certificates at 398 days for years, but a phased reduction already dropped that ceiling to 200 days in March 2026, with 100 days due in March 2027 and 47 days by March 2029.
Limiting blast radius if a CA is compromised
- Offline root: the root's private key never touches a network-connected machine, so remote compromise of issuing infrastructure cannot reach it directly.
- Short-lived certificates: if certificates expire in days rather than years, a compromised issuing CA's damage window is bounded by how quickly you detect and revoke it, and even undetected mis-issuance ages out fast.
- Cross-certification: having two independent CAs certify the same intermediate, or maintaining alternate trusted paths, means a single CA's revocation does not instantly break every relying party, giving a rollover path instead of a hard outage.
End-to-end walkthrough for one certificate
- Request and issuance: the requester generates a key pair and a certificate signing request (CSR), a self-signed message asserting "I hold the private key matching this public key, and I am requesting a certificate for this identity." The RA/CA validates control of the domain (a DNS record, an HTTP file, or an out-of-band check) or identity, then the issuing CA signs the CSR's public key and identity into a certificate.
- Client validation: a client receiving the certificate walks the chain up to a trusted root (chain verification), checks the certificate has not expired or been revoked, and checks the hostname it connected to matches a name listed in the certificate (hostname/Subject Alternative Name check). Any one of those failing should hard-fail the connection.
- Automated issuance at scale: the Automatic Certificate Management Environment (ACME) protocol, used by Let's Encrypt and most modern CAs, replaces manual CSR handling with an API: the client proves domain control programmatically (an HTTP or DNS challenge) and gets a certificate issued and renewed automatically, often on a 90-day lifetime. This is what makes short-lived certificates operationally viable at scale; doing this by hand across a large fleet would be a full-time job, but ACME turns it into unattended automation.
Worked example
example.com's certificate might be signed by "Acme Issuing CA G2," which is signed by "Acme Intermediate CA," which is signed by the offline "Acme Root CA X1" baked into an operating system's trust store; verifying the chain means checking each of those two signatures in turn before trusting example.com at all.
Trade-offs and pitfalls
Relying only on the default trust-store setup without your own operational monitoring for renewal and revocation is exactly what causes expired-certificate outages. OCSP stapling (the server proactively attaches a signed revocation-status response) exists because live client-side OCSP lookups add latency and leak browsing metadata to the CA; understanding why matters more than memorizing the acronym.
Provide pseudocode (or Python) for a token/JWT verification and rotation scheme that supports zero-downtime rotation of signing keys for session tokens or JWTs: accept tokens signed with the current key and recently-rotated keys (identified by a key ID), reject deprecated algorithms, return detailed errors (expired, invalid-signature, unknown-kid, deprecated-alg), allow graceful expiration of old tokens, and support immediate revocation of a compromised key without forcing every user to re-authenticate. Describe your public-key caching strategy.
Sample Answer
Direct answer
Give every signing key a stable key ID (kid), keep a small registry of currently-valid keys tagged active, grace, or revoked, and have verification look the token's kid up in that registry instead of assuming a single fixed key. Rotation becomes "add a new active key, demote the previous one to grace," zero downtime, since both keys stay verifiable during the grace window. Revocation becomes "mark one specific kid revoked," invalidating only tokens signed with that exact key, not every session, and a real deployment pairs this with short-lived access tokens plus a separately-keyed refresh flow so most users never notice a signing-key incident at all.
Approach
- Every token carries its signing key's ID in the header (kid) alongside the algorithm (alg).
- A KeyRegistry tracks each key's secret, its declared algorithm, and a status: active (used for new signing), grace (still verifiable, no longer used to sign, this is what makes rotation zero-downtime), or revoked (verification fails immediately regardless of signature validity).
- Verification always re-checks the algorithm on the token against the algorithm registered for that specific kid, and rejects a fixed set of deprecated algorithms outright, including the classic alg-none downgrade attack, so an attacker can't simply relabel a token's header to something weaker or unauthenticated and have it accepted.
- Errors are specific and typed (expired, invalid-signature, unknown-kid, deprecated-alg, revoked) so a caller, or a security team reviewing logs, can distinguish "this token's time simply ran out" from "someone is actively trying to forge or replay something."
Implementation
import base64, hashlib, hmac, json, time
class TokenError(Exception):
def __init__(self, code, message):
self.code = code
super().__init__(message)
def b64url_encode(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def b64url_decode(s: str) -> bytes:
padding = "=" * (-len(s) % 4)
return base64.urlsafe_b64decode(s + padding)
ALLOWED_ALGS = {"HS256"} # algorithms still accepted for verification
DEPRECATED_ALGS = {"HS1", "none"} # explicitly rejected even if a key claims to support them
class KeyRegistry:
"""Tracks signing keys by kid: current key plus a grace window of recently
rotated keys, with immediate revocation for a specific compromised key."""
def __init__(self):
self.keys = {} # kid -> {"secret": bytes, "alg": str, "status": "active"|"grace"|"revoked"}
self.current_kid = None
def add_key(self, kid, secret, alg="HS256"):
if alg in DEPRECATED_ALGS or alg not in ALLOWED_ALGS:
raise TokenError("deprecated-alg", f"refusing to register key with alg {alg}")
self.keys[kid] = {"secret": secret, "alg": alg, "status": "active"}
if self.current_kid and self.current_kid in self.keys:
self.keys[self.current_kid]["status"] = "grace" # zero-downtime rotation
self.current_kid = kid
def revoke(self, kid):
if kid in self.keys:
self.keys[kid]["status"] = "revoked"
def signing_key(self):
return self.current_kid, self.keys[self.current_kid]
def verifying_key(self, kid):
entry = self.keys.get(kid)
if entry is None:
raise TokenError("unknown-kid", f"kid {kid} is not registered")
if entry["status"] == "revoked":
raise TokenError("revoked", f"kid {kid} has been revoked")
if entry["alg"] in DEPRECATED_ALGS or entry["alg"] not in ALLOWED_ALGS:
raise TokenError("deprecated-alg", f"kid {kid} uses disallowed alg {entry['alg']}")
return entry
def issue_token(registry: KeyRegistry, claims: dict, ttl_seconds: int) -> str:
kid, entry = registry.signing_key()
header = {"alg": entry["alg"], "kid": kid, "typ": "JWT"}
now = int(time.time())
payload = {**claims, "iat": now, "exp": now + ttl_seconds}
signing_input = (
b64url_encode(json.dumps(header, separators=(",", ":")).encode()) + "." +
b64url_encode(json.dumps(payload, separators=(",", ":")).encode())
)
sig = hmac.new(entry["secret"], signing_input.encode(), hashlib.sha256).digest()
return signing_input + "." + b64url_encode(sig)
def verify_token(registry: KeyRegistry, token: str) -> dict:
try:
header_b64, payload_b64, sig_b64 = token.split(".")
except ValueError:
raise TokenError("invalid-signature", "malformed token")
try:
header = json.loads(b64url_decode(header_b64))
except (ValueError, UnicodeDecodeError):
raise TokenError("invalid-signature", "malformed header")
entry = registry.verifying_key(header.get("kid")) # unknown-kid / revoked / deprecated-alg
if header.get("alg") != entry["alg"]:
raise TokenError("deprecated-alg", "token alg does not match registered alg for this kid")
signing_input = f"{header_b64}.{payload_b64}"
expected_sig = hmac.new(entry["secret"], signing_input.encode(), hashlib.sha256).digest()
if not hmac.compare_digest(expected_sig, b64url_decode(sig_b64)):
raise TokenError("invalid-signature", "signature mismatch")
payload = json.loads(b64url_decode(payload_b64))
if payload.get("exp", 0) < int(time.time()):
raise TokenError("expired", "token has expired")
return payload
if __name__ == "__main__":
registry = KeyRegistry()
registry.add_key("k1", secret=b"first-signing-secret-32-bytes!!!")
token_old = issue_token(registry, {"sub": "user-42"}, ttl_seconds=3600)
registry.add_key("k2", secret=b"second-signing-secret-32-bytes!!") # zero-downtime rotation
token_new = issue_token(registry, {"sub": "user-42"}, ttl_seconds=3600)
print("k1 (grace) still verifies:", verify_token(registry, token_old)["sub"])
print("k2 (active) verifies:", verify_token(registry, token_new)["sub"])
registry.revoke("k1") # immediate revocation of a compromised key
try:
verify_token(registry, token_old)
except TokenError as e:
print("k1 after revocation ->", e.code)
print("k2 still verifies after revoking k1:", verify_token(registry, token_new)["sub"])
try:
verify_token(registry, "not.a.token")
except TokenError as e:
print("malformed token ->", e.code)
tampered = token_new.rsplit(".", 1)[0] + ".Zm9ydGhlbHVsemo"
try:
verify_token(registry, tampered)
except TokenError as e:
print("tampered signature ->", e.code)
Actual output from running python3 jwt_rotate.py:
k1 (grace) still verifies: user-42
k2 (active) verifies: user-42
k1 after revocation -> revoked
k2 still verifies after revoking k1: user-42
malformed token -> invalid-signature
tampered signature -> invalid-signature
Key points
- Rotation is registering a new active key, existing valid tokens under the old key keep verifying via its grace status, not replacing the key everyone uses, which is what makes it zero-downtime.
- Revocation is per-kid and immediate: only tokens signed with the specifically compromised key stop verifying, every other currently-issued token, signed with a different, uncompromised key, keeps working, so a single leaked key does not force a mass re-authentication event on its own.
- The algorithm is checked twice, once implicitly (only HS256 is ever registered) and once explicitly against the token's own header, so an attacker cannot present a token whose header claims a different, weaker, or unauthenticated algorithm and have it silently accepted.
Public-key caching strategy
This demo uses a shared HMAC secret per key so the whole example runs with no external dependencies. A production session or JWT system more commonly signs with an asymmetric algorithm, RS256 or ES256, specifically so verifying services never need the signing secret at all, only the public half. In that shape, the registry above becomes a public-key cache: verifiers fetch a JSON Web Key Set from the issuer's published endpoint, cache each public key by its kid with a TTL, and on a cache miss for an unrecognized kid, refresh from that endpoint before failing with unknown-kid. Guard that refresh-on-miss path against abuse, an attacker crafting tokens with random, non-existent kids could otherwise force a verifier into repeatedly hammering the key-set endpoint, a trivial denial-of-service amplification, so rate-limit refreshes per unknown kid and treat a kid still not found after a refresh as a hard unknown-kid rather than retrying indefinitely. Refresh proactively on a background schedule too, well before the issuer's stated rotation cadence, not only reactively on cache miss, so routine rotation never depends on the timing of the very next verification request to notice it.
Complexity
issue_token and verify_token are O(1): one dictionary lookup by kid, one HMAC computation, one constant-time comparison, independent of how many keys are registered. Registry size is bounded by design, at most one active key, a small number of grace keys depending on the acceptance window, and revoked entries that can be pruned once their own tokens would have expired anyway, so lookup stays effectively O(1) in practice rather than growing with every rotation ever performed historically.
Edge cases
A token whose kid was never registered (unknown-kid); a token signed with a key that has since been revoked (revoked, checked before signature verification so a revoked key's tokens fail fast and consistently regardless of whether the signature itself is still technically valid); a token claiming an algorithm that doesn't match what's registered for that kid (deprecated-alg, blocking both a genuine downgrade attempt and a simple mismatch); a structurally malformed token (invalid-signature, caught by the parse step rather than an unhandled exception); and a token that parses and verifies correctly but has simply expired (expired, checked last, after cryptographic validity, so an attacker can't use timing or error-ordering to learn whether an invalid signature was at least attached to real, unexpired claims).
Trade-offs and pitfalls
Common wrong turn: checking expiration before verifying the signature, that lets a caller learn something about a token's claims without the token ever having been cryptographically authenticated, always verify the signature first. Common wrong turn: comparing signatures with a plain equality check instead of a constant-time comparison, hmac.compare_digest here, a naive comparison can leak timing information about how many leading bytes matched. Common wrong turn: deleting a revoked key's registry entry entirely instead of marking it revoked, deletion turns future lookups into unknown-kid, which looks like a client error, instead of revoked, the actually meaningful, alertable signal that a specific key is known-compromised. Senior signal: recognizing that "immediate revocation without forcing every user to re-authenticate" really depends on session design upstream of this code, pairing short-lived access tokens with a separately-keyed, individually-revocable refresh-token flow means revoking one signing key only affects holders of an unexpired access token signed with it, who transparently get a new one on their next refresh, not the entire user base.
Explain the differences between symmetric and asymmetric encryption. For each category, name two concrete algorithms you have used or implemented in production, describe typical system-level use cases (data at rest, key exchange, digital signatures), and discuss the performance, key-distribution, and operational trade-offs that push you toward one over the other. Where does envelope encryption and hardware-backed key storage fit into that choice?
Sample Answer
Direct answer
Symmetric encryption uses one shared secret key for both encrypting and decrypting (for example AES-256-GCM or ChaCha20-Poly1305): it is fast and cheap per byte, but both sides must already possess the same key. Asymmetric encryption uses a mathematically linked key pair, a public key anyone can hold and a private key only the owner holds (for example RSA-2048/3072 or elliptic-curve schemes like ECDH/Ed25519): it is orders of magnitude more expensive per byte, but it solves the problem of getting a shared secret to a party you have never met, and it enables digital signatures. Production systems almost never pick one exclusively; they combine both in a hybrid, envelope-encryption pattern.
Symmetric algorithms and use cases
- AES-256-GCM: an Advanced Encryption Standard (AES) block cipher run in Galois/Counter Mode (GCM), which gives you encryption and a built-in authentication tag in one pass. This is the default choice for encrypting data at rest: database columns, object storage, backups.
- ChaCha20-Poly1305: a stream cipher paired with a message-authentication code, popular where hardware AES acceleration is not available or reliable (some mobile chipsets) or where constant-time behavior in pure software matters. It is one of the two cipher suites every modern Transport Layer Security (TLS) 1.3 stack must support.
Asymmetric algorithms and use cases
- RSA (2048 or 3072-bit modulus): used for digital signatures (RSA-PSS) and, historically, for wrapping a symmetric key during key exchange. It is being phased out for key exchange because it provides no forward secrecy by default.
- Elliptic-Curve Diffie-Hellman Ephemeral (ECDHE), typically over curve X25519 or P-256: the workhorse of modern key exchange. A fresh key pair is generated per session and discarded afterward, which gives forward secrecy: compromising the server's long-term identity key later does not expose past session traffic.
Where each shows up
- Data at rest: symmetric (AES-256-GCM), because you encrypt large volumes repeatedly and asymmetric operations at that scale would be prohibitively slow.
- Key exchange: asymmetric (ECDHE, or historically RSA), because the two parties do not share a secret yet.
- Digital signatures: asymmetric (RSA-PSS or ECDSA/EdDSA), because only a private-key holder should be able to produce a valid signature that anyone with the public key can verify, which symmetric algorithms cannot do (both sides hold the same secret, so either could have produced it).
Worked example: why you cannot just use RSA for everything
RSA with Optimal Asymmetric Encryption Padding (OAEP) can only encrypt messages up to a fixed size, given directly by the padding scheme's own specification:
mLen≤k−2hLen−2
where k is the RSA modulus size in bytes and hLen is the padding hash's output size. For RSA-2048 (k = 2048/8 = 256 bytes) with SHA-256 padding (hLen = 32 bytes):
190=256−2(32)−2
A single RSA-2048 operation can protect at most 190 bytes directly, nowhere near enough for a 1 KB file, let alone a multi-gigabyte backup. That is the concrete reason production systems use envelope encryption instead of asymmetric encryption directly on bulk data.
Where envelope encryption and hardware-backed storage fit
Envelope encryption resolves that mismatch: generate a random symmetric data-encryption key (DEK), use it to encrypt the actual data with AES-256-GCM (fast, no size-limit problem), then encrypt (wrap) that small DEK with a separate key-encryption key (KEK) that never leaves a hardware security module (HSM) or a cloud key-management service (KMS). The wrapped DEK is stored next to the ciphertext; decrypting sends only the small wrapped DEK to the KMS/HSM to unwrap (one cheap operation), then does the bulk decryption locally. Hardware-backed storage matters because the KEK, whose compromise would expose every DEK it ever wrapped, is generated and used inside a tamper-resistant boundary designed to never export raw key material, so a compromised application server can ask the HSM to perform operations but cannot exfiltrate the key itself, and every such operation can be audited and rate-limited.
Trade-offs and pitfalls
- Performance: this is the trade-off the worked example above quantifies directly. Symmetric operations process bulk data at native memory speed with hardware acceleration; a single asymmetric operation like RSA-2048-OAEP costs orders of magnitude more per byte and, as shown, can only touch 190 bytes at a time, which is precisely why performance-sensitive systems route bulk work through the symmetric side of the hybrid design.
- Key distribution: symmetric-only designs need every pair of communicating parties to share a secret in advance, which does not scale (N parties need on the order of N-squared pairwise keys); asymmetric key exchange scales linearly because a public key can be published openly.
- Operational and security-level scaling: asymmetric key sizes must grow much faster than symmetric ones for equivalent strength; roughly, matching AES-128's strength needs an RSA modulus around 3072 bits, and matching AES-256 needs a modulus in the tens of thousands of bits, which is why elliptic-curve schemes (far smaller keys for equal strength) have displaced RSA for key exchange, and why operating a large RSA-based public key infrastructure carries more certificate and bandwidth overhead than an equivalent elliptic-curve one.
- Never hand-roll a mode: use a vetted library's authenticated modes (AES-GCM, ChaCha20-Poly1305) rather than composing an unauthenticated cipher yourself; unauthenticated ciphertext is silently malleable.
What is (perfect) forward secrecy? Explain how it prevents retroactive decryption after a long-term key is compromised, which protocols provide it by default, and situations where it might be impractical to use.
Sample Answer
Direct answer
(Perfect) forward secrecy (PFS) means that even if an attacker later obtains a party's long-term private key, they cannot decrypt session traffic recorded in the past, because each session used its own ephemeral key that was discarded and is never derivable from the long-term key alone.
How it works
Instead of using the long-term private key directly to encrypt or derive the session key, each session performs a fresh (EC)DHE key exchange: a temporary key pair generated once and destroyed after the handshake. The session key is a function of that ephemeral exchange, not of the long-term key, so having the long-term key later gives an attacker nothing about past sessions.
Why static RSA key exchange lacks it
In an RSA key-transport handshake, the client encrypts the actual session secret directly under the server's long-term RSA public key. If an attacker records the ciphertext today and steals the server's private key years later, they can decrypt every recorded session, because the same long-term key that identifies the server also directly protects every session's secret, forever.
Which protocols provide it by default
TLS 1.3 mandates an ephemeral (EC)DHE exchange for every handshake, so forward secrecy is not optional. TLS 1.2 only gets it if the negotiated cipher suite uses ECDHE/DHE (a suite name containing "ECDHE" or "DHE"), so a TLS 1.2 server can still be misconfigured to prefer static RSA key exchange and silently lose it. Signal's Double Ratchet, SSH's default key exchange, and most modern VPN protocols (WireGuard, IKEv2 with PFS enabled) also provide it by design.
Situations where it can be impractical
Systems that need to decrypt archived traffic for legitimate reasons (regulatory replay, intrusion forensics, some enterprise deep-packet-inspection appliances) work directly against forward secrecy, since those use cases need a durable way to reconstruct the session key later, exactly what forward secrecy prevents. Some legacy hardware load balancers and older embedded TLS stacks also could not do the ephemeral key-exchange computation cheaply enough at their required connection rate, though that has become a weaker excuse as ECDHE has gotten cheap.
Worked example
If an adversary records encrypted VPN traffic today and compromises the VPN gateway's private key three years from now, whether they can retroactively decrypt today's traffic depends entirely on whether the key exchange used was ephemeral (safe) or static (exposed). This is the same "harvest now, decrypt later" pattern seen in the post-quantum threat discussion, just with a stolen private key as the trigger instead of a future quantum computer.
Trade-offs and pitfalls
A cipher suite name containing "RSA" alone (not "ECDHE_RSA") signals static key exchange with no forward secrecy, even though RSA can still be doing the authentication (signing) in the ECDHE_RSA case; do not confuse RSA-for-authentication with RSA-for-key-exchange. Forward secrecy protects past sessions from a future key compromise; it does nothing against an attacker actively controlling the connection right now, which is what authentication and integrity are for.
Unlock Full Question Bank
Get access to all 16 Applied Cryptography and Key Management interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.