Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Applied Cryptography for Developers  Module 10 – Public-key cryptography

Public-key cryptography: RSA and elliptic curves

Key pairs for key encapsulation, key agreement and signatures, why RSA needs OAEP or PSS, curves and sizes for 128 bits, and keys you can trust.

  • Intermediate
  • 20 minutes
  • Examples run with Node.js 24.21.0, Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Name the three jobs of public keys, key encapsulation, key agreement and signatures, and what crosses between the sides in each
  • Explain why textbook RSA is unsafe and which paddings real systems use
  • Choose curves and key sizes that meet 128-bit security, and say how long they stay acceptable
  • Map each job to the Web Crypto algorithms that perform it
  • Verify that a public key is authentic before trusting what it checks

Before you start

What you’ll learn · Before you start

What you will learn

  • Name the three jobs of public keys, key encapsulation, key agreement and signatures, and what crosses between the sides in each
  • Explain why textbook RSA is unsafe and which paddings real systems use
  • Choose curves and key sizes that meet 128-bit security, and say how long they stay acceptable
  • Map each job to the Web Crypto algorithms that perform it
  • Verify that a public key is authentic before trusting what it checks

Before you start

On this page

A 32-byte X25519 public key can be printed on a web page, read out on a call or handed to an attacker without weakening it, and a 3,072-bit RSA public key lets anyone wrap a key that only one private key can unwrap. Public-key cryptography splits a key into a half for everyone and a half for its owner alone, and that split does what a shared key cannot: two parties who have never met can protect data and prove who made it.

This lesson names the three jobs a key pair does, shows on toy numbers why RSA without padding leaks what it encrypts, picks curves and sizes for 128-bit security, maps every job to the Web Crypto algorithm that ran it on Node.js 24, and ends with the check people skip: making sure a public key really belongs to its owner.

Three jobs: wrap a key, agree on one, or sign

A shared AES key works only when both sides already have it. Public-key cryptography gets around that with a key pair: A private key and the public key computed from it. The public key may be given to anyone; the private key stays with its owner, and only it can unwrap, sign or finish a key agreement for that pair.: a private key that stays with its owner, and a public key computed from it that anyone may hold. Every use of a key pair is one of three jobs:

  • key encapsulation: Delivering a fresh symmetric key under a recipient's public key, so that only the matching private key recovers it. RSA-OAEP key wrapping and ML-KEM do this; an AEAD then encrypts the data under that key.. Anyone with the public key wraps a fresh symmetric key, and only the private key unwraps it. RSA-OAEP does this today; ML-KEM is its post-quantum successor.
  • key agreement: Two parties each combine their own private key with the other's public key and arrive at the same shared secret without sending it, as Diffie-Hellman, ECDH and X25519 do. A KDF then turns that secret into keys.. Two sides each combine their own private key with the other’s public key and arrive at the same shared secret, as X25519 and ECDH do.
  • digital signature: A value made from a message with a private key that anyone holding the matching public key can check. Any change to the message or the signature makes the check fail; Ed25519, ECDSA and RSA-PSS make them.. The private key signs a message and anyone with the public key checks it, with Ed25519, ECDSA or RSA-PSS.

None of the three encrypts a file or a stream itself. They deliver or agree on a symmetric key, and AES-GCM or ChaCha20-Poly1305 does the bulk work, a pattern the hybrid encryption lesson of this module builds. Step through the three jobs between the owner of a key pair and someone who holds only its public key:

Interactive · message by message What one key pair can do, job by job Step through the three jobs. Watch what crosses between the two sides, and notice that the private key never does.
What one key pair can do, job by job, step 8 of 8. Messages so far: Owner of the key pair to Holder of the public key: public key, 32 to 422 bytes; Holder of the public key to Owner of the key pair: wrapped AES key; Holder of the public key to Owner of the key pair: holder's public key; Owner of the key pair to Holder of the public key: release and signature.Holder of the public keysignature verifiesOwner of the key pairhas the AES keypublic key, 32 to 422 byteswrapped AES keyholder's public keyrelease and signatureprivate key never sent

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. One key pair. The owner generates a key pair. The private key stays where it was made; the public key is sent out and may be read by anyone, an attacker included.
  2. Job 1: encapsulate a key. The holder makes a fresh 32-byte AES key and wraps it under the public key, here with RSA-OAEP. Anyone could wrap a key this way; nobody else can unwrap it.
  3. Only the owner unwraps it. The owner unwraps the AES key with the private key. Both sides now share a key for AES-GCM, which encrypts the actual data.
  4. Job 2: agree on a key. For key agreement the holder needs a key pair of its own, for example X25519. It sends its public key to the owner.
  5. The same secret on both sides. Each side combines its own private key with the other side's public key. Both arrive at the same 32 bytes, although neither secret crossed the wire.
  6. Job 3: sign. The owner signs a release with the private key, here with Ed25519, and publishes the release with its 64-byte signature.
  7. Anyone can verify. The holder checks the signature with the public key. It verifies for this release and fails for any change, even one digit of the version.
  8. What never moved. Across all three jobs only public keys, a wrapped key and a signature crossed between the sides. The private key never left its owner.

Only public keys, a wrapped key and a signature crossed between the sides. That is why a public key may be published: it lets others wrap keys for its owner, agree on keys with it and check its signatures, and it never lets them do what only the private key does.

Checkpoint

Check: The three jobs of a key pair

  1. Question 1 of 2 A release server must prove to a million laptops that an update came from it, and the laptops share no secret with it. Which job of a key pair fits?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Signatures, made with the server's private key and checked with its public key

    Signing is the job in which the private key acts and everyone else checks: one key pair serves any number of verifiers, and no verifier can make a signature.

  2. Question 2 of 2 An attacker gets a copy of your public key. Which of these can they now do?

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • Wrap a key that only you can unwrap
    • Check signatures that you made

    A public key lets anyone do the half of each job that is meant for everyone: wrap a key for its owner, agree on a key with its owner or check its owner's signatures. The other half needs the private key.

Textbook RSA leaks equal messages and multiplies ciphertexts

RSA’s arithmetic fits in one line. Pick two primes p and q, publish n = p × q and an exponent e, keep the private exponent d, and encrypt a number m as:

c=memodn,m=cdmodnc = m^{e} \bmod n, \qquad m = c^{d} \bmod n

In words: raise the message to the public exponent modulo n to encrypt, and the ciphertext to the private exponent to decrypt. On the program’s toy key, n = 1013 × 1019 = 1,032,247 and e = 65537, the number 250 encrypts to 148,245. Used exactly like that, as textbook RSA: RSA used as bare arithmetic, c = m^e mod n, with no padding scheme. It is deterministic, so equal messages give equal ciphertexts, and malleable, so a ciphertext times 2^e mod n opens as twice the message., it fails twice. It is deterministic, so the same message always gives the same ciphertext and anyone watching learns when two messages are equal. And it is malleable: Said of a cipher whose ciphertext can be changed so that the plaintext changes in a predictable way, as a flipped bit of a keystream cipher flips the same plaintext bit. Authentication tags prevent it. More in “Transposition, product ciphers and one-time pads”: the product of two ciphertexts is the ciphertext of the product, so multiplying a ciphertext by 2ᵉ mod n turns it into a valid ciphertext of twice the message, with no private key involved.

The program shows both on the toy key, then does the same task with RSA-OAEP from Web Crypto, and puts each through two checks: equal messages must look different, and a changed ciphertext must not open.

Textbook RSA against RSA-OAEP, put through the same two checks JavaScript · textbook-js/textbook_rsa.mjs
// RSA with no padding ("textbook RSA") on toy primes, then RSA-OAEP from Web Crypto, put through the same two checks:
// equal messages must give different ciphertexts, and a ciphertext changed without the private key must not open.
// The toy key is far too small for real use; it only makes the numbers readable.

const modPow = (base, exp, mod) => {
  let result = 1n;
  base %= mod;
  for (; exp > 0n; exp >>= 1n) {
    if (exp & 1n) result = (result * base) % mod;
    base = (base * base) % mod;
  }
  return result;
};
const modInverse = (a, m) => {
  let [oldR, r, oldS, s] = [a, m, 1n, 0n];
  while (r !== 0n) {
    const q = oldR / r;
    [oldR, r] = [r, oldR - q * r];
    [oldS, s] = [s, oldS - q * s];
  }
  return ((oldS % m) + m) % m;
};

// A toy key pair: n = p × q, e = 65537, d = e⁻¹ mod (p − 1)(q − 1).
const p = 1013n;
const q = 1019n;
const n = p * q;
const e = 65537n;
const d = modInverse(e, (p - 1n) * (q - 1n));
const encrypt = (m) => modPow(m, e, n);
const decrypt = (c) => modPow(c, d, n);

console.log(`textbook RSA, n = ${p} × ${q} = ${n}, e = ${e}`);
const amount = 250n;
const c1 = encrypt(amount);
const c2 = encrypt(amount);
console.log(`  250 encrypted twice: ${c1} and ${c2}, equal: ${c1 === c2}`);
const doubled = (c1 * encrypt(2n)) % n; // made from the ciphertext and the public key only
console.log(`  ciphertext × 2^e mod n = ${doubled}, which decrypts to ${decrypt(doubled)}`);
// Textbook RSA has nothing to check: any number below n decrypts to some number, so a changed ciphertext is never refused.
const refusedByTextbook = (() => {
  try {
    decrypt(doubled);
    return false;
  } catch {
    return true;
  }
})();
console.log(`  check 1, equal messages look different: ${c1 !== c2 ? 'pass' : 'FAIL'}`);
console.log(`  check 2, a changed ciphertext is refused: ${refusedByTextbook ? 'pass' : `FAIL (it opened as ${decrypt(doubled)})`}`);

// The same task with RSA-OAEP: random padding inside the encryption, and a structure that decryption checks.
const { subtle } = globalThis.crypto;
const keys = await subtle.generateKey(
  { name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' },
  true,
  ['encrypt', 'decrypt'],
);
const text = new TextEncoder().encode('amount=250');
const o1 = new Uint8Array(await subtle.encrypt({ name: 'RSA-OAEP' }, keys.publicKey, text));
const o2 = new Uint8Array(await subtle.encrypt({ name: 'RSA-OAEP' }, keys.publicKey, text));
const same = o1.every((b, i) => b === o2[i]);
const opened = new TextDecoder().decode(await subtle.decrypt({ name: 'RSA-OAEP' }, keys.privateKey, o1));
console.log('\nRSA-OAEP, 2048-bit key, SHA-256');
console.log(`  "amount=250" encrypted twice: ${o1.length} bytes each, equal: ${same}, first one opens as "${opened}"`);

// The same doubling, done on the OAEP ciphertext as a number, with n and e taken from the public key.
const jwk = await subtle.exportKey('jwk', keys.publicKey);
const big = (b64u) => BigInt(`0x${Buffer.from(b64u, 'base64url').toString('hex')}`);
const [N, E] = [big(jwk.n), big(jwk.e)];
const c = BigInt(`0x${Buffer.from(o1).toString('hex')}`);
const forged = Buffer.from(((c * modPow(2n, E, N)) % N).toString(16).padStart(512, '0'), 'hex');
let refused = '';
try {
  await subtle.decrypt({ name: 'RSA-OAEP' }, keys.privateKey, forged);
} catch (err) {
  refused = err.name;
}
console.log(`  ciphertext × 2^e mod n: decryption throws ${refused || 'nothing'}`);
console.log(`  check 1, equal messages look different: ${!same ? 'pass' : 'FAIL'}`);
console.log(`  check 2, a changed ciphertext is refused: ${refused ? 'pass' : 'FAIL'}`);

Output

textbook RSA, n = 1013 × 1019 = 1032247, e = 65537
  250 encrypted twice: 148245 and 148245, equal: true
  ciphertext × 2^e mod n = 487698, which decrypts to 500
  check 1, equal messages look different: FAIL
  check 2, a changed ciphertext is refused: FAIL (it opened as 500)

RSA-OAEP, 2048-bit key, SHA-256
  "amount=250" encrypted twice: 256 bytes each, equal: false, first one opens as "amount=250"
  ciphertext × 2^e mod n: decryption throws OperationError
  check 1, equal messages look different: pass
  check 2, a changed ciphertext is refused: pass

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node textbook_rsa.mjs

Textbook RSA failed both checks: 250 became 148,245 both times, and the ciphertext times 2ᵉ opened as 500. RSA-OAEP passed both: “amount=250” gave two different 256-byte ciphertexts, and the doubled one was refused with OperationError. RFC 8017, the PKCS #1 standard, says its RSA primitives are not meant to give security apart from a scheme, and it pairs each job with schemes of its own:

Scheme in RFC 8017 Job Use it?
RSAES-OAEP encryption and key wrapping yes: required for new applications
RSAES-PKCS1-v1_5 encryption no: kept for old systems; ASVS 11.3.1 rules it out
RSASSA-PSS signatures yes: required for new applications
RSASSA-PKCS1-v1_5 signatures only to talk to old systems; ASVS Appendix C disallows it

RSA-OAEP: RSAES-OAEP, RSA encryption with Optimal Asymmetric Encryption Padding (RFC 8017). Random padding makes every ciphertext different, and decryption refuses any ciphertext whose padding does not check. puts random padding inside every encryption and checks its structure on the way out, and RSA-PSS: RSASSA-PSS, RSA signatures with the Probabilistic Signature Scheme of RFC 8017, which encodes the message's hash with a random salt before the RSA operation. RFC 8017 requires it for new applications. adds a random salt to every signature; ASVS 5.0’s Appendix C approves RSA-PSS for signatures.

Random bytes in front of the message are not padding

Wrong: raw RSA (RSA_NO_PADDING in Node.js, or modular exponentiation written by hand), or a few random bytes glued to the message before it. Right: RSA-OAEP with SHA-256 to wrap keys and RSA-PSS to sign, from the platform’s crypto library. The OWASP Cryptographic Storage Cheat Sheet says plainly that prepending random bytes is no substitute for OAEP, whose padding decryption checks before it returns anything.

Checkpoint

Check: Why RSA needs OAEP or PSS

  1. Question 1 of 2 A service encrypts each vote, "yes" or "no", with textbook RSA under one public key. What can someone who sees the ciphertexts learn, with no private key?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Every vote, by encrypting "yes" and "no" with the public key and comparing

    With few possible messages, anyone holding the public key can encrypt each candidate and compare. RSA-OAEP's random padding makes every encryption different, which is why it passed the program's first check.

  2. Question 2 of 2 Which RSA scheme should a new system use to sign, according to RFC 8017?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: RSASSA-PSS

    RFC 8017 pairs each job with a scheme for new applications: RSAES-OAEP for encryption and RSASSA-PSS for signatures. The PKCS #1 v1.5 schemes stay only for talking to old systems.

Elliptic curves reach the same strength with far smaller keys

In elliptic-curve cryptography (ECC): Public-key cryptography on the points of an elliptic curve: the private key is a number and the public key that number times a base point. A 256-bit curve such as P-256 or Curve25519 gives about 128 bits of security. the private key is a number and the public key is a point: that number times a fixed base point of a curve. Working the number back out of the point is the elliptic-curve discrete logarithm problem, and on a 256-bit curve the best known attacks need about 2¹²⁸ steps, the same security strength: The work needed to break an algorithm, in bits: s bits means about 2^s operations. NIST uses the levels 112, 128, 192 and 256 to compare unlike algorithms, such as AES-128 and RSA-3072. More in “Security levels, key sizes and attack cost” as RSA-3072. Two families of curves cover nearly every system:

  • The NIST P-curves, P-256, P-384 and P-521, from SP 800-186. They serve ECDSA signatures and ECDH key agreement, and they are the curves SP 800-186 lists for key establishment.
  • Curve25519 and Curve448, from RFC 7748, which serve key agreement as X25519 and X448, and their Edwards forms, which serve signatures as Ed25519 and Ed448 (RFC 8032). RFC 7748 describes them as curves that lend themselves to constant-time code, and FIPS 186-5 approves EdDSA beside ECDSA and RSA.

The algorithm map later in this lesson generated one key of each kind and measured them:

Key Jobs Strength (SP 800-186, SP 800-57) Public key, as measured
P-256 ECDSA, ECDH 128 bits 65 bytes raw, 91 bytes as SPKI
Curve25519 X25519 key agreement 128 bits 32 bytes raw, 44 bytes as SPKI
Edwards25519 Ed25519 signatures 128 bits 32 bytes raw, 44 bytes as SPKI
RSA-3072 all three jobs 128 bits 422 bytes as SPKI

RFC 8032 adds why Ed25519 is the usual first choice for signing: it needs no fresh random number per signature, its keys and 64-byte signatures are small, and its formulas hold for every point, so untrusted public keys need no costly checks. P-256 stays the choice where a standard or a platform asks for NIST curves.

Checkpoint

Check: Elliptic curves and what each one is for

  1. One question A new service signs its API responses, and no rule forces NIST curves on it. Which algorithm does the lesson point to first, and why?

    Choose one answer.

    Show the answer: One question

    Answer: Ed25519, which needs no fresh random number per signature and has 32-byte keys and 64-byte signatures

    Ed25519 is the usual first choice for signatures at 128 bits; ECDSA on P-256 is the choice where a standard or a platform asks for NIST curves.

Key sizes for 128-bit security, and how long they last

NIST SP 800-57 lines up the sizes that reach each strength: RSA needs a 3,072-bit modulus for 128 bits and 7,680 for 192, while a curve needs about twice the strength in bits, so 256 bits for 128-bit strength. ASVS 5.0 requirement 11.2.3 asks for 128 bits as a floor and gives the same pair as its example. The program holds both tables as data; change the questions at the bottom and run it again:

Which RSA size and which curve reach each strength Python · sizes-py/sizes.py
"""Key sizes as data: the RSA modulus for each security strength (NIST SP 800-57 Part 1 Rev. 5, Table 2), the curves
SP 800-186 places at each strength (its Table 1), and the transition dates of the draft NIST IR 8547. Change the
questions at the bottom and run it again."""

RSA_MODULUS = {112: 2048, 128: 3072, 192: 7680, 256: 15360}  # strength in bits: RSA modulus in bits
CURVES = {
    112: ["P-224"],
    128: ["P-256", "Curve25519 (X25519)", "Edwards25519 (Ed25519)"],
    192: ["P-384"],
    224: ["Curve448 (X448)", "Edwards448 (Ed448)"],
    256: ["P-521"],
}


def rsa_strength(modulus_bits):
    """The strength an RSA key reaches: the highest level whose modulus it meets."""
    met = [s for s, k in RSA_MODULUS.items() if modulus_bits >= k]
    return max(met) if met else None


def curve_strength(name):
    return next(s for s, names in CURVES.items() if any(n.startswith(name) for n in names))


def status(strength, year):
    """IR 8547 (draft) for RSA and elliptic curves: 112-bit keys deprecated after 2030, all disallowed after 2035."""
    if year > 2035:
        return "disallowed: move to ML-KEM and ML-DSA"
    if strength < 128 and year > 2030:
        return "deprecated"
    return "acceptable"


print("RSA modulus for each strength:")
print("  " + "   ".join(f"{s} bits: {k}" for s, k in RSA_MODULUS.items()))
print("RSA size that matches P-384:", RSA_MODULUS[curve_strength("P-384")], "bits")
for bits in (2048, 3072, 4096):
    print(f"RSA-{bits} reaches {rsa_strength(bits)} bits")
for name in ("P-256", "Edwards25519", "P-521"):
    print(f"{name} reaches {curve_strength(name)} bits")
print(f"At 128 bits: an RSA-3072 modulus is {3072 // 8} bytes, an Ed25519 public key 32 ({3072 // 8 // 32} to 1)")
for key, year in (("RSA-2048", 2029), ("RSA-2048", 2032), ("P-256", 2032), ("P-256", 2036)):
    s = rsa_strength(int(key[4:])) if key.startswith("RSA") else curve_strength(key)
    print(f"{key} signing in {year}: {status(s, year)}")

Output

RSA modulus for each strength:
  112 bits: 2048   128 bits: 3072   192 bits: 7680   256 bits: 15360
RSA size that matches P-384: 7680 bits
RSA-2048 reaches 112 bits
RSA-3072 reaches 128 bits
RSA-4096 reaches 128 bits
P-256 reaches 128 bits
Edwards25519 reaches 128 bits
P-521 reaches 256 bits
At 128 bits: an RSA-3072 modulus is 384 bytes, an Ed25519 public key 32 (12 to 1)
RSA-2048 signing in 2029: acceptable
RSA-2048 signing in 2032: deprecated
P-256 signing in 2032: acceptable
P-256 signing in 2036: disallowed: move to ML-KEM and ML-DSA

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 sizes.py

RSA-2048 reaches only 112 bits, and RSA-4096 still reaches only 128, because the next level up needs 7,680 bits; to match P-384 an RSA key needs 7,680 bits. At 128 bits the RSA-3072 modulus is 384 bytes against a 32-byte Ed25519 key, 12 to 1. Those strengths assume classical computers, and NIST’s draft transition plan, IR 8547, puts end dates on them. Walk through its three phases:

Timeline · 3 events in order How long RSA and elliptic-curve keys stay in use Walk through the three phases of NIST's draft transition plan, and see which keys each one still allows.
  1. Now

    112 and 128 bits both acceptable

    SP 800-57 rates RSA-2048 at 112 bits of strength, and RSA-3072, P-256, X25519 and Ed25519 at 128. Keys at either level may protect new data today.

  2. After 2030

    112-bit keys deprecated RSA-2048 deprecated

    The draft NIST IR 8547 deprecates RSA, ECDSA and key establishment at 112 bits: RSA-2048 may still be used where the risk is accepted, but new designs should start at 128 bits.

  3. After 2035

    RSA and elliptic curves disallowed all disallowed

    The same draft disallows RSA, ECDSA, EdDSA and Diffie-Hellman at every strength, because a large quantum computer would break them. ML-KEM and ML-DSA take their place.

The timeline has 3 events, from Now to After 2035.

So a key that must sign or agree beyond 2035 needs a post-quantum algorithm, which the post-quantum module of this track covers, and RSA-2048 is a poor choice for anything new even before then.

Key idea

For 128-bit security use a 256-bit curve, X25519 or P-256 for key agreement and Ed25519 or P-256 for signatures, and RSA-3072 only where a system needs RSA; RSA-2048 gives 112 bits, which NIST plans to deprecate after 2030.

Checkpoint

Check: Key sizes for 128-bit security

  1. Question 1 of 2 How many bits must an RSA modulus have to match the 192-bit strength of P-384?

    Type a number.

    A hint

    SP 800-57's Table 2 lists 2,048, 3,072, 7,680 and 15,360 bits for its four levels.

    Show the answer: Question 1 of 2

    Answer: 7680 bits

    SP 800-57 gives 7,680 bits for 192-bit strength, the number the program printed for "the RSA size that matches P-384". RSA-4096 still reaches only 128 bits.

  2. Question 2 of 2 A new design proposes RSA-2048 signatures that must stay trusted until 2034. What is wrong with it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: RSA-2048 gives 112 bits, below the 128-bit floor of ASVS 11.2.3, and NIST's draft deprecates it after 2030

    Choose 128 bits for anything new: RSA-3072, or better a 256-bit curve such as Ed25519 or P-256. Signatures that must stay trusted past 2035 need a plan for ML-DSA as well.

Each job has its own algorithms in Web Crypto

Web Crypto names its algorithms by job, and every key it makes is bound to one algorithm and to the operations it may perform, its usages. The program generates one key pair for each algorithm a new design is likely to need, runs the job once, and tries one thing a key was not made for:

One key pair per job in Web Crypto, with sizes and one run each JavaScript · map-js/algorithm_map.mjs
// One key pair for each job of public-key cryptography in Web Crypto (Node.js 24): what each half of the pair may do,
// how big the public key is, and one run of the job. Every key is new on each run; only sizes and results are printed.
const { subtle } = globalThis.crypto;
const RSA = { publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256', modulusLength: 3072 };
const size = async (format, key) => new Uint8Array(await subtle.exportKey(format, key)).length;
const equal = (a, b) => a.length === b.length && a.every((x, i) => x === b[i]);
const message = new TextEncoder().encode('release 4.2.0');

const show = (job, name, pair, sizes, result) => {
  console.log(`${job}: ${name}`);
  console.log(`  public key ${sizes}; private key may ${pair.privateKey.usages.join(', ')}`);
  console.log(`  ${result}`);
};

// Job 1, key encapsulation: wrap a fresh AES key under the public key; only the private key unwraps it.
const oaep = await subtle.generateKey({ name: 'RSA-OAEP', ...RSA }, true, ['wrapKey', 'unwrapKey']);
const aes = await subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
const wrapped = new Uint8Array(await subtle.wrapKey('raw', aes, oaep.publicKey, { name: 'RSA-OAEP' }));
const unwrapped = await subtle.unwrapKey('raw', wrapped, oaep.privateKey, { name: 'RSA-OAEP' }, 'AES-GCM', true, ['encrypt']);
const sameKey = equal(new Uint8Array(await subtle.exportKey('raw', aes)), new Uint8Array(await subtle.exportKey('raw', unwrapped)));
show('encapsulate', 'RSA-OAEP 3072, SHA-256', oaep, `${await size('spki', oaep.publicKey)} bytes (SPKI)`, `a 32-byte AES key wrapped in ${wrapped.length} bytes, unwrapped intact: ${sameKey}`);

// Job 2, key agreement: each side combines its own private key with the other's public key.
for (const alg of [{ name: 'X25519' }, { name: 'ECDH', namedCurve: 'P-256' }]) {
  const a = await subtle.generateKey(alg, true, ['deriveBits']);
  const b = await subtle.generateKey(alg, true, ['deriveBits']);
  const ab = new Uint8Array(await subtle.deriveBits({ name: alg.name, public: b.publicKey }, a.privateKey, 256));
  const ba = new Uint8Array(await subtle.deriveBits({ name: alg.name, public: a.publicKey }, b.privateKey, 256));
  const label = alg.namedCurve ? `ECDH ${alg.namedCurve}` : alg.name;
  show('agree', label, a, `${await size('raw', a.publicKey)} bytes raw, ${await size('spki', a.publicKey)} SPKI`, `both sides derived the same ${ab.length} bytes: ${equal(ab, ba)}`);
}

// Job 3, signatures: the private key signs, anyone with the public key verifies.
const signers = [
  [{ name: 'Ed25519' }, 'Ed25519', 'Ed25519'],
  [{ name: 'ECDSA', namedCurve: 'P-256' }, { name: 'ECDSA', hash: 'SHA-256' }, 'ECDSA P-256, SHA-256'],
  [{ name: 'RSA-PSS', ...RSA }, { name: 'RSA-PSS', saltLength: 32 }, 'RSA-PSS 3072, SHA-256'],
];
for (const [gen, params, label] of signers) {
  const pair = await subtle.generateKey(gen, true, ['sign', 'verify']);
  const sig = new Uint8Array(await subtle.sign(params, pair.privateKey, message));
  const ok = await subtle.verify(params, pair.publicKey, sig, message);
  const changed = await subtle.verify(params, pair.publicKey, sig, new TextEncoder().encode('release 4.2.1'));
  const sizes = gen.name === 'RSA-PSS' ? `${await size('spki', pair.publicKey)} bytes (SPKI)` : `${await size('raw', pair.publicKey)} bytes raw, ${await size('spki', pair.publicKey)} SPKI`;
  show('sign', label, pair, sizes, `${sig.length}-byte signature verifies: ${ok}; after one changed digit: ${changed}`);
}

// A key does only the job it was made for: the RSA-OAEP key cannot sign.
try {
  await subtle.sign({ name: 'RSA-PSS', saltLength: 32 }, oaep.privateKey, message);
} catch (err) {
  console.log(`\nsigning with the RSA-OAEP private key: ${err.name}`);
}

Output

encapsulate: RSA-OAEP 3072, SHA-256
  public key 422 bytes (SPKI); private key may unwrapKey
  a 32-byte AES key wrapped in 384 bytes, unwrapped intact: true
agree: X25519
  public key 32 bytes raw, 44 SPKI; private key may deriveBits
  both sides derived the same 32 bytes: true
agree: ECDH P-256
  public key 65 bytes raw, 91 SPKI; private key may deriveBits
  both sides derived the same 32 bytes: true
sign: Ed25519
  public key 32 bytes raw, 44 SPKI; private key may sign
  64-byte signature verifies: true; after one changed digit: false
sign: ECDSA P-256, SHA-256
  public key 65 bytes raw, 91 SPKI; private key may sign
  64-byte signature verifies: true; after one changed digit: false
sign: RSA-PSS 3072, SHA-256
  public key 422 bytes (SPKI); private key may sign
  384-byte signature verifies: true; after one changed digit: false

signing with the RSA-OAEP private key: InvalidAccessError

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node algorithm_map.mjs

Every job worked, and every signature failed after one digit of the release changed. The RSA-OAEP private key refused to sign with InvalidAccessError, although an RSA-PSS key of the same size could: a key pair is made for one algorithm and keeps its usages. The map the program printed, as a table:

Job Web Crypto algorithms The private key’s usage
Key encapsulation RSA-OAEP; ML-KEM-768 in Node.js 24 as an experimental algorithm unwrapKey or decrypt
Key agreement X25519, ECDH with P-256 or P-384 deriveBits or deriveKey
Signatures Ed25519, ECDSA with P-256 or P-384, RSA-PSS sign

Node.js 24 also has X448 and Ed448, and the post-quantum ML-KEM and ML-DSA behind its modern-algorithms proposal, which its documentation marks as in active development.

Ask the platform before you build on an algorithm

SubtleCrypto.supports() exists in Node.js 24.21.0, but browsers differ in which algorithms they offer: feature-test the algorithm you need, and keep RSA-OAEP, ECDSA and ECDH P-256 as the forms every engine has.

Checkpoint

Check: The Web Crypto algorithm for each job

  1. Question 1 of 2 Your code calls subtle.sign() with the private key of an RSA-OAEP key pair. What does Node.js 24 do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It throws InvalidAccessError, because the key was made for RSA-OAEP and its usages

    A key pair is generated for one algorithm and a list of usages, and Web Crypto refuses everything else. Make one key pair per job instead of reusing one.

  2. Question 2 of 2 Which Web Crypto call performs X25519 key agreement?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: deriveBits({ name: "X25519", public: peerPublicKey }, myPrivateKey, 256)

    deriveBits and deriveKey are Web Crypto's key-agreement calls for X25519 and ECDH. The private key carries the deriveBits usage; the peer's public key is a parameter.

A public key is only as good as your reason to trust it

A public key needs no secrecy, but it needs authenticity. If an attacker can replace the vendor’s key on its way to your app, they can sign their own update with their own key and send that key along, and a signature check passes, because every key verifies what it signed itself. The defence is to decide which key is the vendor’s before you check anything with it. Step through two deliveries of an update, one with a swapped key:

Interactive · message by message An update checked against a pinned key Step through two deliveries of an update to your app, one of them with a swapped key, and see which check stops it.
An update checked against a pinned key, step 7 of 7. Messages so far: Vendor to Network path: update, key, signature; Network path to Your app: the same three; Network path to Your app: other key, changed update.Vendorsigns with Ed25519Network pathcan replace bytesYour apppin differs: refuseupdate, key, signaturethe same threeother key, changed update

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The pin ships with the app. Your app carries the SHA-256 fingerprint of the vendor's public key, learned once through a separate channel, such as the app's own signed build.
  2. A genuine delivery. The vendor publishes update 7.1.0, its signature and its public key.
  3. Delivered unchanged. The bytes arrive as they were sent.
  4. Pin, then signature. The key's fingerprint matches the pin, and the signature verifies under that key. The app installs the update.
  5. A swapped delivery. In your test, the path replaces all three: a changed update, signed with a different key, delivered with that key.
  6. The signature still verifies. Checked only against the key that came with it, the signature verifies: any key verifies what it signed itself. A signature check alone accepts.
  7. The pin refuses it. The received key's fingerprint is deb2ded3…, not the pinned one, so the app refuses the update before it checks anything else.

The program runs the same two deliveries with RFC 8032’s two sample key pairs, so it prints the same bytes every time:

A pinned fingerprint against a swapped key JavaScript · fingerprint-js/fingerprint.mjs
// Why a public key must be authentic: an app checks updates with the vendor's Ed25519 key. If a key can be swapped on
// the way, a signature check alone accepts whatever the swapped key signed; a pinned fingerprint of the vendor's key,
// shipped with the app, refuses it. The two key pairs are RFC 8032's TEST 1 and TEST 2, so every run prints the same.
const { subtle } = globalThis.crypto;
const unhex = (h) => Uint8Array.from(Buffer.from(h, 'hex'));
const hex = (b) => Buffer.from(b).toString('hex');
const b64u = (b) => Buffer.from(b).toString('base64url');

async function keyPair(secret, publicKey) {
  const jwk = { kty: 'OKP', crv: 'Ed25519', d: b64u(unhex(secret)), x: b64u(unhex(publicKey)) };
  const privateKey = await subtle.importKey('jwk', jwk, 'Ed25519', false, ['sign']);
  const pub = await subtle.importKey('raw', unhex(publicKey), 'Ed25519', true, ['verify']);
  return { privateKey, publicKey: pub };
}
// The fingerprint: SHA-256 over the key's SubjectPublicKeyInfo bytes, the form every platform can export.
const fingerprint = async (key) => hex(await subtle.digest('SHA-256', await subtle.exportKey('spki', key)));

const vendor = await keyPair('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60', 'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a');
const other = await keyPair('4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb', '3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c');
const PINNED = await fingerprint(vendor.publicKey); // what the app ships with, learned through a separate channel
console.log(`pinned in the app: ${PINNED.slice(0, 16)}… (SHA-256 of the vendor key's SPKI)`);

const genuine = new TextEncoder().encode('update 7.1.0 as the vendor built it');
const altered = new TextEncoder().encode('update 7.1.0 with something added');
const deliveries = [
  { what: 'genuine key and update', key: vendor.publicKey, update: genuine, sig: await subtle.sign('Ed25519', vendor.privateKey, genuine) },
  { what: 'swapped key and update', key: other.publicKey, update: altered, sig: await subtle.sign('Ed25519', other.privateKey, altered) },
];

for (const d of deliveries) {
  const signatureOk = await subtle.verify('Ed25519', d.key, d.sig, d.update);
  const pinOk = (await fingerprint(d.key)) === PINNED;
  console.log(`\n${d.what}: key ${(await fingerprint(d.key)).slice(0, 16)}…`);
  console.log(`  signature check alone: ${signatureOk ? 'accept' : 'refuse'}`);
  console.log(`  pin check, then signature check: ${pinOk && signatureOk ? 'accept' : 'refuse'}`);
}

Output

pinned in the app: 06e3fd8fda29bb60… (SHA-256 of the vendor key's SPKI)

genuine key and update: key 06e3fd8fda29bb60…
  signature check alone: accept
  pin check, then signature check: accept

swapped key and update: key deb2ded39dc26fce…
  signature check alone: accept
  pin check, then signature check: refuse

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node fingerprint.mjs

The signature check alone accepted both deliveries; the fingerprint (of a key): A hash of a public key's encoded bytes, such as SHA-256 over its SubjectPublicKeyInfo, short enough to pin in an app or compare through a separate channel to confirm whose key it is. check refused the swapped key, deb2ded3… against the pinned 06e3fd8f…. A pin is one way to make a key authentic. The others carry the same idea at a larger scale:

  • Certificates: a certificate authority you already trust signs a statement binding a name to a key, as TLS does; the TLS module of this track validates such chains in code.
  • A pinned key or fingerprint: the expected key ships with the app or the configuration, as above; it works when the set of keys is small and you control their rotation.
  • A directory you trust: keys looked up in a source with its own authenticity, such as a signed key list fetched over a verified connection.

Checkpoint

Check: Checking that a public key is authentic

  1. Question 1 of 2 An app verifies each update with the public key that arrives alongside it. Why is that not enough?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Whoever can replace the update can replace the key too, and their key verifies their own signature

    Decide which key is the vendor's before checking anything with it: a pinned fingerprint, a certificate chain or a trusted directory answers that; the signature check then answers whether the update is unchanged.

  2. Question 2 of 2 Your app pins the vendor's key fingerprint, and the vendor must move to a new key next year. What keeps updates working without giving up the pin?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Ship the new key's fingerprint beside the old one in an update signed with the old key, then drop the old pin later

    Pins need a planned rotation: carry the next key's fingerprint in advance, authenticated by the current key, and retire the old one once every client has the new one.

Practise: choose the algorithm for each job

Turn the lesson’s choices into code with sample tests you can run here: given a job, a strength and the needs of the system, return the Web Crypto parameters the lesson recommends, or refuse a job RSA cannot do.

Exercise · Easy · Python

Pick the Web Crypto algorithm for each job

Write recommend(job, needs=frozenset(), strength=128) in recommend.py. It returns the Web Crypto algorithm parameters, as a dict, for one job of a key pair at a security strength of 128 or 192 bits, following the lesson:

  • job is "encapsulate", "agree" or "sign"; strength is 128 or 192. Anything else raises ValueError.
  • Encapsulation uses RSA-OAEP: {"name": "RSA-OAEP", "modulusLength": 3072, "hash": "SHA-256"} at 128 bits, and a 7,680-bit modulus with SHA-384 at 192.
  • Key agreement uses {"name": "X25519"} at 128 bits, or {"name": "ECDH", "namedCurve": "P-256"} when needs contains "nist-curves" (a policy that allows only the NIST P-curves). At 192 bits it is ECDH on P-384.
  • Signing uses {"name": "Ed25519"} at 128 bits, or {"name": "ECDSA", "namedCurve": "P-256", "hash": "SHA-256"} with "nist-curves". At 192 bits it is ECDSA on P-384 with SHA-384.
  • When needs contains "rsa-only" (the other side supports only RSA), signing uses RSA-PSS with the RSA modulus and hash of that strength, and key agreement raises ValueError, because RSA cannot agree keys: encapsulate instead.

The sample tests cover five of the cases. The starter code answers Ed25519 for every job.

Starter code · recommend.py

def recommend(job: str, needs: frozenset = frozenset(), strength: int = 128) -> dict:
    """Web Crypto parameters for one job ("encapsulate", "agree" or "sign") at 128 or 192 bits."""
    return {"name": "Ed25519"}  # right for signing at 128 bits only
The sample tests · test_recommend.py
from recommend import recommend


def raises_value_error(call):
    try:
        call()
    except ValueError:
        return True
    return False


def test_sign_default():
    """signing at 128 bits with no other needs: Ed25519"""
    assert recommend("sign") == {"name": "Ed25519"}


def test_agree_under_a_nist_curves_policy():
    """a policy that allows only the P-curves: ECDH on P-256"""
    assert recommend("agree", frozenset({"nist-curves"})) == {"name": "ECDH", "namedCurve": "P-256"}


def test_encapsulate_at_192_bits():
    """192 bits needs a 7,680-bit modulus and SHA-384"""
    assert recommend("encapsulate", strength=192) == {"name": "RSA-OAEP", "modulusLength": 7680, "hash": "SHA-384"}


def test_rsa_cannot_agree_keys():
    """RSA has no key agreement, so asking for one is an error"""
    assert raises_value_error(lambda: recommend("agree", frozenset({"rsa-only"})))


def test_unknown_job_or_strength_is_an_error():
    """only the three jobs and the strengths 128 and 192 exist"""
    assert raises_value_error(lambda: recommend("decrypt"))
    assert raises_value_error(lambda: recommend("sign", strength=112))
A hint

Check job and strength first. Then work out the RSA modulus and the hash once from the strength (3072 and SHA-256, or 7680 and SHA-384), and the P-curve name (P-256 or P-384), before you look at the job and the needs.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

RSA Key Pair Generator Generate a 3,072-bit RSA key pair in your browser and compare its SPKI size with the 422 bytes measured here. Elliptic Curve Key Generator (ECDSA / Ed25519 / X25519) Generate P-256, Ed25519 and X25519 key pairs and see the 32- and 65-byte public keys of the table.

Then check yourself, one question at a time, weakest idea first:

Practice that adapts to you

Practise this lesson

18 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 18 Which job leaves two sides with the same secret although neither one sends it?

    Choose one answer.

    Show the answer: Question 1 of 18

    Answer: Key agreement

    In key agreement, such as X25519, only public keys cross; the shared secret is computed separately on each side.

  2. Question 2 of 18 A browser must hand a fresh 32-byte AES key to a server whose public key it already knows, in one message. Which job is that, and which algorithm does it?

    Choose one answer.

    Show the answer: Question 2 of 18

    Answer: Key encapsulation, with RSA-OAEP wrapping the key under the server's public key

    Wrapping a key under a known public key is key encapsulation. Web Crypto's wrapKey with RSA-OAEP turned a 32-byte AES key into 384 bytes in the program, and only the private key unwrapped it.

  3. Question 3 of 18 A system already has a key pair for every user. Why does it still encrypt their files with AES-GCM?

    Choose one answer.

    Show the answer: Question 3 of 18

    Answer: The public-key jobs deliver or agree on a symmetric key; an AEAD such as AES-GCM encrypts the data itself

    Every public-key design ends in a symmetric key: wrapped by RSA-OAEP, agreed by X25519 or encapsulated by ML-KEM. The data is then encrypted and authenticated with an AEAD under that key.

  4. Question 4 of 18 What makes textbook RSA malleable?

    Choose one answer.

    Show the answer: Question 4 of 18

    Answer: Multiplying two ciphertexts gives the ciphertext of the product of their messages

    Textbook RSA respects multiplication, which is why the program's ciphertext times 2ᵉ opened as 500. OAEP's padding breaks that structure, and decryption refuses anything whose padding does not check.

  5. Question 5 of 18 On the program's toy key, someone multiplies the ciphertext of 250 by 3ᵉ mod n. What does the result decrypt to?

    Type a number.

    A hint

    (m₁ᵉ)(m₂ᵉ) = (m₁m₂)ᵉ mod n, and 750 is smaller than n = 1,032,247.

    Show the answer: Question 5 of 18

    Answer: 750

    3ᵉ mod n is the ciphertext of 3, so the product is the ciphertext of 3 × 250 = 750, made without the private key, just as the program's 2ᵉ made 500.

  6. Question 6 of 18 A colleague proposes making textbook RSA safe by putting 8 random bytes in front of each message before encrypting. What does OAEP have that this lacks?

    Choose one answer.

    Show the answer: Question 6 of 18

    Answer: A padding structure that decryption checks, so a ciphertext changed by multiplication is refused

    OAEP is a full encoding with a mask generation function, and decryption checks it before returning anything. That check is what refused the program's doubled ciphertext.

  7. Question 7 of 18 On which curve does X25519 work?

    Choose one answer.

    Show the answer: Question 7 of 18

    Answer: Curve25519, from RFC 7748

    RFC 7748 specifies Curve25519 and Curve448 for key agreement, as X25519 and X448; their Edwards forms carry the signatures Ed25519 and Ed448.

  8. Question 8 of 18 A policy allows only the curves that SP 800-186 lists for key establishment. Which can you use for ECDH?

    Choose one answer.

    Show the answer: Question 8 of 18

    Answer: P-256

    SP 800-186's Table 2 gives each curve its allowed uses: the P-curves serve ECDSA and key establishment, Edwards25519 and Edwards448 serve EdDSA. A system bound by it agrees keys with ECDH on P-256.

  9. Question 9 of 18 Why can Ed25519 accept untrusted public keys without the costly point checks that some curve code needs, according to RFC 8032?

    Choose one answer.

    Show the answer: Question 9 of 18

    Answer: Its formulas are complete, valid for every point on the curve with no exceptions

    RFC 8032's list of advantages includes complete formulas, a deterministic signature that needs no fresh random number, side-channel resilience and small keys and signatures.

  10. Question 10 of 18 What security strength does RSA-2048 give, in bits?

    Type a number.

    Show the answer: Question 10 of 18

    Answer: 112 bits

    SP 800-57 rates RSA-2048 at 112 bits, below the 128-bit floor of ASVS 11.2.3. The program printed the same.

  11. Question 11 of 18 A team plans RSA-4096 to reach 192-bit strength. Is it enough?

    Choose one answer.

    Show the answer: Question 11 of 18

    Answer: No; RSA-4096 reaches 128 bits, and 192 bits needs a 7,680-bit modulus or P-384

    Strengths come in steps: 3,072 bits for 128, 7,680 for 192, 15,360 for 256. Everything between two steps counts as the lower one, and a 384-bit curve gets there far more cheaply.

  12. Question 12 of 18 Records signed today must stay verifiable as authentic until 2040. What does NIST's draft transition plan imply for the design?

    Choose one answer.

    Show the answer: Question 12 of 18

    Answer: Plan for ML-DSA, since RSA, ECDSA and EdDSA are disallowed after 2035 in the draft

    Classical strengths assume classical computers. For anything that must stay trusted past 2035, NIST's draft points to ML-DSA for signatures and ML-KEM for key establishment.

  13. Question 13 of 18 Which algorithm name does Web Crypto use for signatures on Edwards25519?

    Choose one answer.

    Show the answer: Question 13 of 18

    Answer: Ed25519

    Web Crypto has one algorithm per scheme: Ed25519 signs, X25519 agrees keys, ECDSA and ECDH take a named P-curve.

  14. Question 14 of 18 A browser app must agree keys with a server whose policy requires NIST curves. Which parameters does it pass to generateKey()?

    Choose one answer.

    Show the answer: Question 14 of 18

    Answer: { name: "ECDH", namedCurve: "P-256" }

    Key agreement on a NIST curve is ECDH with a named P-curve. The private key gets the deriveBits usage, as in the program's ECDH P-256 run.

  15. Question 15 of 18 Why does the lesson suggest feature-testing an algorithm such as X25519 before a web app relies on it?

    Choose one answer.

    Show the answer: Question 15 of 18

    Answer: Browsers differ in the algorithms they offer, so code should test for the one it needs and keep a widely available fallback

    Engines add algorithms at different times. Ask first, with SubtleCrypto.supports() where it exists or a careful try of the operation, and fall back to an algorithm every engine has.

  16. Question 16 of 18 A public key needs no secrecy. What does it need?

    Choose one answer.

    Show the answer: Question 16 of 18

    Answer: Authenticity, a reason to believe it belongs to whom you think

    Certificates, pinned fingerprints and trusted directories all answer one question: is this public key really the owner's? A signature check answers a different one.

  17. Question 17 of 18 In the program, which delivery did the signature check alone refuse?

    Choose one answer.

    Show the answer: Question 17 of 18

    Answer: Neither; both signatures verified under the key that came with them

    A signature only ties a message to a key. The program's pin check, a SHA-256 fingerprint of the vendor key's SPKI, refused the swapped key, deb2ded3…, against the pinned 06e3fd8f….

  18. Question 18 of 18 A service talks to thousands of partners whose keys change often. Why is pinning each key a poor fit, and what fits better?

    Choose one answer.

    Show the answer: Question 18 of 18

    Answer: A pin list that large changes constantly; certificates from an authority you trust bind names to keys instead

    Choose the mechanism by the number of keys and how often they change: a pin for a few keys you control, certificates or a trusted directory for many.

Interview questions

  1. Fresher to mid · Warm-up

    What is the difference between a public key and a private key, and what are the three jobs public-key cryptography does?

    A key pair is a private key that never leaves its owner and a public key computed from it that anyone may hold. The public key lets others do the half of each job meant for everyone; the private key does the owner's half. The three jobs are key encapsulation (anyone wraps a fresh symmetric key under the public key, and only the private key unwraps it, as RSA-OAEP does), key agreement (two sides combine their own private key with the other's public key and both get the same secret, as X25519 and ECDH do) and signatures (the private key signs, and anyone with the public key verifies, as Ed25519, ECDSA and RSA-PSS do). None of them encrypts bulk data: that is AES-GCM's job, under the key the public-key step delivered. A public key needs no secrecy, but it does need authenticity.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A partner sends card tokens encrypted to your RSA-2048 public key with PKCS#1 v1.5 padding, and an API of yours decrypts them. What risks does this design carry, and how do you move it to a safe scheme without breaking the partner?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to staff

    A mobile app downloads the server's RSA public key at first launch and encrypts crash reports to it. Where can a wrong key enter, what does an attacker gain if it does, and how do you make the key authentic, including when the server's key has to change?

    Model answer, Pro. First step and follow-ups

Flashcards · 8 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What are the three jobs of a key pair, and which algorithms do them?

    Key encapsulation: anyone with the public key wraps a fresh symmetric key and only the private key unwraps it (RSA-OAEP; ML-KEM later). Key agreement: each side combines its own private key with the other's public key and gets the same secret (X25519, ECDH). Signatures: the private key signs and anyone with the public key checks (Ed25519, ECDSA, RSA-PSS). None encrypts a file itself; AES-GCM or ChaCha20-Poly1305 does the bulk work.

  2. Why does textbook RSA fail?

    With c = m^e mod n it is deterministic, so the same message always gives the same ciphertext and a watcher learns when two messages are equal; and malleable, since the product of two ciphertexts is the ciphertext of the product. On the toy key 250 gave 148,245 both times, and a ciphertext times 2^e mod n opened as 500 with no private key. RSA-OAEP passed both checks.

  3. Which RSA schemes should new code use?

    RSAES-OAEP for encryption and key wrapping, and RSASSA-PSS for signatures; RFC 8017 requires both for new applications. RSAES-PKCS1-v1_5 is kept for old systems (ASVS 11.3.1 rules it out), and RSASSA-PKCS1-v1_5 only to talk to old systems (ASVS Appendix C disallows it). A few random bytes glued to a message are not padding.

  4. Which curves serve which jobs, and why is Ed25519 the usual first choice for signing?

    NIST P-256, P-384 and P-521 (SP 800-186) serve ECDSA and ECDH. Curve25519 and Curve448 (RFC 7748) serve key agreement as X25519 and X448, and their Edwards forms serve signatures as Ed25519 and Ed448 (RFC 8032). Ed25519 needs no fresh random number per signature, has small keys and 64-byte signatures, and needs no costly checks of untrusted public keys. P-256 stays where a standard asks for NIST curves.

  5. How large are public keys at 128-bit security?

    P-256: 65 bytes raw (91 as SPKI). Curve25519 and Edwards25519: 32 bytes raw (44 as SPKI). RSA-3072: 422 bytes as SPKI, a 384-byte modulus against a 32-byte Ed25519 key, 12 to 1. RSA-2048 reaches only 112 bits, RSA-4096 still only 128, and 192 bits need 7,680.

  6. What does NIST's draft transition plan do to 112-bit keys, RSA and elliptic curves?

    It deprecates 112-bit security after 2030 and disallows RSA and elliptic curves after 2035, in favour of ML-KEM and ML-DSA. A key that must sign or agree beyond 2035 needs a post-quantum algorithm, and RSA-2048 is a poor choice for anything new even before then.

  7. What does Web Crypto bind to every key it makes?

    One algorithm and the operations it may perform, its usages: an RSA-OAEP private key unwraps or decrypts, X25519 and ECDH keys use deriveBits or deriveKey, and Ed25519, ECDSA and RSA-PSS keys sign. An RSA-OAEP private key refused to sign with InvalidAccessError, although an RSA-PSS key of the same size could.

  8. A signature check passes. Why is that not enough to trust an update?

    Every key verifies what it signed itself. An attacker who swaps the vendor's key can sign their own update and send their key along. Decide which key is authentic first: a pinned fingerprint (the swapped key's deb2ded3… did not match the pinned 06e3fd8f…), a certificate from an authority you trust, or a directory you trust.

Key takeaways

  • A key pair does three jobs, key encapsulation, key agreement and signatures, and none of them moves the private key; the bulk data is always encrypted with a symmetric key such as AES-GCM.
  • Textbook RSA is deterministic and malleable: on the toy key 250 encrypted to the same number twice, and a ciphertext times 2ᵉ opened as 500. Real systems use RSA-OAEP to wrap keys and RSA-PSS to sign.
  • A 256-bit curve gives 128-bit security with a 32- or 65-byte public key, where RSA needs 3,072 bits; RSA-2048 gives only 112 bits.
  • NIST’s draft plan deprecates 112-bit keys after 2030 and disallows RSA and elliptic curves after 2035, in favour of ML-KEM and ML-DSA.
  • In Web Crypto each key is bound to one algorithm and its usages: RSA-OAEP to wrap, X25519 or ECDH to agree, Ed25519, ECDSA or RSA-PSS to sign.
  • A signature proves only that a key made it; decide which key is authentic first, through a pin, a certificate or a trusted directory.

Terms in this lesson

digital signature
A value made from a message with a private key that anyone holding the matching public key can check. Any change to the message or the signature makes the check fail; Ed25519, ECDSA and RSA-PSS make them.
elliptic-curve cryptography (ECC)
Public-key cryptography on the points of an elliptic curve: the private key is a number and the public key that number times a base point. A 256-bit curve such as P-256 or Curve25519 gives about 128 bits of security.
fingerprint (of a key)
A hash of a public key's encoded bytes, such as SHA-256 over its SubjectPublicKeyInfo, short enough to pin in an app or compare through a separate channel to confirm whose key it is.
key agreement
Two parties each combine their own private key with the other's public key and arrive at the same shared secret without sending it, as Diffie-Hellman, ECDH and X25519 do. A KDF then turns that secret into keys.
key encapsulation
Delivering a fresh symmetric key under a recipient's public key, so that only the matching private key recovers it. RSA-OAEP key wrapping and ML-KEM do this; an AEAD then encrypts the data under that key.
key pair
A private key and the public key computed from it. The public key may be given to anyone; the private key stays with its owner, and only it can unwrap, sign or finish a key agreement for that pair.
RSA-OAEP
RSAES-OAEP, RSA encryption with Optimal Asymmetric Encryption Padding (RFC 8017). Random padding makes every ciphertext different, and decryption refuses any ciphertext whose padding does not check.
RSA-PSS
RSASSA-PSS, RSA signatures with the Probabilistic Signature Scheme of RFC 8017, which encodes the message's hash with a random salt before the RSA operation. RFC 8017 requires it for new applications.
textbook RSA
RSA used as bare arithmetic, c = m^e mod n, with no padding scheme. It is deterministic, so equal messages give equal ciphertexts, and malleable, so a ciphertext times 2^e mod n opens as twice the message.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.