---
title: 'TLS Handshake'
source: 'https://academia.sh/en/courses/identity-and-cryptography/tls-handshake'
course: 'Identity, Access and Cryptography'
language: en
updated: '2026-09-26T03:02:53+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Maps the handshake's identity steps to the claim each proves, and counts the items that must hold per step"
    - 'Measures, by running it, how many presentations pass unproven when a step is turned off'
    - 'Calculates the claim mutual verification adds, and its item cost per connection and at fleet scale'
---

# TLS Handshake

The handshake is taken apart along its identity steps: each of six claims ties to a single step, when any of the four identity steps is turned off exactly one more of five fictional presentations gets accepted, and with no identity step at all all five of the five pass; full verification requires 11 items to hold at once, mutual verification requires 15, and at fleet scale it adds 17,552 items.

The previous three lessons measured the certificate's fields, its chain, and its revocation status
separately. None of these checks happens on its own; all of them come together inside a single
exchange, while the connection is being established, in under a second. That exchange is the
handshake.

The handshake is usually called "where the encrypted channel gets set up." That definition is
incomplete: the encrypted channel can be set up independent of identity, and when it is, what
results is a secret channel where **who is being talked to is unknown**. This lesson takes the
handshake apart not by encryption but by identity, and measures three things: which step proves
which claim, which claim stays unproven when a step is skipped and how many presentations pass,
and how many items the client also presenting an item adds. Configuration items and response
headers are outside this lesson; they were measured in the Backend Security and Cloud Security
courses.

- **PK16.** The handshake is modeled as four identity steps: chain verification, name check,
  revocation check, item-possession signature. The items are fictional; signature generation, key
  exchange, and digest verification really run with `node:crypto`.
- **PK17.** The item-possession signature is the other party signing the handshake digest with the
  private key that matches the certificate's public key. The certificate is presented publicly;
  producing the signature requires the private key.
- **PK18.** Ephemeral keys are regenerated on every handshake, and the shared secret is derived
  from these ephemeral pairs; the certificate's key never enters the shared secret.
- **PK19.** A presentation attempt is the other party presenting an item and signing the digest
  with a key. One of five fictional presentations is legitimate.
- **PK20.** A step turned off in a setting never runs at all; the claim that step proves is counted
  as unproven, and that step's items drop from the count.

## Steps and What They Prove

Each identity step makes a separate statement. The run below maps these statements to steps and
counts the items each step requires to hold.

```js
// step.mjs — the handshake's identity steps; items are fictional, signature and key generation are real.
import { generateKeyPairSync, sign, verify, diffieHellman, createHmac } from "node:crypto";

const ed = () => generateKeyPairSync("ed25519");
export const ROOT = ed(), INTERMEDIATE = ed(), FOREIGN_INTERMEDIATE = ed();
const SERVER = ed(), BILLING = ed(), FOREIGN_LEAF = ed(), REVOKED = ed(), CLIENT = ed();

let serial = 7300;
const register = (subject, altName, issuer, priv, pair, isAuthority = false) => {
  const k = { subject, altName, issuer, serial: serial++, isAuthority,
    public: pair.publicKey.export({ type: "spki", format: "der" }).toString("hex") };
  k.signature = sign(null, body(k), priv);
  return k;
};
export const body = (k) => Buffer.from(JSON.stringify([k.subject, k.altName, k.issuer, k.serial,
  k.isAuthority, k.public]));

export const INTERMEDIATE_ITEM = register("measurement-intermediate", ["measurement-intermediate"],
  "org-root", ROOT.privateKey, INTERMEDIATE, true);
const FOREIGN_ITEM = register("foreign-intermediate", ["foreign-intermediate"], "foreign-root",
  FOREIGN_INTERMEDIATE.privateKey, FOREIGN_INTERMEDIATE, true);
export const ITEM = {
  valid: register("measurement-collector", ["measurement-collector"], "measurement-intermediate", INTERMEDIATE.privateKey, SERVER),
  billing: register("billing-service", ["billing-service"], "measurement-intermediate", INTERMEDIATE.privateKey, BILLING),
  foreign: register("measurement-collector", ["measurement-collector"], "foreign-intermediate",
    FOREIGN_INTERMEDIATE.privateKey, FOREIGN_LEAF),
  revoked: register("measurement-collector", ["measurement-collector"], "measurement-intermediate", INTERMEDIATE.privateKey, REVOKED),
  client: register("field-terminal-17", ["field-terminal-17"], "measurement-intermediate",
    INTERMEDIATE.privateKey, CLIENT),
};
export const PRIVATE = { valid: SERVER.privateKey, billing: BILLING.privateKey,
  foreign: FOREIGN_LEAF.privateKey, revoked: REVOKED.privateKey, client: CLIENT.privateKey };
export const REVOKED_LIST = [ITEM.revoked.serial];
export const INTERMEDIATE_REGISTRY = { "measurement-intermediate": INTERMEDIATE_ITEM, "foreign-intermediate": FOREIGN_ITEM };

// A handshake run: ephemeral keys are generated, the shared secret is derived, the digest is signed.
export function handshake(presentation, targetName, setting) {
  const clientEphemeral = generateKeyPairSync("x25519"), serverEphemeral = generateKeyPairSync("x25519");
  const digest = Buffer.concat([Buffer.from("client-hello|server-hello|"),
    clientEphemeral.publicKey.export({ type: "spki", format: "der" }),
    serverEphemeral.publicKey.export({ type: "spki", format: "der" })]);
  const presented = ITEM[presentation.item];
  const possessionSignature = sign(null, digest, PRIVATE[presentation.signer]);

  const result = { chain: true, name: true, revocation: true, possession: true };
  if (setting.chain) {
    const intermediate = INTERMEDIATE_REGISTRY[presented.issuer];
    result.chain = Boolean(intermediate) && verify(null, body(intermediate), ROOT.publicKey, intermediate.signature) &&
      intermediate.isAuthority && verify(null, body(presented), INTERMEDIATE.publicKey, presented.signature);
  }
  if (setting.name) result.name = presented.altName.includes(targetName);
  if (setting.revocation) result.revocation = !REVOKED_LIST.includes(presented.serial);
  if (setting.possession) {
    const pub = { key: Buffer.from(presented.public, "hex"), format: "der", type: "spki" };
    result.possession = verify(null, digest, pub, possessionSignature);
  }
  const accept = Object.values(result).every(Boolean);
  // The shared secret is derived only in an accepted handshake.
  let keysEqual = false, finished = false;
  if (accept) {
    const a = diffieHellman({ privateKey: clientEphemeral.privateKey, publicKey: serverEphemeral.publicKey });
    const b = diffieHellman({ privateKey: serverEphemeral.privateKey, publicKey: clientEphemeral.publicKey });
    keysEqual = a.equals(b);
    finished = createHmac("sha256", a).update(digest).digest()
      .equals(createHmac("sha256", b).update(digest).digest());
  }
  return { ...result, accept, keysEqual, finished };
}

export const write = (g) => (h) =>
  h.map((v, i) => (i ? String(v).padStart(g[i]) : String(v).padEnd(g[0]))).join("");
```

```js
// measure.mjs — the claim proven per step, the path a skipped step opens, mutual verification
import { handshake, write } from "./step.mjs";

// Which step proves which claim.
export const CLAIM = [
  ["the item presented chains to a familiar root", "chain"],
  ["the item was issued for the name being discussed", "name"],
  ["the item has not been revoked", "revocation"],
  ["the other party holds the item's private key", "possession"],
  ["the session key is the same on both sides and unknown to a third party", "key"],
  ["the step digest is the same on both sides", "finished"],
];
// The items each step requires to hold.
const ITEMS = {
  chain: ["root store record", "intermediate item's validity", "intermediate item's private key"],
  name: ["item's name list", "client's target name"],
  revocation: ["revocation source is current", "access to the revocation source"],
  possession: ["server's private key stays secret"],
  key: ["the ephemeral key is unique to every handshake"],
  finished: ["the digest is produced in the same order on both sides"],
};
const FIXED = ["verifier's clock"];
const CLIENT_ITEMS = ["client item's chain", "client item's revocation status",
  "client's private key stays secret", "client item's distribution"];

// Fictional presentation attempts: which item is presented, which key signs the digest.
const PRESENTATION = [
  ["valid item, valid name", { item: "valid", signer: "valid" }, "measurement-collector", "legit"],
  ["item issued for a different name", { item: "billing", signer: "billing" },
    "measurement-collector", "name"],
  ["item from an unknown authority", { item: "foreign", signer: "foreign" },
    "measurement-collector", "chain"],
  ["revoked item", { item: "revoked", signer: "revoked" }, "measurement-collector", "revocation"],
  ["copied item, no private key", { item: "valid", signer: "client" },
    "measurement-collector", "possession"],
];

const SETTING = [
  ["full verification", { chain: 1, name: 1, revocation: 1, possession: 1 }, false],
  ["name check off", { chain: 1, name: 0, revocation: 1, possession: 1 }, false],
  ["chain verification off", { chain: 0, name: 1, revocation: 1, possession: 1 }, false],
  ["revocation check off", { chain: 1, name: 1, revocation: 0, possession: 1 }, false],
  ["possession signature skipped", { chain: 1, name: 1, revocation: 1, possession: 0 }, false],
  ["no identity step at all", { chain: 0, name: 0, revocation: 0, possession: 0 }, false],
  ["full verification + mutual", { chain: 1, name: 1, revocation: 1, possession: 1 }, true],
];

const first = handshake(PRESENTATION[0][1], PRESENTATION[0][2], { chain: 1, name: 1, revocation: 1, possession: 1 });
console.log(`legit handshake under full verification: accept ${first.accept}, ` +
  `shared secret equal ${first.keysEqual}, finished verification ${first.finished}`);
console.log(`${PRESENTATION.length} fictional presentation attempts, ${SETTING.length} settings\n`);

const fmt1 = write([74, 18, 8]);
console.log(fmt1(["claim", "proving step", "items"]));
for (const [text, step] of CLAIM)
  console.log(fmt1([text, step, (ITEMS[step] ?? []).length]));

const fmt2 = write([32, 12, 10, 11, 18, 8]);
console.log("\n" + fmt2(["setting", "proven", "unproven", "accept/5", "unproven passed", "items"]));
for (const [name, setting, mutual] of SETTING) {
  const result = PRESENTATION.map(([, s, target]) => handshake(s, target, setting));
  const accept = result.filter((r) => r.accept).length;
  const proven = CLAIM.filter(([, a]) => setting[a] === undefined || setting[a]).length +
    (mutual ? 1 : 0);
  const items = [...Object.entries(ITEMS).filter(([a]) => setting[a] === undefined || setting[a])
    .flatMap(([, l]) => l), ...FIXED, ...(mutual ? CLIENT_ITEMS : [])].length;
  const totalClaims = CLAIM.length + (mutual ? 1 : 0);
  console.log(fmt2([name, proven, totalClaims - proven, accept, accept - 1, items]));
}

console.log(`\nclaim mutual verification adds: the client is a subject the organization recognizes`);
console.log(`items it adds (per connection ${CLIENT_ITEMS.length}):`);
for (const k of CLIENT_ITEMS) console.log(`  ${k}`);
const MUTUAL = handshake({ item: "client", signer: "client" }, "field-terminal-17",
  { chain: 1, name: 1, revocation: 1, possession: 1 });
console.log(`the same four steps ran in the client direction: accept ${MUTUAL.accept}`);
const FLEET = [["meter endpoint", 4100], ["internal service", 26], ["staff workstation", 262]];
const total = FLEET.reduce((t, [, n]) => t + n, 0);
console.log(`items added at fleet scale: ${total} leaf items x ` +
  `${CLIENT_ITEMS.length} = ${total * CLIENT_ITEMS.length}`);
```

```
legit handshake under full verification: accept true, shared secret equal true, finished verification true
5 fictional presentation attempts, 7 settings

claim                                                                           proving step   items
the item presented chains to a familiar root                                           chain       3
the item was issued for the name being discussed                                        name       2
the item has not been revoked                                                     revocation       2
the other party holds the item's private key                                      possession       1
the session key is the same on both sides and unknown to a third party                   key       1
the step digest is the same on both sides                                           finished       1

setting                               proven  unproven   accept/5   unproven passed   items
full verification                          6         0          1                 0      11
name check off                             5         1          2                 1       9
chain verification off                     5         1          2                 1       8
revocation check off                       5         1          2                 1       9
possession signature skipped               5         1          2                 1      10
no identity step at all                    2         4          5                 4       3
full verification + mutual                 7         0          1                 0      15

claim mutual verification adds: the client is a subject the organization recognizes
items it adds (per connection 4):
  client item's chain
  client item's revocation status
  client's private key stays secret
  client item's distribution
the same four steps ran in the client direction: accept true
items added at fleet scale: 4388 leaf items x 4 = 17552
```

The first line says the legitimate handshake really ran: the item was accepted, the shared secret the
two sides derived matches, and the finished value computed over the digest came out the same on
both sides. These three results were verified separately, because each corresponds to a separate
claim.

The claim table shows the steps' division of labor. **Chain verification** says the item chains
to a familiar root, but it does not say who the item identifies. **Name check** says the item was
issued for the name being discussed, but it does not say whether the item is valid. The
**item-possession signature**, though, says something none of the others say: the other party
is not just showing this item, it also holds the item's private key. A certificate is a document
presented publicly; copying it requires no obstacle at all. Producing the possession signature
requires something the certificate itself does not carry.

The last two claims are independent of the identity steps. The shared secret matching on both
sides and the digest staying unchanged hold even if no item is presented at all. This is the
lesson's most commonly confused point, and it shows up in the last setting row.

## When a Step Is Skipped

The second table lines up four settings side by side, each one turning off a single step. The
same thing happens in all four: the proven claims drop from six to five, and accepted
presentations rise from one to two. In other words, **each step stops exactly one presentation**,
and none is a backup for another. When the name check is turned off, the item issued for the
billing service passes as the collector; when chain verification is turned off, the item produced
by an unknown authority passes; when the revocation check is turned off, the item that had been
considered invalid passes.

The fifth row breaks from the other four. When the possession signature is skipped, the number of
items that must hold drops from eleven to **ten** — the smallest drop of all the settings. What
passes instead is a party carrying a copy of the item and no private key at all. The skip that
saves the fewest items is the most expensive one from a defense standpoint, because the one item
it removes is the one thing the certificate does not itself carry.

The sixth row gives the measure's floor. With no identity step at all, all five of the five
presentations are accepted, four pass unproven, and three items must hold. But the number of
proven claims is not zero, it is **two**: the shared secret is still the same on both sides, and the
digest still has not changed. In this setting the connection is encrypted, and a listening third
party cannot read the content — only who is on the other end is unknown. Encryption and
authentication are two separate jobs; the first completes without the second, and the moment it
completes, there is a connection sitting there that looks successful.

There is also something none of the four steps say. The name check compares the item against the
name the client **intends to reach**; it does not ask whether that name is correct. If the client
has been directed to the wrong name and the other party carries a valid item for that name, all
four of the four steps pass. Chain verification carries a similar limit: it says the item chains
to a familiar root, not that the party behind that item is trustworthy.

## What Mutual Verification Adds

Everything measured up to now was one-directional: the client was asking who the server was, and
the server was not asking the client the same question. The last setting adds that question, and
the same four steps run in the reverse direction — the run confirms this separately, the client's
item gets accepted too.

The gain is a claim: the client is a subject the organization recognizes. The cost is four items
per connection, fifteen total. Three of those four items are the mirror image of the server-side
ones; the fourth is new, and it carries the real weight: **the client item's distribution**. On the
server side there is one item and it sits in one place; on the client side, an item has to be
placed separately on every machine that connects.

At fleet scale that means **17,552 items** for those 4,388 leaf items. The number gains meaning
read alongside the second lesson's inventory: mutual verification adds one claim on the
authentication side while opening a four-thousand-three-hundred-eighty-eight-item load on the
lifecycle side. That load is not itself a defense; it is the condition for the defense being
sustainable, and when it is not paid, the added claim stays unproven too.

This points to a conclusion that holds for all four steps. Every claim measured at the moment of
the handshake rests on work done long before the connection: issuing the item, writing its name
correctly, chaining it to the root, and renewing it before it expires. The measurement happens at
the moment of connection; the work happens at another time.

## Summary

- The handshake's four identity steps prove four of six claims; the remaining two claims (the
  shared secret's equality and the digest staying unchanged) hold independently of the identity
  steps.
- When any of the four steps is turned off, proven claims go from 6 to 5 and accepted
  presentations go from 1 to 2: each step stops exactly one presentation, and none is a backup for
  another.
- Skipping the possession signature reduces the items that must hold by only 1 (11 to 10) but
  accepts a party carrying a copy of the certificate with no private key. The smallest saving is
  the most expensive opening.
- With no identity step at all, 5 of 5 presentations pass, but 2 claims are still proven: the
  connection is encrypted and its content cannot be read — only who is on the other end is unknown.
  Encryption completes without authentication.
- Mutual verification adds 1 claim, raises items per connection from 11 to 15, and at fleet scale
  brings a 17,552-item load for 4,388 leaf items; most of that load is distribution and lifecycle
  work, not verification.

## Next Step

This lesson's last sentence is the next one's question. Every claim measured in the handshake was
the result of work done before the connection, and the most fragile part of that work concerns
time: every item has a validity window, and when the window closes, the item stops passing even if
nothing else has changed. The next lesson measures that window — how many items go unnoticed when
renewal is done manually, how many connections a single expired item drops, which gap automation
closes, and how many items' worth of maintenance work shortening item lifetime adds.
