---
title: 'Identity, Access and Cryptography'
source: 'https://academia.sh/en/courses/identity-and-cryptography'
language: en
updated: '2026-09-26T03:02:52+00:00'
license: 'CC BY-SA 4.0'
lessons: 21
outcomes:
    - 'Distinguishes hashing, encryption, and encoding along the axes of reversibility and purpose'
    - 'Builds the chain of trust of a public key infrastructure and counts the items that fall when the root breaks'
    - 'Measures the trust root and renewal cost of enterprise identity mechanisms'
---

# Identity, Access and Cryptography

What verification rests on and where that foundation breaks: the reversibility difference between hashing, encryption, and encoding, collision resistance and deprecated algorithms, how salt and the work factor affect the cost of guessing, the key distribution problem of symmetric encryption, asymmetric key roles, producing a shared secret over an insecure channel, the items a signature proves and does not prove, certificate fields and the chain of trust, the window difference between CRLs and OCSP, which step of the handshake proves which claim, the certificate lifecycle, directory services, ticket-based authentication, single sign-on, multi-factor authentication and its bypass paths, group policies, privilege escalation, and credential theft.

## Cryptography Fundamentals

Measuring cryptographic operations in terms of trust root and breaking point: the number of items hashing, encryption, and encoding each need for reversal, measuring collision resistance against the birthday bound, the work factor's cost curve, and how many rainbow-table items a salt renders useless, the key distribution arithmetic of symmetric encryption, which direction the public and private key each perform their operation, producing a shared secret over an insecure channel, and the items a signature proves and does not prove.

### Hashing, Encryption, and Encoding

Source: https://academia.sh/en/courses/identity-and-cryptography/hashing-encryption-and-encoding

Separating three operations in terms of reversibility: the encoded, encrypted, and hashed form of the same meter reading, the number of secret items needed for reversal being 0, 2, and none respectively, only four of the twelve matches between four fields and three operations coming out appropriate, and a wrong match destroying the reversal of 24,800 records.

The previous course counted the claims on the network and arrived at the same place across
twenty lessons: a claim is accepted only to the extent it can be proven. The question left open
at the close concerned the proof itself — what does that proof rest on, who holds the key,
where exactly does trust begin? This course names that starting point and counts what falls
when it breaks.

Before naming it, a distinction is needed. In the fictional regional measurement network, a meter
reading passes through three separate operations on its way from the field to the center, and
all three render the output unreadable. To someone looking at a record dump, all three look the
same: a meaningless string of letters and digits. Yet the security value of the three is
independent of one another, and the difference between them is measured by a single question —
how many secret items does one need on hand to go from this output back to the input?

#### Three Operations, Three Separate Questions

**CF1:** encoding is a change of representation; it writes the data's bytes in a different
alphabet, and reversal needs no secret item. Its purpose is portability: a byte passing through
the transport path without corruption.

**CF2:** encryption is a reversible transformation; reversal depends on a countable number of
secret items. Its purpose is confidentiality: a party without the item being unable to learn
the content.

**CF3:** hashing is a one-way digest; no item suffices to go from the output back to the input,
because the inverse is not a defined operation. Its purpose is integrity and comparison: testing
whether two pieces of data are the same without holding the data itself.

What the three sentences share is that none of them uses the word "strong" or "secure." All
three say something countable: the number of items needed for reversal is, respectively, zero,
more than one, and none.

#### The Same Input Passes Through Three Operations

The unit below passes a fictional meter reading through three operations. The key is derived
from a fixed seed so the output stays the same from run to run; it is not a real key, and in a
real setup a key is not produced this way.

```js
// crypto/triad.mjs — passes the same input through three operations and measures
// reversibility: encoding (base64), encryption (AES-256-GCM), hashing (SHA-256). The input is
// a meter reading from the fictional regional measurement network. There is no real key; the key
// is derived from a fixed seed so the output stays the same from run to run. No randomness.
import { createHash, createCipheriv, createDecipheriv, scryptSync } from "node:crypto";

// CF1: encoding is a change of representation; reversal needs no secret item.
// CF2: encryption is a reversible transformation; reversal depends on exactly a
//      countable number of secret items.
// CF3: hashing is a one-way digest; no item gives back the reversal.

export const READING = "SAY-4417|2026-03-01T06:00Z|41982";
export const LONG    = READING + "|SAY-4418|2026-03-01T06:00Z|8813304";
const KEY = scryptSync("kurgu-tohum", "kurgu-tuz", 32, { N: 2 ** 12, r: 8, p: 1 });
const NONCE = Buffer.alloc(12, 7);                    // fictional, fixed

export const encode = (m) => Buffer.from(m).toString("base64");
export const hash   = (m) => createHash("sha256").update(m).digest("base64");
export function encrypt(m) {
  const s = createCipheriv("aes-256-gcm", KEY, NONCE);
  const g = Buffer.concat([s.update(m, "utf8"), s.final()]);
  return Buffer.concat([g, s.getAuthTag()]).toString("base64");
}
export function decrypt(g64, key = KEY) {
  const t = Buffer.from(g64, "base64");
  const c = createDecipheriv("aes-256-gcm", key, NONCE);
  c.setAuthTag(t.subarray(t.length - 16));
  return Buffer.concat([c.update(t.subarray(0, t.length - 16)), c.final()]).toString();
}
export const attempt = (f) => { try { return f() === READING ? "yes" : "no"; }
                                catch { return "no"; } };
export const WRONG = scryptSync("baska-tohum", "kurgu-tuz", 32, { N: 2 ** 12, r: 8, p: 1 });
```

```js
// crypto/measure.mjs — the output form, determinism, and reversal of the three operations.
import { READING, LONG, encode, hash, encrypt, decrypt, attempt, WRONG } from "./triad.mjs";

const printRow = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const A = [-11, 7, 7, 15, 14];
printRow(A, "operation", "short", "long", "deterministic", "secret item");
for (const [name, f, item] of [["encoding", encode, 0], ["encryption", encrypt, 2],
                               ["hashing", hash, "-"]])
  printRow(A, name, f(READING).length, f(LONG).length,
      f(READING) === f(READING) ? "yes" : "no", item);

console.log("\nreversal actually attempted (did it give back the exact input):");
const B = [-34, 6];
for (const [name, f] of [
  ["encoding, no item on hand", () => Buffer.from(encode(READING), "base64").toString()],
  ["encryption, with the correct key", () => decrypt(encrypt(READING))],
  ["encryption, with the wrong key", () => decrypt(encrypt(READING), WRONG)],
  ["encryption, with one byte changed", () => {
    const b = Buffer.from(encrypt(READING), "base64"); b[3] ^= 1;
    return decrypt(b.toString("base64")); }],
]) printRow(B, name, attempt(f));
printRow(B, "hashing, no inverse function", "no");

console.log("\nthe three output forms of the same input:");
console.log("  encoding  :", encode(READING));
console.log("  encryption:", encrypt(READING));
console.log("  hashing   :", hash(READING));
```

```
operation    short   long  deterministic   secret item
encoding        44     92            yes             0
encryption      64    112            yes             2
hashing         44     44            yes             -

reversal actually attempted (did it give back the exact input):
encoding, no item on hand            yes
encryption, with the correct key     yes
encryption, with the wrong key        no
encryption, with one byte changed     no
hashing, no inverse function          no

the three output forms of the same input:
  encoding  : U0FZLTQ0MTd8MjAyNi0wMy0wMVQwNjowMFp8NDE5ODI=
  encryption: KtcWlbMqgsctrQ9mH5TfZXklUgBZe3RQghkBaCASMIF/zJIXfxBHDQ8B0sIX7nzA
  hashing   : 6MaKWYn3eYX57Bdf2685XV76MBSSJCOE1SLYc6Rz7zg=
```

#### What Is Measured: The Items Needed for Reversal

The last three lines are the lesson's starting point. All three outputs look unreadable, and
all three are roughly the same length. Appearance says nothing; the tables do.

The length columns give the first distinction. As the input grows, encoding's output goes from
44 to 92, and encryption's output goes from 64 to 112 — both grow with the input, because both
carry the entire input. Hashing's output is 44 characters for both inputs. That fixed length is
direct proof that hashing does not carry the input: a 64-character input and a 6-kilobyte file
both come down to the same 32 bytes. Information that does not fit inside cannot be extracted
back out.

In the determinism column, all three give `yes`; the same input always produces the same
output. This property is what makes verification possible, and it is also the source of a
constraint the second lesson measures: if two records are equal, their outputs are equal too.

The reversal table is the real measurement, and all four rows were actually attempted. The
encoded value was opened with zero items on hand — reversal is an operation anyone who sees the
output can perform. The encrypted value opened with the correct key and did not open with the
wrong key. The third row shows the second secret item: when a single byte of the ciphertext
changed, decryption failed, because the mode of operation in use appends an authentication tag
to the output and checks it before decrypting. Encryption's reversal therefore depends on not
one but two items: the key and an uncorrupted ciphertext. The hashing row has nothing to even
attempt; there is no such thing as an inverse function.

#### Where Encoding Fits and Telling It Apart from the Output

Encoding not having a security function does not mean it is unnecessary. In the fictional
measurement network, the packet transferred between branches passes through a text-carrying
channel, and part of the raw bytes gets interpreted on that channel as a delimiter or control
character. Encoding solves exactly that: it writes the bytes into an alphabet the channel can
carry safely. That is why encoding comes out appropriate in the table's `transfer packet` row —
what is wanted in that field is not confidentiality, but arriving without corruption.

The confusion comes from an encoded value looking unreadable. Telling the three apart by
looking at the output is nevertheless possible, and the distinguishing marks are right there in
the output above. An encoded value's length is proportional to the input's length, and decoding
it yields meaningful text; the trailing `=` padding is also a mark of the alphabet in use.
Hashing's output is always the same length regardless of the input — 32 bytes, 44 characters in
this alphabet. An encrypted value grows with the input but decrypting it yields meaningless
bytes and it usually carries a fixed overhead: the 16-byte authentication tag here is that
overhead.

If a record dump shows fixed-length 44-character fields, that is a hashing column; fields whose
length varies from row to row are encoding or encryption. The distinction is not a guess — it is
a measurable property, and it is the first column to check during an audit.

#### The Countable Result of Mixing Them Up

Separating the three operations is not a terminology debate. The fictional measurement network has
four fields, and each field has two known properties: whether the value needs to be retrieved
later, and whether the value needs to stay secret. These two properties determine the correct
operation; a wrong choice costs two separate numbers.

```js
// crypto/match.mjs — matching the four fields' purpose against the three operations in
// the fictional measurement network (model): how many items each wrong match results in.
// CF4: the cost of using an operation outside its purpose is measured not in appearance but
//      in item count: the items needed for reversal and the records whose reversal is lost.
const FIELD = [                      // record counts are fictional
  { name: "meter reading value",  records: 12400, reversed: true,  secret: true,  correct: "encryption" },
  { name: "field crew password",  records:   340, reversed: false, secret: true,  correct: "hashing" },
  { name: "device serial digest", records: 12400, reversed: false, secret: false, correct: "hashing" },
  { name: "transfer packet",      records: 12400, reversed: true,  secret: false, correct: "encoding" },
];
const REVERSAL = { encoding: 0, encryption: 1, hashing: null };   // items needed for reversal

function decide(a, op) {
  if (op === a.correct)                  return ["fits", REVERSAL[op], 0];
  if (op === "hashing")                  return ["no reversal", null, a.records];
  if (op === "encoding" && a.secret)     return ["reads with 0 items", 0, 0];
  if (op === "encoding")                 return ["raw value stays", 0, 0];
  if (!a.reversed)                       return ["single root opens everything", 1, 0];
  return ["unnecessary root", 1, 0];
}
const printRow = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const G = [-22, -11, 9, 8, 10, 8, -32];
printRow(G, "field", "operation", "reversed", "secret", "reversal", "lost", "  decision");
let fitCount = 0, zeroCount = 0, lostTotal = 0;
for (const a of FIELD) for (const op of ["encoding", "encryption", "hashing"]) {
  const [d, rv, lost] = decide(a, op);
  if (d === "fits") fitCount++; else if (rv === 0) zeroCount++;
  lostTotal += lost;
  printRow(G, a.name, op, a.reversed ? "yes" : "no", a.secret ? "yes" : "no",
      rv === null ? "-" : rv, lost || "-", "  " + d);
}
console.log(`\n${FIELD.length * 3} matches, ${fitCount} fit; ${zeroCount} matches bring the ` +
  `raw value back with 0 items, and a wrong hash destroys the reversal of ${lostTotal.toLocaleString("en-US")} records`);
```

```
field                 operation   reversed  secret  reversal    lost  decision
meter reading value   encoding         yes     yes         0       -  reads with 0 items
meter reading value   encryption       yes     yes         1       -  fits
meter reading value   hashing          yes     yes         -   12400  no reversal
field crew password   encoding          no     yes         0       -  reads with 0 items
field crew password   encryption        no     yes         1       -  single root opens everything
field crew password   hashing           no     yes         -       -  fits
device serial digest  encoding          no      no         0       -  raw value stays
device serial digest  encryption        no      no         1       -  single root opens everything
device serial digest  hashing           no      no         -       -  fits
transfer packet       encoding         yes      no         0       -  fits
transfer packet       encryption       yes      no         1       -  unnecessary root
transfer packet       hashing          yes      no         -   12400  no reversal

12 matches, 4 fit; 3 matches bring the raw value back with 0 items, and a wrong hash destroys the reversal of 24,800 records
```

Four of the twelve matches fit; eight carry a cost, and the costs are not of the same kind.

The quietest flaw is in the first row. If the meter reading is encoded, the record looks
unreadable and whoever is looking assumes it is protected; yet the reversal item count is zero.
Because encoding has no secret item at all, treating an encoded value as protected is the same
thing as recording an unprotected value as protected. The same flaw repeats in the password
row, where the record count is 340.

The second flaw runs the other way. If the field crew password is encrypted, reversal becomes
possible — and that is exactly what is unwanted. Encryption ties all 340 records to a single
item, the key. When that item breaks, the number of records that fall is 340; in hashing,
there is no shared item to break. This row is the course's first concrete example of its
measurement axis: a wrong choice of operation creates a root, and the cost of creating a root
is the number of items that fall if that root breaks.

The third flaw is the irreversible one. If the meter reading or the transfer packet is hashed,
the value is not protected — it is lost; across the two fields, 24,800 records total become
unreadable again. Unlike the other two, this flaw surfaces not at the moment of a leak but at
the moment of first writing, and it cannot be undone.

#### The Course's Three Columns

This lesson sets up the three questions that will be asked of every mechanism throughout the
course, and answers the first one.

**Trust root.** What does the mechanism assume to be true from the start? Encoding has no root
— it assumes nothing and promises nothing. Encryption's root is the key staying secret.
Hashing's root is that two different inputs do not produce the same output, and that root is the
subject of the next lesson.

**How many items must be true at once.** From root to use, how many components need to hold
together? In encryption, the number measured is two: the key must stay secret and the
ciphertext must be uncorrupted. In encoding, it is zero. In hashing, it is one: the function's
collision resistance.

**How many items fall if the root breaks.** If the key leaks, every record encrypted with that
key falls; in the table's password row, that is 340. In encoding, there is no root to break,
because nothing is closed. In hashing, if the root breaks, what falls is not the records'
secrecy but the meaning of the comparison.

Every lesson throughout the course fills in these three columns. "That algorithm is strong"
carries no decision. The root's name, the chain's length, and the number of items that fall at
the moment of breaking do. A fourth question always accompanies the third column: what does
renewal cost once the root breaks? When a key leaks, producing a new key is a single operation,
but re-encrypting the 12,400 records that key protected and getting the key to every endpoint
that needs it are separately counted items. The cost measured in this course is not the break
itself, but the number of items touched by hand after the break.

#### Summary

- The three operations are separated by the number of secret items needed for reversal:
  encoding 0, encryption 2 (the key and an uncorrupted ciphertext), and for hashing, no item
  suffices.
- Encoding's and encryption's output grow with the input (44→92 and 64→112 characters);
  hashing's output is 44 characters for every input, and that fixed length is proof that the
  input is not carried.
- All three operations are deterministic: the same input always gives the same output.
- Of the twelve matches between four fields and three operations, only four fit; in three
  matches, the raw value comes back with 0 items, and in two matches, the reversal of 24,800
  records is destroyed.
- Every mechanism is measured with three columns: the trust root's name, the number of items
  that must be true at once from root to use, and the number of items that fall when the root
  breaks.

#### Next Step

In this lesson, hashing's root was passed over in one sentence: two different inputs do not give
the same output. But that sentence, as it stands, cannot be true. Hashing's output has a fixed
length and the number of possible inputs is unlimited; therefore, different inputs that give
the same output certainly exist. The root is the assumption not that a collision does not
exist, but that it **cannot be found**. The next lesson turns that assumption into a number: at
how many bits of output is the first collision found in how many attempts, how closely does the
measured number match the theoretical bound, and what happens to the number of attempts needed
when the output length doubles. The same measure also explains why deprecated hashing
algorithms are retired step by step.

### Hash Functions

Source: https://academia.sh/en/courses/identity-and-cryptography/hash-functions

Measuring collision resistance against the birthday bound: fixed output length, a single-bit input change flipping about half the output bits, the first collision in a truncated output being found — averaged over 32 rounds — at 0.97–1.15 times the theoretical bound, and an n-bit output being bounded by 2^(n/2) tying 12,502 of the 25,242 items in the usage set to collision resistance.

The previous lesson passed over hashing's root in one sentence: two different inputs do not give
the same output. That sentence, as it stands, cannot be true. Hashing's output has a fixed
length, while the number of possible inputs is unlimited; therefore, different inputs that give
the same output certainly exist, and their number is unlimited too. The root is not that a
collision does not exist, but that it cannot be found.

"Cannot be found," though, is a number. This lesson measures that number: at how many bits of
output does the first collision appear in how many attempts, how closely does the measured
value match the theoretical bound, and what happens to the number of attempts needed when the
output length doubles.

#### Four Properties, Two Separate Resistances

**CF5:** hashing's output space is finite, its input space unlimited; different inputs that
give the same output certainly exist. The root is not the absence of a collision, but the
search cost being unaffordable.

**CF6:** in an n-bit output, the cost of searching for a collision is upper-bounded by 2^(n/2),
and that bound is independent of the algorithm's internal design; it comes only from the output
length.

The two resistances are distinct from one another. Preimage resistance makes it hard to find
any input that produces a given output; its cost is on the order of 2^n. Collision resistance
makes it hard to find *any two* inputs that give the same output, and its cost is 2^(n/2). The
difference comes from the attacker's freedom in choosing the output: in the first, the target
is fixed; in the second, both inputs are free. That freedom takes the square root of the cost.

```js
// hash/properties.mjs — measures the four properties of hashing: fixed length, determinism, avalanche
// effect, and the theoretical bound the output length brings. The input is a meter reading from the
// fictional regional measurement network. No randomness; every run gives the same numbers.
import { createHash } from "node:crypto";

// CF5: hashing's output space is finite, its input space unlimited; different inputs that give the
//      same output certainly exist. The root is not the absence of a collision, but that it cannot be found.
// CF6: in an n-bit output, the cost of searching for a collision is upper-bounded by 2^(n/2); this
//      bound is independent of the algorithm's internal design, and comes only from the output length.

const READING = "SAY-4417|2026-03-01T06:00Z|41982";
const ALG = ["md5", "sha1", "sha256", "sha512"];
const bitCount = (b) => { let n = 0; for (const x of b) n += (x.toString(2).match(/1/g) || []).length; return n; };
const printRow = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));

const A = [-10, 7, 12, 12, 12];
printRow(A, "algorithm", "bytes", "output bit", "preimage", "collision");
for (const a of ALG) {
  const n = createHash(a).update(READING).digest().length * 8;
  printRow(A, a, n / 8, n, "2^" + n, "2^" + n / 2);
}

console.log("\navalanche effect: one bit of the input is flipped, how many bits change in the output");
const B = [-10, 10, 8, 10, 8, 10];
printRow(B, "algorithm", "flipped", "min", "average", "max", "expected");
for (const a of ALG) {
  const original = createHash(a).update(READING).digest();
  const bit = original.length * 8;
  let lo = Infinity, hi = 0, t = 0, times = 0;
  for (let i = 0; i < READING.length * 8; i++) {
    const g = Buffer.from(READING); g[i >> 3] ^= 1 << (i & 7);
    const y = createHash(a).update(g).digest();
    let f = 0;
    for (let j = 0; j < y.length; j++) f += bitCount(Buffer.from([original[j] ^ y[j]]));
    lo = Math.min(lo, f); hi = Math.max(hi, f); t += f; times++;
  }
  printRow(B, a, times, lo, (t / times).toFixed(1), hi, bit / 2);
}

console.log("\ndoes the same input give the same output:",
  createHash("sha256").update(READING).digest("hex") ===
  createHash("sha256").update(READING).digest("hex") ? "yes" : "no");
```

```
algorithm   bytes  output bit    preimage   collision
md5            16         128       2^128        2^64
sha1           20         160       2^160        2^80
sha256         32         256       2^256       2^128
sha512         64         512       2^512       2^256

avalanche effect: one bit of the input is flipped, how many bits change in the output
algorithm    flipped     min   average     max  expected
md5              256      50      64.1      78        64
sha1             256      62      79.7     100        80
sha256           256     110     128.8     149       128
sha512           256     219     256.4     288       256

does the same input give the same output: yes
```

The avalanche-effect table measures that hashing leaves no structural trace between input and
output. When a single bit of the input is flipped, SHA-256's output changes by an average of
128.8 bits — about half of 256 bits. The lowest measurement is 110, the highest 149; for all
four algorithms, the average settles at half the output's bits. That half ratio is the sign
that the output behaves as if independent of the input. In encoding, a one-bit input change
would touch only that region of the output; here, both the location and the amount of change
are unrelated to the input.

Determinism together with the avalanche effect is what turns hashing into a fingerprint.
Without determinism, two digests of the same data could not be compared; without the avalanche
effect, similar data would produce similar digests, and whoever looked at the digest could
infer part of the data. Together, the two make it possible to answer the question "is it the
same" without holding the data itself.

The two columns on the right, though, are this lesson's real subject. Every algorithm's
collision bound is the square root of its preimage bound: a 128-bit output is bounded to 2^64,
a 160-bit output to 2^80, a 256-bit output to 2^128. This number has nothing to do with the
algorithm's quality — it has to do with the output length itself, and it is the same even for a
flawless hash function.

#### Measuring the Birthday Bound

The theoretical bound is a measurable claim. The unit below takes only the first n bits of the
SHA-256 output and searches for the first collision on this output that **it itself
truncated**. What is measured is not the algorithm; it is the bound that the output length brings.
For each output size, thirty-two independent rounds are run and averaged, because a single
round's result swings across a wide band.

```js
// hash/collision.mjs — measuring collision resistance against the birthday bound. The first n bits of
// the SHA-256 output are taken, and the first collision is searched for on this output that we
// ourselves truncated. What is measured is not the algorithm itself, but the bound that the output
// length brings. Inputs are generated with a counter; no randomness, every run gives the same numbers.
import { createHash } from "node:crypto";

// CF7: the expected number of attempts until the first collision in an n-bit output is 1.2533 *
//      2^(n/2); when the output length doubles, the expected number of attempts grows by its square.

const truncate = (m, bit) => {                 // first `bit` bits of the SHA-256 output
  const h = createHash("sha256").update(m).digest();
  let v = 0n;
  for (let i = 0; i < 8; i++) v = (v << 8n) | BigInt(h[i]);
  return (v >> BigInt(64 - bit)).toString(16);
};
const firstCollision = (bit, round) => {       // `round` gives a different input sequence each round
  const seen = new Map();
  for (let i = 0; ; i++) {
    const m = `t${round}-olcum-${i}`, k = truncate(m, bit);
    if (seen.has(k)) return i + 1;
    seen.set(k, m);
  }
};
const ROUNDS = 32;                             // independent round count for each output size
const printRow = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const G = [7, 18, 13, 11, 8, 10, 10];
printRow(G, "output", "possible outputs", "theoretical", "measured", "ratio", "earliest", "latest");
for (const bit of [8, 12, 16, 20, 24, 28, 32]) {
  let total = 0, lo = Infinity, hi = 0;
  for (let j = 0; j < ROUNDS; j++) {
    const d = firstCollision(bit, j);
    total += d; lo = Math.min(lo, d); hi = Math.max(hi, d);
  }
  const avg = total / ROUNDS, theory = 1.2533 * 2 ** (bit / 2);
  printRow(G, bit + " bit", (2 ** bit).toLocaleString("en-US"), theory.toFixed(0),
      avg.toFixed(0), (avg / theory).toFixed(2), lo, hi);
}
```

```
 output  possible outputs  theoretical   measured   ratio  earliest    latest
  8 bit               256           20         21    1.03         7        48
 12 bit             4,096           80         92    1.15        28       188
 16 bit            65,536          321        368    1.15        91       675
 20 bit         1,048,576         1283       1397    1.09       127      3395
 24 bit        16,777,216         5134       4986    0.97      1630     11910
 28 bit       268,435,456        20534      21479    1.05      2771     48277
 32 bit     4,294,967,296        82136      86497    1.05     10808    167350
```

In all seven rows, the measured value stays between 0.97 and 1.15 times the theoretical bound.
This is proof that the formula is not an approximation but a measurable estimate. The truncated
output is not a real cracking attempt; the discarded bits of the SHA-256 output keep getting
computed, and the search runs only over the n bits kept on hand. What is measured is the bound
that those n bits carry.

The real result is in comparing the second and fourth columns. The 32-bit output has more than
four billion possible values; the first collision is found in an average of 86,497 attempts.
The ratio is roughly one **in** fifty thousand. Intuition fails here: it seems like the
collision should get harder as the space grows, but what is being sought is not landing on a
specific value — it is *any two* attempts landing on each other, and as the number of attempts
grows, the number of pairs grows quadratically.

It is possible to confirm this quadratic growth with the table's numbers. In 86,497 attempts,
the number of pairs that can be compared against each other is 86,497 × 86,496 / 2, which is
roughly 3.74 billion; the 32-bit output space carries 4.29 billion values. The two numbers
arriving at the same order of magnitude is the moment a collision is expected. What is sought
is not the number of attempts approaching the output space — it is the **number of pairs**
approaching it, and the number of pairs grows with the square of the number of attempts. The
entire birthday bound follows from this single observation.

The last two columns show the width of the band. For 32 bits, the earliest collision was found
on attempt 10,808, the latest on attempt 167,350 — a fifteen-fold difference. The average
matches theory, but a single run does not. This is why security bounds are written not against
the average but against the **earliest** case: a defense has to hold in every run, not half of
them.

The row-to-row progression is countable too. When the output goes from 16 to 32 bits, the
number of possible outputs goes from 65,536 to over four billion — its square; the measured
number of attempts goes from 368 to 86,497, roughly the same ratio. Each bit added to the
output multiplies the needed number of attempts by only 1.41. Adding eight bits brings a
sixteen-fold increase, adding sixty-four bits brings a four-billion-fold increase. That is the
root: collision resistance gives security bits equal to **half** the output length, and that
ratio cannot be changed by any design.

#### The Cost of Truncation

Per the course's security rule, the measurement above was not run against a real algorithm but
against an output **we truncated ourselves**. This truncation is not just a measurement trick;
it is a decision genuinely encountered in setup. To save space in a log line or a database
column, not the entire digest but only its first few bytes is stored, and this is often assumed
to be just a storage choice.

The number shows this is not a storage choice. If the first 64 bits of a 256-bit output are
kept, the collision bound drops from 2^128 to 2^32. That is exactly what is measured in the
table's last row: for a 32-bit output, the first collision was found in an average of 86,497
attempts. For a 64-bit output, the same formula gives roughly 5.4 billion attempts — it looks
like a large number, but it is work an ordinary machine can do on the order of hours.
Truncation's cost is not linear: discarding half the output leaves not half the security but its
**square root**.

Truncation does not produce the same result in every use. For a matching field in a log, 64 bits
might be enough, because what is sought there is not two records giving the same digest, but
tracking a specific record. In an integrity check, though, the same truncation directly breaks
the root. The criterion fits in one sentence: if the claim a field carries rests on collision
resistance, the output is not truncated.

#### Which Use Depends on Which Resistance

The cost of a broken root is the number of items tied to that root. In the fictional regional
measurement network, hashing is used in five separate places, and the five do not rest on the same
resistance.

| Use | Resistance relied on | Item | If collision resistance drops |
|---|---|---|---|
| Device firmware integrity digest | collision | 12,400 | falls |
| Serial number digest in the log | preimage | 12,400 | stands |
| Digest signed between branches | collision | 6 | falls |
| Field crew password record | preimage | 340 | stands |
| Backup file fingerprint | collision | 96 | falls |

Of the twenty-five thousand two hundred forty-two items, 12,502 are tied to collision
resistance, 12,740 to preimage resistance. The distinction is sharp: when collision resistance
drops, two different device firmwares that produce the same digest become producible, and the
integrity check accepts both. In the password record, though, the attacker holds a fixed target
output and has to search for an input that lands on it; this is a preimage problem, and
collision resistance dropping does not make it any easier.

MD5 and SHA-1 are concrete examples of this distinction. Both are open specifications, both are
still computable, and neither has had its preimage resistance broken. The reason they were
deprecated lies on the collision side: the 2^64 and 2^80 bounds dropped to a reachable order of
magnitude, and structural weaknesses led to going even below those bounds. The result is that
the table's two columns are affected separately — a password record and an integrity digest
produced with these algorithms do not carry the same urgency.

The cost of renewal is the third column, and it is counted item by item. For firmware digests,
renewal is a single batch operation: 12,400 digests are recomputed, and the number of items
touched by hand is zero. For backup file fingerprints, it is different — the original digest of
the 96 backups sits in old records, and matching it to the new one is done by hand. For the
digest signed between branches, it is not just the digest that is renewed but the signature too;
six separate approval steps are needed for six branches, and transfers cannot be verified during
the renewal window. The same root breaking costs zero, 96, and 6 items by hand across the three
uses; what determines the item count is not how many records are affected, but how many records
**need a human decision to be renewed**.

#### Summary

- Because hashing's output space is finite, a collision certainly exists; the trust root is not
  the absence of a collision, but the search cost being unaffordable.
- When a single bit of the input is flipped, about half the output bits change: for SHA-256,
  the measured average is 128.8 out of 256 bits.
- The expected number of attempts until the first collision in an n-bit output is on the order
  of 2^(n/2); across seven separate output sizes, the measured value came out at 0.97–1.15
  times the theoretical bound.
- The average matches theory, but a single run does not: at 32 bits, the earliest collision is
  at attempt 10,808, the latest at attempt 167,350; the security bound is written against the
  earliest case.
- Collision resistance gives security bits equal to half the output length; of the fictional
  network's 25,242 items, 12,502 are tied to this resistance, 12,740 to preimage resistance.

#### Next Step

The cost measured in this lesson rested on the assumption that the attacker has **no
information at all**: inputs were generated by a counter, and every attempt was equally likely.
When it comes to passwords, this assumption falls apart. Strings that people choose are not
evenly distributed across the possible input space; a candidate list of a few hundred million
covers a computable portion of the space. In that case, the 2^256 preimage bound carries no
weight at all, because the search is not done over the whole space but inside the list. The next
lesson measures two separate defenses against this situation: the work factor, which raises the
unit cost of every attempt, and salt, which multiplies a rainbow table by the number of
records. Both have their own bound, and that bound is countable.

### Salt and Password Storage

Source: https://academia.sh/en/courses/identity-and-cryptography/salt-and-password-storage

Measuring the work factor and salt as two mutually independent factors: unit cost growing within a measured band of up to 16x when N doubles, a rainbow table becoming equal to doing the work from scratch per record at a 9-bit salt threshold, what determines salt length being the birthday bound rather than table size, and neither factor being able to fix the password's unpredictability root.

All the measurements in the previous lesson rested on a single assumption: inputs were
generated by a counter, and every attempt was equally likely. When it comes to passwords, this
assumption falls apart. Strings that people choose are not evenly distributed across the
possible input space; a candidate list of a few hundred million covers a computable portion of
the space. In that case, the 2^256 preimage bound carries no weight at all, because the search
is not done over the whole space but inside the list.

What this lesson measures is how that list gets made expensive. There are two separate
factors, and the two multiply different things. All the calculations below are on our own
model; running trials against a real password set, **without written authorization and a
defined scope, is both illegal and unprofessional**.

#### What Multiplies What

**CF8:** the key derivation function's **work factor** grows a single attempt's unit cost
linearly. The defender pays this cost once per login; the attacker pays it once per candidate.
The asymmetry is right there.

**CF9:** a **rainbow table** fits all records at once that share the same salt. If the
salt is generated separately per record, the same table fits only one record.

The two sit at different points of the factor. The work factor grows the **cost per attempt**,
salt grows the **number of attempt sets that have to be repeated**. Neither substitutes for the
other; when used together, the total work is their product. This distinction requires the two
defenses to be measured separately: if the work factor is raised and salt is skipped, the total
work gets divided by the record count; if salt is added but the factor is left low, the unit
cost stays too small to matter.

#### The Trial Cost Curve

The unit below measures the work factor's effect on cost. The measurement pattern is special:
raw duration is never printed anywhere. The expected multiplier is derived from the input —
that is, from the ratio of the chosen `N` values; the measured value is only run through a band
check, and the result is printed as `yes` or `no`. The reason is that absolute duration depends
on the machine and the runtime; what is meant to be measured is the ratio, which is independent
of the machine.

```js
// password/factor.mjs — the work factor's cost curve on trial cost. Duration is not printed raw: the
// expected multiplier is derived from the input (the ratio of N values); the measured value is only
// run through a band check and printed as yes/no. A fictional field crew password is used.
import { scryptSync } from "node:crypto";

// CF8: the key derivation function's work factor grows a single attempt's unit cost
//      linearly; the defender pays this cost once, the attacker pays it once per
//      candidate.

const PASSWORD = "fictional-field-crew-login";
const SALT = Buffer.alloc(16, 3);                // fictional, fixed
const BASE_EXP = 12;
const config = (exp) => ({ N: 2 ** exp, r: 8, p: 1, maxmem: 512 * 1024 * 1024 });
const measure = (exp, times) => {
  scryptSync(PASSWORD, SALT, 32, config(exp));                 // warm-up
  const t0 = process.hrtime.bigint();
  for (let i = 0; i < times; i++) scryptSync(PASSWORD, SALT, 32, config(exp));
  return Number(process.hrtime.bigint() - t0) / times;
};
const baseline = measure(BASE_EXP, 8);
const CANDIDATES = 1_000_000;                    // fictional candidate list size
const printRow = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const G = [8, 9, 11, 13, 24];
console.log(`baseline work factor N=2^${BASE_EXP}; unit = one trial at the baseline factor`);
printRow(G, "N", "memory", "expected", "in band", "candidate list (units)");
for (const exp of [12, 13, 14, 15, 16]) {
  const expected = 2 ** (exp - BASE_EXP);                      // derived from the input
  const ratio = measure(exp, exp > 14 ? 1 : 3) / baseline;      // band check only
  const inBand = ratio >= expected * 0.55 && ratio <= expected * 1.8 ? "yes" : "no";
  printRow(G, "2^" + exp, (128 * 8 * 2 ** exp / 2 ** 20) + " MiB", expected + "x", inBand,
      (CANDIDATES * expected).toLocaleString("en-US"));
}
```

```
baseline work factor N=2^12; unit = one trial at the baseline factor
       N   memory   expected      in band  candidate list (units)
    2^12    4 MiB         1x          yes               1,000,000
    2^13    8 MiB         2x          yes               2,000,000
    2^14   16 MiB         4x          yes               4,000,000
    2^15   32 MiB         8x          yes               8,000,000
    2^16   64 MiB        16x          yes              16,000,000
```

In all five rows, the measured ratio stayed within the expected multiplier's band. The curve is
linear: when `N` doubles, both the unit cost and the memory consumption double. The last column
is this cost's counterpart on the attacking side — the entire candidate list of a million takes
one million units at the baseline factor, sixteen million units at the `N=2^16` factor.

The asymmetry does not show up in this table, but it is the only way to read the table. When the
defender raises the factor sixteen-fold, they pay sixteen times more **per login**, and that is
a delay the user does not notice. The attacker pays the same decision's cost **per candidate**.
On one side there are a few thousand logins a day, on the other a million candidates; the same
factor adds up to very different totals on the two sides.

To turn the asymmetry into a number, it is enough to write out both sides' totals. The fictional
network has 340 field accounts, and each account logs in an average of three times a day;
that is 1,020 verifications a day. At the `N=2^16` factor, the defender's daily total is 16,320
units. The attacker, though, has to go through the list from scratch for every record because
of the salt: 340 × 16,000,000, or 5.44×10^9 units. The ratio between the two totals is over
three hundred thousand. Raising the factor costs the defender a daily, fixed amount; it costs
the attacker an amount multiplied by the number of records.

The memory column is a second, separate lever. At `N=2^16`, each attempt takes 64 MiB. This
amount is negligible for a server doing a single verification; it is decisive for a party that
wants to run many attempts in parallel, because parallelism is now bounded not by the number of
processing units but by the amount of memory. The work factor slows the computation down; the
memory coefficient makes the computation undistributable to cheap hardware.

#### At How Many Items Does the Table Become Useless

There is a path the work factor does not close. If the candidate list's hashes are computed once
and stored, the work needed for a leaked record drops to a single lookup; the computation is
already done. Salt's measured effect is exactly here, and its effect is written in item counts.

```js
// password/salt.mjs — at how many items a rainbow table becomes useless against salt, and what
// determines salt length (model). The fictional regional measurement network has 340 field crew
// accounts. No randomness; every run gives the same numbers.

// CF9: a rainbow table fits all records at once that share the same salt; if the
//      salt is generated separately per record, the same table fits only one record.
// CF10: what determines salt length is not table size, but the salts not colliding with
//       each other; the criterion is the birthday bound measured in the second lesson.

const CANDIDATES = 1_000_000;                      // fictional candidate list
const RECORDS = 340;                               // fictional field crew account count
const pairs = (n) => (n * (n - 1)) / 2;
const printRow = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const format = (x) => x === 0 ? "0" : x >= 1e6 ? x.toExponential(2)
  : x >= 1 ? x.toLocaleString("en-US", { maximumFractionDigits: 2 })
  : x >= 1e-3 ? x.toFixed(3) : x.toExponential(1);

console.log(`candidate list ${format(CANDIDATES)}, records ${RECORDS}, record pairs ${format(pairs(RECORDS))}`);
const G = [9, 15, 14, 14, 20];
printRow(G, "salt bits", "distinct salt", "table items", "records fit", "expected collision");
for (const bit of [0, 8, 16, 32, 64, 128]) {
  const space = 2 ** bit;
  const distinct = Math.min(space, RECORDS);       // distinct salts in use
  printRow(G, bit, format(distinct), format(CANDIDATES * distinct),   // table items covering all
      format(Math.max(1, Math.round(RECORDS / space))), format(pairs(RECORDS) / space));
}
const threshold = Math.ceil(Math.log2(RECORDS));
console.log(`\nthe moment the salt space exceeds ${RECORDS} records (${threshold} bits), table items become ` +
  `${format(CANDIDATES * RECORDS)}: the same as doing the work from scratch per record.`);

console.log("\nthe collision criterion is stricter: expected salt collision should drop below 1");
const H = [12, 16, 12, 22];
printRow(H, "records", "record pairs", "min bits", "collision at 128 bit");
for (const n of [340, 12_400, 1e6, 1e9])
  printRow(H, format(n), format(pairs(n)), Math.ceil(Math.log2(pairs(n))), format(pairs(n) / 2 ** 128));
```

```
candidate list 1.00e+6, records 340, record pairs 57,630
salt bits  distinct salt   table items   records fit  expected collision
        0              1       1.00e+6           340              57,630
        8            256       2.56e+8             1              225.12
       16            340       3.40e+8             1               0.879
       32            340       3.40e+8             1              1.3e-5
       64            340       3.40e+8             1             3.1e-15
      128            340       3.40e+8             1             1.7e-34

the moment the salt space exceeds 340 records (9 bits), table items become 3.40e+8: the same as doing the work from scratch per record.

the collision criterion is stricter: expected salt collision should drop below 1
     records    record pairs    min bits  collision at 128 bit
         340          57,630          16               1.7e-34
      12,400         7.69e+7          27               2.3e-31
     1.00e+6        5.00e+11          39               1.5e-27
     1.00e+9        5.00e+17          59               1.5e-21
```

The first row is the unsalted setup, and the critical number is right there: a single table of
a million items fits all 340 records. The work is done once and divided across the number of
records.

In the following rows, the `records fit` column drops to 1 and never rises again. Even at eight
bits of salt, a table now fits an average of just one record. This shows that salt's real
effect does not come from its length but from being separate per record.

This distinction also measures the most common flaw in setup. Using a single salt across the
whole application, even if the salt is 128 bits long, falls into the zero-bit row of the table:
the number of distinct salts is one, the table has a million items, and it fits all 340
records. Lengthening the salt does not change this row at all; the only thing that changes it is
generating the salt separately per record. In the same way, a salt derived from a user code or
a record timestamp also brings the table back, because the table can precompute that derivation
too.

The threshold is read in the `table items` column. The moment the salt space exceeds the number
of records — nine bits here — fitting the table to all records needs 3.4×10^8 items, and that is
the same as doing the work from scratch for every record. Past nine bits, the column stays
fixed; a longer salt does not make the table any more useless, because it is already completely
useless. The rainbow table loses its advantage against salt at **9 bits**.

#### What Determines Salt Length

**CF10:** what determines salt length is not table size, but the salts not colliding with each
other; the criterion is the birthday bound measured in the previous lesson.

If nine bits is enough, why is the standard choice sixteen bytes? The second table gives the
answer, and the criterion comes from the previous lesson. Salt has to be separate per record;
two records landing on the same salt brings back the single-table advantage between those two
records. The probability of landing on the same salt, though, is subject to the birthday bound.

The table turns this into a number. For the expected collision at 340 records to drop below
one, at least sixteen bits are needed — not nine. When the record count rises to 12,400, the
threshold goes to twenty-seven; to a million, thirty-nine; to a billion, fifty-nine. The
threshold moves together with the record count, and how many records a setup will have is not
known while it is being written.

A sixteen-byte salt is the choice that removes this uncertainty. At 128 bits, the expected
collision is 1.5×10^-21 even at a billion records; whatever order of magnitude the record count
reaches, the criterion holds. The rationale for the choice is not "longer is better," but that
it **removes the need to compute the threshold**. Since the salt is not secret and is stored
alongside the hash, lengthening it costs nothing either: sixteen bytes each for 340 records is
5,440 bytes total, and that is too small to measure next to the rest of a single record. In all
four rows of the last column, the collision probability stays at a magnitude that rounds to
zero; choosing the parameter generously once is cheaper than recomputing it for every setup.

#### The Root the Two Factors Cannot Fix

This lesson's third number is where the first two are not enough. The work factor multiplies
unit cost, salt multiplies the number of sets that need repeating; neither touches the
candidate list **itself**.

The trust root is right here, and its name is this: the password not being in the candidate
list. If the root holds, the search is done not in the list but across the entire space, and
the previous lesson's 2^n bound applies. When the root breaks — that is, when the password is
in the list — the one item that breaks is that account's record, but the cost of breaking it is
proportional to its position in the list. A candidate near the front of a million-entry list is
found in a handful of units at the `N=2^16` factor; a sixteen-fold work factor does not change
that.

The item count is asymmetric because of this. Salt and the work factor protect all 340 records
from falling **at once**: breaking happens record by record, not in bulk. The unpredictability
root, though, is separate per record, and one record breaking does not affect the other 339. The
three defenses each do a separate thing, and none of them replaces the other.

The cost of renewal is three separate numbers across the three defenses. Raising the work
factor is zero items by hand: the record is regenerated with the new factor on the user's next
successful login, and nobody does anything. Renewing the salt is also zero items, because the
salt is already renewed on every password change. In the case of leaked records, though,
renewal is 340 items by hand — every account's password has to be reset, and that takes
individual human action, one by one. The legitimate work sitting in the leak window counts too:
until the passwords are renewed, field crew logins either stay closed or stay open with a
record considered broken. This course's third column is read the same way in every lesson —
not the size of the break, but **the number of items touched by hand after the break**.

#### Summary

- The work factor grows unit cost per attempt linearly: when `N` goes from 2^12 to 2^16, all
  five measurements stay within the expected multiplier's band, and a list of a million goes
  from 1 million units to 16 million units.
- The same decision adds up to different totals on the two sides: the defender pays per login,
  the attacker pays per candidate; the memory coefficient additionally bounds parallelism by
  memory.
- In an unsalted setup, a single table of a million items fits all 340 records; when salt is
  separate per record, the same table fits only one record.
- The rainbow table loses its advantage against salt at 9 bits; beyond that, nothing makes it
  any more useless.
- The criterion determining salt length is not table size but collision: at 340 records the
  threshold is 16 bits, at a billion it is 59 bits; 128 bits makes computing the threshold
  unnecessary.

#### Next Step

The three lessons so far have revolved around one-way operations: hashing did not reverse, and
not reversing was the whole point. The fictional measurement network's actual traffic, though, has
to reverse — a reading coming in from the field is opened at the center and turned into a bill.
The first lesson tied this operation to two items: the key and an uncorrupted ciphertext. The
next lesson takes up that first item together with its real problem. If both sides need to know
the same key, how did that key get there? How many separate keys are kept between six branches
and the center, how many items get added when a new branch joins the network, and how many
connections have to be renewed when a single key leaks — all of it is countable.

### Symmetric Encryption

Source: https://academia.sh/en/courses/identity-and-cryptography/symmetric-encryption

The shared key's trust root and distribution arithmetic: decryption depending on four items — key, nonce, tag, and body — all being correct at the same time, a per-pair key scheme needing 7.69×10^7 key items at 12,401 endpoints, a star scheme cutting that number to 12,400 while turning the center into a single root, and a single shared key scheme where one item falling brings down every connection.

The previous three lessons revolved around one-way operations, and not reversing was the whole
point. The fictional measurement network's actual traffic, though, has to reverse: a reading
coming in from the field is opened at the center and turned into a bill. The first lesson tied
this operation to two items — the key and an uncorrupted ciphertext. This lesson takes up that
first item together with its real problem. If both sides need to know the same key, how did
that key get there, and how many are kept?

This is not a question of convenience. The key count is the direct counterpart of this course's
second column — how many items must be correct at the same time — and how it grows together
with the number of participants is countable.

#### The Same Key on Both Sides

**CF11:** in symmetric encryption, the encrypting and decrypting sides use the same key. The
trust root is that key existing only on these two sides.

**CF12:** for decryption to succeed, the key, the **nonce** (a value used once per encryption),
the authentication tag, and the ciphertext body must all be correct at the same time; if one of
the four items is wrong, decryption does not happen.

```js
// symmetric/mode.mjs — the items that must be correct at the same time, from root to use, in
// symmetric encryption are actually counted. A meter reading from the fictional regional measurement
// network is encrypted with AES-256-GCM. The key is derived from a fixed seed; no randomness.
import { createCipheriv, createDecipheriv, scryptSync } from "node:crypto";

// CF11: in symmetric encryption, the encrypting and decrypting sides use the same
//       key; the trust root is that key existing only on these two sides.
// CF12: for decryption to succeed, the key, nonce, and authentication tag must be correct at the same
//       time; if one of the three items is wrong, decryption does not happen.

const READING = "SAY-4417|2026-03-01T06:00Z|41982";
const deriveKey = (t) => scryptSync(t, "fictional-salt", 32, { N: 2 ** 12, r: 8, p: 1 });
const KEY = deriveKey("branch-01-center"), OTHER_KEY = deriveKey("branch-02-center");
const N1 = Buffer.alloc(12, 1), N2 = Buffer.alloc(12, 2);

function encrypt(m, key, nonce) {
  const s = createCipheriv("aes-256-gcm", key, nonce);
  return { body: Buffer.concat([s.update(m, "utf8"), s.final()]), tag: s.getAuthTag() };
}
function decrypt({ body, tag }, key, nonce) {
  const c = createDecipheriv("aes-256-gcm", key, nonce);
  c.setAuthTag(tag);
  return Buffer.concat([c.update(body), c.final()]).toString();
}
const attempt = (f) => { try { return f() === READING ? "yes" : "no"; } catch { return "no"; } };
const printRow = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));

const packet = encrypt(READING, KEY, N1);
const corrupted = { body: Buffer.from(packet.body), tag: packet.tag };
corrupted.body[2] ^= 1;
const D = [-9, -9, -9, -10, 9];
printRow(D, "key", "nonce", "tag", "body", "decrypted");
for (const [a, n, e, g, f] of [
  ["correct", "correct", "correct", "intact",    () => decrypt(packet, KEY, N1)],
  ["wrong", "correct", "correct", "intact",      () => decrypt(packet, OTHER_KEY, N1)],
  ["correct", "wrong", "correct", "intact",      () => decrypt(packet, KEY, N2)],
  ["correct", "correct", "wrong", "intact",      () => decrypt({ ...packet, tag: Buffer.alloc(16) }, KEY, N1)],
  ["correct", "correct", "correct", "corrupted", () => decrypt(corrupted, KEY, N1)],
]) printRow(D, a, n, e, g, attempt(f));

console.log("\nsame key, same plaintext:");
const p1 = encrypt(READING, KEY, N1).body.toString("base64");
const p2 = encrypt(READING, KEY, N1).body.toString("base64");
const p3 = encrypt(READING, KEY, N2).body.toString("base64");
console.log("  are two encryptions with the same nonce equal      :", p1 === p2 ? "yes" : "no");
console.log("  are two encryptions with different nonces equal    :", p1 === p3 ? "yes" : "no");
console.log("  items that must be correct at the same time        : 4 (key, nonce, tag, body)");
```

```
key      nonce    tag      body      decrypted
correct  correct  correct  intact          yes
wrong    correct  correct  intact           no
correct  wrong    correct  intact           no
correct  correct  wrong    intact           no
correct  correct  correct  corrupted        no

same key, same plaintext:
  are two encryptions with the same nonce equal      : yes
  are two encryptions with different nonces equal    : no
  items that must be correct at the same time        : 4 (key, nonce, tag, body)
```

Only one of the five rows gives `yes`. All four items of the chain have to be correct at the
same time, and this is the full expansion, in this mode of operation, of the chain that was
counted as two in the first lesson. The four have separate jobs: the key carries secrecy, the
nonce makes every encryption come out separately, and the tag and body together carry
integrity.

The last two lines show why the nonce is a separate item in the chain. The same plaintext
encrypted with the same key and the same nonce gives the same ciphertext. This is encryption's
counterpart to the determinism measured for hashing in the second lesson, and it produces the
same consequence: if two records are equal, their outputs are equal too. In the fictional
measurement network, this means that the packets of two meters sending the same reading are
indistinguishable from each other. The nonce's job is not secrecy, it is breaking equality — the
same job salt has in a password record.

The nonce is not secret and travels along with the packet; its only condition is that it **not
be used twice with the same key**. This condition is the chain's silent item: when forgotten,
it produces no error message at all, but its cost is not just that the same plaintext gives the
same output — a nonce repeated with the same key gives away the subkey that produces the
authentication tag, and the entire protection collapses.

#### How Many Messages Does One Key Carry

The nonce condition has a countable bound, and that bound is 96 bits in this mode. If the nonce
is chosen randomly for every message, two messages landing on the same nonce is subject to the
birthday bound from the second lesson: in a 96-bit space, the expected collision arises at
around 2^48 messages, that is, 2.8×10^14 messages.

In the fictional network, this number is not a real constraint. If 12,400 endpoints each send
twenty-four readings a day, the daily message count is 297,600; reaching the bound takes
9.5×10^8 days, or more than two and a half million years. Still, the number's existence turns
into a design decision, because in a setup where message volume grows, the same calculation
gives a different result.

The second option is to generate the nonce with a counter instead of choosing it randomly. In
that case the collision probability drops to zero and the bound rises to 2^96 messages; but a
new item is added to the chain — the counter has to **persist across restarts**. A random nonce
is stateless and carries a countable bound; a counter-based nonce removes the bound and brings
in a persistent-state item. The choice between the two comes down to whether the setup's
endpoints can hold persistent state.

Key length is a separate column, though, and it is not an item expected to run out. AES's block
size is 128 bits, and its key length is 128, 192, or 256 bits; a 256-bit key's search space is
2^256, and this number is on the same order as the preimage bound written in the second lesson.
The fragile item in the chain is not the key's length, but where the key sits and how it got
there.

#### Key Distribution Arithmetic

One of the four items — the key — is set apart from the others: it cannot travel with the
packet. It has to already be present on both sides beforehand, and that brings a countable
burden.

```js
// symmetric/distribution.mjs — symmetric key distribution arithmetic (model). The fictional regional
// measurement network has a center, six branches, and field endpoints; participant counts are fictional.

// CF13: in symmetric encryption, a separate key is kept for each participant pair; the key item count
//       grows with the square of the participant count, while the cost of a new participant grows linearly.

const printRow = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const format = (x) => x >= 1e7 ? x.toExponential(2) : Math.round(x).toLocaleString("en-US");
const pairs = (n) => (n * (n - 1)) / 2;

console.log("separate key per participant pair:");
const A = [13, 12, 12, 12, 16];
printRow(A, "participant", "key", "newcomer", "if one key", "if an endpoint");
printRow(A, "", "count", "adds", "leaks", "is compromised");
for (const n of [2, 4, 7, 12, 40, 12_400])
  printRow(A, format(n), format(pairs(n)), format(n), 1, format(n - 1));

console.log("\nthree separate schemes, the same 7 participants (center + 6 branches):");
const n = 7;
const B = [-27, 9, 12, 22, 11];
printRow(B, "scheme", "key", "newcomer", "if one item falls", "renewal");
printRow(B, "", "count", "adds", "connections affected", "endpoints");
const schemes = [
  ["separate key per pair", pairs(n), n, 1, 2],
  ["single shared key", 1, 0, pairs(n), n],
  ["star via the center", n - 1, 1, 1, 2],
];
for (const [name, k, added, affected, ep] of schemes) printRow(B, name, format(k), format(added), format(affected), format(ep));

console.log("\nsame three schemes at the field endpoints (12,400 endpoints + 1 aggregator):");
const m = 12_401;
for (const [name, k, added, affected, ep] of [
  ["separate key per pair", pairs(m), m, 1, 2],
  ["single shared key", 1, 0, pairs(m), m],
  ["star via the aggregator", m - 1, 1, 1, 2],
]) printRow(B, name, format(k), format(added), format(affected), format(ep));
```

```
separate key per participant pair:
  participant         key    newcomer  if one key  if an endpoint
                    count        adds       leaks  is compromised
            2           1           2           1               1
            4           6           4           1               3
            7          21           7           1               6
           12          66          12           1              11
           40         780          40           1              39
       12,400     7.69e+7      12,400           1          12,399

three separate schemes, the same 7 participants (center + 6 branches):
scheme                           key    newcomer     if one item falls    renewal
                               count        adds  connections affected  endpoints
separate key per pair             21           7                     1          2
single shared key                  1           0                    21          7
star via the center                6           1                     1          2

same three schemes at the field endpoints (12,400 endpoints + 1 aggregator):
separate key per pair        7.69e+7      12,401                     1          2
single shared key                  1           0               7.69e+7     12,401
star via the aggregator       12,400           1                     1          2
```

The first table gives the arithmetic. For seven participants, 21 keys are kept; for forty
participants, 780; for 12,400 endpoints, 7.69×10^7. When a new participant joins, the item
added equals the number of participants currently in the network — adding one endpoint to a
12,400-endpoint network requires 12,400 new keys. The key item count grows quadratically, the
participation cost grows linearly.

The last two columns measure breaking. One key leaking drops only one connection; this is the
sole rationale for keeping a separate key per pair. An endpoint being fully compromised,
though, drops every key that endpoint carries: 12,399 connections in a 12,400-endpoint network.

#### Three Schemes, Three Separate Costs

The second and third tables compare the same set of participants across three schemes, and
each of the three has an unacceptable number in one column.

**Separate key per pair** is the scheme that best bounds breaking — when one item falls, one
connection drops, and renewal is done at two endpoints. Its cost is in the key item count: for
12,401 endpoints, 7.69×10^7 keys would have to be kept, and every new endpoint would add
12,401 items. This number is not a management problem, it is an impossibility.

**Single shared key** is at the opposite end. The key item count is one, and a new participant
adds no item at all; in exchange, when that one item falls, all 7.69×10^7 connections drop, and
renewal is done at 12,401 endpoints. This is the sharpest example of the course's axis:
reducing the item count to one also reduces the root to one.

**Star scheme** sits between the two, and it is mostly the scheme chosen in real setups. Every
endpoint shares one key with the center alone; the item count drops to 12,400, and the cost of
a new endpoint drops to one item. If one endpoint's key leaks, one connection drops, and
renewal is done at two endpoints. The cost is where the table does not show it: endpoints cannot
talk to each other directly, and the center becomes the single point that sees all traffic in
the clear. The center being compromised drops all 12,400 keys at once.

The renewal cost across the three schemes is 2, 12,401, and 2 endpoints, respectively. But the
phrase "renewal at two endpoints" is not a count of operations, it is an **access** question: how
will the new key be delivered to those two endpoints?

#### The Item the Tag Proves

The authentication tag proves the body has not been corrupted — but it proves one more thing,
and that thing depends on the scheme. Since the tag can only be produced by a party that knows
the key, a valid tag says "someone who knows the key sent this message." How much information
that statement carries depends on how many parties know the key.

The number is three separate values across the three schemes. In the separate-key-per-pair
scheme, two parties know the key; the tag narrows the sender down to **two candidates**. The
same number holds in the star scheme: the endpoint and the center. In the single-shared-key
scheme, 12,401 parties know the key, and the tag narrows the sender down to 12,401 candidates —
that is, it says nothing.

This is symmetric encryption's structural limit, and it is measured by the item count. Even in
the best case, the candidate count is two; there is no item to distinguish a reading sent by a
meter endpoint from one sent by the center, because the center can produce the same tag too.
Since nothing stops the receiver from attributing a message it produced itself to the sender, a
symmetric tag cannot be presented as proof to a third party. The seventh lesson takes up this
gap by name.

#### The Chain's Unresolved Item

The same item stays unresolved in all three schemes. The key has to have reached both sides
**before** it is used, and this delivery cannot be done by encrypting it — encrypting it would
already require a key.

In the fictional network, this translates into operations done by hand: loading the key onto
the device while the meter endpoint is set up, carrying the key to the branch server over a
separate channel. For 12,400 endpoints, that is 12,400 items by hand, and the items are paid
again at every renewal, not just once at setup. When a key leaks, most of the cost is here, not
in the encryption operations.

The trust root is therefore in the distribution, not in the algorithm. AES itself is the
chain's strongest item; where the chain breaks is the channel the key travels over, and how
many times that channel is used.

#### Summary

- Symmetric decryption requires four items to be correct at the same time: key, nonce,
  authentication tag, and uncorrupted body; only one of five attempts decrypts.
- The nonce is not secret, but it must not be used twice with the same key; if it is, the same
  plaintext gives the same ciphertext, and equality becomes visible.
- In the separate-key-per-pair scheme, the key item count grows quadratically: 21 at 7
  participants, 7.69×10^7 at 12,401 endpoints; a new participant adds as many items as the
  current participant count.
- A single shared key reduces the item count to one, but it reduces the root to one too: when
  the one item falls, every connection drops, and renewal is done at 12,401 endpoints.
- In all three schemes, delivering the key to both sides for the first time cannot be done by
  encrypting it; in a 12,400-endpoint network, that is 12,400 items by hand, repeated at every
  renewal.
- The authentication tag narrows the sender down to the parties that know the key: 2 candidates
  in the per-pair scheme, 12,401 candidates in the single-shared-key scheme; a symmetric tag
  cannot offer proof to a third party.

#### Next Step

This lesson's unresolved item stands in a single sentence: to start an encrypted conversation,
a pre-shared secret is needed, and sharing that secret itself needs an encrypted channel. The
loop cannot be broken from inside symmetric encryption, because a single key carries both the
lock and the key that opens it. The next lesson splits this lock in two: one key locks, a
separate key unlocks, and one of the two can be given to everyone. The split itself turns into
a countable question — of the four possible key-operation combinations, which works, which
gives secrecy, which gives identity, and what none of them gives.

### Asymmetric Encryption

Source: https://academia.sh/en/courses/identity-and-cryptography/asymmetric-encryption

The separation of the public and private key's roles: two of four possible key-operation combinations working, one giving secrecy and the other a source claim, a 190-byte upper bound in a 2048-bit modulus, the secret item count dropping from 7.69×10^7 to 12,401 at 12,401 participants and the transported secret item falling to zero, and hybrid use cutting overhead from 38 percent to 7 percent.

The previous lesson's unresolved item stood in a single sentence: to start an encrypted
conversation, a pre-shared secret is needed, and sharing that secret itself needs an encrypted
channel. The loop cannot be broken from inside symmetric encryption, because a single key
carries both the lock and the key that opens it.

This lesson splits the lock in two. Of the two keys generated together, one locks and the
other unlocks; the two are not interchangeable, and one of them can be given to everyone. The
split itself turns into a countable question: of the four possible key-operation combinations,
which ones work, and what do the ones that work provide?

#### Two Keys, One Pair

**CF14:** in asymmetric encryption, two keys are generated together and are mathematically
linked. An operation done with one is only reversed with the other; it is not reversed with the
same key.

**CF15:** encrypting with the public key gives secrecy — only the private key's owner can read
the output. Encrypting with the private key does not give secrecy, because the public key is
with everyone; what it carries is a source claim.

```js
// asymmetric/role.mjs — which operation the public and private key perform in which
// direction is actually run. A key pair is generated for the center in the fictional
// regional measurement network; key material is not printed, only length and outcome are.
import { generateKeyPairSync, publicEncrypt, privateDecrypt,
         privateEncrypt, publicDecrypt, constants } from "node:crypto";

// CF14: in asymmetric encryption, two keys are generated together; an operation done
//       with one is only reversed with the other, not with the same key.
// CF15: encrypting with the public key gives secrecy (only the private key's owner reads
//       it); encrypting with the private key does not give secrecy, it carries a source claim.

const BITS = 2048;
const { publicKey: PUBLIC, privateKey: PRIVATE } = generateKeyPairSync("rsa", { modulusLength: BITS });
const READING = "SAY-4417|2026-03-01T06:00Z|41982";
const OAEP = { oaepHash: "sha256", padding: constants.RSA_PKCS1_OAEP_PADDING };
const attempt = (f) => { try { return f().toString() === READING ? "yes" : "no"; }
                      catch { return "no"; } };
const printRow = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));

const oaepEncrypted = publicEncrypt({ key: PUBLIC, ...OAEP }, Buffer.from(READING));
const privateEncrypted = privateEncrypt(PRIVATE, Buffer.from(READING));
const R = [-12, -12, 9, -24];
printRow(R, "encryptor", "decryptor", "worked", "  provides");
for (const [a, b, f, provides] of [
  ["public", "private", () => privateDecrypt({ key: PRIVATE, ...OAEP }, oaepEncrypted), "secrecy"],
  ["public", "public", () => publicDecrypt(PUBLIC, oaepEncrypted), "-"],
  ["private", "public", () => publicDecrypt(PUBLIC, privateEncrypted), "source claim"],
  ["private", "private", () => privateDecrypt(PRIVATE, privateEncrypted), "-"],
]) printRow(R, a, b, attempt(f), "  " + provides);

const CAP = BITS / 8 - 2 * 32 - 2;                  // upper bound for OAEP-SHA256
const fits = (n) => { try { publicEncrypt({ key: PUBLIC, ...OAEP }, Buffer.alloc(n));
                             return "yes"; } catch { return "no"; } };
console.log(`\nmodulus ${BITS} bits = ${BITS / 8} bytes; OAEP-SHA256 cap ${CAP} bytes`);
const S = [10, 9, 14];
printRow(S, "plaintext", "fits", "cipher bytes");
for (const n of [32, CAP, CAP + 1])
  printRow(S, n + " bytes", fits(n), fits(n) === "yes"
    ? publicEncrypt({ key: PUBLIC, ...OAEP }, Buffer.alloc(n)).length + " bytes" : "-");
console.log(`reading ${Buffer.byteLength(READING)} bytes; ciphertext form ${oaepEncrypted.length} bytes`);
```

```
encryptor   decryptor      worked  provides
public      private           yes  secrecy
public      public             no  -
private     public            yes  source claim
private     private            no  -

modulus 2048 bits = 256 bytes; OAEP-SHA256 cap 190 bytes
 plaintext     fits  cipher bytes
  32 bytes      yes     256 bytes
 190 bytes      yes     256 bytes
 191 bytes       no             -
reading 32 bytes; ciphertext form 256 bytes
```

Two of the four combinations work and two do not. The two rows that do not work say the same
thing: an operation done with one key is not reversed with that same key. This is the one
structural difference from the symmetric scheme, and the rest of the lesson counts this
difference's consequences.

The two rows that work are each other's opposite. The **encrypt with the public key, decrypt
with the private key** direction gives secrecy: since the public key is with everyone, anyone
can send an encrypted reading to the center, but the only side that can read it is the private
key's owner. In the fictional measurement network, this means none of the 12,400 meter endpoints
can read what another one sends.

The **encrypt with the private key, decrypt with the public key** direction, on the other hand,
does not give secrecy — since the public key is with everyone, anyone can decrypt the output.
What it gives is a source claim: if the decryption succeeds, the side that performed the
operation is the side holding the private key. This direction is the foundation of the digital
signature and is taken up in its own form in the seventh lesson.

The lower table gives asymmetric encryption's second constraint. The modulus is 256 bytes, and
the padding scheme consumes 66 bytes of it; the largest plaintext that can be transported is
190 bytes. 190 bytes works, 191 bytes does not. The output, though, is 256 bytes no matter what
the input is — even a 32-byte reading grows to 256 bytes, an eightfold overhead.

#### The Secret Item Count and Hybrid Use

The size limit looks like an obstacle, but the real measurement is elsewhere: how many secret
items does this scheme require, and how many of them are transported?

```js
// asymmetric/item.mjs — the asymmetric scheme's effect on the secret item count and hybrid
// use for large data (model + running measurement). Participant counts are fictional.
import { generateKeyPairSync, publicEncrypt, privateDecrypt, randomBytes,
         createCipheriv, createDecipheriv, constants } from "node:crypto";

// CF16: the asymmetric scheme reduces the secret item count from n(n-1)/2 to n, and none
//       of the secret items are transported; the only thing transported is the public key, and it is not secret.

const printRow = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const format = (x) => x >= 1e7 ? x.toExponential(2) : Math.round(x).toLocaleString("en-US");
const A = [12, 12, 11, 11, 20, 20];
printRow(A, "participant", "symmetric", "asymmetric", "asymmetric", "transported secret", "transported secret");
printRow(A, "", "secret item", "secret", "public", "item (sym)", "item (asym)");
for (const n of [2, 7, 40, 12_401]) {
  const c = (n * (n - 1)) / 2;
  printRow(A, format(n), format(c), format(n), format(n), format(c), 0);
}

const { publicKey: PUBLIC, privateKey: PRIVATE } = generateKeyPairSync("rsa", { modulusLength: 2048 });
const OAEP = { oaepHash: "sha256", padding: constants.RSA_PKCS1_OAEP_PADDING };
const BATCH = Buffer.alloc(4096, 65);                // fictional daily reading batch
const CAP = 190;                                     // OAEP-SHA256 upper bound

const key = randomBytes(32), nonce = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", key, nonce);
const body = Buffer.concat([cipher.update(BATCH), cipher.final()]), tag = cipher.getAuthTag();
const envelope = publicEncrypt({ key: PUBLIC, ...OAEP }, key);
const decipher = createDecipheriv("aes-256-gcm", privateDecrypt({ key: PRIVATE, ...OAEP }, envelope), nonce);
decipher.setAuthTag(tag);
const recovered = Buffer.concat([decipher.update(body), decipher.final()]);

console.log(`\nif a ${BATCH.length}-byte batch were transported two ways:`);
const B = [-16, 12, 12, 14, 10];
printRow(B, "path", "asymmetric", "symmetric", "output bytes", "overhead");
const direct = Math.ceil(BATCH.length / CAP);
printRow(B, "direct RSA", direct, 0, format(direct * 256),
    Math.round((direct * 256 / BATCH.length - 1) * 100) + "%");
const hybrid = envelope.length + nonce.length + body.length + tag.length;
printRow(B, "hybrid", 1, 1, format(hybrid), Math.round((hybrid / BATCH.length - 1) * 100) + "%");
console.log("did the hybrid path return the batch unchanged:", recovered.equals(BATCH) ? "yes" : "no");
```

```
 participant   symmetric asymmetric asymmetric  transported secret  transported secret
             secret item     secret     public          item (sym)         item (asym)
           2           1          2          2                   1                   0
           7          21          7          7                  21                   0
          40         780         40         40                 780                   0
      12,401     7.69e+7     12,401     12,401             7.69e+7                   0

if a 4096-byte batch were transported two ways:
path              asymmetric   symmetric  output bytes  overhead
direct RSA                22           0         5,632       38%
hybrid                     1           1         4,380        7%
did the hybrid path return the batch unchanged: yes
```

The first table's second and third columns close the previous lesson's question. For 12,401
participants, the symmetric scheme needs 7.69×10^7 secret items while the asymmetric scheme
needs 12,401 — dropping from quadratic to linear. The public key count is also 12,401, but
those items are not secret, and the management cost of a non-secret item is an entirely
different matter.

The real number is in the last column, and it is zero. In the symmetric scheme, every secret
item has to be **transported** to both sides; in a 12,401-endpoint network, that means
7.69×10^7 transports, and that was the source of the manual items counted in the previous
lesson. In the asymmetric scheme, no secret item is transported: each side generates its own
private key itself and sends it nowhere. The only thing transported is the public key, and it
can travel over an open channel.

The second table shows how the size limit is worked around. If a 4096-byte batch is
transported directly with asymmetric encryption, 22 separate operations are needed and the
output rises to 5,632 bytes, a 38 percent overhead. On the hybrid path, asymmetric encryption
transports only a 32-byte symmetric key; the batch itself is encrypted with a symmetric mode.
The operation count drops to one, the output becomes 4,380 bytes, and the overhead falls to 7
percent. The last line confirms that this path actually works.

The difference in operation count is not just a byte calculation. Asymmetric operations are
many times more expensive than symmetric operations, and the direct path repeats this
expensive operation 22 times for a 4-kilobyte batch. On the hybrid path, the expensive
operation is done once and the whole batch is carried by the cheap operation. If the fictional
network processes 12,400 batches a day, the difference between the two paths is 260,400 extra
asymmetric operations a day.

The hybrid scheme's split is clear: asymmetric encryption **carries the key**, symmetric
encryption **carries the data**. Each one's chain stays as it was in its own lesson; on the
hybrid path, the chain gets longer, because now both the private key's secrecy and the
symmetric chain's four items have to be correct at the same time.

#### Why Key Lengths Cannot Be Compared

The symmetric key here was 256 bits, the asymmetric key 2048 bits. The numbers cannot be read on
the same scale, because the two schemes' security comes from different places. With a
symmetric key, the only path is trying the key one by one, and the cost is 2^256. With an
asymmetric key, the public key is mathematically linked to the private key, and there are
cheaper ways to break that link than trying one by one; the key length is made larger to
counter those cheaper paths.

| Scheme | Key bits | Equivalent symmetric security | Key/output size |
|---|---|---|---|
| symmetric | 128 | 128 bit | 16 bytes |
| symmetric | 256 | 256 bit | 32 bytes |
| factoring-based | 2048 | approximately 112 bit | 256 bytes |
| factoring-based | 3072 | approximately 128 bit | 384 bytes |
| elliptic-curve-based | 256 | approximately 128 bit | 32 bytes |

The table reads as follows: a 2048-bit asymmetric key does not come close to the security a
256-bit symmetric key provides; it is equivalent to roughly a 112-bit symmetric key. To raise
the equivalence, the asymmetric key has to be pushed up to 3072 bits, and that carries every
operation's output to 384 bytes.

The last line is a separate option. In the elliptic-curve-based scheme, the same equivalence is
reached with a 256-bit key, that is, 32 bytes. In the fictional measurement network, this
difference translates directly into an item: if 12,400 endpoints each carry a 256-byte envelope
per message, the daily load is eight times what it would be with a 32-byte envelope. On
constrained endpoints, what decides the choice is not the algorithm's name, it is this size
arithmetic.

#### The Root's Name and the Unproven Item

The trust root here is a single item: the private key existing only with its owner. This item
is born the moment the key is generated, and where that generation happens is the root's first
item. If the key pair is generated at the center and distributed to the endpoints, the private
key has been transported at least once, and the previous lesson's transport problem comes back;
if each endpoint generates its own pair itself, the transported secret item count stays at
zero. Generating it yourself has its own condition: the endpoint's random number generator has
to be unpredictable. In a pair generated with a predictable generator, the private key can be
regenerated even if it never leaks at all. The item count that falls when the root breaks is
different from the symmetric scheme. If a private key leaks, every past and future message sent
to that key's owner drops; in a network with 12,401 participants, that is all the traffic
reaching the center. Renewal, though, is cheap — a new pair is generated and the **public** key
is distributed; no secret item is transported.

Where the root sits is a separate item count. If the private key sits in a file at the center,
the items that can reach it can be counted one by one: the file's permission record, the memory
of the process reading the file, its backup copy, the medium the backup travels on, and the
copy the distribution automation accesses — five items. All five of the five have to be kept
correct at the same time, and a flaw in any one of them opens the root.

Generating and keeping the key inside a **hardware security module** it never leaves changes
this count. Since the key never leaves the module, there is no file to read, no backup to take,
and no copy to transport; a single item remains — authorization to use the module. The item
count drops from five to one. The cost is countable too: since the key cannot be exported, it
cannot be backed up either, and if the module is lost, the key is lost permanently —
renewal means public key distribution at 12,401 endpoints.

But there is one more item in the chain that has not been named, and this lesson does not close
it. A meter endpoint uses the center's public key to send an encrypted reading to the center.
Nothing proves that public key really belongs to the center. Since the public key is not secret,
it comes over an open channel, and anything that comes over an open channel can be altered. A
reading encrypted with the wrong public key is read by whoever holds that key's private
counterpart.

This item is the entire subject of the course's second topic. In this lesson it can only be
counted: two of four combinations work, the secret item count drops to linear, the transported
secret item is zero — and **the public key's owner is still unverified**.

#### Summary

- The public and private key are generated together; two of four key-operation combinations
  work, and an operation done with one key is not reversed with the same key.
- Encrypting with the public key gives secrecy; encrypting with the private key does not give
  secrecy, it carries a source claim.
- In a 2048-bit modulus, the largest plaintext that can be transported is 190 bytes, and the
  output is 256 bytes no matter what the input is.
- The asymmetric scheme drops the secret item count from 7.69×10^7 to 12,401 at 12,401
  participants; the transported secret item count is zero.
- In hybrid use, asymmetric encryption carries the key, symmetric encryption carries the data:
  for a 4096-byte batch, the operation count drops from 22 to 1, and the overhead drops from 38
  percent to 7 percent.
- The trust root is the private key's secrecy; who the public key belongs to is not proven in
  this scheme.

#### Next Step

The hybrid path transported the key, but the side that sent the transported key **chose** it.
This has two consequences. First, the session key's quality depends entirely on that side's
generator. Second, and heavier: recorded encrypted traffic is stored, and if the center's
private key leaks years later, every session key up to that point is extracted back out of its
envelope, and all past traffic is opened. The next lesson actually runs, with small parameters,
a method that removes both of these consequences at once: a shared secret that both sides
contribute to, that never crosses the channel, and that neither side determines on its own. The
items left in the eavesdropper's hands will be counted one by one.

### Key Exchange

Source: https://academia.sh/en/courses/identity-and-cryptography/key-exchange

Running shared-secret generation over an insecure channel with small parameters: both sides reaching the same shared secret without ever sending their secret exponent, the intersection of the 4 items crossing the channel and the 3 items sufficient for the secret being zero, ephemeral key exchange dropping the number of retroactively opened sessions from 52,560 to 0, and the exchange verifying two of its three items while not verifying the other side's identity.

In the previous lesson, the hybrid path transported the key, but the side that sent the
transported key chose it. This had two consequences. First, the session key's quality depended
entirely on that side's generator. Second, and heavier: recorded encrypted traffic can be
stored, and if the private key leaks years later, every session key up to that point is
extracted back out of its envelope.

This lesson takes up the method that removes both at once. The two sides reach the same shared
secret without ever sending their secret values; the shared secret never crosses the channel
and neither side determines it on its own. The method will actually be run with small
parameters, then the items left in the eavesdropper's hands will be counted one by one.

#### The Secret That Does Not Cross the Channel

**CF17:** two sides can produce a shared secret without ever sending their secret exponent;
there is no direct computational path between the items crossing the channel and the shared
secret.

**CF18:** the shared secret is formed by both sides' contribution; neither side determines it
on its own.

The method is this. The sides openly agree on a prime `p` and a generator `g`. Each side picks
a secret exponent, computes `g` to that exponent modulo `p`, and sends the **result** to the
other side. Both sides, raising the value received from the other side to their own secret
exponent, land on the same value, because exponentiation does not depend on order. The
parameters below are teaching-sized and are not used in a real deployment.

```js
// exchange/dh.mjs — key exchange is actually run with small parameters. p and g are
// small fictional values chosen for teaching; they are not used in a real deployment.
// The secret exponents are fixed so the output stays the same run to run. No randomness.
import { hkdfSync, createCipheriv, createDecipheriv } from "node:crypto";

// CF17: two sides can produce a shared secret without ever sending their secret exponent;
//       there is no direct computational path from the items crossing the channel to the shared secret.
// CF18: the shared secret is formed by both sides' contribution; neither side determines it on its own.

const p = 65267n, g = 2n;                     // fictional, teaching-sized prime and generator
const power = (t, u, m) => { let r = 1n; t %= m;
  while (u > 0n) { if (u & 1n) r = r * t % m; t = t * t % m; u >>= 1n; } return r; };

const a = 6421n, b = 8973n;                   // fictional secret exponents: branch and center
const A = power(g, a, p), B = power(g, b, p);   // public values crossing the channel
const branchSecret = power(B, a, p), centerSecret = power(A, b, p);

console.log(`items crossing the channel   : p=${p} g=${g} A=${A} B=${B}`);
console.log("item not crossing the channel: a and b (the two secret exponents)");
console.log("branch shared secret         :", String(branchSecret));
console.log("center shared secret         :", String(centerSecret));
console.log("did both sides produce the same secret:", branchSecret === centerSecret ? "yes" : "no");
console.log("did the shared secret cross the channel:",
  [p, g, A, B].includes(branchSecret) ? "yes" : "no");

// The shared secret is not used directly as a key; key derivation pulls it to a fixed size.
const raw = Buffer.from(branchSecret.toString(16).padStart(4, "0"), "hex");
const key = Buffer.from(hkdfSync("sha256", raw, Buffer.alloc(16, 9),
  "measurement-net-session", 32));
const nonce = Buffer.alloc(12, 4), READING = "SAY-4417|2026-03-01T06:00Z|41982";
const cipher = createCipheriv("aes-256-gcm", key, nonce);
const body = Buffer.concat([cipher.update(READING, "utf8"), cipher.final()]), tag = cipher.getAuthTag();
const decipher = createDecipheriv("aes-256-gcm", Buffer.from(hkdfSync("sha256",
  Buffer.from(centerSecret.toString(16).padStart(4, "0"), "hex"), Buffer.alloc(16, 9),
  "measurement-net-session", 32)), nonce);
decipher.setAuthTag(tag);
console.log("derived key length           :", key.length, "bytes");
console.log("center decrypted branch's packet:",
  Buffer.concat([decipher.update(body), decipher.final()]).toString() === READING ? "yes" : "no");
```

```
items crossing the channel   : p=65267 g=2 A=10224 B=40077
item not crossing the channel: a and b (the two secret exponents)
branch shared secret         : 41173
center shared secret         : 41173
did both sides produce the same secret: yes
did the shared secret cross the channel: no
derived key length           : 32 bytes
center decrypted branch's packet: yes
```

The fifth line is the whole method: the branch reached 41173, the center reached 41173, and
that value was never sent. Only 65267, 2, 10224, and 40077 crossed the channel.

The last two lines show how the shared secret is used. The raw shared secret is not used
directly as a key — it is a numeric value, its length depends on the parameter, and its
distribution is not the distribution expected of a key. The key derivation step turns it into a
fixed-size, well-distributed key. The packet encrypted with the derived key can be decrypted by
the other side with the key it derives from its own shared secret; the chain works end to end.

#### The Items in the Eavesdropper's Hands

What the method rests on is that going from `g^a mod p` back to `a` — the discrete
logarithm, for short — is expensive. The count below writes this out item by item.

```js
// exchange/item.mjs — key exchange's item count (model): the items in the eavesdropper's
// hands, candidate exponent count by parameter size, the number of retroactively opened
// sessions, and the number of verified items. Fictional regional measurement network numbers are used.

// CF19: key exchange produces a shared secret but does not verify the other side's identity;
//       in an unverified exchange, two sides can each agree on two separate secrets.

const printRow = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));

console.log("items in the eavesdropper's hands:");
const A = [-29, 12, 19];
printRow(A, "item", "on channel", "enough for secret");
for (const [name, channel, enough] of [
  ["prime p", "yes", "no"], ["generator g", "yes", "no"],
  ["branch's public value A", "yes", "no"], ["center's public value B", "yes", "no"],
  ["branch's secret exponent a", "no", "yes"], ["center's secret exponent b", "no", "yes"],
  ["shared secret", "no", "yes"],
]) printRow(A, name, channel, enough);
console.log("items on the channel: 4, items sufficient for the secret: 3, their intersection: 0");

console.log("\ncandidate exponent count by parameter size:");
const B = [10, 22, 26];
printRow(B, "p bits", "candidate exponents", "fictional-network trial");
for (const bit of [16, 32, 64, 2048])
  printRow(B, bit, bit <= 32 ? (2 ** bit).toLocaleString("en-US") : "2^" + bit,
      bit <= 32 ? "countable" : "uncountable");

const BRANCHES = 6, SESSIONS_PER_YEAR = 24 * 365;   // fictional: hourly sessions per branch
console.log(`\nretroactive opening (fictional ${BRANCHES} branches, ${SESSIONS_PER_YEAR} sessions/year per branch):`);
const C = [-32, -23, 16];
printRow(C, "scheme", "secret determined by", "sessions opened");
printRow(C, "asymmetric envelope transport", "the sender", (BRANCHES * SESSIONS_PER_YEAR).toLocaleString("en-US"));
printRow(C, "ephemeral key exchange", "both sides together", 0);

console.log("\nitem the exchange verifies:");
const D = [-39, 13];
printRow(D, "item", "verified");
for (const [name, v] of [["the shared secret's secrecy", "yes"],
                       ["both sides producing the same secret", "yes"],
                       ["the other side's identity", "no"]]) printRow(D, name, v);
console.log("2 of 3 items are verified; the unverified item leaves the door open for a");
console.log("party in the middle to run two separate exchanges (model, 2 separate shared secrets).");
```

```
items in the eavesdropper's hands:
item                           on channel  enough for secret
prime p                               yes                 no
generator g                           yes                 no
branch's public value A               yes                 no
center's public value B               yes                 no
branch's secret exponent a             no                yes
center's secret exponent b             no                yes
shared secret                          no                yes
items on the channel: 4, items sufficient for the secret: 3, their intersection: 0

candidate exponent count by parameter size:
    p bits   candidate exponents   fictional-network trial
        16                65,536                 countable
        32         4,294,967,296                 countable
        64                  2^64               uncountable
      2048                2^2048               uncountable

retroactive opening (fictional 6 branches, 8760 sessions/year per branch):
scheme                          secret determined by    sessions opened
asymmetric envelope transport   the sender                       52,560
ephemeral key exchange          both sides together                   0

item the exchange verifies:
item                                        verified
the shared secret's secrecy                      yes
both sides producing the same secret             yes
the other side's identity                         no
2 of 3 items are verified; the unverified item leaves the door open for a
party in the middle to run two separate exchanges (model, 2 separate shared secrets).
```

The first table's last line is the method's essence: the **intersection** of the four items
crossing the channel and the three items sufficient for the secret **is zero**. The
eavesdropper records all four items, and none of them carries it into the third set. This is
the numerical equivalent of the statement "the eavesdropper cannot reach the secret."

The second table shows what this separation depends on. Going from the items crossing the
channel back to the secret exponent is not impossible — it is only expensive, and the expense is
set by the parameter size. For this lesson's 16-bit prime, the candidate exponent count is
65,536, and that is a set an ordinary machine can enumerate. That is why it was chosen as the
teaching parameter; the running example is fast because it is small, not because it is secure.
The size used in real deployments is 2048 bits and up; the candidate count there is 2^2048.

The same method can also be built with an operation defined over an elliptic curve instead of
exponentiation, and the logic does not change: each side picks a secret value, sends its
counterpart, and processes what comes from the other side with its own secret value. The only
thing that changes is the parameter size needed for the same security. According to the
previous lesson's table, a 3072-bit prime's equivalent is a 256-bit curve parameter; that is,
the public value carried each round drops from 384 bytes to 32 bytes.

In the fictional measurement network, this difference writes straight into the traffic. If
297,600 rounds a day carry a 384-byte public value per round, the daily load is roughly 109
MiB; with a 32-byte public value, the same load drops to 9.1 MiB. That twelvefold difference
decides whether the exchange is feasible at all on constrained endpoints.

#### Two Consequences, Two Separate Numbers

The third table closes the previous lesson's second problem. When the key is transported in an
asymmetric envelope, the sender decides the session key, and the envelope is opened with the
long-lived private key. If a party holding recorded traffic reaches that private key years
later, it opens every one of the fictional network's 52,560 sessions; it makes no difference
when the sessions took place.

The assumption underneath this line is that encrypted traffic can be recorded. Recording is
cheap, and the cost of breaking it can be deferred; in the fictional network, a whole year's
sessions fit into an archive of a few hundred gigabytes. That is why the "the key has not
leaked" condition is not measured against today's question, but against the archive's lifetime.

In ephemeral key exchange, the number is zero. New secret exponents are chosen for every
session, the shared secret is limited to that session, and the exponents are deleted when the
session ends. The long-lived key leaking later opens no session retroactively, because past
sessions' secrets are not stored anywhere. This property carries the name **forward secrecy**,
and its cost is generating a pair of exponents per session plus one extra round of exchange.

#### The Chain's Seven Items

The answer to how many items of this mechanism have to be correct at the same time is seven,
and six of them are inside the mechanism itself.

The first and second are the parameters: `p` has to actually be prime and large enough, `g`
has to be a proper generator for that prime. The parameters are not secret and are usually
constants shared by both sides; that makes them easy to audit, but it also carries a flawed
parameter set into every session. A badly chosen prime drops the candidate exponent count far
below the table's 2^2048 value, and this produces no symptom at runtime — the exchange still
works, and still yields the same secret.

The third and fourth items are the two secret exponents. The fifth item is the exponents being
renewed every session; when they are not renewed, the zero in the forward-secrecy table reverts
to the session count. The sixth item is the derivation step: if the raw shared secret is used
as the key, the distribution stays broken, and the rest of the chain is built on top of that
broken key.

The seventh item is outside the mechanism and is the subject of the next section. Even if all
six of the six items are correct, who the other side is is not determined in any way inside this
method.

Its cost is countable too. The exchange adds at least one extra round to connection setup and
requires an exponentiation computation on both sides. In the fictional measurement network, if
12,400 endpoints open a session once an hour, that is 297,600 extra rounds a day and the same
number of exponentiation operations. What is gained in return is the retroactive closing of the
third table's 52,560 sessions.

#### The Unverified Item

The fourth table is this lesson's boundary. The exchange verifies two of three items: the
shared secret stays secret, and both sides produce the same secret. The third item — the other
side's identity — is not verified.

The consequence can be written as a number. A party that gets between the exchange's two ends
can run two separate exchanges: one shared secret with one side, another shared secret with the
other side. Both ends see that things worked correctly, because each produced a valid secret on
its own side. The verified item count is two, the item left open is one, and the number of
shared secrets that come out of it is two. This is not a configuration flaw; identity is
not within the exchange method's scope.

Agreeing on the parameters is itself inside the same gap. Since `p` and `g` cross the channel
openly, which parameters the two sides agree on is also an unverified item; a party in the
middle can steer both ends into picking the smallest parameter set each supports, and the
table's 2^2048 value silently drops to 2^32. The exchange still completes, both sides still
produce the same secret, and no symptom appears. That is why deployments write the smallest
accepted parameter size down separately as a policy item — it is not left to negotiation.

The method's name itself carries this boundary: key **exchange**, not identity verification.
In the fictional measurement network, an exchange between a branch and the center does not say the
other side is really the center — it only says a secret was shared with whoever was on the
other end of the conversation.

#### Summary

- Both sides reach the same shared secret without ever sending their secret exponent: in the
  running example, both sides landed on 41173, and that value never crossed the channel.
- The intersection of the 4 items crossing the channel and the 3 items sufficient for the
  secret is zero; the eavesdropper cannot get from the items it recorded to the shared secret.
- Security depends on parameter size: for a 16-bit prime, the candidate exponent count is
  65,536 and countable; the teaching example is fast because it is small, not because it is
  secure.
- The raw shared secret is not used directly as a key; key derivation turns it into a
  fixed-size, well-distributed key.
- In ephemeral exchange, the long-lived key leaking opens 0 sessions retroactively; in
  asymmetric envelope transport, the same leak opens every one of the fictional network's
  52,560 sessions.
- The exchange verifies two of three items; the other side's identity is not verified, and an
  unverified exchange can produce two separate shared secrets.

#### Next Step

The unverified item pushes one step further back. If the exchange itself cannot give the other
side's identity, an outside proof is needed, and that proof was already seen once, in the fifth
lesson: an operation done with the private key is reversed with the public key, and being
reversible showed the source. The next lesson takes up this direction in its own form — a
signature is produced and verified, the count of how verification drops when a single bit of
the signed data is flipped is measured, and the items the signature proves and the items it
does not prove are written down separately. The difference between integrity and
non-repudiation is also tied to a number there. The topic does not close with this lesson; the
real gap will surface at the end of the seventh lesson.

### Digital Signatures

Source: https://academia.sh/en/courses/identity-and-cryptography/digital-signatures

The separation of the items a signature proves and does not prove: verification not surviving a single one of 952 attempts when every bit of the 440-bit batch and every bit of the 512-bit signature is flipped one at a time, two of six items being proven and four not, keyed hash narrowing the sender candidate to 2 and a signature narrowing it to 1, and 2,190 past signatures becoming disputed when the signing key leaks.

The previous lesson closed with an unverified item: key exchange produces a shared secret but
does not say who the other side is. The proof had to come from outside, and the fifth lesson
already caught a glimpse of that proof's direction once — an operation done with the private
key is reversed with the public key, and being reversible showed the source.

This lesson takes up that direction in its own form. A **digital signature** is a value produced
with the private key and verified with the public key; verification does not need any secret
item. What is measured is not whether the signature works, but exactly what it proves and what it
does not prove.

#### The Signature and a Single-Bit Change

**CF20:** a signature is produced by the party holding the private key and is verified by
anyone using the public key; verification does not need any secret item.

**CF21:** a signature depends on the entire signed byte sequence; changing a single bit drops
verification.

```js
// signature/generate.mjs — a signature is generated and verified, then each bit of the
// signed data is flipped and the number of surviving verifications is counted. A fictional
// measurement network's daily reading batch is signed; key material is not printed.
import { generateKeyPairSync, sign, verify } from "node:crypto";

// CF20: a signature is produced by the party holding the private key and is verified by
//       anyone using the public key; verification does not need any secret item.
// CF21: a signature depends on the entire signed byte sequence; changing a single bit
//       drops verification.

const { publicKey: PUBLIC, privateKey: PRIVATE } = generateKeyPairSync("ed25519");
const BATCH = Buffer.from("BRANCH-03|2026-03-01|reads=4180|total=8813304|recs=1240");
const signature = sign(null, BATCH, PRIVATE);

console.log("signed batch    :", BATCH.length, "bytes,", BATCH.length * 8, "bits");
console.log("signature size  :", signature.length, "bytes");
console.log("unchanged batch :", verify(null, BATCH, PUBLIC, signature) ? "valid" : "invalid");

let survived = 0;
for (let i = 0; i < BATCH.length * 8; i++) {
  const b = Buffer.from(BATCH); b[i >> 3] ^= 1 << (i & 7);
  if (verify(null, b, PUBLIC, signature)) survived++;
}
console.log(`batch bit flipped    : ${BATCH.length * 8} attempts, verification survived: ${survived}`);

let signatureSurvived = 0;
for (let i = 0; i < signature.length * 8; i++) {
  const s = Buffer.from(signature); s[i >> 3] ^= 1 << (i & 7);
  if (verify(null, BATCH, PUBLIC, s)) signatureSurvived++;
}
console.log(`signature bit flipped: ${signature.length * 8} attempts, verification survived: ${signatureSurvived}`);

const { publicKey: OTHER_PUBLIC } = generateKeyPairSync("ed25519");
console.log("verification with a different public key:",
  verify(null, BATCH, OTHER_PUBLIC, signature) ? "valid" : "invalid");
```

```
signed batch    : 55 bytes, 440 bits
signature size  : 64 bytes
unchanged batch : valid
batch bit flipped    : 440 attempts, verification survived: 0
signature bit flipped: 512 attempts, verification survived: 0
verification with a different public key: invalid
```

Verification did not survive a single one of nine hundred fifty-two attempts. Whether any of the
440 bits of the batch or any of the 512 bits of the signature is flipped, the result is the
same: invalid. This is the signature-side counterpart of the avalanche effect measured in the
second lesson — a signature binds the data as a whole, and no part of it is handled separately.

The last line is a separate item. A signature is verified with the public key that is the
**counterpart** of the private key it was produced with; verifying with a different public key
returns invalid. The verifying party holding the correct public key is a separate link in this
chain, and it is exactly the item left open at the end of this lesson.

Signature size is also a decision. The 64 bytes here, compared with the 256-byte output from
the fifth lesson, is a quarter of it; if the fictional network produces 12,400 signatures a
day, the difference is a 2.3 MiB load per day. A signature's size does not determine its
security — it determines its transport cost.

#### The Signature Is Produced Over a Digest

The batch was 55 bytes, but what gets signed is not directly that 55 bytes. Signature schemes
first run the data through a hash function and perform the asymmetric operation on the
**digest**. The reason is the size limit measured in the fifth lesson: the asymmetric operation
takes a fixed, small input, while the data to be signed can be any length. A fixed-length digest
ties these two requirements together.

The direct consequence is that the signature's security depends on the hash function's
**collision resistance**. This is why the second lesson's table lists the "digest signed between
branches" row under the collision column. If two different batches that produce the same digest
can be found, a signature produced for one also fits the other; while signing one batch, the
signing party ends up signing another batch it never saw.

The number follows from this. When collision resistance drops, the count of signatures that
become disputed is the same as the count from the signing key leaking — 2,190 in the fictional
network. But the reason differs: in one case the key has fallen into someone else's hands, in
the other the key was never touched and a different link in the chain broke. This is why the
chain's items are counted separately; a signature's security is the security of its weakest
link.

The second rule that follows is key separation. Using the same key pair for both encryption and
signing halves the item count but ties two breaks to a single root: the signing key leaking also
opens all the traffic encrypted with that pair. In the fictional network, using separate pairs
means two key items per participant — 14 items for 7 participants — and what that buys is that
one break does not bring down the other use.

#### What the Signature Proves and Does Not Prove

**CF22:** a signature proves that the signing party held the private key; it does not prove who
that key belongs to, when it was used, or whether it was stolen at that moment.

```js
// signature/item.mjs — the items a signature proves and does not prove, the split between
// integrity and non-repudiation, and the items that drop if the signing key leaks (model).
// Fictional regional measurement network figures are used.

// CF22: a signature proves that the signing party held the private key; it does not prove
//       who that key belongs to, when it was used, or whether it was stolen at that
//       moment.

const printRow = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const ITEMS = [
  ["the data has not changed since it was signed", "yes"],
  ["the signing party held the private key", "yes"],
  ["who that private key belongs to", "no"],
  ["when the signature was produced", "no"],
  ["whether the signer saw and accepted the content", "no"],
  ["whether the key was stolen at that moment", "no"],
];
const A = [-49, 13];
printRow(A, "item", "proven");
for (const [name, p] of ITEMS) printRow(A, name, p);
const proven = ITEMS.filter(([, p]) => p === "yes").length;
console.log(`${ITEMS.length} items, ${proven} proven, ${ITEMS.length - proven} not proven`);

const PARTICIPANTS = 7;                              // fictional: center + 6 branches
console.log(`\nif the same batch were signed two ways (${PARTICIPANTS} participants):`);
const B = [-22, 12, 12, 16];
printRow(B, "method", "verifying", "sender", "third party");
printRow(B, "", "party", "candidate", "proof");
printRow(B, "keyed hash", 2, 2, "no");
printRow(B, "digital signature", PARTICIPANTS, 1, "yes");

const BRANCHES = 6, DAYS = 365;
console.log(`\nif the signing key leaks (${BRANCHES} branches, 1 batch per day):`);
const C = [-26, 14, 14];
printRow(C, "item", "per branch", "network-wide");
printRow(C, "disputed past signatures", DAYS, BRANCHES * DAYS);
printRow(C, "new key pair", 1, BRANCHES);
printRow(C, "public key to distribute", 1, BRANCHES);
printRow(C, "batches to re-sign", DAYS, BRANCHES * DAYS);
```

```
item                                                    proven
the data has not changed since it was signed               yes
the signing party held the private key                     yes
who that private key belongs to                             no
when the signature was produced                             no
whether the signer saw and accepted the content             no
whether the key was stolen at that moment                   no
6 items, 2 proven, 4 not proven

if the same batch were signed two ways (7 participants):
method                   verifying      sender     third party
                             party   candidate           proof
keyed hash                       2           2              no
digital signature                7           1             yes

if the signing key leaks (6 branches, 1 batch per day):
item                          per branch  network-wide
disputed past signatures             365          2190
new key pair                           1             6
public key to distribute               1             6
batches to re-sign                   365          2190
```

Two of six items are proven, four are not. This ratio is not the signature's weakness; it is the
definition of its scope — each of the four unproven items calls for its own mechanism. For time,
an independent timestamp is needed; for whether the key was stolen, a revocation scheme; for the
key's owner, an identity document — all three lie outside this topic, and all three bring their
own trust root.

The second item a signature produces — that the signing party held the private key — is not an
identity. **Who** the key belongs to is not written inside the signature. Nor is when it was
signed; a timestamp can be added to a signature, but the signing party is the one who puts that
timestamp there too, so the timestamp is the signer's own claim. Whether the key was stolen at
the moment of signing does not show up in the signature in any form: a signature produced with a
stolen key is a valid signature.

#### Integrity and Non-Repudiation

The second table ties this lesson's conceptual distinction to a number. The keyed hash measured
in the fourth lesson also provided integrity — if the body changed, the tag did not hold. The
difference lies beyond integrity.

**Integrity** shows that the data has not changed, and both mechanisms do that.
**Non-repudiation**, though, is the signer not being able to say "I did not produce this," and
that requires the sender candidate to narrow down to **one**.

With a keyed hash, both sides know the key; the number of parties who can verify is two, and the
sender candidate count is also two. Because the recipient could have produced the same tag
itself, it cannot prove anything to a third party. With a digital signature, the secret item sits
with a single side: the number of parties who can verify is seven — everyone who has the public
key — and the sender candidate count is one. Proof can be presented to a third party.

The word "can" is conditional, and that condition sits in the first table. Non-repudiation holds
only if it is accepted that the private key truly sits with just that one party. If the key sits
in a file and several people can reach that file, even a mathematically flawless signature
does not refute the "I did not do it" defense. This is why non-repudiation is not a cryptographic
property but an **operational** one, supported by cryptography.

#### The Chain's Six Items

The number of items that must be true at the same time to draw a conclusion from a signature's
verification is six, and five are technical, one is operational.

The first is the signing party's private key staying secret. The second is the collision
resistance of the hash function the signature is produced over. The third is the verifying party
holding the correct public key. The fourth is the scheme's parameters being chosen correctly.

The fifth item is the quietest one: the signed byte sequence and the verified byte sequence
being **byte-for-byte identical**. A signature is produced over a byte sequence, not over a
meaning. In the fictional network, a branch turns its daily batch into a text line and signs it;
if the center parses that line, rewrites it in its own format, and then tries to verify,
verification drops — even if the data has not changed. The reverse case is worse: if verification
is done on the raw incoming bytes but the business logic runs on a **re-parsed** copy, what was
signed and what is operated on diverge. This is why the byte sequence a signature verifies and
the data that gets used must come from the same source.

The sixth item depends on the scheme choice. Some signature schemes require a fresh random value
for every signature, and reusing that value exposes the private key; this is the signature-side
counterpart of the fourth lesson's nonce condition. The scheme used in this lesson produces the
signature deterministically and removes the item. One of the six items can be reduced away to
five by the choice of scheme — the cheapest way to cut an item count is making an item
unnecessary rather than managing it.

#### If the Root Breaks

The third table fills in the course's third column for this mechanism. If the signing key
leaks, how many items fall? The item that falls directly is every past batch signed with that
key: 365 signatures per branch, 2,190 across the whole fictional network become disputed.
"Disputed" does not mean invalid — the signatures still verify, but the sender candidate is no
longer one, and the non-repudiation property drops retroactively.

The renewal cost is counted in two separate items. Generating a new key pair and distributing
the public key is six items — cheap, because the distributed item is not secret. Rescuing the
past, though, is 2,190 items: re-signing every batch with the new key. The second number goes
unpaid in most setups, and the past is left disputed.

One way to narrow the disputed past is to have a party independent of the signer record when a
signature was produced. In that case, signatures from before the leak date stay outside the
dispute, and only part of the 2,190 items falls. The cost is an extra call to an outside party
for every signature, and that party itself becoming a new trust root. The item count does not
shrink — it moves.

This takes a different shape compared to the renewal costs in earlier lessons. When a symmetric
key leaked, renewal required carrying a **secret** item; the item carried for a signing key is
public. On the other hand, renewing a symmetric key does not make the past disputed — renewing a
signing key does.

#### Summary

- A signature is produced with the private key, verified with the public key, and verification
  does not require any secret item.
- A signature depends on the entire signed data: when the bits of the 440-bit batch and the
  512-bit signature are flipped one at a time, verification does not survive a single one of 952
  attempts.
- A signature proves two of six items; who the key belongs to, when it was used, whether the
  content was seen and accepted, and whether the key was stolen at that moment are not proven.
- Both mechanisms provide integrity; non-repudiation requires the sender candidate to narrow to
  one — the candidate count is 2 for a keyed hash and 1 for a digital signature.
- Non-repudiation is conditional: it rests on the assumption that the private key truly sits with
  a single party, and that assumption is an operational item.
- If the signing key leaks, 2,190 past signatures become disputed; renewal is 6 public item
  distributions, while rescuing the past is 2,190 re-signing items.

#### Next Step

This topic built a chain across seven lessons. How many items a reversal depends on was
counted, collision resistance was measured, the attempt-cost curve was drawn, key
distribution's arithmetic was written out, roles were separated, a shared secret was produced
over an insecure channel, and a signature proved integrity. Every link in the chain named its
own root.

The roots, in order, were these: encoding had no root; encryption's root was the key staying
secret; hashing's root was a collision not being found; password storage's root was the password
not being in the candidate list; the symmetric scheme's root was the shared key existing only on
two sides; the asymmetric scheme's root was the private key sitting with a single side; key
exchange's root was the secret exponents never being sent; and the signature's root was the
signing key staying with its owner. None of the eight roots is the algorithm itself — each one
is about where an item sits.

But one line stayed open in the last output block: the signature returned invalid when verified
with a different public key — meaning verification rests on holding the correct public key. That
public key reached the verifying party over an open channel. The question asked in the fifth
lesson goes unanswered here too: nothing proves **who** the public key that verifies the
signature belongs to. A verifier that mistakes the wrong public key for the right one treats
every signature produced with that key's counterpart as valid, and sees no error while doing so.
The next topic, **Public Key Infrastructure**, ties this single item into a chain: the document
that says a public key belongs to an identity, the party that signs that document, and the root
sitting at the very top of the chain.

## Public Key Infrastructure

What a public key's ownership rests on, and where that foundation breaks: the claim each certificate field proves and the path each field closes when checked, the number of items one root carries in the chain-of-trust graph and the renewal cost of what falls when the root breaks, the learning window of the CRL versus OCSP and the path a failed query opens, which step of the handshake proves which claim, the renewal window and the connections an expired item drops, and who can read the content in end-to-end encryption.

### Certificates

Source: https://academia.sh/en/courses/identity-and-cryptography/certificates

Certificate fields are measured in terms of the claim they prove: which claim each of the nine fields binds, the eight items that must hold at once for a single leaf item to be usable, and how five checks close eight of thirteen usage attempts while stopping none of the five legitimate ones.

The previous lesson measured the signature and left exactly one question open: a signature
proves integrity, and it even proves that the signer held that private key, but nothing proves
**who** the public key used to verify it belongs to. The verifying party has to know, from
somewhere, that the key it holds really belongs to the other party.

A certificate fills that gap with a document: a record that binds a public key to a name and
has a third party confirm that binding with its own signature. This lesson treats a certificate
not as a "list of fields" but as a **list of claims**, and counts three things: which claim each
field binds, how many things have to be true at once for a single item to be usable, and how
many usage paths close when the fields are checked.

- **PK1.** This topic's unit of count is the **item**: a component, configuration, or process in
  the chain, from the trust root to use, that must independently hold true.
- **PK2.** Certificate fields are modeled as a data structure; the **signature is genuinely
  produced and verified**. The work runs on a fictional regional measurement network; no real
  domain name, real certificate, or real authority name is written.
- **PK3.** The moment of evaluation is fixed and fictional. The clock is assumed to be correct;
  the clock itself counts as a separate item.
- **PK4.** A usage attempt is a triple: the item presented, the name it is presented for, and
  the job to be done. A legitimate attempt is one where this triple matches the item's fields.
- **PK5.** The issuer of the issuing item counts as a single step in this lesson; the full chain
  and the root are built in the next lesson.

#### The Claim, Not the Field

Looking at a certificate as a list of fields is misleading, because the fields do not all do the
same job. Each field carries a claim, and when that field goes **unchecked**, the claim stays
unproven — it is not enough for the field to be present in the record.

```js
// field.mjs — fictional certificate fields and the claim each field binds. THIS IS A MODEL.
// The regional measurement network is FICTIONAL: names, serials, and dates are made up; there is no real certificate.
export const FIELD = [
  // field, claim it binds, what stays unproven if the field is absent or unchecked
  ["subject", "the key belongs to this name's owner", "the key binds to any name"],
  ["SAN", "the item is valid for these three names", "presented for another name"],
  ["issuer", "who approved the fields", "no chain can be built"],
  ["serial", "the item can be referred to uniquely", "a revocation record has no target"],
  ["validity", "the claim covers a fixed interval", "the claim has no time limit"],
  ["public key", "this is the key to verify against", "no way to ask whose signature it is"],
  ["usage", "the key is only for these jobs", "used for any job"],
  ["basic constraints", "can this item sign certificates", "every leaf item becomes an authority"],
  ["signature", "the fields were approved by the issuer", "the fields can be altered"],
];

export const NOW = Date.parse("2026-06-01T00:00:00Z");   // fictional evaluation moment

export const RECORD = [
  ["c1", "measurement-collector", ["measurement-collector", "collector-backup"], "server-intermediate",
    "2026-01-10", "2027-01-10", ["server-identity"], false],
  ["c2", "meter-endpoint-0042", ["meter-endpoint-0042"], "device-intermediate",
    "2026-03-02", "2027-03-02", ["client-identity"], false],
  ["c3", "branch-transfer", ["branch-transfer"], "signing-intermediate",
    "2026-02-14", "2027-02-14", ["digital-signature"], false],
  ["c4", "measurement-collector", ["measurement-collector"], "server-intermediate",
    "2024-05-01", "2025-05-01", ["server-identity"], false],
  ["c5", "server-intermediate", ["server-intermediate"], "org-root",
    "2025-01-01", "2030-01-01", ["certificate-signing"], true],
  ["c7", "multi-intermediate", ["multi-intermediate"], "org-root",
    "2025-06-01", "2029-06-01", ["certificate-signing", "server-identity"], true],
].map(([id, subject, altName, issuer, start, end, usage, isAuthority], i) =>
  ({ id, subject, altName, issuer, serial: 5100 + i, validFrom: Date.parse(start + "T00:00:00Z"),
     validTo: Date.parse(end + "T00:00:00Z"), usage, isAuthority }));

// Usage attempt: which item, presented for which name, for which job.
export const ATTEMPT = [
  ["c1", "measurement-collector", "server-identity", "legit"],
  ["c1", "collector-backup", "server-identity", "legit"],
  ["c2", "meter-endpoint-0042", "client-identity", "legit"],
  ["c3", "branch-transfer", "digital-signature", "legit"],
  ["c5", "server-intermediate", "certificate-signing", "legit"],
  ["c6", "measurement-collector", "server-identity", "field altered"],
  ["c4", "measurement-collector", "server-identity", "expired"],
  ["c4", "collector-backup", "server-identity", "expired, name missing"],
  ["c1", "billing-service", "server-identity", "different name"],
  ["c3", "branch-transfer", "server-identity", "signing item instead of server"],
  ["c2", "meter-endpoint-0042", "server-identity", "device item instead of server"],
  ["c5", "server-intermediate", "server-identity", "authority item instead of leaf"],
  ["c7", "multi-intermediate", "server-identity", "authority item with broad usage"],
];

export const day = (ms) => new Date(ms).toISOString().slice(0, 10);
export const write = (g) => (h) =>
  h.map((v, i) => (i ? String(v).padStart(g[i]) : String(v).padEnd(g[0]))).join("");
export const left = (g) => (h) => h.map((v, i) => String(v).padEnd(g[i])).join("").trimEnd();
```

```js
// claim.mjs — the claim each field binds and what opens if the field goes unchecked
import { FIELD, RECORD, ATTEMPT, day, left } from "./field.mjs";

const fmt = left([19, 42, 40]);
console.log(`${RECORD.length} fictional items, ${ATTEMPT.length} usage attempts, ` +
  `${FIELD.length} fields`);
console.log("\n" + fmt(["field", "claim it binds", "if unchecked"]));
for (const [name, claim, open] of FIELD) console.log(fmt([name, claim, open]));

const k = RECORD[0];
console.log(`\nexample item ${k.id}: subject ${k.subject}, issuer ${k.issuer}, serial ${k.serial}`);
console.log(`  SAN ${k.altName.join(", ")} | usage ${k.usage.join(", ")} | ` +
  `authority ${k.isAuthority}`);
console.log(`  validity ${day(k.validFrom)} -> ${day(k.validTo)}, ` +
  `window ${Math.round((k.validTo - k.validFrom) / 86400000)} days`);
```

```
6 fictional items, 13 usage attempts, 9 fields

field              claim it binds                            if unchecked
subject            the key belongs to this name's owner      the key binds to any name
SAN                the item is valid for these three names   presented for another name
issuer             who approved the fields                   no chain can be built
serial             the item can be referred to uniquely      a revocation record has no target
validity           the claim covers a fixed interval         the claim has no time limit
public key         this is the key to verify against         no way to ask whose signature it is
usage              the key is only for these jobs            used for any job
basic constraints  can this item sign certificates           every leaf item becomes an authority
signature          the fields were approved by the issuer    the fields can be altered

example item c1: subject measurement-collector, issuer server-intermediate, serial 5100
  SAN measurement-collector, collector-backup | usage server-identity | authority false
  validity 2026-01-10 -> 2027-01-10, window 365 days
```

The table's third column is the actual subject of this lesson. The field sits there, readable,
its format valid; but when it goes unchecked, the claim the certificate carries stays in the
record while **no evidence backs it**. An item whose validity window goes unchecked turns into a
claim with no time limit; a key whose usage purpose goes unchecked gets pressed into any job.

Two fields stand apart from the rest. **SAN** is more binding than the subject itself:
verification looks for the presented name in this list, not in the subject. **Basic
constraints**, with a single true-false value, states whether an item can sign other
certificates; when this field goes unchecked, every leaf item's holder becomes able to produce
their own certificates.

#### How Many Items Must Hold at Once

This course's measure is the length of the chain. Counting how many separate things must be true
at once for a single leaf item to be accepted in a connection turns a certificate from a file
into a **set of conditions**.

```js
// check.mjs — real signature production, the number of items that must hold, and the path each closes
import { generateKeyPairSync, sign, verify } from "node:crypto";
import { RECORD, ATTEMPT, NOW, write, left } from "./field.mjs";

// A key pair is generated for each issuer; the signature is real, the fields are fictional.
const KEYS = {};
for (const v of new Set(RECORD.map((k) => k.issuer))) KEYS[v] = generateKeyPairSync("ed25519");
const body = (k) => Buffer.from(JSON.stringify([k.subject, k.altName, k.issuer, k.serial,
  k.validFrom, k.validTo, k.usage, k.isAuthority]));
const ITEM = Object.fromEntries(RECORD.map((k) =>
  [k.id, { ...k, signature: sign(null, body(k), KEYS[k.issuer].privateKey) }]));
// c6: an item with c1's field list widened but its signature copied as-is.
ITEM.c6 = { ...ITEM.c1, id: "c6", altName: [...ITEM.c1.altName, "billing-service"] };

const CHECK = [
  ["signature", (k) => verify(null, body(k), KEYS[k.issuer].publicKey, k.signature)],
  ["validity", (k) => NOW >= k.validFrom && NOW <= k.validTo],
  ["name", (k, name) => k.altName.includes(name)],
  ["usage", (k, name, job) => k.usage.includes(job)],
  ["basic constraints", (k, name, job) => k.isAuthority === (job === "certificate-signing")],
];

// The items that must hold at once for item c1 to be usable.
const c1 = ITEM.c1;
const CHAIN = [
  ["issuer public key obtained", () => KEYS["server-intermediate"].publicKey !== undefined],
  ["fields unchanged", () => verify(null, body(c1), KEYS["server-intermediate"].publicKey, c1.signature)],
  ["inside the time window", () => NOW >= c1.validFrom && NOW <= c1.validTo],
  ["presented name is listed", () => c1.altName.includes("measurement-collector")],
  ["usage purpose matches", () => c1.usage.includes("server-identity")],
  ["basic constraints says leaf item", () => c1.isAuthority === false],
  ["issuer carries authority privilege", () => ITEM.c5.isAuthority === true],
  ["private key stayed secret", () => true],
];
console.log(`items that must hold at once for c1 to be usable: ${CHAIN.length}`);
console.log(`result when all hold: ${CHAIN.every(([, f]) => f()) ? "passes" : "fails"}   ` +
  `signature verification ran via node:crypto`);

// Tampering one at a time: which check catches it, and which path opens if not caught.
const TAMPER = [
  ["field altered", "c6", "measurement-collector", "server-identity", "the name list can be written freely"],
  ["outside the time window", "c4", "measurement-collector", "server-identity", "the item stays valid forever"],
  ["presented for another name", "c1", "billing-service", "server-identity", "presented for any name"],
  ["signing item instead of server", "c3", "branch-transfer", "server-identity", "a single key for every job"],
  ["authority item instead of leaf", "c7", "multi-intermediate", "server-identity", "a leaf item signs certificates"],
];
const fmt1 = left([33, 22, 30]);
console.log("\n" + fmt1(["tampered item", "caught by", "path opened if not caught"]));
for (const [name, id, target, job, open] of TAMPER) {
  const caught = CHECK.filter(([, d]) => !d(ITEM[id], target, job)).map(([n]) => n);
  console.log(fmt1([name, caught.join(", ") || "none", open]));
}

// The number of attempts that pass as checks are added one by one.
const passCount = (n, sel = () => true) => ATTEMPT.filter(([id, name, job, tag]) => sel(tag) &&
  CHECK.slice(0, n).every(([, d]) => d(ITEM[id], name, job))).length;
const LEGIT = ATTEMPT.filter(([, , , n]) => n === "legit").length;
const fmt2 = write([31, 10, 10, 16]);
console.log(`\n${ATTEMPT.length} usage attempts, ${LEGIT} of them legitimate`);
console.log(fmt2(["added check", "passing", "closed", "legit passing"]));
let prev = ATTEMPT.length;
console.log(fmt2(["no checks", prev, 0, LEGIT]));
CHECK.forEach(([name], i) => {
  const g = passCount(i + 1);
  console.log(fmt2([`+ ${name}`, g, prev - g, passCount(i + 1, (n) => n === "legit")]));
  prev = g;
});
console.log(`remaining paths ${prev - LEGIT}; legitimate work stopped ${LEGIT - passCount(CHECK.length,
  (n) => n === "legit")}`);
```

```
items that must hold at once for c1 to be usable: 8
result when all hold: passes   signature verification ran via node:crypto

tampered item                    caught by             path opened if not caught
field altered                    signature             the name list can be written freely
outside the time window          validity              the item stays valid forever
presented for another name       name                  presented for any name
signing item instead of server   usage                 a single key for every job
authority item instead of leaf   basic constraints     a leaf item signs certificates

13 usage attempts, 5 of them legitimate
added check                       passing    closed   legit passing
no checks                              13         0               5
+ signature                            12         1               5
+ validity                             10         2               5
+ name                                  9         1               5
+ usage                                 6         3               5
+ basic constraints                     5         1               5
remaining paths 0; legitimate work stopped 0
```

A single leaf item being accepted in a single connection depends on **eight items holding at
once**. Five of these are read from certificate fields — four from the leaf item's own fields,
one from the issuer's item; the remaining three sit entirely outside the certificate. The
verifier already holding the issuer's public key is one item, and this lesson assumes it is
already in place. The clock being correct is a separate item: the shortest path to getting an
item accepted past its window is not touching the certificate, it is turning the verifier's clock
back. The eighth cannot be measured by any check — the subject must have kept its private key
secret, and the verifier has no tool at all to test that.

The tampering table shows that each field closes **a separate path**. Each of the five tampering
cases gets caught by exactly one check; none get caught by two checks at once. The most notable
row is the last one: the `multi-intermediate` item carries server identity in its usage list, its
name is correct, its signature is valid, it has not expired — the only thing that stops it is the
basic constraints field forbidding authority items from being used as leaf items.

#### The Path Basic Constraints Closes

The last table adds checks one at a time and gives the number of attempts that pass at each step.
With no checks at all, all thirteen of the thirteen attempts pass; presenting a certificate is
enough. Once five checks are added, the passing count drops to five, and those five are exactly
the legitimate attempts themselves.

The closed paths are not distributed evenly. The usage-purpose check closes the most paths
(three): the signing item, the device item, and the authority item all stop here when presented
in place of server identity. The validity window closes two, the signature closes one, the name
closes one, basic constraints closes one. The order itself changes the outcome — if the checks
were added in a different order, the number each one closes individually would change, though the
total would not. So the answer to "how valuable is each check" is not independent of order; the
only value that stays fixed is **the number of paths that reopen when that check is removed**.

The validity window's role is broader than what the table shows. The window does not just say how
long the item will be used; it also says **the moment a claim that is wrong and never noticed will
fall on its own**. The fictional `c1` item carries a three-hundred-sixty-five-day window. Even if
its private key leaked today and nobody noticed, that item stops passing at the end of the
window; if the window were infinite, there would be no such ending. Narrowing it shortens that
span and multiplies renewal work in return — the balance between these two numbers is the subject
of the fifth lesson.

The last line records the cost of the defense: legitimate work stopped is zero. None of the five
checks blocks a legitimate use, because all of them test only what is already written in the
item's own fields. This is a property that sets certificate checks apart from the other defenses
measured in this course: the cost is not paid at run time, it is paid at **configuration time**.
Filling in the fields correctly, thinking through usage purposes in advance, and writing names
completely — all of that falls on whoever produces the item. The real size of this cost is
measured together with renewal frequency, and that measurement happens in the fifth lesson of
this topic.

There is one thing none of these checks tests. Across all thirteen attempts, the issuer's public
key was taken as already in the verifier's hands; the `c1` item's signature was verified with the
`server-intermediate` key because that key was already there. This is the very first item, and it
stands as an assumption in this lesson.

#### Summary

- A certificate's nine fields bind nine separate claims; it is not enough for a field to be
  present in the record — it must be **checked**. An unchecked field leaves the claim unproven,
  with no trace of that left in the record.
- The field that determines the presented name is the SAN list, not the subject itself; basic
  constraints, with a single true-false value, states whether an item can sign certificates.
- A single leaf item being accepted in a single connection depends on 8 items holding at once;
  5 of these are read from certificate fields, 3 sit outside the certificate. The clock's
  correctness and the private key staying secret cannot be tested by looking at the certificate.
- Each of the five tampering cases gets caught by exactly one check: each field closes a
  separate path, and none is a backup for another.
- Without checks, 13 of 13 attempts pass; five checks close 8 paths, bring the passing count
  down to 5, and stop 0 legitimate work. The distribution of closed paths depends on order; the
  only order-independent measure is the number of paths that reopen when a check is removed.

#### Next Step

This entire lesson's measurement rested on a single item: the verifier already holding the
issuer's public key. How that key got there, and who stands behind it, is next. A leaf item is
signed by an intermediate authority, the intermediate authority is signed by a root, and the root
at the end of the chain signs itself — meaning nothing verifies it, it is trusted outright. The
next lesson builds this graph and ties a single question to a number: how many intermediate
authorities the fictional measurement network has, how many leaf items stand under each one, how
many items a single root carries at once, and how many items fall — and how many manual touches
renewing them takes — when that root breaks.

### Certificate Authorities and the Chain of Trust

Source: https://academia.sh/en/courses/identity-and-cryptography/certificate-authorities-and-chain-of-trust

The load a single root carries is measured in numbers: 4,397 leaf items and 4,400 trust store copies tied to one root under four intermediate authorities, an intermediate authority breaking drops between 10 and 4,101 items while a root breaking drops 8,802 items and takes 95 workdays of manual touch; a backup root zeroes out the outage but does not change the touch count.

The previous lesson measured the certificate as a set of claims and left one item as an
assumption: the verifying party already held the issuer's public key. That key did not get there
on its own. A leaf item is signed by an intermediate authority, the intermediate authority is
signed by a root; nobody signs the root — the root signs itself, and it is valid **not because
it is verified, but because it is accepted outright**. This is exactly where the trust root sits:
the first link of the chain is placed by decision, not by proof.

This lesson counts the load that decision carries. The fictional regional measurement network's
certificate inventory is built as a graph, and three things are measured: how many items a single
root carries at once, how many items adding an intermediate authority adds to every verification,
and how many items drop — and how many touches renewing them takes — when an authority breaks.

- **PK6.** An authority **breaking** means its private key has gone outside its control, or the
  authority has signed an unauthorized item. Both have the same result: that authority's
  signature no longer carries evidence.
- **PK7.** The trust store is where the root item is written. Each machine holds one copy, and
  the number of copies equals the number of machines.
- **PK8.** A machine class is reached either remotely or by hand. In the remotely-reached class,
  renewal goes through the distribution channel; in the manually-reached class, each machine
  needs one touch, and the touch's duration in minutes is a fictional model value.
- **PK9.** A severed leaf is a leaf item that cannot be verified from the break until renewal
  finishes; the connection that item carries cannot be established during that span.
- **PK10.** A backup root means a second root item has already been written to every store **in
  advance**, and the intermediate authorities have also been signed with this second root. The
  counts, classes, and minutes are fictional.

#### The Items the Root Carries

The graph carries two kinds of node: authorities and machines. An authority is an item that can
sign other certificates; a machine is a node that carries a leaf item and holds a copy of the
root in its trust store.

```js
// chain.mjs — fictional chain-of-trust graph and machine inventory. THIS IS A MODEL.
// The regional measurement network is FICTIONAL: authority names are role names, counts and minutes are made up.
export const AUTHORITY = [
  // id, issuer, type, where the private key sits (fictional)
  ["org-root", null, "root", "offline vault"],
  ["device-intermediate", "org-root", "intermediate", "hardware security module"],
  ["server-intermediate", "org-root", "intermediate", "hardware security module"],
  ["staff-intermediate", "org-root", "intermediate", "directory server disk"],
  ["signing-intermediate", "org-root", "intermediate", "hardware security module"],
].map(([id, issuer, type, location]) => ({ id, issuer, type, location }));

export const MACHINE = [
  // class, count, issuer of the leaf item it carries, has root store?, reached remotely?, manual min
  ["meter endpoint", 4100, "device-intermediate", true, false, 11],
  ["internal service", 26, "server-intermediate", true, true, 2],
  ["staff workstation", 262, "staff-intermediate", true, true, 1],
  ["branch gateway", 9, "signing-intermediate", true, true, 4],
  ["partner verifier", 3, null, true, false, 90],
].map(([cls, count, issuer, store, remote, min]) => ({ class: cls, count, issuer, store, remote, min }));

export const intermediate = () => AUTHORITY.filter((o) => o.type === "intermediate");
export const subLeaves = (id) =>
  MACHINE.filter((m) => m.issuer === id).reduce((t, m) => t + m.count, 0);
export const LEAF = intermediate().reduce((t, o) => t + subLeaves(o.id), 0);
export const STORE = MACHINE.filter((m) => m.store).reduce((t, m) => t + m.count, 0);

// The number of items that must hold true at once in a verification.
// validity per certificate + signature per link + basic constraints per authority
// + name and usage for the leaf item + secrecy per key + store record + clock
export const itemCount = (chain) => chain + (chain - 1) + (chain - 1) + 2 + chain + 1 + 1;

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

```js
// breaking.mjs — the items the graph carries, the items a broken authority drops, and the renewal cost
import { AUTHORITY, MACHINE, intermediate, subLeaves, LEAF, STORE, itemCount, write } from "./chain.mjs";

const fmt1 = write([22, 10, 15, 26, 8]);
console.log(`graph: 1 root, ${intermediate().length} intermediate authorities, ${LEAF} leaf items, ` +
  `${STORE} trust store copies, ${MACHINE.reduce((t, m) => t + m.count, 0)} machines`);
console.log("\n" + fmt1(["authority", "leaf items", "machine class", "private key", "chain"]));
for (const o of AUTHORITY) {
  const sub = o.type === "root" ? LEAF : subLeaves(o.id);
  const classes = MACHINE.filter((m) => m.issuer === o.id).length;
  console.log(fmt1([o.id, sub, o.type === "root" ? MACHINE.length : classes, o.location,
    o.type === "root" ? 1 : 2]));
}
console.log(`\nitems that must hold at once while verifying a leaf item:`);
for (const z of [2, 3]) console.log(`  ${z}-certificate chain (${z === 2 ? "root signs directly"
  : "root -> intermediate -> leaf"}): ${itemCount(z)} items`);
console.log(`  cost of adding an intermediate authority per verification: ` +
  `${itemCount(3) - itemCount(2)} items`);

// Breaking: which authority breaking drops how many items, touches how many machines, takes how many minutes.
const touched = (classes) => classes.reduce((t, m) => t + m.count, 0);
const minutes = (classes) =>
  classes.filter((m) => !m.remote).reduce((t, m) => t + m.count * m.min, 0);
const ROOT_STORE = MACHINE.filter((m) => m.store);

const SCENARIO = [];
for (const o of intermediate()) {
  const s = MACHINE.filter((m) => m.issuer === o.id);
  SCENARIO.push([`${o.id} breaks`, 1 + subLeaves(o.id), subLeaves(o.id), s, subLeaves(o.id)]);
}
SCENARIO.push(["org-root breaks", 1 + intermediate().length + LEAF + STORE, LEAF, ROOT_STORE, LEAF]);
SCENARIO.push(["org-root breaks, backup root exists", 1 + STORE, 0, ROOT_STORE, 0]);

const fmt2 = write([34, 10, 11, 11, 12, 14]);
console.log("\n" + fmt2(["scenario", "dropped", "renewed", "touched", "manual min", "severed leaf"]));
for (const [name, dropped, renewed, classes, severed] of SCENARIO)
  console.log(fmt2([name, dropped, renewed, touched(classes), minutes(classes), severed]));

// Manually-reached and remotely-reached store copies.
const manual = ROOT_STORE.filter((m) => !m.remote);
const remote = ROOT_STORE.filter((m) => m.remote);
console.log(`\nroot store ${STORE} copies: remote ${touched(remote)}, manual ${touched(manual)}`);
for (const m of manual)
  console.log(`  ${m.class.padEnd(24)}${String(m.count).padStart(6)} copies, ` +
    `${String(m.count * m.min).padStart(6)} min`);
console.log(`manual touch total ${minutes(ROOT_STORE)} min = ` +
  `${Math.round(minutes(ROOT_STORE) / 60)} hours = ${Math.round(minutes(ROOT_STORE) / 480)} workdays`);
console.log(`machines that keep accepting the broken root until it is removed: ` +
  `${touched(manual)}`);
```

```
graph: 1 root, 4 intermediate authorities, 4397 leaf items, 4400 trust store copies, 4400 machines

authority             leaf items  machine class               private key   chain
org-root                    4397              5             offline vault       1
device-intermediate         4100              1  hardware security module       2
server-intermediate           26              1  hardware security module       2
staff-intermediate           262              1     directory server disk       2
signing-intermediate           9              1  hardware security module       2

items that must hold at once while verifying a leaf item:
  2-certificate chain (root signs directly): 10 items
  3-certificate chain (root -> intermediate -> leaf): 14 items
  cost of adding an intermediate authority per verification: 4 items

scenario                             dropped    renewed    touched  manual min  severed leaf
device-intermediate breaks              4101       4100       4100       45100          4100
server-intermediate breaks                27         26         26           0            26
staff-intermediate breaks                263        262        262           0           262
signing-intermediate breaks               10          9          9           0             9
org-root breaks                         8802       4397       4400       45370          4397
org-root breaks, backup root exists      4401          0       4400       45370             0

root store 4400 copies: remote 297, manual 4103
  meter endpoint            4100 copies,  45100 min
  partner verifier             3 copies,    270 min
manual touch total 45370 min = 756 hours = 95 workdays
machines that keep accepting the broken root until it is removed: 4103
```

The first table gives the root's load in a single row: 4,397 leaf items stand under the four
intermediate authorities, and all of them trace back to a single item's signature. The root
itself does not sign a single leaf item directly; even so, it sits above all four thousand three
hundred ninety-seven of them, because it is at the end of the chain.

More than half the load is not even a certificate. The **4,400 trust store copies** are copies of
the same root item written to machines, and in the graph they are tied in by **distribution**, not
signature. Each of these copies is an independent item: if one is missing, that machine cannot
verify any chain; if one is extra, that machine accepts an extra root. The partner verifiers show
this plainly — three machines carry no leaf item at all, only a root store, meaning their role in
the network is purely to verify.

The column showing where the private key sits says something the shape of the graph cannot. The
root key sits in an offline vault and is only brought out to sign an intermediate authority; a
key used a few times a year has no reason to sit online. Three intermediate authorities' keys sit
in a hardware security module — the key never leaves the module, the signing request is sent in.
The fourth, `staff-intermediate`, keeps its key on a directory server's disk. In the graph, these
four nodes are equals; their odds of breaking are not equal, and this is the only column that
carries that difference. 262 leaf items stand under the key on disk.

The chain-length measure builds the second table. If the root signed leaf items directly, the
number of items that must hold at once in a verification would be ten. Once an authority is
inserted in between, that number climbs to fourteen: one more certificate's validity window, one
more signature link, one more basic constraints check, and one more private key staying secret.
**An intermediate authority is not free**; it adds four items to every verification, and that cost
is paid again on every verification of each of the four thousand three hundred ninety-seven items.

#### Breaking: Which Root, How Many Items

The payoff shows up in the second table. The same graph produces completely different numbers
depending on which authority breaks. When `signing-intermediate` breaks, the number of items
dropped is **ten**; when `device-intermediate` breaks, it is **4,101**. The gap between them is
more than four-hundred-fold, and it has nothing to do with the graph's shape — it is about the
size of the subtrees. This is the real job of splitting into intermediate authorities: it
confines the break to a single subtree.

The manual-touch column does not tell the same story. The manual-touch cost of `server-intermediate`,
`staff-intermediate`, and `signing-intermediate` breaking is zero, because these three classes'
machines are reached remotely and renewal goes through the distribution channel. A
`device-intermediate` break, on the other hand, means 4,100 site visits: 45,100 minutes. There is
no fixed ratio between the number of items dropped and the renewal cost; what decides it is not
the item count, it is **how the machines those items sit on are reached**.

On the device side, there is one more reason for this. For a meter endpoint to receive its new
item, it has to establish a connection with the collector; and the item it presents to establish
that connection is exactly the item that needs renewing. If the renewal channel depends on the
very item being renewed, remote renewal cannot happen. This is a limit in the measurement network
that is independent of item count, and it takes the site visit out of being a choice.

There is one more number that shows up in none of the table's columns: **when the break is
noticed**. The model treats the moment of breaking and the start of renewal as the same instant.
In reality there is a span between the two, and throughout that span every item the broken
authority signs passes all fourteen of the verification's fourteen items — because breaking
leaves no trace in any of the fields being checked. This lesson's numbers give the cost of
renewal, not the length of time to notice; the latter is not measured in this course.

The root row changes the scale entirely. When the root breaks, the number of items dropped is
**8,802**: one root, four intermediate authorities, 4,397 leaf items, and 4,400 trust store
records. 4,400 of that is not certificates, it is records; meaning the work of renewing the root
is not just re-signing, it is changing the writing on four thousand four hundred machines. The
manual-touch total is 45,370 minutes, or **95 workdays**. What this number means is not a cost,
it is a **duration**: when the root breaks, closing the incident takes longer than three months.

#### What a Backup Root Saves

The last row on its own is the most instructive part of the lesson. If a second root has already
been written to every store, and the intermediate authorities have also been signed with that
second root, then when the root breaks, the items dropped fall from 8,802 to 4,401, and **the
severed-leaf count falls from 4,397 to zero**. No connection breaks; the measurement network
keeps running, because every leaf item has a second path to the root.

The two columns that do not change are the ones that actually decide the outcome. The number of
machines touched is still 4,400, the manual touch is still 45,370 minutes. A backup root does not
eliminate the renewal work, because the broken root still has to be **removed** from the stores,
and that means touching every machine individually. What the backup root saves is not the work,
it is the **outage**: the same 95 workdays pass with service still up.

What decides the duration is not the number of store copies, it is their distribution. Of the four
thousand four hundred copies, 297 are reached remotely, and their touch cost does not enter the
total minutes at all; the remaining 4,103 produce the entire duration. Reducing the copy count
does not change this table, because the copies that would be reduced sit on the remotely-reached
side. The only thing that changes it is opening a distribution channel to the manually-reached
classes — and that channel itself needs a trust root too.

This is why the last row also shows where the root falls short. The 4,103 manually-reached
machines keep accepting the broken root, and that lasts until the very last machine, whatever the
touch order happens to be. A broken root is not considered revoked; **it stays valid on every
machine it has not been removed from.** If an average meter endpoint's visit falls somewhere in
the middle of that order, the window for that endpoint lasts roughly forty-seven workdays. During
that span, a new item signed with the broken root can be produced and it will be accepted on that
machine; nothing in the graph stops that.

This gap does not close with renewal. The only thing that can close it is the verifier being able
to ask "is this item still valid" **somewhere other than** the item's own fields. This is
information that is not written in any certificate field, and it stands outside the graph.

#### Summary

- The fictional graph has 1 root, 4 intermediate authorities, and 4,397 leaf items; the root sits
  above all of them even though it signs none of them directly. 4,400 of the load's items are not
  certificates but trust store copies, tied into the graph by distribution rather than signature.
- If the root signed leaf items directly, a verification would need 10 items to hold; an
  intermediate authority raises that to 14. An intermediate authority confines a break to a
  single subtree, but it adds 4 items to every verification.
- Depending on which authority breaks, the items dropped range from 10 to 4,101; manual touch
  ranges from 0 to 45,100 minutes. What decides it is not the item count, it is how the machines
  are reached.
- When the root breaks, 8,802 items drop, 4,400 machines get touched, and manual touch takes 95
  workdays. Because the renewal channel depends on the item being renewed, remote renewal cannot
  happen on 4,100 meter endpoints.
- A backup root brings the items dropped down to 4,401 and severed leaves down to 0, but it
  does not change the machines touched or the duration at all: what it saves is the outage, not
  the work. A broken root keeps staying valid on the 4,103 machines it has not been removed from.

#### Next Step

The gap that opens when the root breaks is not about the speed of renewal. When an item loses its
validity, the verifying party has to **learn** that, and there is no field inside the certificate
that says so; as long as its fields have not changed, an item keeps presenting itself as valid.
There are two ways to learn it: distribute a list of revoked items, or ask the authority
individually on every use. The next lesson compares the two on the same footing — how many hours
pass between the revocation decision and the client learning it, how many connections get
accepted in that window, and how many paths the decision opens when a query gets no answer.

### Certificate Revocation

Source: https://academia.sh/en/courses/identity-and-cryptography/certificate-revocation

The window between the revocation decision and the verifier learning it is measured: 29.5 hours on average and 271,877 accepted connections with daily list publication, 1.5 hours and 13,925 connections with the Online Certificate Status Protocol (OCSP), 0.5 hours and 4,621 connections with server-carried response; narrowing the list's window to two hours raises the daily download from 152 MB to 1,828 MB, and when the query goes unanswered, soft-fail opens 2 paths while hard-fail drops 6,161 legitimate connections.

The previous lesson showed that a broken root keeps staying valid on every machine it has not been
removed from. The same problem holds for a single leaf item too: when a server's private key goes
outside its control, or a meter endpoint is pulled from the field, that item has to be considered
invalid — but the item's fields do not change. The validity window is still open, the signature
still verifies, the name still matches. The previous lesson's fourteen items hold in full for a
revoked item too.

Revocation therefore stands outside the certificate: it is information the verifier has to learn
**somewhere else**. There are two basic ways to learn it — distribute a list of revoked items, or
ask the authority individually on every use. This lesson compares the two on the same footing:
how many hours pass between the decision and learning it, how many connections get accepted in
that window, and how many paths the decision made opens when the learning channel is not working.

- **PK11.** Revocation is the decision to consider an item invalid before its validity window
  expires. The decision is made at the authority and does not touch the item's fields.
- **PK12.** The learning window is the span between the revocation decision and that decision
  reaching a given verifier. Clients' refresh moments are evenly distributed; this is a modeling
  choice.
- **PK13.** The connections accepted in the window are the product of the window's duration and
  the client's connection rate. The counts, rates, list size, and outage duration are fictional.
- **PK14.** Soft-fail is accepting the connection when status cannot be learned; hard-fail is
  rejecting it. There is no third option.
- **PK15.** Server-carried response is the server refreshing the status response itself and
  handing it to the client during the connection; the client does not need to query an external
  endpoint.

#### The Window of Learning

The difference between the three methods is not a difference in mechanism, it is a difference in
**latency**. In the certificate revocation list (CRL), the decision first enters a publication,
then the client downloads that publication on its own schedule; the two delays stack. In the
online status query (OCSP) there is no publication delay, only the client's cache.

```js
// revocation.mjs — model of the window between the revocation decision and learning it. THIS IS A MODEL.
// Fictional regional measurement network: counts, rates, and hours are made up.
export const CLASS = [
  // name, client count, connections per hour per client
  ["meter endpoint", 4100, 2],
  ["internal service", 26, 30],
  ["staff workstation", 262, 1],
].map(([name, count, rate]) => ({ name, count, rate }));

export const REVOKE = 30;             // fictional revocation-decision moment, hour
export const OUTAGE = 40;             // fictional query-service outage, minutes
export const RECORD_BYTES = 28;       // bytes per item in the revocation list (fictional)
export const REVOKED = 1240;          // number of revoked items sitting in the list (fictional)

export const METHOD = [
  // name, publication interval (0 = no delay), client's refresh interval, does the client query externally
  ["revocation list", 24, 24, false],
  ["online status query", 0, 4, true],
  ["server-carried response", 0, 2, false],
].map(([name, publish, refresh, external]) => ({ name, publish, refresh, external }));

// A client's moment of learning: the decision enters publication first, then the client refreshes on its own schedule.
export const window = (y, offset) => {
  const publishMoment = y.publish ? Math.ceil(REVOKE / y.publish) * y.publish : REVOKE;
  const learn = publishMoment + (((offset - publishMoment) % y.refresh) + y.refresh) % y.refresh;
  return learn - REVOKE;
};

export const CLIENT = CLASS.flatMap((s) =>
  Array.from({ length: s.count }, (_, i) => ({ class: s.name, rate: s.rate, offset: i })));

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

```js
// window.mjs — the learning window, the connections within it, and the paths the failure decision opens
import { CLASS, METHOD, CLIENT, REVOKE, OUTAGE, RECORD_BYTES, REVOKED, window, write }
  from "./revocation.mjs";

const TOTAL = CLIENT.length;
const HOURLY = CLIENT.reduce((t, i) => t + i.rate, 0);
console.log(`${TOTAL} clients, ${HOURLY} connections per hour, revocation decision at hour ${REVOKE}`);
console.log(`list size: ${REVOKED} revoked items x ${RECORD_BYTES} bytes = ` +
  `${REVOKED * RECORD_BYTES} bytes`);

const fmt1 = write([26, 10, 10, 10, 14, 12]);
console.log("\n" + fmt1(["method", "shortest", "longest", "average", "accepted", "daily ext"]));
for (const y of METHOD) {
  const p = CLIENT.map((i) => window(y, i.offset));
  const accepted = CLIENT.reduce((t, i, k) => t + p[k] * i.rate, 0);
  const avg = p.reduce((a, b) => a + b, 0) / TOTAL;
  const extQuery = y.external ? TOTAL * (24 / y.refresh)
    : y.publish ? TOTAL * (24 / y.refresh) : 24 / y.refresh;
  console.log(fmt1([y.name, `${Math.min(...p)} h`, `${Math.max(...p)} h`, `${avg.toFixed(1)} h`,
    accepted, extQuery]));
}

// The effect of narrowing the list's publication interval on the window and download volume.
const fmt4 = write([16, 12, 12, 14, 16]);
console.log("\n" + fmt4(["list interval", "longest", "average", "accepted", "daily MB"]));
for (const d of [24, 12, 6, 2]) {
  const y = { publish: d, refresh: d };
  const p = CLIENT.map((i) => window(y, i.offset));
  const mb = (TOTAL * (24 / d) * REVOKED * RECORD_BYTES) / 1e6;
  console.log(fmt4([`${d} h`, `${Math.max(...p)} h`,
    `${(p.reduce((a, b) => a + b, 0) / TOTAL).toFixed(1)} h`,
    CLIENT.reduce((t, i, k) => t + p[k] * i.rate, 0), mb.toFixed(0)]));
}

// The same window's distribution per class (revocation list versus online query).
const fmt2 = write([24, 10, 16, 16, 12]);
console.log("\n" + fmt2(["class", "clients", "list accepted", "query accepted", "ratio"]));
for (const s of CLASS) {
  const group = CLIENT.filter((i) => i.class === s.name);
  const [list, query] = [METHOD[0], METHOD[1]].map((y) =>
    group.reduce((t, i) => t + window(y, i.offset) * i.rate, 0));
  console.log(fmt2([s.name, group.length, list, query, `${(list / query).toFixed(1)}x`]));
}

// The decision made when the query service is unreachable: soft and hard failure.
const outageConnections = Math.round(HOURLY * (OUTAGE / 60));
const DECISION = [
  // name, does the client query externally, accepted?, path opened, legitimate work stopped
  ["revocation list, cache valid", false, true, 0, 0],
  ["online query, soft failure", true, true, 2, 0],
  ["online query, hard failure", true, false, 0, outageConnections],
  ["server-carried response, hard", false, true, 0, 0],
];
const fmt3 = write([40, 14, 10, 13, 21]);
console.log(`\nquery service outage ${OUTAGE} min -> ${outageConnections} connections affected`);
console.log(fmt3(["decision", "external dep", "accepted", "path opened", "legit work stopped"]));
for (const [name, ext, accepted, path, stopped] of DECISION)
  console.log(fmt3([name, ext ? "yes" : "no", accepted ? "yes" : "no", path, stopped]));

const PATH = ["a revoked item is accepted",
  "the party that cuts access to the query service makes the revocation invisible"];
console.log("\npaths soft-fail opens:");
PATH.forEach((y, i) => console.log(`  ${i + 1}. ${y}`));
console.log(`cost of hard-fail: ${outageConnections} legitimate connections, ` +
  `over ${OUTAGE} min`);
```

```
4388 clients, 9242 connections per hour, revocation decision at hour 30
list size: 1240 revoked items x 28 bytes = 34720 bytes

method                      shortest   longest   average      accepted   daily ext
revocation list                 18 h      41 h    29.5 h        271877        4388
online status query              0 h       3 h     1.5 h         13925       26328
server-carried response          0 h       1 h     0.5 h          4621          12

list interval        longest     average      accepted        daily MB
24 h                    41 h      29.5 h        271877             152
12 h                    17 h      11.5 h        105941             305
6 h                      5 h       2.5 h         22973             609
2 h                      1 h       0.5 h          4621            1828

class                      clients   list accepted  query accepted       ratio
meter endpoint                4100          241820           12300       19.7x
internal service                26           22350            1230       18.2x
staff workstation              262            7707             395       19.5x

query service outage 40 min -> 6161 connections affected
decision                                  external dep  accepted  path opened   legit work stopped
revocation list, cache valid                        no       yes            0                    0
online query, soft failure                         yes       yes            2                    0
online query, hard failure                         yes        no            0                 6161
server-carried response, hard                       no       yes            0                    0

paths soft-fail opens:
  1. a revoked item is accepted
  2. the party that cuts access to the query service makes the revocation invisible
cost of hard-fail: 6161 legitimate connections, over 40 min
```

The first table gives the window's shape. With a daily-published list, the revocation decision is
learned best-case eighteen hours later, worst-case **forty-one hours** later; twenty-nine and a
half hours on average. That the number is greater than twenty-four is surprising at first glance,
and it comes from two delays stacking: because the decision is made at hour thirty, it misses that
day's publication, enters the publication at hour forty-eight, and only then does each client
download it on its own refresh schedule.

With OCSP the publication delay disappears, and the window drops to at most three
hours. When the server carries the response itself, at most one hour. The number of connections
within the window makes these three methods comparable: **271,877**, **13,925**, **4,621**.
There is a gap of nearly twentyfold between the first two methods, and the whole of that gap is
temporal — the class table confirms this, the ratio falls between eighteen and twenty across all
three client classes. The window depends not on what the client does, but on when it asks.

The average column can be misleading. In a situation where the party presenting a revoked item
can choose which client it targets, the number that matters is not the average, it is the
**longest** window: forty-one hours. The average tells you how many connections are affected; the
longest window tells you how long a single connection stays possible. The two numbers look at two
separate questions, and both get written down when choosing a method.

#### The Cost of Narrowing the Window

The second table shows that the list method's window is not fate. When the publication interval is
narrowed from twenty-four hours to two hours, the average window drops from 29.5 hours to 0.5
hours, and accepted connections drop from 271,877 to 4,621 — the **exact same** value as the
server-carried response. The list method is not structurally worse than OCSP; it is only
worse with its default interval.

The cost is in the last column. The two-hour interval raises the daily download from 152 MB to
**1,828 MB**, twelvefold. The reason is that the list's **entirety** gets re-downloaded on every
refresh: each of the four thousand three hundred eighty-eight clients pulls the whole 1,240-item
list twelve times over — even though only one item was revoked. The CRL carries the
entire ledger every time just to find the one record it is looking for.

The list's size is not fixed, either. The twelve hundred forty items come from every item ever
revoked staying in the list until its validity window expires; as the revocation count grows, the
list grows, and the bytes carried on every refresh grow with it. The cost of increasing the
frequency therefore rises on its own over time — the same interval is more expensive a year later.

The external-query column draws a table pointing the other way. With the daily list there is one
download per client, 4,388 total. With OCSP there are six queries per client,
**26,328** total; each one goes to a separate endpoint and each one requires that endpoint to be
up. With the server-carried response, the daily external query count is **twelve**, because the
party asking is not the client but the server, and it asks once and distributes the answer to
every client. The method that gives the narrowest window is the one that produces the fewest
external queries; the two measures look the same direction here.

#### When the Query Goes Unanswered

The last table sets up the lesson's real distinction. What OCSP adds is not just
speed, it is a **dependency on the outside**; and when that dependency goes down, the verifier is
left with two options.

Soft-fail accepts the connection. During the forty-minute outage, six thousand one hundred
sixty-one connections get established without status being known, and this opens two paths: a
revoked item is accepted, and the party that can cut access to the query service makes the
revocation invisible. The second contains the first — if the only channel of learning can be
knocked down, the revocation decision itself can effectively be knocked down.

Hard-fail opens no path and drops six thousand one hundred sixty-one **legitimate**
connections. For forty minutes, the measurement network's data intake stops. This is a measurable
cost, and it is the counterpart to the two paths soft-fail opens; there is no correct choice
between the two, only a written one.

The first and fourth rows show how this dilemma does not get set up. In the list method, when the
client holds a valid cache, there is no external dependency; whether the query service is up or
down does not affect the connection. There is none in the server-carried response either, because
the party asking is not the client. The dilemma does not disappear — when the server cannot refresh
the response, the same two options get set up again on the server's side — but **it moves from
four thousand three hundred eighty-eight clients to one server**. Making the soft-fail / hard-fail
decision once instead of four thousand times does not change the decision itself; it changes where
the decision is made, and that place is the only place it can be tracked.

One type of item, though, cannot be revoked by any of these three methods. Whoever signs the
CRL is the authority that issued the item being revoked; for the root, that means the
root would have to announce its own revocation with its own signature. Since a broken root's
signature carries no evidence, that announcement carries no evidence either. The previous lesson's
count of 4,400 machine touches is paid for exactly this reason: because the root cannot be revoked,
it has to be **removed**, and removal is not done by publication, it is done machine by machine.

Underneath all of this measurement sits a limit that does not change. Revocation is the one
decision that leaves no trace in the fields being verified: a revoked item passes all fourteen of
the fourteen items until learning happens. The window can be narrowed; it cannot be zeroed out. The
only way to zero it out is to keep the item's lifetime shorter than the learning window — then the
item drops on its own before the revocation announcement even finishes. That turns revocation from
an announcement problem into a renewal problem; that measurement is made in this topic's fifth
lesson.

#### Summary

- The revocation decision does not touch the certificate's fields; a revoked item passes every
  check in verification until learning happens. What is measured is not the mechanism, it is the
  **learning delay**.
- With a daily-published list, the window is 18–41 hours (29.5 average) and connections within it
  are 271,877; with OCSP, 0–3 hours and 13,925; with a server-carried response, 0–1
  hours and 4,621. The ratio falls between 18 and 20 times across all three client classes,
  because the gap is entirely temporal.
- Narrowing the publication interval to 2 hours pulls the list below even OCSP, to
  4,621 connections; its cost is the daily download climbing from 152 MB to 1,828 MB, because the
  entire 1,240-item list is re-downloaded on every refresh.
- The external-query count ranks in reverse: list 4,388, OCSP 26,328, server-carried
  response 12. The method that gives the narrowest window is the one that produces the least
  external dependency.
- During the query service's 40-minute outage, soft-fail opens 2 paths and stops 0 legitimate
  work, hard-fail opens 0 paths and drops 6,161 legitimate connections. The server carrying the
  response does not eliminate this decision, it moves it from 4,388 clients to 1 server.

#### Next Step

Everything these three lessons have measured — field checks, the chain's items, revocation status
— comes together in one place, in the first seconds a connection is established. That exchange is
the handshake, and it holds a separate claim that each step proves: that the item the other party
presents is valid, that it truly holds that item's private key, that the name being discussed is
written on the item. The next lesson takes the handshake apart step by step and asks a single
question: which step proves which claim, which claim stays unproven when a step is skipped, and
how many items the client also presenting an item adds.

### TLS Handshake

Source: https://academia.sh/en/courses/identity-and-cryptography/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.

### Certificate Lifecycle Management

Source: https://academia.sh/en/courses/identity-and-cryptography/certificate-lifecycle-management

Measured in terms of the renewal window: manually renewing 4,397 items takes 99 workdays a year, 111 items go unnoticed, and 1,458 connections drop; automation zeroes out all three numbers while adding 5 system items and turning 111 scattered breakage points into a single point that carries 4,397 items at once; shortening lifetime to one day makes the revocation notice unnecessary but raises yearly transactions from 4,397 to 1,604,905 and zeroes out the spare-attempt margin.

The previous lesson showed that every claim measured in the handshake rests on work done long
before the connection. The most fragile part of that work is time: every item has a validity
window, and when the window closes, the item stops passing even if its fields have not changed, its
signature has not broken, and its authority is still standing. This is the most common cause of
outage a certificate infrastructure runs into, and the cause is not a break — it is a **calendar**.

This lesson measures the lifecycle in terms of the renewal window: how many workdays manual
renewal takes and how many items go unnoticed, how many connections an expired item drops, which
items automation closes and which it opens, and how many items' worth of maintenance shortening
lifetime adds. The key storage layer and the secret rotation window were measured in the Cloud
Governance course; what is measured here is the item's own calendar.

- **PK21.** A renewal transaction is replacing an item with a new one before it expires. The
  yearly transaction count is the number of items times the number of renewals within the year.
- **PK22.** Manual renewal has a touch-time in minutes per item, and a portion of renewals go
  unnoticed. The miss rate, touch minutes, and detection time are fictional model values.
- **PK23.** An expired item drops the connections it carries until it is noticed and fixed; dropped
  connections are the item count times the connection rate times the detection time.
- **PK24.** The renewal window is a third of the lifetime, and one attempt is made per day within
  the window; the attempt count gives the number of spare opportunities left if the first attempt
  fails.
- **PK25.** Automation's system items are shared: they do not multiply with item count, but when
  one breaks it affects the whole inventory.

#### The Ledger of Manual Renewal

Manual renewal's cost is three separate numbers, and none substitutes for another: time spent,
items that go unnoticed, and connections the missed item drops.

```js
// lifetime.mjs — fictional item inventory and renewal model. IS A MODEL.
// The regional measurement network is FICTIONAL: counts, rates, minutes, and ratios are made up.
export const CLASS = [
  // name, count, connections per hour, manual renewal minutes
  ["meter endpoint", 4100, 2, 11],
  ["internal service", 26, 30, 18],
  ["staff identity", 262, 1, 6],
  ["branch transfer", 9, 4, 25],
].map(([name, count, rate, min]) => ({ name, count, rate, min }));

export const ITEM = CLASS.reduce((t, s) => t + s.count, 0);
export const MISS_RATE = 0.025;    // rate missed in manual renewal (fictional)
export const DETECT_TIME = 6;      // time to notice and fix an expired item, hours
export const LEARNING_WINDOW = 29.5;     // from the third lesson: revocation learning window, hours
export const AUTOMATION = ["renewal service", "the service's own identity",
  "reload after renewal", "monitoring and alerting", "spare renewal path"];

export const yearly = (lifetime) => Math.ceil(365 / lifetime);
// The renewal window is a third of the lifetime; one attempt is made per day.
export const attempts = (lifetime) => Math.floor(lifetime / 3);

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

```js
// renewal.mjs — the cost of manual and automatic renewal, missed items, and the lifetime filter
import { CLASS, ITEM, MISS_RATE, DETECT_TIME, LEARNING_WINDOW, AUTOMATION, yearly, attempts, write }
  from "./lifetime.mjs";

const compute = (lifetime, auto) => {
  let [txn, minutes, missed, dropped] = [0, 0, 0, 0];
  for (const s of CLASS) {
    const n = yearly(lifetime) * s.count;
    txn += n;
    if (auto) continue;
    minutes += n * s.min;
    const k = Math.round(n * MISS_RATE);
    missed += k;
    dropped += k * s.rate * DETECT_TIME;
  }
  return { txn, minutes, missed, dropped };
};

console.log(`inventory ${ITEM} items, ${CLASS.length} classes; ` +
  `rate missed in manual renewal ${MISS_RATE * 100}% (fictional)`);
console.log(`an expired item is noticed and fixed in ${DETECT_TIME} hours (fictional)\n`);

const fmt1 = write([28, 12, 12, 11, 14, 16]);
console.log(fmt1(["path (365-day lifetime)", "renewals", "manual min", "workdays", "missed items",
  "dropped conn."]));
for (const [name, auto] of [["manual renewal", false], ["automatic renewal", true]]) {
  const h = compute(365, auto);
  console.log(fmt1([name, h.txn, h.minutes, Math.round(h.minutes / 480), h.missed, h.dropped]));
}
const manual365 = compute(365, false);
console.log(`items automation closes: ${manual365.missed} missed renewals, ` +
  `${manual365.minutes} min of manual work`);
console.log(`system items automation adds: ${AUTOMATION.length}`);
for (const k of AUTOMATION) console.log(`  ${k}`);
console.log(`items affected if one of these (the service's own identity) breaks: ${ITEM}`);

// Cost of manual renewal per class (at a 365-day lifetime).
const fmt2 = write([20, 10, 12, 10, 14, 16]);
console.log("\n" + fmt2(["class", "items", "manual min", "missed", "conn./hr", "dropped conn."]));
for (const s of CLASS) {
  const n = yearly(365) * s.count, k = Math.round(n * MISS_RATE);
  console.log(fmt2([s.name, s.count, n * s.min, k, s.count * s.rate, k * s.rate * DETECT_TIME]));
}

// Lifetime filter: the effect of shortening on renewal volume, attempt margin, and revocation need.
const fmt3 = write([12, 14, 14, 12, 18, 20]);
console.log("\n" + fmt3(["lifetime", "yearly txns", "daily txns", "attempts", "manual workdays",
  "revocation needed"]));
for (const lifetime of [365, 90, 30, 7, 1]) {
  const auto = compute(lifetime, true), man = compute(lifetime, false);
  console.log(fmt3([`${lifetime} days`, auto.txn, Math.round(auto.txn / 365), attempts(lifetime),
    Math.round(man.minutes / 480), lifetime * 24 > LEARNING_WINDOW ? "yes" : "no"]));
}
console.log(`revocation learning window ${LEARNING_WINDOW} hours (third lesson); ` +
  `an item with a shorter lifetime than this drops on its own before the notice`);
```

```
inventory 4397 items, 4 classes; rate missed in manual renewal 2.5% (fictional)
an expired item is noticed and fixed in 6 hours (fictional)

path (365-day lifetime)         renewals  manual min   workdays  missed items   dropped conn.
manual renewal                      4397       47365         99           111            1458
automatic renewal                   4397           0          0             0               0
items automation closes: 111 missed renewals, 47365 min of manual work
system items automation adds: 5
  renewal service
  the service's own identity
  reload after renewal
  monitoring and alerting
  spare renewal path
items affected if one of these (the service's own identity) breaks: 4397

class                    items  manual min    missed      conn./hr   dropped conn.
meter endpoint            4100       45100       103          8200            1236
internal service            26         468         1           780             180
staff identity             262        1572         7           262              42
branch transfer              9         225         0            36               0

lifetime       yearly txns    daily txns    attempts   manual workdays   revocation needed
365 days              4397            12         121                99                 yes
90 days              21985            60          30               493                 yes
30 days              57161           157          10              1283                 yes
7 days              233041           638           2              5230                 yes
1 days             1604905          4397           0             36017                  no
revocation learning window 29.5 hours (third lesson); an item with a shorter lifetime than this drops on its own before the notice
```

A one-year lifetime means manually renewing four thousand three hundred ninety-seven items takes
47,365 minutes, meaning **99 workdays**. That is one person doing no other work for ninety-nine
days, and the number comes not from the inventory itself but from the touch minutes.

The class table shows where this load comes from: 45,100 minutes, ninety-five percent of the
total, from a single class — meter endpoints. Four thousand one hundred items and eleven minutes
per touch. The remaining three classes together take 2,265 minutes. Ninety-three percent of the
inventory produces ninety-five percent of the manual work; the lifecycle decision is therefore not
an inventory decision, it is a **class decision**.

The missed-items column writes the second cost. The two-and-a-half-percent rate corresponds to
111 items, and these 111 items drop entirely on their own, without an attack, because they have
expired. The connections they drop number 1,458. The distribution reverses here: internal services
miss only one item, but because they carry 780 connections per hour the dropped connections come
to 180 — while staff identities miss seven items but drop only 42. **The number of missed items
does not give the risk; the traffic the missed item carries does.**

The branch-transfer row shows zero, and this is not a guarantee — it is a rounding result: two and a
half percent of nine items is under one item. In small classes rate-based measurement does not
produce meaning; there, counting one by one is required.

The six-hour detection time is also an assumption, and what it depends on has to be stated. An
expired item produces no warning at all; the only thing it produces is a verification failure on
the other side. Who that failure reaches varies by class — in an internal service a call chain
breaks immediately and gets noticed, while at a meter endpoint only that endpoint's data stops
arriving. Detection time is therefore the result of a monitoring decision: without an audit that
scans the validity window in advance, the time depends on how noisily the item falls.

This table's silent assumption is that the inventory is complete. Renewal only happens to an item
sitting on the list; an item not on the list does not get renewed, and when it expires, no one is
expecting it. A certificate infrastructure always has items like this — issued for a test and left
in place, unclaimed by anyone during a handover, embedded inside a device image and forgotten. An
item invisible in the inventory has no measure either: the 99 workdays and 111 missed items above
are only as accurate as the inventory is correct.

Time distribution is also an assumption. The calculation above counts 4,397 renewals as spread
evenly across the year; in practice, items are mostly issued in batches — hundreds get produced on
the same day during a rollout, a migration, or a renewal campaign. Items produced on the same day
drop on the same day. In that case the 99 workdays of load is not spread across the year but
crammed into a few days, and the miss rate rises on those days too. The second number the
lifecycle needs to measure is therefore not the total, but the **renewal count on the busiest
day**.

#### What Automation Closes and Opens

Automatic renewal zeroes out all three columns at once: 47,365 minutes, 111 missed items, and
1,458 dropped connections. This is the cleanest result among the defenses measured in this course;
in exchange for the items it closes it does not stop any legitimate work. Its cost is paid not at
runtime but at setup time, and setup happens once.

In exchange it adds five system items, and these items are not like the others. They do not multiply
with the inventory — there are five of them whether it is four thousand three hundred ninety-seven
items or not. In exchange, because they are shared, **when one breaks it affects the whole
inventory**. The clearest example of this on the list sits second: the renewal service's own
identity. That identity is one that can request items on the authority's behalf, and when it
breaks, the number of items affected is 4,397.

The third item on the list says why renewal is not just a signing job. When a new item is produced
and written to disk, the work is not done; the process using the item has to reread it. A service
that keeps holding the old item in memory keeps presenting the expired item while a brand-new item
sits on disk, and no external check can tell the two apart — the configuration is correct, the
file is current, the item presented is stale. The last link in the renewal chain is not the
signature — it is the reload.

The trade-off here is the lesson's central sentence. In manual renewal there are 111 scattered
breakage points, and each one drops exactly one item; none affects another, all are visible, and
all are fixed separately. Automation zeroes out these 111 points and replaces them with one — but
that single point carries 4,397 items at once. Total breakage count falls, and the scope of a
single breakage grows. This has the same shape as the second lesson's root table: consolidation
shrinks the count while growing the radius.

#### The Bill for Shortening Lifetime

The last table closes the question the third lesson left open. The revocation notice's learning
window averaged 29.5 hours and could be narrowed but not zeroed out. Shortening lifetime to one
day makes that window unnecessary: a twenty-four-hour item **drops on its own** before the
revocation notice reaches clients. The revocation problem is not solved — it disappears.

The bill is in the same row. Yearly transactions rise from 4,397 to **1,604,905**, daily
transactions from twelve to four thousand three hundred ninety-seven. This volume cannot be managed
manually — the manual workdays column shows 36,017, meaning a hundred-person team's year and a
half. Short-lived items turn automation from a choice into a requirement.

The third column writes a less visible cost. Because the renewal window is a third of the
lifetime, the **spare-attempt margin** shrinks along with the shrinking lifetime: at a one-year
lifetime you can retry every day for 121 days, at ninety days thirty, at seven days two, at one day
**zero**. At a one-day lifetime, if the first renewal attempt fails there is no second chance; the
item drops. Shortening does not shrink the risk, it changes the kind of risk — at a long lifetime
the risk is an item being stolen and used for a long time, at a short lifetime the risk is the
renewal channel not working for forty-eight hours.

The table's middle rows say something too. The last column changes only in the bottom row: at a
ninety-day or seven-day lifetime the revocation notice is still needed, because the
twenty-nine-and-a-half-hour learning window stays below both. Shortening lifetime from a year to
seven days cuts the time a stolen item could be used by fifty times but does not remove the
revocation mechanism; removing it requires dropping below twenty-four hours, and just before that
threshold daily transactions jump from six hundred thirty-eight to four thousand three hundred
ninety-seven. The gain is continuous, but the cost rises in a threshold; that is why the
lifetime-shortening decision usually lands somewhere in the middle, at a point where transaction
volume stays manageable and the spare-attempt margin is not zeroed out.

That is why choosing a lifetime does not look at a single number but at where two curves cross: how
long an item could remain in the wrong hands, versus how long the renewal channel is expected to
run without interruption. Assuming the second duration without measuring it turns a short lifetime
from a defense into a fragility.

#### Summary

- Manually renewing 4,397 items at a one-year lifetime takes 99 workdays; ninety-five percent of
  that comes from a single class, 4,100 meter endpoints. The lifecycle decision is a class
  decision, not an inventory decision. The number rests on the assumption that the inventory is
  complete and durations are spread evenly across the year.
- Manual renewal misses 111 items and drops 1,458 connections. The number of missed items does not
  give the risk: 1 internal-service item drops 180 connections while 7 staff items drop 42.
- Automation zeroes out all three columns at once and stops no legitimate work; in exchange it
  adds 5 shared system items. These items do not multiply with the inventory, but when one breaks
  it affects 4,397 items at once.
- The trade-off is between breakage count and scope: 111 scattered, mutually independent breakage
  points in manual renewal, versus 1 point and a 4,397-item radius in automation.
- Shortening lifetime to 1 day makes the revocation notice unnecessary, because the item drops
  before the notice; the cost is yearly transactions rising to 1,604,905 and the spare-attempt
  margin in the renewal window dropping from 121 to 0.

#### Next Step

Across five lessons, everything measured protected the same thing: **the connection**. A
certificate ties together the two ends of a transport channel, and its job ends when that channel
ends. Where the channel ends, though, usually is not where the content is headed — it reaches a log
collector, gets decrypted there, processed, stored, and continues on its way over another channel.
At every hop the content becomes plaintext, and there is a layer that can see it in that moment. The
next lesson closes out this topic by carrying the question to the content itself: how many
intermediate layers see the content, how many items keeping the key at the edge pulls off the
server, and what capabilities are lost in exchange.

### End-to-End Encryption Applications

Source: https://academia.sh/en/courses/identity-and-cryptography/end-to-end-encryption-applications

What counts is who can read the content: in transport encryption, 10 of 10 layers see the content in the clear; in end-to-end encryption, 2 do. Trusted items required drop from 36 to 4, and records seen in the clear go from 30,930 thousand to 5,460 thousand a day. In exchange, 6 capabilities are lost — 4 have a 16-item edge-side counterpart, 2 do not; keeping the key only at the edge makes 172,200 records a year unrecoverable, and an escrow key that can read the content raises the layer count from 2 to 3.

The previous five lessons protected the same thing: the connection. A certificate ties together
the two ends of a transport channel, and its job ends when the channel ends. Where the channel
ends, though, usually is not where the content is headed. In the fictional measurement network, a
meter record leaves the field terminal, passes through the branch gateway, the reverse proxy, the
application server, the queue, the processor, the database, and the backup, and reaches the
work-order terminal. At every hop the channel is re-established, and **between every hop the
content is in the clear**.

This lesson closes out the topic by carrying the question to the content itself. The unit of
measure is no longer the item, it is **who can read it**: how many intermediary layers see the
content, how many items the key staying at the edge pulls off the server, and what capabilities
are lost in exchange.

- **PK26.** A layer "reading" the content means the content exists in the clear at that layer.
  Transport encryption protects the channel, not the inside of the layer.
- **PK27.** A trusted item is a separate path that can access the content at a layer: an admin
  account, a disk, a backup copy, a log entry, debug output. The numbers are fictional.
- **PK28.** In end-to-end encryption the key exists only at the edges; no layer in between can
  decrypt the content — it just carries what it carries.
- **PK29.** The device loss rate, records per device, and record volumes are fictional model
  values.
- **PK30.** An escrow key is a separate key that can decrypt the edges' key, and it is kept
  somewhere within the organization.

#### Who Reads the Content

The measurement runs two settings over the same layer list. The only thing that changes is where
the key sits; the layers, volumes, and paths stay the same.

```js
// layer.mjs — model of the layers that see content and the capabilities lost. IS A MODEL.
// Fictional regional measurement network: the layer list, volumes, rates, and item counts are made up.
export const LAYER = [
  // name, type, records seen per day (thousands), trusted items required
  ["field terminal", "edge", 4820, 2],
  ["branch gateway", "intermediary", 4820, 3],
  ["reverse proxy", "intermediary", 4820, 4],
  ["application server", "intermediary", 4820, 5],
  ["queue", "intermediary", 3100, 3],
  ["processor", "intermediary", 3100, 4],
  ["database", "intermediary", 1960, 6],
  ["backup and archive", "intermediary", 1960, 4],
  ["log collector", "intermediary", 890, 3],
  ["work-order terminal", "edge", 640, 2],
].map(([name, type, records, items]) => ({ name, type, records, items }));

export const CAPABILITY = [
  // name, present in transport encryption, end-to-end counterpart, extra items for the counterpart
  ["search over content", true, "edge-side index", 4],
  ["server-side summary generation", true, "edge-side computation", 3],
  ["anomaly detection", true, "-", 0],
  ["content inspection", true, "-", 0],
  ["recovery on key loss", true, "recovery key escrow", 5],
  ["access to history from a new device", true, "key transfer", 4],
];

// End-to-end encryption closes the content; metadata fields stay in the clear.
export const METADATA = ["sender endpoint", "recipient endpoint", "timestamp", "record size",
  "send frequency"];

export const DEVICE = 4100;        // meter endpoints plus field terminals combined (fictional)
export const LOSS_RATE = 0.03;   // annual device loss rate (fictional)
export const DEVICE_RECORDS = 1400;  // records stuck on a device, unrecoverable elsewhere (fictional)

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

```js
// reader.mjs — who reads the content, items pulled off the server, and capability lost
import { LAYER, CAPABILITY, METADATA, DEVICE, LOSS_RATE, DEVICE_RECORDS, write } from "./layer.mjs";

const sum = (l, f) => l.reduce((t, x) => t + f(x), 0);
const INTERMEDIARY = LAYER.filter((k) => k.type === "intermediary");
const EDGE = LAYER.filter((k) => k.type === "edge");

const fmt1 = write([24, 17, 17, 16, 14]);
console.log(`${LAYER.length} layers: ${EDGE.length} edge, ${INTERMEDIARY.length} intermediary`);
console.log("\n" + fmt1(["layer", "type", "records (k/day)", "in transit", "end-to-end"]));
for (const k of LAYER)
  console.log(fmt1([k.name, k.type, k.records, "reads", k.type === "edge" ? "reads" : "cannot read"]));

const fmt2 = write([30, 14, 16, 18, 14]);
console.log("\n" + fmt2(["metric", "in transit", "end-to-end", "pulled off server", "share"]));
const metric = [
  ["content-visible layers", LAYER.length, EDGE.length],
  ["trusted items required", sum(LAYER, (k) => k.items), sum(EDGE, (k) => k.items)],
  ["records seen in clear (k/day)", sum(LAYER, (k) => k.records), sum(EDGE, (k) => k.records)],
];
for (const [name, a, b] of metric)
  console.log(fmt2([name, a, b, a - b, `${((1 - b / a) * 100).toFixed(0)}%`]));

// Capability lost and the cost of its edge-side counterpart.
const fmt3 = write([40, 34, 15]);
console.log("\n" + fmt3(["capability", "end-to-end counterpart", "extra items"]));
for (const [name, , counterpart, items] of CAPABILITY) console.log(fmt3([name, counterpart, items]));
const noEquivalent = CAPABILITY.filter(([, , k]) => k === "-").length;
console.log(fmt3(["total", `${CAPABILITY.length - noEquivalent} with equivalent, ${noEquivalent} without`,
  sum(CAPABILITY, ([, , , n]) => n)]));

// When the key stays at the edge: devices lost, records unrecoverable.
const lostDevices = Math.round(DEVICE * LOSS_RATE);
console.log(`\ndevice ${DEVICE}, annual loss rate ${LOSS_RATE * 100}% -> ` +
  `${lostDevices} devices (fictional)`);
console.log(`key on server side: unrecoverable records 0`);
console.log(`key only at the edge: unrecoverable records ` +
  `${lostDevices * DEVICE_RECORDS} (${lostDevices} devices x ${DEVICE_RECORDS})`);
const escrow = CAPABILITY.find(([a]) => a === "recovery on key loss")[3];
console.log(`recovery key escrow: unrecoverable records 0, extra items ${escrow}, ` +
  `layers that can read content ${EDGE.length} -> ${EDGE.length + 1}`);

console.log(`\nmetadata fields left in the clear after end-to-end encryption: ${METADATA.length}`);
for (const u of METADATA) console.log(`  ${u}`);
```

```
10 layers: 2 edge, 8 intermediary

layer                                type  records (k/day)      in transit    end-to-end
field terminal                       edge             4820           reads         reads
branch gateway               intermediary             4820           reads   cannot read
reverse proxy                intermediary             4820           reads   cannot read
application server           intermediary             4820           reads   cannot read
queue                        intermediary             3100           reads   cannot read
processor                    intermediary             3100           reads   cannot read
database                     intermediary             1960           reads   cannot read
backup and archive           intermediary             1960           reads   cannot read
log collector                intermediary              890           reads   cannot read
work-order terminal                  edge              640           reads         reads

metric                            in transit      end-to-end pulled off server         share
content-visible layers                    10               2                 8           80%
trusted items required                    36               4                32           89%
records seen in clear (k/day)          30930            5460             25470           82%

capability                                          end-to-end counterpart    extra items
search over content                                        edge-side index              4
server-side summary generation                       edge-side computation              3
anomaly detection                                                        -              0
content inspection                                                       -              0
recovery on key loss                                   recovery key escrow              5
access to history from a new device                           key transfer              4
total                                         4 with equivalent, 2 without             16

device 4100, annual loss rate 3% -> 123 devices (fictional)
key on server side: unrecoverable records 0
key only at the edge: unrecoverable records 172200 (123 devices x 1400)
recovery key escrow: unrecoverable records 0, extra items 5, layers that can read content 2 -> 3

metadata fields left in the clear after end-to-end encryption: 5
  sender endpoint
  recipient endpoint
  timestamp
  record size
  send frequency
```

The first table's fourth column carries the same value throughout: in transport encryption, all
ten of ten layers read the content. This is not a shortcoming of transport encryption — it is its
definition. The channel is protected; where the channel ends, the content opens up. A record is
encrypted on one hop, re-encrypted on the next, and in between it sits in the clear in some
process's memory.

The summary table sets three measures side by side. Layers that see content drop from ten to
**two**, trusted items required from thirty-six to **four**, and the volume of records seen in the
clear from 30,930 thousand to 5,460 thousand a day. The biggest drop is in the items column, and
the reason is clear: a layer seeing content is not a single path, it is every access path at that
layer. The database row carries six items — an admin account, a disk, a backup, a log, and two
more; end-to-end encryption closes all six of them at once, because there is no key to decrypt with
at that layer.

The table's eight intermediary rows are not equal in weight. The backup-and-archive layer sees only
1,960 thousand records a day, less than most of the others; in exchange, it **retains** what it
sees. A record sits in the queue for seconds, in the processor for milliseconds; in the backup it
sits for years. The layer count does not show this difference, because a count measures a moment.
The same number has to be read together with its time dimension: eight of ten layers see the
content, but the layer that **accumulates** content is one, and what accumulates there stays
retroactively in the clear even if the encryption changes today.

The logic of the measurement shows up in this line. Tightening a control on the server side
reduces the item count at that layer; moving the key to the edge **takes the layer out of the
equation**. The first shrinks the item count, the second zeroes it out. This is the reverse of the
same shape as the root count in the second lesson: there, a single root gathered items into
itself; here, the key scatters items out to the edges.

#### The Bill for Moving the Key to the Edge

The second table writes what is lost. Four of the six capabilities have an edge-side counterpart,
and these counterparts add sixteen items in total. The counterparts do not all do the same job:
searching over content requires building an index on the edge side, but that index only covers the
records at that edge — the server-side search was over all records. The counterpart is not a
replacement, it is a **narrowing**.

Two capabilities have no counterpart. Anomaly detection and content inspection, by definition,
require reading the content; a layer that cannot read cannot do them. These two rows show the
decision-like nature of end-to-end encryption: the eight layers it closes shut out not just
unwanted access, but **wanted access too**. In the fictional measurement network, the counterpart
to this is that an anomalous record produced by a broken meter gets noticed not at the collector
but only at the edge or at the destination.

There is a middle ground the model does not measure. The two settings here put the extremes against
each other: the content is either entirely in the clear or entirely closed. In practice the
decision is usually made field by field — a record's subscriber ID might be closed while the meter
reading value is left in the clear. In that case the number of closed layers stays the same, but
the number of **fields** each layer sees drops, and in exchange server-side processing keeps
working on the fields left open. The unit of measure then is not the layer, it is the layer times
the field; this lesson's table gives the two extreme values of that product.

The third section writes the harshest cost. When the key exists only at the edge, a lost device
takes its records down with it: 123 devices a year and **172,200 unrecoverable records**. When the
key is kept server-side, this number is zero, because there is always a party that can decrypt. The
strength of end-to-end encryption and the cause of this loss are the same thing; you cannot have one
without the other.

#### The Layer Escrow Reopens

The known way to zero out the loss is an escrow key: a separate key that can decrypt the edges'
key, kept somewhere within the organization. Unrecoverable records drop to zero and five items get
added. But the measurement changes in another column: layers that can read content go from two to
**three**.

This third layer is not like the others. Each intermediary layer only read the volume it itself
saw — the queue 3,100 thousand records a day, the log collector 890 thousand. The escrow key,
though, makes no distinction; it can decrypt **every** record the edges encrypt. A decision that
closed eight layers reopens a layer wider than all of them combined, for the cost of one item.
This is exactly the definition of the course's measurement axis: the escrow key is a new trust
root, and when it breaks, the number of items that drop is larger than the sum of the eight layers
it closed.

There is a way to avoid keeping this root in a single place: the key is split into shares, and a
certain number of shares have to come together to decrypt it. This raises the number of items
required to break it from one to the share threshold, and adds the same number of maintenance jobs
to the item column — share storage, share renewal, share-holder handover. The measurement does not
change, only another chain enters beneath the root, and that chain's items get counted too.

An escrow key is not a wrong decision; it is wrong when it is an unmeasured decision. Once measured,
the question is not "should there be escrow," it is "where does this root sit, how many people can
reach it, and what drops when it breaks" — the exact question the second lesson asked, over a
different item.

The remaining two edges are not outside the count either. After end-to-end encryption, four trusted
items remain, and all four sit on the edge devices: the device's disk and the account that can
access that device. When the eight intermediary layers are closed, protection does not disappear —
it **relocates**: the only place content can be read is now the edge device itself. At first
glance this is a gain, because four items are fewer than thirty-six; on a second look it is a
**concentration**, because what those four items hold is no longer backed up at any other layer.
In the fictional measurement network, a party that can access a field terminal reads the entirety
of the 4,820 thousand records that terminal sees per day, and no control anywhere in the chain sees
this happen; the certificate is valid, the signature is correct, the handshake is complete.

The final output block writes a limit. End-to-end encryption closes the content, **it does not
close the relationship**: the sender endpoint, recipient endpoint, timestamp, record size, and
send frequency stay in the clear at the intermediary layers, because they are needed to route the
record. This five-field set of metadata leaves visible when and how often a meter sends data,
which terminal a work order was opened for, and which endpoints are talking to each other. A
change in a meter's send frequency can be read as a sign of a malfunction or a tampering attempt,
without the record's content ever being read; the same visibility also shows how much the
intermediary layers still know. Content being unreadable does not mean nothing can be known about
it.

#### Summary

- In transport encryption, all 10 of 10 layers read the content; this is not a shortcoming, it is
  the definition — the channel is protected, and where it ends the content opens up.
- End-to-end encryption drops the layers that see content from 10 to 2, trusted items from 36 to 4
  (89%), and records seen in the clear from 30,930 thousand to 5,460 thousand a day. Tightening a
  control shrinks the item count; moving the key to the edge takes the layer out of the equation.
- In exchange, 6 capabilities are lost: 4 have a 16-item edge-side counterpart with narrower
  scope; anomaly detection and content inspection have no counterpart, because both require
  reading the content.
- When the key exists only at the edge, 123 lost devices a year make 172,200 records
  unrecoverable; when the key is kept server-side, this number is 0. Strength and loss are two
  faces of the same trait.
- An escrow key zeroes out the loss, adds 5 items, and raises the layers that can read from 2 to
  3; this layer sees wider than any of the 8 closed layers ever did. Metadata, though, stays in
  the clear: even with the content closed, 5 fields remain visible.

#### Next Step

This topic opened with a question: a signature proves integrity, but what proves who the public
key used to verify it belongs to? Six lessons tied that question to a chain — the claims carried
by fields, the items carried by the root, revocation's learning window, the handshake's identity
steps, renewal's calendar, and the cost of keeping the key at the edge were all counted. Who the
key belongs to now rests on a named root.

What is left open is the name itself. An item's subject reading `field-terminal-17` does not say who
uses that terminal, what role that person holds, which records they can access, or how that
decision gets enforced across thousands of machines. Who is who within the organization, who can
access what, and how that gets enforced machine by machine has never been measured. The next
topic, **Enterprise Identity**, takes up these three questions: where identity is centrally held,
how many applications go down when that center breaks, and how many rounds it takes a policy to
reach how many machines.

## Enterprise Identity

Where the trust root stands at enterprise scale and what it costs when it breaks: the number of systems bound to the central identity repository and the items replication rescues, the ticket flow reduced to the ticket-granting service's key, the paths a fallback behavior opens when the central network authentication service is unreachable, the applications bound to a single identity provider along with the assertion's lifetime and revocation window, the paths each factor type closes and fails to close, the central policy's propagation rounds and the items it never reaches, the count of horizontal and vertical privilege escalation paths, and the paths controls close against credential theft.

### Directory Services

Source: https://academia.sh/en/courses/identity-and-cryptography/directory-services

Measuring the central identity repository as a trust root: 13 of 16 systems being bound to a single repository, the chain from root to login holding 9, 8, or 2 items depending on connection mode, a 12-hour outage stopping 609 account logins across 13 systems, the third replica rescuing no new system, and 646 items needing manual renewal when integrity breaks.

The previous topic bound who a public key belongs to into a chain: certificate, intermediate
authority, root. The chain was counted and the items that fall when the root breaks were
derived. The question left open at the close was this — the key's owner was bound to a name,
but who that name **is** inside the organization, which team it sits in, which systems it can
log into, and how that is enforced across thousands of machines was never asked. This topic
takes up that question at enterprise scale and measures it with the same three columns.

The first mechanism is the bottommost one: how does the organization know who exists. If every
system keeps its own account list, a person joining means opening records on however many
systems, and a person leaving means closing however many records — and a record that is not
closed leaves an opening no one can see. The mechanism that collects this load in one place is
the **directory service**: a queryable central identity repository that holds user, device, and
service accounts in a single tree. What this lesson measures is not what the directory does, but
how many things depend on it.

#### The Central Repository As a Root

**EI1:** the central identity repository is a trust root; every system bound to it assumes from
the outset that the record the repository returns is correct, and does not verify it on its own.
**EI2:** there is a chain of items running from root to use; every item in the chain must be
correct **at the same time** — if one is broken, login either stops or returns the wrong result.
**EI3:** replication multiplies the root's **availability**, not its **correctness** — more
copies shorten outage time, not the impact of an integrity break. **EI4:** a system left outside
the root escapes both the root's outages and its oversight.

This course's three columns are the same in this topic too: the trust root is named, the number
of items that must be correct at the same time from root to use is counted, and how many items
fall if the root breaks and what renewal costs is written down. The directory's query syntax,
tree schema, and replication protocol are not measured here; what is measured is these three
numbers.

#### Sixteen Systems, One Repository

The inventory below is a **model**: the systems of the organization running the fictional
regional measurement network, and how each one connects to the central repository. There is no
real organization, no real directory product, and no real account.

```js
// identity/directory.mjs — how the systems of the fictional regional measurement network connect
// to the central identity repository (model): no real organization, no real directory product,
// and no real account. No randomness.

// EI1: the central identity repository is a trust root; every system bound to it assumes from
// the outset that the record the repository holds is correct.
// Columns: mode = connection mode (directory: asks on every login; cache: keeps the last
// successful check for a while; local: keeps its own account store), accounts = number of
// accounts logging into that system, refs = number of directory replicas in the system's
// configuration, secret = number of the system's own service-account secrets in the directory.
export const SYSTEM = `
  reading-collector        directory   34   2   1
  validator                directory   12   2   1
  billing                  directory   48   1   2
  work-order                directory   61   1   1
  field-mobile-endpoint     cache      140   2   1
  branch-workstation        cache       96   2   1
  warehouse-terminal        cache       18   1   1
  maintenance-scheduler     directory   22   1   1
  measurement-archive       directory    9   2   1
  reporting-portal          directory   55   1   1
  network-switch            directory   14   2   0
  vpn-endpoint               directory   27   2   0
  printer-pool              cache       73   1   0
  legacy-scada-console      local        6   0   0
  firmware-server           local        4   0   0
  backup-console            local        3   0   0`
  .trim().split("\n").map((r) => {
    const p = r.trim().split(/\s+/);
    return { name: p[0], mode: p[1], accounts: +p[2], refs: +p[3], secret: +p[4] };
  });

// EI2: every use bound to the root sets up a chain of items running from root to use; every
// item in the chain must be correct at the same time. Each item records which modes require it.
export const CHAIN = [
  { item: "account record exists",       mode: ["directory", "cache"] },
  { item: "account active",              mode: ["directory", "cache"] },
  { item: "password verifier",           mode: ["directory", "cache"] },
  { item: "group membership",            mode: ["directory", "cache"] },
  { item: "attribute mapping",           mode: ["directory", "cache"] },
  { item: "live connection to directory", mode: ["directory"] },
  { item: "channel protection",          mode: ["directory", "cache"] },
  { item: "name resolution",             mode: ["directory", "cache"] },
  { item: "clock synchronization",       mode: ["directory", "cache"] },
  { item: "local account store",         mode: ["local"] },
  { item: "local password record",       mode: ["local"] },
];

export const MODE = ["directory", "cache", "local"];
export const chainLength = (mode) => CHAIN.filter((c) => c.mode.includes(mode)).length;
```

```js
// identity/tally.mjs — the central identity repository counted as a trust root: how many items
// the chain holds, how many systems stop logging in when the repository goes down, how many
// systems replication rescues, and how many items renewal costs if the root breaks.
import { SYSTEM, CHAIN, MODE, chainLength } from "./directory.mjs";

const print = (w, ...c) => console.log(c.map((v, i) =>
  (w[i] < 0 ? String(v).padEnd(-w[i]) : String(v).padStart(w[i]))).join(""));
const sum = (f, s = SYSTEM) => s.reduce((a, x) => a + f(x), 0);
const bound = SYSTEM.filter((s) => s.mode !== "local");

console.log(`${SYSTEM.length} systems, ${sum((s) => s.accounts)} account logins, ` +
  `${bound.length} systems bound to a single repository, ${SYSTEM.length - bound.length} systems with their own repository`);

const A = [-10, 9, 10, 8, 8, 8];
console.log("\n1. chain length and load by connection mode");
print(A, "mode", "systems", "accounts", "chain", "items", "secret");
for (const m of MODE) {
  const s = SYSTEM.filter((x) => x.mode === m);
  print(A, m, s.length, sum((x) => x.accounts, s), chainLength(m),
    s.length * chainLength(m), sum((x) => x.secret, s));
}
print(A, "total", SYSTEM.length, sum((s) => s.accounts), "-",
  sum((s) => chainLength(s.mode)), sum((s) => s.secret));

const B = [-10, 11, 15, 11, 13];
console.log("\n2. when the repository is unreachable (single copy)");
print(B, "outage", "down sys", "down accounts", "up sys", "up accounts");
for (const hour of [1, 4, 12, 48]) {
  const down = SYSTEM.filter((s) => s.mode === "directory" ||
    (s.mode === "cache" && hour > 8));
  print(B, `${hour} h`, down.length, sum((s) => s.accounts, down),
    SYSTEM.length - down.length, sum((s) => s.accounts) - sum((s) => s.accounts, down));
}

const C = [-10, 11, 11, 15];
console.log("\n3. how many systems does replication rescue in a 12-hour outage");
print(C, "copies", "rescued", "down", "down accounts");
for (const copies of [1, 2, 3]) {
  const rescued = SYSTEM.filter((s) => s.mode !== "local" && s.refs >= 2 && copies >= 2);
  const down = bound.filter((s) => !rescued.includes(s));
  print(C, `${copies}`, rescued.length, down.length, sum((s) => s.accounts, down));
}

const D = [-24, 8, 8, 20];
console.log("\n4. if the root breaks (repository integrity is compromised): items lost and renewal");
const accounts = sum((s) => s.accounts, bound);
const rows = [
  ["directory-bound systems", bound.length, bound.length, "remapped"],
  ["account logins", accounts, accounts, "passwords reset"],
  ["service account secrets", sum((s) => s.secret), sum((s) => s.secret), "manually changed"],
  ["group membership checks", bound.length, bound.length, "manually verified"],
  ["locally stored systems", SYSTEM.length - bound.length, 0, "unaffected"],
];
print(D, "item", "count", "lost", "renewal");
for (const r of rows) print(D, ...r);
console.log(`total manually renewed items ${rows.reduce((a, r) => a + r[2], 0)}, ` +
  `chain item definitions ${CHAIN.length}`);
```

```
16 systems, 622 account logins, 13 systems bound to a single repository, 3 systems with their own repository

1. chain length and load by connection mode
mode        systems  accounts   chain   items  secret
directory         9       282       9      81       8
cache             4       327       8      32       3
local             3        13       2       6       0
total            16       622       -     119      11

2. when the repository is unreachable (single copy)
outage       down sys  down accounts     up sys  up accounts
1 h                 9            282          7          340
4 h                 9            282          7          340
12 h               13            609          3           13
48 h               13            609          3           13

3. how many systems does replication rescue in a 12-hour outage
copies        rescued       down  down accounts
1                   0         13            609
2                   7          6            277
3                   7          6            277

4. if the root breaks (repository integrity is compromised): items lost and renewal
item                       count    lost             renewal
directory-bound systems       13      13            remapped
account logins               609     609     passwords reset
service account secrets       11      11    manually changed
group membership checks       13      13   manually verified
locally stored systems         3       0          unaffected
total manually renewed items 646, chain item definitions 11
```

Numbers are of the **measurement** kind; their inputs are EI1–EI4 and the inventory values.

#### How Many Items the Chain Holds

The first table splits chain length by connection mode. On a system that asks the repository on
every login, the chain is **nine items**: the record existing, being active, the password
verifier holding, group membership being correct, attributes mapping to the fields the system
expects, a live connection to the repository, the channel being protected, the name resolving,
and the two sides' clocks being close enough. None of these is a "security feature"; all nine
are ordinary items required for login to work, and **if one breaks, login stops**.

On a system that verifies from cache, the chain drops to eight, because the live-connection item
falls away. This one-item difference produces the whole difference in the second table. On a
system that keeps its own repository, the chain is **two** — the local account list and the
local password record. The total item count for sixteen systems is 119; this is the number of
independent conditions that must be simultaneously correct for the event "a person can log into
a system" to occur in the organization.

A two-item chain looks short, but the second half of EI4 is here. The six, four, and three
accounts on the three locally-stored systems are bound to no central record: when a person
leaves, the central record is closed, and the account on these three systems stays open. A
short chain buys resilience to outages and lack of oversight together.

#### The Chain's Items Outside the Repository

Not all nine items live in the repository, and this distinction draws the root's boundary. Five
are inside the repository: the record's existence, its activeness, the password verifier, group
membership, and attribute mapping. The remaining **four are not in the repository** — the live
connection belongs to the network, channel protection to the certificate, name resolution to the
name service, and clock synchronization to the time source. On a system verifying from cache,
this ratio is five to three.

Each of these four items carries **its own root**. Channel protection rests on the chain of trust
measured in the previous topic: encrypting the connection to the repository alone is not
enough — the far end must be proven, with a certificate chain, to actually be the repository. On
an unverified channel, even if the repository holds the correct record, someone else can answer
the query. Name resolution is the same kind of dependency: the system asks another service to
turn the repository's name into an address, and does not verify that service's answer.

Clock synchronization is the quietest item on the list, and the whole of the next lesson is
built on it. The validity of a timed proof depends on the two sides' clocks being close enough;
when the clocks drift apart, login stops even though no account has changed at all. **A trust
root is as fragile as the number of items outside itself**: in this model there are four separate
items that can stop login even while the repository itself works perfectly, and none of them is
under the directory administrator's control.

#### When the Repository Goes Down

The second table counts down systems by outage length. In the first hour, nine systems and 282
account logins stop; the four systems verifying from cache keep working and 327 accounts are
unaffected. After the eighth hour, the cache window closes: down systems rise to 13, down
accounts to 609. The three still-up systems have 13 accounts in total.

The real number here is not 13, it is the gap between the two states. When the outage lasts one
hour, the affected share of accounts is 282/622; when it lasts twelve hours, it is 609/622. The
cache window directly sets the organization's outage resilience, and this window is a security
setting: the longer it is, the longer a disabled account keeps working. The same number cuts
both ways at once — eight hours is both the time rescued during an outage and the time a
disabled account stays valid.

#### What Replication Rescues and What It Does Not

The third table turns the first half of EI3 into a number. With a single copy, none of the 13
bound systems is rescued in a twelve-hour outage. Adding a second copy rescues seven systems,
six stay down, and down accounts drop from 609 to 277. Adding a third copy leaves the table
**unchanged**: still seven rescued, still six down.

The reason the third copy adds nothing is not in the repository — it is in the systems'
configuration. Six systems have a single configured reference address; even if a second copy
sits on the network, those systems never ask it. **Replication is a property of the repository;
resilience is a property of client configuration**, and the first cannot be measured until the
second is written. The item that decides how many systems get rescued is not the copy count, but
how many systems recognize the second reference.

#### Renewal If the Root Breaks

The fourth table is the second half of EI3. An outage and an integrity break are different
events: an outage is temporary and leaves nothing behind once fixed, while an integrity break is
**permanent**. If the repository's records could be altered, who is a member of which group and
which password verifier is valid are no longer known.

The renewal bill has four items: remapping 13 systems, resetting the passwords of 609 account
logins, manually changing 11 service-account secrets, and manually verifying the group
memberships of 13 systems. Total: **646 items**. Of these, 609 touch people and can be done in
parallel; 11 are service-account secrets, and each carries the risk of stopping a system's
operation, so they are done one at a time. The number that sets the renewal window's length is not
609, it is **11**.

More decisive than the size of the bill is **when** it gets issued. An integrity break does not
announce itself like an outage: the repository keeps working, logins succeed, and no system
raises an error. Knowing a break happened depends on comparing the records against an
independent copy; without such a comparison, the 646-item renewal decision cannot be made,
because the event requiring renewal is not known to have happened. **The cost of a root break
does not accrue at renewal — it accrues in every login made during the time the break goes
unnoticed.**

The three locally-stored systems come out to zero in this table. This does not mean those three
systems are safe; they have their own roots, and that root is outside this measurement. Being
outside a root does not mean escaping breakage — it means adding **a separate breaking point**.

#### Summary

- The central identity repository is a trust root: 13 of 16 systems and 609 of 622 account
  logins are bound to a single repository.
- The chain from root to login is 9, 8, and 2 items depending on connection mode; all 119 total
  items must be correct at the same time.
- The number of down systems during an outage is set by the cache window: 9 systems and 282
  accounts in the first hour, 13 systems and 609 accounts by the twelfth hour.
- Replication multiplies availability, not correctness; the third copy rescues no new system,
  because 6 systems have a single reference defined in their configuration.
- If integrity breaks, 646 items are renewed by hand; the window's length is set not by the 609
  passwords, but by the 11 service-account secrets changed one at a time.

#### Next Step

This lesson counted what the repository says about **who exists**, but a record existing does not
show that a person is actually there right now. Querying the repository on every login produces
the dependency seen in the second table; enterprise networks instead produce a **timed proof**
from a single verification, and that proof is presented instead of the password on every
subsequent access. The service producing the proof has its own key, and that key is a new root.
The next lesson measures ticket-based authentication on this axis: how many steps the flow takes,
which step proves which claim, how many items fall if the ticket-granting service's key is
compromised, and how many steps renewing that key takes.

### Ticket-Based Authentication

Source: https://academia.sh/en/courses/identity-and-cryptography/ticket-based-authentication

Reducing the ticket flow to the ticket-granting service's key: the claims proven across six steps and the key each step rests on, 471 key items dividing across five types, a single root key's compromise dropping all 214 principals and 21 services, and renewal holding to two rounds and a 176-hour window.

The previous lesson counted 609 account logins stalling across 13 systems during the central
repository's outage. The source of this dependency is that every system asks the repository on
every login and the password crosses the network every time. On enterprise networks, a
**time-limited proof** is produced from a single verification instead, and that proof is
presented in place of the password on subsequent accesses. The proof is called a **ticket**; the
open specification of the mechanism that produces it is known by the name Kerberos.

This lesson does not measure how the ticket works, but what the ticket **rests on**. How many
steps the flow takes, which claim is proven at which step, how many key items must be correct at
the same time, how many items drop if which key is compromised, and how many rounds renewing
that key takes.

#### The Proof That Replaces the Password

**EI5:** in ticket-based authentication, the password is used only in the first step; what is
presented at every access after that is a time-limited ticket, and the ticket does not carry the
password. **EI6:** whoever can produce a ticket can produce all the claims that ticket carries;
this is why the ticket-granting service's key is the domain's trust root. **EI7:** renewing a key
does not retroactively invalidate valid proofs produced with that key; the renewal window lasts as
long as the longest-lived proof.

EI5 is a two-way gain. The password crossing the network only once, and not directly but with a
derived key, makes the password unreachable to anyone listening on the wire. The same design also
brings a cost: the proof is now an independent object, it can be copied, and until it expires no
one asks whose hand it is in. **What needs to be measured is not the ticket's power, but the scope
of the key that produces the ticket and the ticket's lifetime.**

#### Six Steps, Five Keys

The flow below is a **model**: in the fictional regional measurement network domain there are 214
user principals and 21 service principals. There is no real domain, real principal, or real key.

```js
// ticket/flow.mjs — the steps of ticket-based authentication and the keys they rest on (model):
// in the fictional regional measurement network domain there are 214 user principals and 21
// service principals. There is no real domain, real principal, or real key. No randomness.

// EI5: in ticket-based authentication the password is used only in the first step; what is presented at every access after that
// is a time-limited ticket. EI6: whoever can produce a ticket can produce all the claims it carries; the ticket-granting service's
// key is therefore the domain's trust root.
export const STEP = `
  1  pre-auth request          I know the password                  user             identity-service
  2  ticket-granting ticket    the center issued this ticket        ticket-service   client
  3  ticket and authenticator  I received this ticket               session-1        ticket-service
  4  service ticket            the center issued this ticket        service-account  client
  5  ticket and authenticator  I received this ticket               session-2        target-service
  6  mutual authentication     the far end really is that service   service-account  client`
  .trim().split("\n").map((r) => {
    const p = r.trim().split(/\s{2,}/);
    return { no: +p[0], message: p[1], claim: p[2], key: p[3], verifier: p[4] };
  });

// Key inventory: count = number of items in the domain, principal = principals dropped directly
// if the key is compromised, service = services dropped, round = how many rounds renewal takes.
export const KEY = {
  "user":            { count: 214, principal: 1,   service: 0,  round: 1, lifeHours: 2160 },
  "service-account": { count: 21,  principal: 214, service: 1,  round: 1, lifeHours: 8760 },
  "session-1":       { count: 214, principal: 1,   service: 0,  round: 0, lifeHours: 10 },
  "session-2":       { count: 21,  principal: 1,   service: 1,  round: 0, lifeHours: 10 },
  "ticket-service":  { count: 1,   principal: 214, service: 21, round: 2, lifeHours: 8760 },
};

export const TICKET_LIFE = 10;      // hours
export const RENEWAL_LIFE = 168;    // hours
export const COPY = 3;
export const PROPAGATION = 4;       // hours for a key change to reach the copies
```

```js
// ticket/tally.mjs — how many steps the flow takes, which claim is proven at which step, how many
// items drop if which key is compromised, and how many steps renewing the ticket-granting service's key takes.
import { STEP, KEY, TICKET_LIFE, RENEWAL_LIFE, COPY, PROPAGATION } from "./flow.mjs";

const print = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const type = Object.keys(KEY);
const item = type.reduce((a, t) => a + KEY[t].count, 0);

console.log(`${STEP.length} steps, ${type.length} key types, ${item} key items; ` +
  `ticket life ${TICKET_LIFE} hours, renewal window ${RENEWAL_LIFE} hours`);

const A = [-3, -26, -40, 18];
console.log("\n1. claim proven per step and the key it rests on");
print(A, "no", "message", "proven claim", "key it rests on");
for (const a of STEP) print(A, a.no, a.message, a.claim, a.key);

const B = [-16, 7, 10, 10, 9, 6];
console.log("\n2. what drops if a key type is compromised");
print(B, "key", "count", "drop pr", "drop svc", "life hr", "rnd");
for (const t of type) {
  const k = KEY[t];
  print(B, t, k.count, k.principal, k.service, k.lifeHours, k.round);
}
const root = KEY["ticket-service"];
console.log(`single-item drop ratio: principal ${root.principal}/${KEY["user"].count}, ` +
  `service ${root.service}/${KEY["service-account"].count}`);

const C = [-4, -30, 7, 27];
console.log("\n3. renewal plan for the ticket-granting service's key");
const plan = [
  ["first key change", 0, "old tickets still decrypt"],
  ["propagation to replicas", PROPAGATION, `${COPY} replicas`],
  ["end of open tickets", TICKET_LIFE, "new ticket obtained"],
  ["second key change", 0, "old key drops"],
  ["propagation to replicas", PROPAGATION, `${COPY} replicas`],
  ["end of renewal tickets", RENEWAL_LIFE - TICKET_LIFE, "re-login"],
];
print(C, "no", "step", "hour", "result");
let elapsed = 0;
plan.forEach((p, i) => { elapsed += p[1]; print(C, i + 1, p[0], p[1], p[2]); });
console.log(`total window ${elapsed} hours, manually touched items ` +
  `${root.round + KEY["service-account"].count}`);

const D = [-18, 14, 20, 18];
console.log("\n4. legitimate work stalled when renewing a key type");
print(D, "key", "manual items", "principals stopped", "services stopped");
for (const t of type.filter((x) => KEY[x].round > 0)) {
  const k = KEY[t];
  print(D, t, k.count * k.round, k.principal, k.service);
}
```

```
6 steps, 5 key types, 471 key items; ticket life 10 hours, renewal window 168 hours

1. claim proven per step and the key it rests on
no message                   proven claim                               key it rests on
1  pre-auth request          I know the password                                   user
2  ticket-granting ticket    the center issued this ticket               ticket-service
3  ticket and authenticator  I received this ticket                           session-1
4  service ticket            the center issued this ticket              service-account
5  ticket and authenticator  I received this ticket                           session-2
6  mutual authentication     the far end really is that service         service-account

2. what drops if a key type is compromised
key               count   drop pr  drop svc  life hr   rnd
user                214         1         0     2160     1
service-account      21       214         1     8760     1
session-1           214         1         0       10     0
session-2            21         1         1       10     0
ticket-service        1       214        21     8760     2
single-item drop ratio: principal 214/214, service 21/21

3. renewal plan for the ticket-granting service's key
no  step                             hour                     result
1   first key change                    0  old tickets still decrypt
2   propagation to replicas             4                 3 replicas
3   end of open tickets                10        new ticket obtained
4   second key change                   0              old key drops
5   propagation to replicas             4                 3 replicas
6   end of renewal tickets            158                   re-login
total window 176 hours, manually touched items 23

4. legitimate work stalled when renewing a key type
key                 manual items  principals stopped  services stopped
user                         214                   1                 0
service-account               21                 214                 1
ticket-service                 2                 214                21
```

The numbers are of the **measurement** class; their inputs are EI5–EI7 and the key inventory.

#### Which Claim Is Proven at Which Step

The first table gives the six steps and the single claim each step proves. Step 1 proves who the
party that knows the password is, and this is the **only** password use in the flow. Steps 2 and
4 carry the same claim — that the ticket came from the center — but rest on different keys: the
first on the ticket-granting service's key, the second on the target service's account key. Steps
3 and 5 prove that the one presenting the ticket is the party that received it; the proof is a
verifier produced with the session key and carrying a timestamp.

The timestamp is this lesson's counterpart to the clock agreement that stayed outside the chain
in the previous lesson. The verifier's freshness rests only on the clock; when the two sides'
clocks drift apart by more than the accepted window, login stalls **even though no account has
changed**. Beneath one trust root sits another service that has nothing to do with security.

The sixth step is the list's only reverse-direction proof: the client verifies that the other end
really is that service. This step is optional, and the flow keeps working when it is skipped; the
one claim left unproven is the identity of the end the client connected to. **A working flow can
leave an unproven claim, and no error message will say so.**

#### The Chain of One Access

The course's second column is here fully occupied by all five key types. For a single principal
to access a single service, all five of the five types must be correct at the same time: the user
key holds the first step, the root key the second, the first session key the third, the service
account key the fourth, and the second session key the fifth. Two more items are added alongside
these — the clock window and the resolution of the name to a principal.

The second item is not visible at first glance. When a client connects to a service, it requests a
ticket via the name it holds; the ticket-granting service resolves that name to a principal and
produces the ticket with that principal's account key. The mapping between name and principal
sits in the directory, and **its correctness is never tested anywhere within this flow**. A
wrongly mapped name carries a correctly produced ticket to the wrong service. Of the chain's seven
items, only five are cryptographic; the remaining two are a record and a clock.

#### The Authorization Inside the Ticket

The ticket carries identity, but not only identity. The target service does not separately ask the
directory which groups the principal belongs to; this information is placed inside the ticket and
the service accepts it. This way, an access decision can be made without ever touching the
directory, and the outage dependency counted in the previous lesson decreases.

The cost of this is that the authorization data is **frozen for as long as the ticket lives**.
When a principal is removed from a group, the ticket in hand keeps stating the old membership for
ten more hours; since the service holds no other item to verify the claim against, it cannot see
the difference. That authentication and authorization are separate layers was measured in the
**Introduction to Cybersecurity** course; what is seen here is that packing the two layers into
**the same proof object** produces a freshness window. Revoking authorization is ten hours slower
than revoking identity.

#### The Root: One Item, Two Hundred Fourteen Principals

The second table is this lesson's center. There are 471 key items in the domain, and 428 of them
carry a scope limited to a single principal or a single session. If a user key is compromised,
the number of principals dropped is one; if a session key is compromised, the item dropped is
also one, and it already becomes worthless on its own after ten hours.

The service account key shows the jump: a single item covers **all** logins made to that service;
all 214 principals can be represented with a forged ticket for that service. The ticket-granting
service's key, though, is the end of the boundary — a single item, all 214 of the 214 principals
and all 21 of the 21 services. The ratio is whole in both rows.

The reason for this is EI6, and it is the mechanism's design, not its flaw. A ticket states that
the claims it carries are true based on having been produced with the ticket-granting service's
key. Whoever knows that key can produce a valid ticket for any principal to any service, and doing
so **requires knowing no password at all**. Producing such a thing without written authorization
and a defined scope is both illegal and unprofessional when done; what is measured here is not how
the production is done, but the number of items that drop assuming it is done.

A detail hidden in the table makes this root even more fragile. The root key is not an independent
value produced in a privileged hardware unit; it is the key of an account in the domain, and it is
derived from that account's password. That is, the widest-scoped of the 471 items is an item of
the **same kind** as the 214 user keys in the list, and its durability is bounded by the cost
curve given by the password's length and the work factor behind it. That curve was measured in
the **Cryptography Fundamentals** topic and is a direct input here: the root's scope is the
widest, yet the mechanism protecting the root is the same as for user accounts.

#### The Renewal Window

The third table turns EI7 into a number. Renewing the root key is not a single change. The old key
has to be kept around for a while, because tickets in circulation were produced with it; after the
first change the old key is still valid. The real renewal is the **second** change, and it waits
first for propagation to complete, then for open tickets to expire.

The window accumulates over six steps: two propagation rounds, eight hours; end of open tickets,
ten hours; end of renewal tickets, 158 hours. Total **176 hours**. 158 of these hours come from a
single setting — the renewal ticket's seven-day lifetime. What determines the root key's renewal
duration is not the key itself but **the lifetime of the longest-lived proof**; the window does not
shorten without shortening the ticket's lifetime.

The number of manually touched items is 23: two rounds of the root key plus the review of 21
service account keys. The fourth table gives the counterpart of this. Renewing user keys takes
214 manual items, but each one stops only one principal; renewing the root key takes two manual
items and stops all 214 principals at once. **The number of manual items and the legitimate work
stopped are two inversely related measures, and the renewal plan is made according to the
second.**

This inverse ratio also explains why renewal gets postponed. Two manual items are a small job; a
seven-day-spread outage for 214 principals is an event that stops every team in the organization.
Renewing the root key is not a technical operation on its own, it is a planned outage, and it will not
be done unless it is planned. The third column — what renewal holds — is therefore not a cost item
but the number that determines how often the root is really renewed. An unrenewed root silently
validates every ticket produced after the date it was compromised, and the 176 hours written in
the table are never spent.

#### Summary

- In ticket-based authentication, the password is used only in the first step; what is presented
  in the remaining five steps is a time-limited proof, and the flow proves 6 distinct claims
  across 6 steps.
- When the sixth step is skipped the flow still works, but the identity of the end the client
  connected to stays unproven.
- 428 of the domain's 471 key items are scoped to a single principal or a single session; the
  scope jump begins at the service account key and completes at the ticket-granting service's
  key.
- The root key is a single item, and when compromised, 214 of 214 principals and 21 of 21
  services drop; this requires knowing no password.
- Renewing the root key takes two rounds, 23 manual items, and a 176-hour window; 158 of the
  window's hours come from the longest-lived proof's lifetime.

#### Next Step

This lesson measured a principal accessing a **service**; for the access to be able to start, the
client already had to be connected to the network. On enterprise networks that connection itself
is also a decision point: when a cable is plugged in or a device wants to join the wireless
network, the network hardware does not verify identity itself, it asks a central authentication
service and opens the connection based on the answer. The next lesson measures this in terms of
access decisions: how many devices ask a single service, which fallback behavior is chosen when
the service is unreachable, and how many paths each of those behaviors opens.

### Authentication for Network Access

Source: https://academia.sh/en/courses/identity-and-cryptography/authentication-for-network-access

Measuring the central network authentication service in terms of access decisions: 80 devices asking a single service, a 6-item chain per device, four fallback behaviors opening between 0 and 1348 paths when the service is unreachable and stopping up to 957 legitimate ends, 436 new paths staying open in today's mix, and renewing the one shared secret meaning manually touching 80 devices.

The previous lesson measured a principal accessing a service and counted the flow's six steps.
All of the steps assumed the client was already connected to the network: a ticket request has
to go from somewhere to somewhere. On enterprise networks, that connection itself is also a
decision point. When a cable is plugged in or a device wants to join the wireless network, the
edge switch or access point does not verify identity itself — it asks a central service and opens
the connection point based on the answer.

This lesson measures that service in terms of **access decisions**. How many devices ask a single
service, how many items must be correct at the same time for the decision to be made, which
fallback behavior is chosen when the service is unreachable, and how many paths each of those
behaviors opens. The secret between the client and the network was measured in the **Network
Security** course; what is measured here is the leg between the device and the service.

#### The Service That Makes the Decision

**EI8:** the device at the access layer does not verify identity itself; it asks a central network
authentication service for the decision. The shared secret between the device and the service is
the trust root of **every** access decision that device makes. **EI9:** when the service is
unreachable, the device picks a fallback behavior; the behavior chosen determines both the number
of paths opened and the legitimate access stopped, and not having picked one does not mean there is
no behavior — the default is a choice too. **EI10:** the number of opened paths alone is not a
ranking criterion; the same number of paths gives two different outcomes depending on how many
systems are reached.

This service's open specification is known by the name RADIUS, and the decision itself has two
parts: the device's right to ask and the identity of the end. The first rests on the shared
secret, the second on the credential the end presents. The service asks somewhere else to verify
the second — the central directory measured in the previous lessons.

#### Eighty Devices, One Service

The inventory below is a **model**: the access layer of the fictional regional measurement network.
There is no real organization, real device, or real secret.

```js
// netaccess/access.mjs — the fictional regional measurement network's access-layer devices
// connecting to the central network authentication service (model): there is no real
// organization, real device, or real secret. No randomness.

// EI8: the device at the access layer does not verify identity itself; it asks a central network
// authentication service for the decision and opens the connection point based on the answer. The
// shared secret between the service and the device is the trust root of every access decision that device makes.
// Columns: count = number of devices, point = connection points that must authenticate,
// end = currently connected legitimate ends, revoked = ends whose access was revoked recently.
export const DEVICE = `
  central-edge-switch          switch     6   144   131    4
  central-wireless-ap          wireless  12   240   186    9
  branch-edge-switch           switch    18   216   174    7
  branch-wireless-ap           wireless  24   360   212   11
  field-remote-access-endpoint remote     3   120    96    6
  warehouse-edge-switch        switch     5    60    41    2
  meter-collection-switch      switch     4    48    44    1
  guest-wireless-ap            wireless   8   160    73    3`
  .trim().split("\n").map((r) => {
    const p = r.trim().split(/\s+/);
    return { name: p[0], type: p[1], count: +p[2], point: +p[3], end: +p[4], revoked: +p[5] };
  });

// EI9: when the service is unreachable the device picks a fallback behavior; the behavior
// chosen determines both the opened path count and the stopped legitimate access. The "reach"
// column is how many systems the fallen-back connection can reach (of the org's 16 systems).
export const FALLBACK = {
  "closed":           { new: (a) => 0,       revoked: (a) => 0,       stopped: (a) => a.end, reach: 0 },
  "open":             { new: (a) => a.point, revoked: (a) => a.revoked, stopped: (a) => 0,    reach: 16 },
  "critical-segment": { new: (a) => a.point, revoked: (a) => a.revoked, stopped: (a) => 0,    reach: 2 },
  "cache":            { new: (a) => 0,       revoked: (a) => a.revoked, stopped: (a) => 0,    reach: 16 },
};

// EI10: the chain from the root to the decision is the items that must be correct at the same time, per device.
export const CHAIN = ["shared secret", "service certificate", "end credential",
  "service's directory link", "policy record", "segment mapping"];
```

```js
// netaccess/tally.mjs — how many devices ask a single service, how many items the chain is, which
// fallback behavior opens how many paths when the service is unreachable, and what renewal costs if the shared secret is compromised.
import { DEVICE, FALLBACK, CHAIN } from "./access.mjs";

const print = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const total = (f, s = DEVICE) => s.reduce((a, x) => a + f(x), 0);
const mode = Object.keys(FALLBACK);
const count = total((a) => a.count);

console.log(`${count} devices ask a single service, ${total((a) => a.point)} connection points, ` +
  `${total((a) => a.end)} connected ends, ${total((a) => a.revoked)} revoked ends`);
console.log(`chain ${CHAIN.length} items, per device; ${count * CHAIN.length} items in the domain`);

const A = [-10, 8, 8, 8, 9, 8];
console.log("\n1. load by device type");
print(A, "type", "devices", "points", "ends", "revoked", "items");
for (const t of ["switch", "wireless", "remote"]) {
  const s = DEVICE.filter((a) => a.type === t);
  print(A, t, total((a) => a.count, s), total((a) => a.point, s), total((a) => a.end, s),
    total((a) => a.revoked, s), total((a) => a.count, s) * CHAIN.length);
}

const B = [-18, 10, 15, 14, 9];
console.log("\n2. same network, four fallback modes, service unreachable");
print(B, "mode", "new paths", "revoked paths", "stopped ends", "reach");
for (const m of mode) {
  const g = FALLBACK[m];
  print(B, m, total(g.new), total(g.revoked), total(g.stopped), `${g.reach}/16`);
}

const C = [-30, -18, 10, 15, 14];
console.log("\n3. today's mode mix across devices, with outcome");
const choice = { "central-edge-switch": "closed", "central-wireless-ap": "cache",
  "branch-edge-switch": "critical-segment", "branch-wireless-ap": "cache",
  "field-remote-access-endpoint": "closed", "warehouse-edge-switch": "open",
  "meter-collection-switch": "closed", "guest-wireless-ap": "open" };
print(C, "device", "mode", "new paths", "revoked paths", "stopped ends");
let y = 0, r = 0, d = 0;
for (const a of DEVICE) {
  const g = FALLBACK[choice[a.name]];
  y += g.new(a); r += g.revoked(a); d += g.stopped(a);
  print(C, a.name, choice[a.name], g.new(a), g.revoked(a), g.stopped(a));
}
print(C, "total", "-", y, r, d);

const D = [-34, 8, 10, 22];
console.log("\n4. what drops if the shared secret is compromised, and renewal");
print(D, "item", "count", "dropped", "renewal");
const row = [
  ["a single device's secret", 1, 1, "1 device manually"],
  ["devices sharing the same secret", count, count, `${count} devices manually`],
  ["affected points", total((a) => a.point), total((a) => a.point), "automatically"],
  ["service certificate", 1, count, `${count} devices manually`],
  ["end credentials", total((a) => a.end), 0, "unaffected"],
];
for (const s of row) print(D, ...s);
console.log(`manual items if the secret is identical across all devices ${count}, if separate 1`);
```

```
80 devices ask a single service, 1348 connection points, 957 connected ends, 43 revoked ends
chain 6 items, per device; 480 items in the domain

1. load by device type
type       devices  points    ends  revoked   items
switch          33     468     390       14     198
wireless        44     760     471       23     264
remote           3     120      96        6      18

2. same network, four fallback modes, service unreachable
mode               new paths  revoked paths  stopped ends    reach
closed                     0              0           957     0/16
open                    1348             43             0    16/16
critical-segment        1348             43             0     2/16
cache                      0             43             0    16/16

3. today's mode mix across devices, with outcome
device                        mode               new paths  revoked paths  stopped ends
central-edge-switch           closed                     0              0           131
central-wireless-ap           cache                      0              9             0
branch-edge-switch            critical-segment         216              7             0
branch-wireless-ap            cache                      0             11             0
field-remote-access-endpoint  closed                     0              0            96
warehouse-edge-switch         open                      60              2             0
meter-collection-switch       closed                     0              0            44
guest-wireless-ap             open                     160              3             0
total                         -                        436             32           271

4. what drops if the shared secret is compromised, and renewal
item                                 count   dropped               renewal
a single device's secret                 1         1     1 device manually
devices sharing the same secret         80        80   80 devices manually
affected points                       1348      1348         automatically
service certificate                      1        80   80 devices manually
end credentials                        957         0            unaffected
manual items if the secret is identical across all devices 80, if separate 1
```

The numbers are of the **measurement** class; their inputs are EI8–EI10 and the device inventory.

#### Chain and Load

The header line gives the size of the dependency: 80 devices ask a single service, 1348
connection points depend on that answer, and there are 957 currently connected ends. For the
decision to be made, six items per device must be correct at the same time — the shared secret,
the service's certificate, the end's credential, the service's directory link, the policy record,
and the segment mapping. None of the total 480 items in the domain holds on its own.

The fourth of these six items is a chained dependency and ties the previous two lessons in here.
The service does not know the end's identity itself; it asks the central directory. That is, behind
the decision between a cable being plugged in and a port being opened stand **two** roots: the
secret between the device and the service, and the link between the service and the directory.
The first is this lesson's subject, the second was measured in the first lesson, where the number
of systems stalling during the directory outage was 13; this lesson adds 1348 connection points
to that list.

The load is not evenly distributed. Wireless devices carry more than half the device count with
44, and 56% of the connection points with 760 points. The remote access endpoint is only three
devices but holds 120 points and 96 ends; the number of points per device is 40, and this is the
densest item on the list. A single device's misconfiguration produces a result proportional not
to the device count, but to the number of points it carries.

#### Four Behaviors When the Service Is Unreachable

The second table runs the same network through four fallback behaviors. In the closed behavior,
no unverified device can get in — new paths zero, revoked paths zero — but **957 ends stop**.
This row is the safest and most expensive choice on the list; an authentication service's outage
turns into the whole network's outage.

The open behavior is the exact opposite: no legitimate end stops, but all 1348 points open to an
unverified device, and the opened connection reaches 16 of 16 systems. The critical-segment
behavior opens **the same 1348 paths**; the difference is that the opened connection reaches only
two of the 16 systems. This is EI10's measure: the two rows' path counts are identical, their
outcomes are not. A measurement that writes path counting without a reach column cannot tell these
two rows apart.

The cache behavior looks better than both at first glance: new paths zero, stopped ends zero. Its
cost is in the third column — 43 ends whose access was revoked keep entering the network. This is
the third appearance of the same pattern counted as the cache window in the first lesson and the
ticket lifetime in the second lesson. **Caching a decision delays the decision's revocation by the
caching duration**; the mechanism changes, the number lands in the same place.

#### Today's Mix

The third table shows the behavior is not chosen uniformly across the network. Of the eight device
clusters, four are set to closed, two to cache, one to critical segment, one to open. In total 436
new paths stay open, 32 revoked paths pass through, and 271 ends stop.

The number 436 cannot be read on its own. 216 of it comes from the branch switches assigned to
critical segment, and those paths reach two systems; the remaining 220 come from the two device
clusters set to open, and they reach all 16 systems. When multiplied by reach, the gap opens up:
while the path counts are almost equal, 216 of critical segment reaches only two systems, while
220 of open reaches all sixteen. Leaving the guest wireless access points open can be a reasonable
decision, but its reasonableness rests on being bound to a separate segment, and as long as that
segment is not written in the table, the decision's correctness cannot be measured.

The 271 stopped ends do not come from one place either: 131 from central switches, 96 from the
remote access endpoint, 44 from the meter collection switch. The remote access endpoint being set
to closed behavior means all 96 field ends going connection-less during a service outage. **A
fallback behavior looks like a security setting; in reality it is part of the outage plan.**

#### What a Second Service Changes

The entire fallback table rests on a single assumption: the service is unreachable. When a second
authentication service is added, this assumption becomes rarer, and the 436 new paths disappear
along with the 271 stopped ends. The gain is real, but its cost is paid at the root itself.

For the second service to be able to make the same decisions, it needs to know the same shared
secrets. That is, the secret of 80 devices sits in two places, and the number of places the root
resides doubles. In the first lesson, what was replicated was a record, and copying it did not
change the record's correctness; what is replicated here is a **secret**, and every time a secret
is copied, the compromise surface grows. The renewal bill also doubles unavoidably: 80 devices
plus two service configurations.

**If the item replicated for availability is a record, the cost is management; if it is a secret,
the cost is surface.** The second-service decision is made between these two sentences, and the
decision's correctness depends on whether the secrets are kept separate per device.

#### If the Shared Secret Is Compromised

The fourth table counts the root's compromise. If a single device's secret is compromised, the
item dropped is one, and renewal ends with touching that one device. The problem is how many
devices use the same secret: if it is the same across all devices, the dropped item rises to 80,
and renewal means touching 80 devices **manually**. When a separate secret is kept per device,
the same event's renewal cost is one.

The service certificate row is this lesson's inverse item. The certificate is a single item, but
when it changes, the configuration of the 80 devices that trust it is updated; the number grows
from 1 to 80 not in the certificate itself but in the number of items that reference it. This is
the counterpart here of the chain of trust measured in the previous topic — if devices recognize
the certificate directly instead of binding it to a chain, every step of the certificate's
lifecycle turns into an 80-item job.

The end credentials row drops zero. The identity of the ends sits in the directory and has
nothing to do with the shared secret; root compromise serves to impersonate the device's **right
to ask**, not to capture the end's identity. The two roots being separate keeps the compromise
separate too.

#### Summary

- The 80 devices at the access layer hand their decision to a single central service; 1348
  connection points and 957 connected ends depend on that answer.
- The decision chain is 6 items per device, and the fourth item is the service's directory link;
  this bond chains the root measured in the first lesson to here.
- Fallback behaviors are extremes: closed behavior opens 0 paths but stops 957 ends, open
  behavior opens 1348 paths and stops no ends.
- The same 1348 paths reach 2 of 16 systems under critical segment and 16 of 16 under open; path
  count cannot be ranked without a reach column.
- In today's mix, 436 new paths stay open, and 220 of them — heavy by reach — come from two
  device clusters; renewing the one shared secret means manually touching 80 devices.

#### Next Step

This lesson measured the access decision **at network entry**. A user who has entered the network
connects to dozens of applications through the day, and is not asked to authenticate separately in
each; they authenticate once, and that authentication is carried to the applications with an
**assertion**. The identity provider's signing key that produces the document is, this
time, a single root for all of the applications. The next lesson measures federation on this axis:
how many applications are bound to a single provider, how many applications drop if the root is
compromised, how long the assertion lives, and how quickly closing one account takes effect
in which application.

### Single Sign-On

Source: https://academia.sh/en/courses/identity-and-cryptography/single-sign-on

Measuring federation along the axis of the assertion: 15 of 18 applications bound to a single provider and 1562 user connections resting on 90 items, the revocation window determined not by the assertion's 5-minute lifetime but by the application's local session reaching 72 hours, and key rotation producing a manual item and an outage in 5 applications.

The previous lesson measured the access decision at network entry: 80 devices, 1348 connection
points, a single service. A user who has entered the network, though, connects to dozens of
applications throughout the day and is not expected to authenticate separately at each one.
**Single sign-on** meets that need: the user authenticates once, the result of that
authentication is carried to applications as an **assertion**, and each application verifies
the assertion on its own.

The assertion's production, signature and issuer verification, audience and time window, and
the conversion from attribute to role were measured in the **Authentication and Authorization**
course; they are not repeated here. What this lesson measures is enterprise scale: how many
applications are bound to a single provider, how many applications fall if the root breaks, how
long the assertion lives, and how long it takes for closing an account to take effect in which
application.

#### One Root, Fifteen Applications

**EI11:** in single sign-on, the application does not authenticate the user itself; it accepts an
assertion signed by the identity provider. The provider's signing key is the **shared** trust
root of every application bound to it. **EI12:** the assertion's lifetime and the application's
own session lifetime are separate numbers; the second one determines how long revocation takes
to take effect. **EI13:** rotating the root key is free in an application that pulls the key
itself; in an application where the key is loaded manually or pinned into configuration, it is a
manual item and an outage.

EI11 is enterprise identity's single biggest gain. The user keeps one password, account closing
happens in one place, and multi-factor authentication is applied at one point. The same design
shows the course's measure at its sharpest: the source of the gain and the source of the
fragility are **the same item**.

#### Eighteen Applications

The inventory below is a **model**: the applications of a fictional regional measurement network
organization. There is no real organization, no real provider, and no real key.

```js
// sso/apps.mjs — the applications of a fictional regional measurement network organization and
// how they bind to the central identity provider (model): no real organization, no real
// provider, and no real key. No randomness.

// EI11: in single sign-on, the application does not authenticate the user itself; it accepts
// an assertion signed by the identity provider. The provider's signing key is the shared
// trust root of every application bound to it.
// Columns: mode = federated (entered only through the provider), mixed (federated and local
// account together), local (not bound to the provider); assertionMin = the assertion's
// validity period, minutes; sessionHour = how long the application holds its own session;
// key = how the signing key reaches the application (pool: pulled from the key set; manual:
// loaded by hand; pinned: fingerprint pinned into configuration).
export const APP = `
  billing                federated   142   5    8    pool
  work-order             federated   118   5   12    pool
  meter-archive          federated    64   5    4    pool
  report-portal          federated   173   5    8    pool
  field-mobile-endpoint  federated   140   5   72    manual
  asset-inventory        federated    47   5    8    pool
  hr-portal              federated   206   2    2    pool
  expense-approval       federated    88   5    8    pinned
  document-archive       federated   151   5   24    pool
  training-portal        federated    96  10   24    manual
  procurement-portal     mixed        39   5    8    manual
  maintenance-plan       mixed        55   5   12    pool
  call-center            mixed       104   5    4    pinned
  ticketing-system       federated    77   5    8    pool
  data-dashboard         federated    62   5    2    pool
  firmware-server        local        11   0    0    none
  backup-console         local         7   0    0    none
  legacy-scada-console   local         9   0    0    none`
  .trim().split("\n").map((r) => {
    const p = r.trim().split(/\s+/);
    return { name: p[0], mode: p[1], users: +p[2], assertionMin: +p[3], sessionHour: +p[4], key: p[5] };
  });

// EI12: the assertion's lifetime and the application's own session lifetime are separate
// numbers; the second one determines how long revocation takes to take effect.
// EI13: rotating the root key is free in an application that pulls the key itself; in one
// that is loaded manually or pinned, it is a manual item and an outage.
export const CHAIN = ["signing key", "key distribution point", "audience record",
  "attribute mapping", "time window", "local session record"];
export const federated = (u) => u.mode !== "local";
```

```js
// sso/tally.mjs — how many applications are bound to a single provider, how many applications fall
// if the root breaks, what the assertion's lifetime is, how many hours revocation takes to take effect, and what key rotation costs.
import { APP, CHAIN, federated } from "./apps.mjs";

const print = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const sum = (f, s = APP) => s.reduce((a, x) => a + f(x), 0);
const bound = APP.filter(federated);
const local = APP.filter((u) => !federated(u));

console.log(`${APP.length} applications, ${bound.length} of them bound to a single provider, ` +
  `${sum((u) => u.users, bound)} user connections, chain ${CHAIN.length} items`);
console.log(`items bound to the root ${bound.length * CHAIN.length}, ` +
  `applications not bound to the root ${local.length} (${sum((u) => u.users, local)} accounts)`);

const A = [-10, 10, 10, 13, 19];
console.log("\n1. by binding mode");
print(A, "mode", "apps", "users", "root breaks", "assertion min avg");
for (const k of ["federated", "mixed", "local"]) {
  const s = APP.filter((u) => u.mode === k);
  const dk = s.filter((u) => u.assertionMin > 0);
  print(A, k, s.length, sum((u) => u.users, s), k === "local" ? 0 : s.length,
    dk.length ? (sum((u) => u.assertionMin, dk) / dk.length).toFixed(1) : "-");
}

const B = [-16, 10, 11, 21, 17];
console.log("\n2. revocation window: how long access lasts after an account is disabled");
print(B, "window", "apps", "users", "assertion min left", "session hr left");
for (const [name, lo, hi] of [["0-4 hr", 0, 4], ["5-12 hr", 5, 12],
  ["13-24 hr", 13, 24], ["25+ hr", 25, 999]]) {
  const s = bound.filter((u) => u.sessionHour >= lo && u.sessionHour <= hi);
  print(B, name, s.length, sum((u) => u.users, s), sum((u) => u.assertionMin, s),
    sum((u) => u.sessionHour, s));
}
const longest = bound.reduce((a, u) => (u.sessionHour > a.sessionHour ? u : a));
console.log(`longest window ${longest.sessionHour} hr (${longest.name}), ` +
  `assertion lifetime ${longest.assertionMin} minutes; ratio ${(longest.sessionHour * 60 / longest.assertionMin)}x`);

const C = [-8, 10, 11, 13, 13];
console.log("\n3. what rotating the signing key costs");
print(C, "key", "apps", "users", "manual item", "outage apps");
for (const a of ["pool", "manual", "pinned"]) {
  const s = bound.filter((u) => u.key === a);
  print(C, a, s.length, sum((u) => u.users, s), a === "pool" ? 0 : s.length,
    a === "pool" ? 0 : s.length);
}

const D = [-26, 8, 10, 22];
console.log("\n4. items that fall if the root breaks");
print(D, "item", "count", "fallen", "renewal");
const manual = bound.filter((u) => u.key !== "pool").length;
for (const s of [
  ["provider-bound apps", bound.length, bound.length, "trusted again"],
  ["user connections", sum((u) => u.users, bound), sum((u) => u.users, bound), "re-login"],
  ["signing key", 1, 1, "1 item produced"],
  ["manually carried key", manual, manual, `${manual} apps manually`],
  ["apps with local accounts", local.length, 0, "unaffected"],
]) print(D, ...s);
```

```
18 applications, 15 of them bound to a single provider, 1562 user connections, chain 6 items
items bound to the root 90, applications not bound to the root 3 (27 accounts)

1. by binding mode
mode            apps     users  root breaks  assertion min avg
federated         12      1364           12                5.2
mixed              3       198            3                5.0
local              3        27            0                  -

2. revocation window: how long access lasts after an account is disabled
window                apps      users   assertion min left  session hr left
0-4 hr                   4        436                   17               12
5-12 hr                  8        739                   40               72
13-24 hr                 2        247                   15               48
25+ hr                   1        140                    5               72
longest window 72 hr (field-mobile-endpoint), assertion lifetime 5 minutes; ratio 864x

3. what rotating the signing key costs
key           apps      users  manual item  outage apps
pool            10       1095            0            0
manual           3        275            3            3
pinned           2        192            2            2

4. items that fall if the root breaks
item                         count    fallen               renewal
provider-bound apps             15        15         trusted again
user connections              1562      1562              re-login
signing key                      1         1       1 item produced
manually carried key             5         5       5 apps manually
apps with local accounts         3         0            unaffected
```

The numbers are of the **measurement** class; their inputs are EI11–EI13 and the application
inventory.

#### How Many Applications Are Bound to a Single Root

The header lines give the scope: 15 of 18 applications are bound to a single provider, 1562
user connections pass through that bond, and when six items are counted per application, a
total of 90 items must be correct at the same time. The remaining three applications are not
bound to the provider and keep 27 accounts on their own list.

The first table separates the modes. Twelve applications are entered only through the provider,
three accept both the provider and their own local account. Mixed mode is put in place as an
outage measure — when the provider goes down, 198 users can enter with a local account. The
same item works in the opposite direction too: when a person is closed out at the provider,
their local account in those three applications does not close. **Mixed mode raises availability
and lowers revocation**; both come from the same row.

Local mode is the same pattern seen in the first lesson, in a different shape. Three
applications are independent of the root and zero fall if the root breaks; against that,
closing the 27 accounts happens through no central operation at all. Staying outside the root
is not protection — it is adding a separate point of failure. These three applications' 27
accounts appear in no central report in the organization's identity inventory and have to be
remembered by hand when the closing list is prepared; when they are not remembered, the number
of accounts left behind is 27, and that number reads on no dashboard.

#### The Chain's Six Items

Only one of the six items counted per application is the key; the remaining five are records
standing around the key that must hold true at the same time. The key distribution point says
where the application gets its signing key from; if that address is unreachable, verification
cannot happen. The audience record binds **which application** the assertion was produced for;
when it is written wrong, an assertion produced for one application is accepted by another.

Attribute mapping translates the field names coming from the provider into the fields the
application expects, and a wrong mapping sets up identity correctly but authorization wrong.
The time window comes up here for the third time: the assertion's five-minute lifetime is
measured against both sides' clocks, and when the clocks drift apart, a valid assertion is
rejected. The sixth item is the application's own session record, and the entire next section
is about it.

For fifteen applications, a total of 90 items is the number of independent records the
organization must keep correct in order to get a single person into a single application.
**Eighty-nine** of these items are not cryptographic; only one, the key at the top of the list,
is both cryptographic and the one forming the attack surface. **The root is a single item, the
root's surroundings are eighty-nine items**, and the entire operating burden is on the second
number.

#### The Revocation Window

The second table is the center of this lesson. The assertion's lifetime is five minutes in most
applications, and that number is often read as "the bond with the identity provider refreshes
every five minutes." It is not. The assertion only carries the **login**; after the application
accepts the login it opens its own session, and on every subsequent request it looks at that
session, not the assertion.

That is why, when an account is disabled, the number that determines how long access takes to
end is the application's session lifetime, not the assertion's. In four applications that
period is under four hours and covers 436 users; in eight applications it is between five and
twelve hours. The longest window is at the field mobile endpoint: **72 hours**. In that same
application the assertion's lifetime is five minutes; the ratio between the two numbers is 864
times.

The number's meaning is direct. A person who leaves the organization or whose account is
suspended keeps working in that application for three more days despite their record being
closed in the central repository, and no log shows this as an error. **The federation's
revocation speed is the speed of the application holding the longest session lifetime**;
averages say nothing at this scale. At enterprise scale, the way to speed up revocation is not
shortening the assertion — it is shortening applications' session lifetimes or requiring regular
re-authentication; both get written to the user as a few extra steps a day.

#### The Cost of Rotating the Key

The third table counts EI13. Ten applications pull the signing key themselves from the key set
the provider publishes; when the provider adds a new key, nothing happens in these applications
and the manual item is zero. In the remaining five applications the key is either loaded
manually or its fingerprint is pinned into configuration; rotation produces one manual item and
one outage in each of these five.

The number five covers 467 of the 1562 users. The rotation plan is built around these five
applications: the new key is published, configuration is updated in five applications, and then
the old key is removed. There is a window in the middle of these three steps, and if the old key
is dropped before the window closes, login stops in five applications.

The number determines how often the organization renews the root. In a portfolio with zero
manual items, key rotation is a routine operation; in a portfolio with five manual items, it
requires a planned maintenance window and gets postponed. **Root renewal frequency is not a
property of the root — it is a property of the automation rate of the items bound to it.**

#### If the Root Breaks

The fourth table counts the break. When the signing key is compromised, all 15 of the 15
applications bound to the provider and all 1562 user connections fall. The assertion states on
its own which user it was produced for; the party who knows the key can produce a valid
assertion for any user it wants, and doing so **needs no password and no second factor**. This
is the application-layer counterpart of the pattern seen in the ticket-granting service's key in
the second lesson.

The renewal bill is surprisingly small: the produced key is a single item. The bill's real item
is getting the new key to 15 applications — automatic in ten, manual in five. At the moment of a
breach, the five manual items are done immediately rather than waiting for a maintenance window,
and an outage in those five applications is unavoidable.

The three local applications again fall zero. In a root breach this looks like an advantage, but
in that same breach the organization's first task is to re-authenticate 1562 connections, and
the 27 accounts of those three applications stay outside that operation: they neither fall nor
get renewed. Being outside central control is not being outside the incident. In the inventory
made after the breach, these three applications never enter the list, and because they do not
enter it, they are never reviewed again; an item left outside the root's scope stays outside the
root breach's **investigation** too.

#### Summary

- 15 of 18 applications are bound to a single identity provider; 1562 user connections rest on a
  chain of 90 items.
- Mixed mode raises availability and lowers revocation: when the provider goes down 198 users can
  log in, while an account closed at the provider stays open in those applications.
- The application's session lifetime, not the assertion's 5-minute lifetime, determines how long
  revocation takes to take effect; the longest window is 72 hours and the ratio between the two
  numbers is 864 times.
- Rotating the signing key costs zero in 10 applications and one manual item plus one outage each
  in 5 applications; this ratio determines root renewal frequency.
- When the root breaks, 15 applications and 1562 connections fall; the newly produced key is 1
  item, but it is carried to 5 applications by hand.

#### Next Step

This lesson counted a single authentication carried to 15 applications; that single
authentication itself still rests on a password. If the password is known, all 1562 connections
open in a single step. That is why a second factor is added to enterprise login, and **which
path** the added factor closes is a separate question: not every factor type closes the same
paths, and none closes all of them. The next lesson measures factor types by the paths they close and
counts how many bypass paths stay open.

### Multi-Factor Authentication

Source: https://academia.sh/en/courses/identity-and-cryptography/multi-factor-authentication

Measuring factor types by the path they close: in a set of 10 paths, no factor type closing more than 6, 4 paths closing with none of the six types, using two factors that close the same path together not increasing the closed path count, and closing costing between 428 and 856 registration items for 214 people, 9 to 31 recovery requests, and up to 41 left-out endpoints.

The previous lesson counted a single authentication carried to 15 applications, and that single
authentication opening 1562 connections. Authentication itself still rested on a password: if
the password is known, all of the connections open in a single step. **Multi-factor
authentication** splits that single step in two and ties login to a second proof.

Factor classes' independence, the derivation of the time-based code, and the storage of
recovery codes were measured in the **Authentication and Authorization** course; they are not
repeated here. This lesson asks a single question: which factor type closes **which path**, and
how many paths stay open. What is measured is not how a path would be walked, but whether a given
type closes it or not; nowhere in this lesson is there a working sequence of steps. Attempting
such a thing without written authorization and a defined scope is both illegal and
unprofessional.

#### A Factor Narrows a Path Set

**EI14:** a factor type closes a **subset** of account takeover paths; the set it closes is a
property of its type, not its strength. **EI15:** no factor type closes every path; the paths
that stay unclosed sit on registration and recovery processes, not on the factor. **EI16:**
using two factors that close the same path together does not increase the number of closed paths
— it only increases the cost.

Measurement runs on three columns. The first is the number of closed paths, the second the
number of paths left open, the third the cost. The cost reads in three items: registration steps
per person, lost-factor requests opened per year, and the number of endpoints left out because
they cannot carry the factor. **A defense whose cost is not written down counts as unmeasured**;
this rule runs through M24 and holds here too.

#### Ten Paths, Six Factor Types

The path set below is a **model**: it assumes a set of 214 accounts at a fictional regional
measurement network organization. There is no real organization, no real account, and no working
sequence of steps.

```js
// factor/paths.mjs — account takeover paths and which paths factor types close (model):
// no real organization, no real account, and no working sequence of steps; only which factor
// type closes which path is counted. No randomness.

// EI14: a factor type closes a subset of a path set; the set it closes is a property of
// its type, not its strength. EI15: no factor type closes every path; the paths that stay
// unclosed sit on registration and recovery processes, not on the factor.
// The class column tells which stage the path operates at.
export const PATH = `
  password-reuse                  credential
  password-guessing               credential
  password-harvesting-page        credential
  real-time-proxy-page            auth-moment
  approval-fatigue                auth-moment
  channel-interception            factor-channel
  backup-recovery-channel         recovery
  post-auth-session                post-auth
  registration-compromise         registration
  process-based-reset             process`
  .trim().split("\n").map((r) => {
    const p = r.trim().split(/\s{2,}/);
    return { name: p[0], cls: p[1] };
  });

// closes = the path indices the factor type closes; registration = registration steps per
// person; recovery = lost-factor requests opened per year for 214 people; leftOut = the
// number of endpoints that cannot carry the factor.
export const FACTOR = {
  "password":           { closes: [], registration: 1, recovery: 0, leftOut: 0 },
  "sms-code":           { closes: [0, 1, 2, 4], registration: 2, recovery: 18, leftOut: 6 },
  "time-code":          { closes: [0, 1, 2, 4, 5], registration: 3, recovery: 31, leftOut: 12 },
  "push-notification":  { closes: [0, 1, 2, 5], registration: 3, recovery: 27, leftOut: 24 },
  "matching-approval":  { closes: [0, 1, 2, 4, 5], registration: 3, recovery: 27, leftOut: 24 },
  "hardware-factor":    { closes: [0, 1, 2, 3, 4, 5], registration: 4, recovery: 9, leftOut: 41 },
};

export const PEOPLE = 214;
export const closedBy = (...t) => new Set(t.flatMap((x) => FACTOR[x].closes));
```

```js
// factor/tally.mjs — how many paths each factor type closes, how many stay open, which
// paths close with no factor at all, and what closing costs in terms of registration,
// recovery, and left-out endpoints.
import { PATH, FACTOR, PEOPLE, closedBy } from "./paths.mjs";

const print = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const types = Object.keys(FACTOR);

console.log(`${PATH.length} paths, ${types.length} factor types, ${PEOPLE} people`);

const A = [-20, 9, 8, 11, 11, 10];
console.log("\n1. paths closed and left open per factor type");
print(A, "factor", "closed", "open", "reg items", "recovery", "left out");
for (const t of types) {
  const f = FACTOR[t];
  print(A, t, f.closes.length, PATH.length - f.closes.length,
    PEOPLE * f.registration, f.recovery, f.leftOut);
}

const B = [-28, -18, 10, 12];
console.log("\n2. per path: how many factor types close it");
print(B, "path", "class", "closing", "left open");
for (let i = 0; i < PATH.length; i++) {
  const closing = types.filter((t) => FACTOR[t].closes.includes(i));
  print(B, PATH[i].name, PATH[i].cls, closing.length, types.length - closing.length);
}
const none = PATH.filter((_, i) => types.every((t) => !FACTOR[t].closes.includes(i)));
console.log(`paths closed by no factor ${none.length}/${PATH.length}: ` +
  none.map((p) => p.cls).join(", "));

const C = [-37, 9, 8, 11];
console.log("\n3. if two factors are used together");
print(C, "combination", "closed", "open", "reg items");
for (const [a, b] of [["sms-code", "time-code"], ["time-code", "matching-approval"],
  ["push-notification", "time-code"], ["matching-approval", "hardware-factor"]]) {
  const c = closedBy(a, b);
  print(C, `${a} + ${b}`, c.size, PATH.length - c.size,
    PEOPLE * (FACTOR[a].registration + FACTOR[b].registration));
}

const D = [-20, 9, 12, 15, 15];
console.log("\n4. cost per closed path");
print(D, "factor", "closed", "reg/path", "recovery/path", "left out/path");
for (const t of types.filter((x) => FACTOR[x].closes.length > 0)) {
  const f = FACTOR[t], n = f.closes.length;
  print(D, t, n, (PEOPLE * f.registration / n).toFixed(1), (f.recovery / n).toFixed(1),
    (f.leftOut / n).toFixed(1));
}
```

```
10 paths, 6 factor types, 214 people

1. paths closed and left open per factor type
factor                 closed    open  reg items   recovery  left out
password                    0      10        214          0         0
sms-code                    4       6        428         18         6
time-code                   5       5        642         31        12
push-notification           4       6        642         27        24
matching-approval           5       5        642         27        24
hardware-factor             6       4        856          9        41

2. per path: how many factor types close it
path                        class                closing   left open
password-reuse              credential                 5           1
password-guessing           credential                 5           1
password-harvesting-page    credential                 5           1
real-time-proxy-page        auth-moment                1           5
approval-fatigue            auth-moment                4           2
channel-interception        factor-channel             4           2
backup-recovery-channel     recovery                   0           6
post-auth-session           post-auth                  0           6
registration-compromise     registration               0           6
process-based-reset         process                    0           6
paths closed by no factor 4/10: recovery, post-auth, registration, process

3. if two factors are used together
combination                             closed    open  reg items
sms-code + time-code                         5       5       1070
time-code + matching-approval                5       5       1284
push-notification + time-code                5       5       1284
matching-approval + hardware-factor          6       4       1498

4. cost per closed path
factor                 closed    reg/path  recovery/path  left out/path
sms-code                    4       107.0            4.5            1.5
time-code                   5       128.4            6.2            2.4
push-notification           4       160.5            6.8            6.0
matching-approval           5       128.4            5.4            4.8
hardware-factor             6       142.7            1.5            6.8
```

The numbers are of the **measurement** class; their inputs are EI14–EI16 and the path set.

#### Closed Path, Open Path

The first table runs all six types over the same path set. Password alone closes zero paths;
all ten of the ten paths are open. When a second factor is added, the number of closed paths
rises to four or five, and **it does not exceed six in any type**. The origin-bound hardware
factor, the most comprehensive type, closes six paths and leaves four open.

The first three of the closed paths are the same in every type: the password being known from
somewhere else, being guessed, or being typed only into a password-harvesting page. The second
factor closes these three because what is missing in all three is the second proof. In enterprise
login, these three rows are the real gain multi-factor authentication provides, and type choice
makes no difference for these three.

The difference starts after the fourth row. The real-time proxy page — a middleman that forwards
the code the user enters straight to the real login at the same moment — is closed by only
**one** type: the origin-bound hardware factor. The reason is not that the type is stronger; it is
that the proof it produces is bound to **the address it is presented to**; when presented to a
different address, the proof is not valid. Any type that reads and types a code cannot test who is
asking for the code.

The fifth and sixth rows knock out two types one at a time. Approval fatigue — repeated,
unrequested approval prompts — is only open with instant push notification; a prompt like that
never arises with types that ask for a code. Channel interception, in turn, is only open with
the SMS code, because the channel itself is the factor's carrier. **Each type opens a path born
of its own mechanics**, and that path is as much a property of the type as the paths it closes.

The fourth row's weight comes from the previous lesson. In the organization, 15 applications are
bound to a single identity provider; the second factor is applied once, at that provider's login
page, and the path closed there closes for all 15 applications. In the same way, the path that
**stays open** there stays open for all 15 applications too. This is centralization's
counterpart in this lesson: the type choice is made at a single point and its result is
multiplied across 1562 connections. The two-path difference between four and six is not two paths
in a single account — it is two paths across the entire organization.

#### The Four Paths No Factor Closes

The second table is this lesson's most important result. Four of the ten paths are closed by
**none** of the six types: the backup recovery channel, use of the post-authentication session,
the registration moment being compromised, and process-based reset. All four sit outside the
factor.

The class column gives the reason. The recovery path is the backup channel that kicks in when
the factor is lost, and if that channel is weaker than the second factor, the account's strength
drops to the backup channel's strength. The post-authentication session is the object produced
after login completes; the factor has finished its job by that point, and the 72-hour session
window from the previous lesson is exactly the size of this row. The registration moment is when
the second factor **first binds** to the account: if it is bound to the wrong person at that
moment, the factor treats the wrong person as correct on every subsequent login. Process-based
reset, meanwhile, is not a technical item — it is the existence of a person who can reset the
factor.

**The strength of multi-factor authentication is the strength of its weakest recovery path.**
Factor type choice determines the first six rows; what determines the last four rows is how
registration and recovery processes are written, and that is not an item that can be bought.

Four rows share one more property: none of them show up on the login screen. The six closed
paths can be checked off and reported on an audit list; the four unclosed ones can only be found
by searching through process documents. An organization reporting its multi-factor
authentication coverage as one hundred percent means these four rows were never asked about at
all. A coverage report measures six paths at best, not ten; the four-row gap between them never
gets written into the report, and because it is unmeasured, it belongs to no one.

#### This Mechanism's Trust Root

The course's first column shows up in an unexpected place here. Multi-factor authentication's
trust root is not the factor itself — it is **the registration moment where the factor binds to
the person**. At registration, the organization must hold proof that whoever is registering is
really that person; without that proof, every subsequent login works flawlessly and treats the
wrong person as correct.

The chain from the root to use is four items: identity proof at the moment of registration, the
record of the factor's binding to the account, the authority governing changes to that binding,
and the recovery record. All four sit in the directory; which means this lesson's root rests on
the integrity of the repository measured in the first lesson. Among the 646 items that must be
renewed when repository integrity breaks, these four are included, and they are the most
expensive to renew — renewing a factor binding requires physically re-recognizing the person.

This is why the second table's registration row closes with no factor at all. **A mechanism
cannot verify its own root**; the factor carries no item that would test whether it is bound to
the right person. The correctness of the registration is the output of a process outside the
mechanism, and that process rests on none of the keys this course measures.

#### Two Factors Are Not Better Than One

The third table counts EI16. Using the SMS code together with the time-based code closes five
paths; the time-based code alone also closes five paths. The closed path is the same; the
registration item rises from 642 to 1070. The combination of matching approval and the
time-based code also stays at five.

The reason is simple: if two types close the same subset, their combination is that subset. No
combination in the table closes more than the widest type it contains closes on its own:
matching approval with the hardware factor closes six, and the hardware factor alone also closes
six. **Duplicating a factor does not close paths; adding a type whose closed set differs does.**

This does not mean two factors are useless. A second type keeps access going when the first cannot
be used, and lowers the number of recovery requests — that is, it gets written to the recovery
column in the first table, not the closed-path column. A decision that does not separate the two
columns doubles the cost while leaving the closed path unchanged.

#### The Cost of Closing

The fourth table divides the cost per closed path. The SMS code costs 107.0 registration items
and 1.5 left-out endpoints per path closed; the hardware factor costs 142.7 registration items
and 6.8 endpoints. The left-out endpoint count for the hardware factor rises to 41, and this is
a portion of the legacy consoles and field devices counted in the first lesson.

There is one more item the table does not cover, and it belongs to coverage more than to cost. The
214 accounts counted are people's accounts; the 21 service accounts counted in the second lesson
are outside this measurement. A second factor cannot be placed in front of a service account,
because there is no one on the other side to give approval. On the organization's identity
surface, multi-factor authentication covers **interactive logins**, and every non-interactive
account stays open to the first three of the ten paths. The coverage ratio is 214/235, and each
of the 21 accounts left out represents an entire service, as measured in the second lesson.

The recovery column reads in the opposite direction. Nine requests are opened per year for the
hardware factor; thirty-one for the time-based code. The difference between them comes from how
often devices get replaced, and every request opens the recovery path from the second table one
more time. **The number of recovery requests is the measure of how many times a year the
unclosable fourth path gets used**; that is why a low recovery count matters as much as a high
closed-path count.

#### Summary

- A factor type closes a subset of the ten-path set; no type closes more than 6, and password
  alone closes 0.
- The first three paths close in every type; only the origin-bound hardware factor closes the
  fourth path, because the proof it produces is bound to the address it is presented to.
- Each type opens a path from its own mechanics: approval fatigue stays open in the notification
  type, channel interception in the SMS type.
- 4 of the ten paths close with none of the six types; all four are in the registration,
  recovery, post-authentication session, and process classes.
- Using two factors that close the same set together does not increase the closed path; it raises
  the registration item from 642 to 1284, and the gain is written only to the recovery column.

#### Next Step

This lesson measured login itself. What the user **can do** after login is determined by
another mechanism: the organization writes, from a single place, which setting is to apply
across thousands of machines, and the machines pull what is written at regular intervals. The gap
between what is written and what is applied directly affects a portion of the items in this
lesson's path set. The next lesson measures central enforcement in terms of propagation: how
many machines the policy reaches in how many rounds, how many items it never reaches, and how
many paths local override opens.

### Group Policies and Authorization Management

Source: https://academia.sh/en/courses/identity-and-cryptography/group-policies-and-authorization-management

Measuring central enforcement in terms of propagation: 319 of 325 machines being set to pull policy but only 295 of them being reached within the 24-hour window, first-round coverage staying at 51.4%, 30 machines never receiving the policy at all, and local overrides on 67 machines dropping 5 of 12 settings to open 335 paths.

The previous lesson measured sign-in itself and counted the four paths no factor closes. What a
user can do once sign-in is complete, though, is determined by a separate
mechanism: the organization writes, from a single place, which setting applies on thousands of
machines, and the machines pull it at regular intervals. This mechanism is called **group
policy**.

Comparing authorization models — role-based, attribute-based, resource-based — was measured in
the **Authentication and Authorization** and **Cloud Governance, Security, and Cost** courses;
it is not repeated here. What this lesson measures is a single question: the gap between what is
written and what is applied. How many machines does the policy reach in how many rounds, how many
items never reach it, and how many paths does local override open. These three numbers give how
much the central management actually governs, and all three are smaller than the number the
central console shows.

#### Writing versus Applying

**EI17:** central policy is not a write operation but a **propagation process**; it takes effect
not the moment it is written but the moment a machine pulls it on its next round. **EI18:** a
machine left outside propagation never receives the policy at all, and no error log reports
this — centrally, the policy shows as "applied." **EI19:** a setting being overridable locally
reopens, per machine, the path that setting closes; central enforcement's coverage is measured by
the number of settings that cannot be overridden.

These three claims turn the course's three columns into this mechanism. The trust root is the
policy repository's integrity and the distribution channel; the chain is the items from the
policy reaching a machine to it being applied; the break is how many machines **silently**
receive the wrong setting if the root changes.

#### Three Hundred Twenty-Five Machines

The inventory and propagation below are a **model**: the machines of a fictional regional
measurement network company. There is no real organization, no real machine, and no real policy.
Randomness is generated with our own generator and the seed is visible; the output is the same
from run to run.

```js
// policy/rollout.mjs — the propagation of central policy to machines (model): the machine
// inventory, renewal round, and online rate of a fictional regional measurement network company.
// There is no real organization, real machine, or real policy.

// EI17: central policy is not a write operation but a propagation process; it takes effect not
// the moment it is written but the moment a machine pulls it on its next round. EI18: a machine
// left outside propagation never receives the policy at all, and no error log reports this.
// Columns: count = number of machines; managed = set to pull policy; p = the chance of being
// online in one round (percent); closed = never turns on during the window; localAdmin =
// number of machines with a local administrator account.
export const CLASS = `
  branch-workstation    96   yes   82    3   11
  field-laptop          140   yes   24   17   38
  warehouse-terminal     18   yes   61    1    4
  server                 21   yes   95    0    6
  meter-collector        44   yes   88    2    2
  legacy-console           6   no     0    6    6`
  .trim().split("\n").map((r) => {
    const p = r.trim().split(/\s+/);
    return { name: p[0], count: +p[1], managed: p[2] === "yes", p: +p[3],
      closed: +p[4], localAdmin: +p[5] };
  });

// EI19: a setting being overridable locally reopens, per machine, the path that setting closes;
// central enforcement's coverage is measured by the number of settings that cannot be overridden.
// The policy carries 12 settings; each setting closes one path. The "local" column is whether
// the setting can be overridden locally by a local administrator on the machine.
export const SETTING = `
  screen-lock-timeout          local
  local-admin-membership       central
  unsigned-script-block        local
  legacy-protocol-disable      central
  log-forwarding               local
  disk-encryption-required     central
  software-source-restriction  central
  remote-desktop-disabled      local
  password-length               central
  auto-mount-disabled          local
  firewall-enabled               central
  cached-logon-timeout          central`
  .trim().split("\n").map((r) => {
    const p = r.trim().split(/\s{2,}/);
    return { name: p[0], local: p[1] === "local" };
  });

export const ROUND = 16;      // number of 90-minute renewal rounds in a 24-hour window
export const SEED = 20250131;
```

```js
// policy/tally.mjs — how many machines does the policy reach in how many rounds, how many items
// never reach it, and how many paths does a local override open.
import { CLASS, SETTING, ROUND, SEED } from "./rollout.mjs";

const print = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const sum = (f, s = CLASS) => s.reduce((a, x) => a + f(x), 0);
// Our own generator; the seed is visible, it gives the same sequence from run to run.
const gen = (seed) => { let s = seed >>> 0;
  return () => (s = (s * 1103515245 + 12345) >>> 0) / 4294967296; };

const machines = sum((s) => s.count);
const managed = sum((s) => (s.managed ? s.count : 0));
console.log(`${machines} machines, ${managed} managed, ${machines - managed} unmanaged; ` +
  `${SETTING.length} settings, ${ROUND} rounds, seed ${SEED}`);

// The round each machine first receives the policy on; closed machines never receive it.
const rnd = gen(SEED);
const received = [];
for (const s of CLASS) {
  for (let i = 0; i < s.count; i++) {
    let round = 0;
    if (s.managed && i >= s.closed) {
      for (let t = 1; t <= ROUND; t++) {
        if (rnd() * 100 < s.p) { round = t; break; }
      }
    } else { for (let t = 1; t <= ROUND; t++) rnd(); }
    received.push({ cls: s.name, round });
  }
}

const A = [-20, 8, 10, 10, 12, 10];
console.log("\n1. coverage by class (at the end of the 24-hour window)");
print(A, "class", "count", "managed", "reached", "not reached", "avg round");
for (const s of CLASS) {
  const g = received.filter((a) => a.cls === s.name);
  const u = g.filter((a) => a.round > 0);
  print(A, s.name, s.count, s.managed ? s.count : 0, u.length, g.length - u.length,
    u.length ? (u.reduce((a, x) => a + x.round, 0) / u.length).toFixed(1) : "-");
}
const reached = received.filter((a) => a.round > 0).length;
print(A, "total", machines, managed, reached, machines - reached, "-");

const B = [-6, 10, 10, 11, 15];
console.log("\n2. round-by-round cumulative coverage");
print(B, "round", "reached", "percent", "remaining", "newly reached");
let prev = 0;
for (const t of [1, 2, 3, 4, 6, 8, 12, 16]) {
  const u = received.filter((a) => a.round > 0 && a.round <= t).length;
  print(B, t, u, `${(100 * u / machines).toFixed(1)}%`, machines - u, u - prev);
  prev = u;
}

const C = [-20, 9, 13, 14, 18];
console.log("\n3. how many paths a local override opens");
const localSetting = SETTING.filter((a) => a.local).length;
print(C, "class", "settings", "local admin", "opened paths", "closed remaining");
for (const s of CLASS) {
  const y = s.managed ? s.localAdmin : s.count;
  print(C, s.name, SETTING.length, y, y * localSetting, s.count * SETTING.length - y * localSetting);
}
const yy = sum((s) => (s.managed ? s.localAdmin : s.count));
console.log(`${localSetting} settings can be overridden locally, ` +
  `${SETTING.length - localSetting} cannot; total opened paths ${yy * localSetting}`);

const D = [-28, 8, 8, 24];
console.log("\n4. the third column of enforcement");
const share = (n) => `${(100 * n / machines).toFixed(1)}%`;
print(D, "item", "count", "share", "source");
for (const s of [
  ["machines never reached", machines - reached, share(machines - reached), "closed and unmanaged"],
  ["reached in 16 rounds", reached, share(reached), "online"],
  ["machines with local admin", yy, share(yy), "inventory"],
  ["opened paths", yy * localSetting, "-", `${localSetting} local settings`],
  ["non-overridable settings", SETTING.length - localSetting, "-", "central"],
]) print(D, ...s);
```

```
325 machines, 319 managed, 6 unmanaged; 12 settings, 16 rounds, seed 20250131

1. coverage by class (at the end of the 24-hour window)
class                  count   managed   reached not reached avg round
branch-workstation        96        96        93           3       1.3
field-laptop             140       140       122          18       3.9
warehouse-terminal        18        18        17           1       2.0
server                    21        21        21           0       1.0
meter-collector           44        44        42           2       1.1
legacy-console             6         0         0           6         -
total                    325       319       295          30         -

2. round-by-round cumulative coverage
round    reached   percent  remaining  newly reached
1            167     51.4%        158            167
2            221     68.0%        104             54
3            241     74.2%         84             20
4            258     79.4%         67             17
6            270     83.1%         55             12
8            282     86.8%         43             12
12           291     89.5%         34              9
16           295     90.8%         30              4

3. how many paths a local override opens
class                settings  local admin  opened paths  closed remaining
branch-workstation         12           11            55              1097
field-laptop               12           38           190              1490
warehouse-terminal         12            4            20               196
server                     12            6            30               222
meter-collector            12            2            10               518
legacy-console             12            6            30                42
5 settings can be overridden locally, 7 cannot; total opened paths 335

4. the third column of enforcement
item                           count   share                  source
machines never reached            30    9.2%    closed and unmanaged
reached in 16 rounds             295   90.8%                  online
machines with local admin         67   20.6%               inventory
opened paths                     335       -        5 local settings
non-overridable settings           7       -                 central
```

The numbers are of the **measurement** class; their inputs are EI17–EI19 and the machine
inventory.

#### Coverage Does Not Happen All at Once

The second table turns EI17 into a number. In the first round after the policy is written, it
reaches 167 of 325 machines; coverage is **51.4%**. It rises to 221 in the second round and to
258 in the fourth. By the end of sixteen rounds, 295 machines have received the policy and 30
machines still have not.

The shape of the curve determines the decision. The first two rounds produce 221 of total
coverage — close to three quarters of the reachable 295 — while the remaining fourteen rounds add
only 74 machines. For a team publishing a security setting, these two numbers carry separate
meanings: in an urgent shutdown decision, the first three hours bring most of the coverage, but
**getting the last ten percent takes longer than a day**, and that ten percent holds exactly the
items that are risky.

The first table gives the distribution. Servers complete in the first round, with an average
round count of 1.0. Branch workstations average 1.3, meter collectors 1.1. Field laptops are a
class apart: an average of 3.9 rounds, and 18 machines never receive it at all. What determines
propagation speed is not the policy repository or the network but the machine's **online rate**;
no improvement made centrally changes this number.

#### The Thirty Machines That Never Get It

The fourth table counts EI18. Thirty machines — 9.2% of the inventory — never receive the policy
at all, and this number comes from two separate sources. Twenty-four are machines that never turn
on during the window: field laptops sitting powered off, terminals waiting in the warehouse. Six
are unmanaged legacy consoles that are not set to pull policy at all.

The difference between the two sources determines the shape of the fix. Machines that are off
receive the policy once they are turned on; their problem is timing, and it is fixed once the
window is extended. The six unmanaged consoles, on the other hand, never receive it in any
window; their problem is in the inventory, and it can only be closed by hand. On the central
console, both look the same — the "policy applied" line counts the 319 managed machines, not the
295 that were reached.

**The coverage rate is the number of verified machines, not the number of applied policies.** A
report that does not make this distinction writes 100% after the first round; the measured number
is 51.4%. The 30 missing machines keep open all twelve of the paths the policy assumes it has
closed.

#### Local Override

The third table counts EI19. Five of the twelve settings the policy carries can be overridden
locally on a machine, seven cannot. The number of machines in the inventory with a local
administrator account is 67, and each of them can drop five settings: a total of **335 paths**
are reopened.

The distribution is not even. 38 field laptops have a local administrator and open 190 paths on
their own; 11 branch workstations open 55 paths. Meter collectors have only two such machines,
opening 10 paths. The same policy, with the same twelve settings, produces a result that varies
nineteenfold from class to class.

The two tables multiply each other. Field laptops are both the slowest-reached class — average
3.9 rounds, 18 machines that never receive it — and the class most open to local override. Where
central enforcement is weakest is the most crowded and most mobile part of the inventory. **The
propagation rate and the override rate are not independent; both come from the same property — the
machine being far from the center.**

The seven central settings are the other side of this table. Even on a machine with a local
administrator, these seven settings do not drop; the number of remaining closed paths is totaled
in a column ranging from 1490 to 42. Central enforcement's real coverage is not the number of
settings distributed but **the number of settings that cannot be overridden locally**, and in this
model that is seven of twelve.

#### The Root Itself: The Policy Repository

What has been measured across the two tables was the policy **reaching**. The course's first
column, though, asks a different question: what proves that what reaches it is correct. A machine
applies the set of settings it pulls as-is; it does not check who wrote the content centrally. The
trust root is the policy repository's integrity and the verification of the distribution channel,
and both are outside this mechanism — the first rests on the directory measured in the first
lesson, the second on the ticket flow measured in the second lesson.

What happens when the root breaks is quieter than in the other lessons. If a setting is changed
centrally, 295 machines pick it up within sixteen rounds and apply the wrong setting **while
working correctly**. No machine errors, no user notices, and the central console stays green.
This is the configuration-side counterpart of the forged-ticket pattern seen in the second lesson:
because the mechanism operates flawlessly, the result is treated as valid.

The renewal bill reads the same way. Rolling back a changed setting means a new propagation round;
295 machines spread across sixteen rounds again, and 30 machines go to manual handling. How many
hours the wrong setting stayed open is calculated with the same propagation curve as before, and
**the speed of rolling back is not different from the speed of enforcing**. The strength of central
enforcement and the price paid when the root breaks are read off the same curve; speeding up the
curve speeds up both outcomes at once, and slowing it down slows both down at once.

#### Summary

- Central policy is a propagation process, not a write operation: it reaches 167 of 325 machines
  in the first round, and coverage starts at 51.4%.
- Most of the coverage forms in the first two rounds; the remaining fourteen rounds add only 74
  machines, and getting the last ten percent takes longer than a day.
- 30 machines never receive the policy: 24 because they never turn on during the window, 6
  because they are unmanaged; on the central console, both look the same.
- The item that determines propagation speed is not the network or the repository but the
  machine's online rate; the average is 3.9 rounds on field laptops and 1.0 round on servers.
- 5 of 12 settings can be overridden locally, and 67 machines with a local administrator open 335
  paths; enforcement's real coverage is the 7 settings that cannot be overridden.

#### Next Step

This lesson counted a setting being locally droppable on 67 machines. Dropping it requires being
a local administrator on that machine; the real question is **how** an ordinary account obtains
that privilege. On an enterprise network, this is not a single event but a set of paths: an account
reaching another account's data at its own level, or rising above its own level. The next lesson
treats horizontal and vertical privilege escalation as path counting: how many distinct escalation
paths exist, which control closes how many of them, and which ones are not closed by any control at
all.

### Privilege Escalation

Source: https://academia.sh/en/courses/identity-and-cryptography/privilege-escalation

Measuring horizontal and vertical escalation as path counting: finding 37 distinct paths from a limited user to a domain administrator across 9 nodes and 20 edges, 32 paths passing through at least one horizontal edge, a single control closing at most 24 paths, three controls together closing all 37, and one control closing no path at all.

The previous lesson counted a setting being locally droppable on 67 machines. Dropping it
requires being a local administrator on that machine, and the real question is how an ordinary
account obtains that privilege. This transition is called **privilege escalation**, and on an
enterprise network it is not a single event but a set of paths.

Two types are distinguished. **Horizontal escalation** is moving to another subject's data or
account at the same level; **vertical escalation** is rising to a higher level. This lesson
treats both as path counting: how many distinct paths exist, which control closes how many of
them, and how many controls are needed to close them all. In the **IT and Systems Fundamentals**
course, escalation was measured as the number of tickets opened once it is assumed to have
happened; what is measured here is **how many distinct paths** escalation is possible through.
This lesson does not write out a step sequence, a command, or a tool; attempting such a thing
without written authorization and a defined scope is both illegal and unprofessional.

#### Edge, Path, and Control

**EI20:** privilege escalation is not a single event but a **set of edges**; each edge arises from
a configuration item, and when that item is removed, the edge disappears too. **EI21:** horizontal
and vertical escalation are counted separately, because they are closed by separate controls.
**EI22:** a control closes an edge, not a path; how many paths close depends on how many paths
that edge appears in.

EI20 determines this lesson's manner of measurement. Escalation is not a skill but a property of
the inventory: behind every edge in the graph stands a written membership, a granted permission,
or a forgotten account. This is why what is measured is not the attacker's skill but the
organization's own configuration items. The number does not change unless the edge list changes;
the number is recalculated every time the list changes, and this calculation is an inventory
task, not an audit task — when the inventory is not kept, the path count is unknown, and in most
organizations it is assumed to be zero.

#### Nine Nodes, Twenty Edges

The graph below is a **model**: the account levels in a fictional regional measurement network
company and the configuration items that make moving between them possible. There is no real
organization, no real account, and no working step sequence.

```js
// escalation/graph.mjs — the graph of privilege escalation paths (model): nodes are account
// levels, edges are the configuration items that make moving from one level to another possible.
// There is no real organization, real account, or working step sequence. No randomness.

// EI20: privilege escalation is not a single event but a set of edges; each edge arises from a
// configuration item, and when that item is removed, the edge disappears too. EI21: horizontal
// escalation is moving to another subject at the same level, vertical escalation is rising to a higher
// level; they are counted separately because they are closed by separate controls. EI22: a control
// closes an edge, not a path; how many paths close depends on how many paths that edge appears in.
// Columns: source, target, type, the item that opens the edge, the control that closes the edge.
export const EDGE = `
  limited-user       other-user         horizontal  shared-local-password         per-machine-password
  limited-user       other-user         horizontal  shared-directory-permission   file-permission-narrowing
  limited-user       local-admin        vertical    local-admin-membership        group-membership-enforcement
  limited-user       local-admin        vertical    unsigned-script               unsigned-script-block
  limited-user       backup-operator    vertical    legacy-group-membership       group-membership-review
  other-user         local-admin        vertical    secondary-account-membership  group-membership-enforcement
  other-user         help-desk          horizontal  help-desk-membership          group-membership-review
  local-admin        service-account    horizontal  machine-running-service       service-account-restriction
  local-admin        help-desk          horizontal  privileged-session-trace      session-isolation
  local-admin        server-admin       vertical    common-local-password         per-machine-password
  local-admin        backup-operator    horizontal  backup-agent-account          backup-access-separation
  service-account    server-admin       vertical    over-privileged-service       least-privilege
  service-account    policy-admin       vertical    service-policy-permission     policy-write-separation
  help-desk          other-user         horizontal  password-reset                reset-approval
  help-desk          domain-admin       vertical    delegated-administration      delegation-restriction
  help-desk          backup-operator    horizontal  group-management-permission   delegation-restriction
  backup-operator    domain-admin       vertical    backup-restore                backup-access-separation
  server-admin       policy-admin       vertical    policy-editing                policy-write-separation
  server-admin       domain-admin       vertical    admin-trace-on-server         session-isolation
  policy-admin       domain-admin       vertical    policy-enforcement            policy-signing`
  .trim().split("\n").map((r) => {
    const p = r.trim().split(/\s{2,}/);
    return { source: p[0], target: p[1], type: p[2], item: p[3], control: p[4] };
  });

export const START = "limited-user";
export const TARGET = "domain-admin";

// Path counting: acyclic, each node is visited at most once per path.
export const paths = (closed = []) => {
  const open = EDGE.filter((k) => !closed.includes(k.control));
  const result = [];
  const walk = (node, visited, path) => {
    if (node === TARGET) { result.push(path); return; }
    for (const k of open.filter((x) => x.source === node && !visited.includes(x.target)))
      walk(k.target, [...visited, k.target], [...path, k]);
  };
  walk(START, [START], []);
  return result;
};
```

```js
// escalation/tally.mjs — how many distinct escalation paths exist, how many of the paths pass through
// a horizontal edge, which control closes how many of them, and how many controls are needed to close them all.
import { EDGE, START, TARGET, paths } from "./graph.mjs";

const print = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const controls = [...new Set(EDGE.map((k) => k.control))];
const node = [...new Set(EDGE.flatMap((k) => [k.source, k.target]))];
const all = paths();

console.log(`${node.length} nodes, ${EDGE.length} edges (` +
  `${EDGE.filter((k) => k.type === "vertical").length} vertical, ` +
  `${EDGE.filter((k) => k.type === "horizontal").length} horizontal), ` +
  `${controls.length} controls`);
console.log(`there are ${all.length} distinct paths from ${START} to ${TARGET}`);

const A = [-10, 8, 15, 16, 11];
console.log("\n1. distribution by path length");
print(A, "length", "paths", "vertical only", "has horizontal", "avg edges");
for (const u of [2, 3, 4, 5]) {
  const s = all.filter((y) => y.length === u);
  if (s.length === 0) continue;
  print(A, u, s.length, s.filter((y) => y.every((k) => k.type === "vertical")).length,
    s.filter((y) => y.some((k) => k.type === "horizontal")).length, u);
}
const hasHorizontal = all.filter((y) => y.some((k) => k.type === "horizontal")).length;
console.log(`paths passing through at least one horizontal edge ${hasHorizontal}/${all.length}, ` +
  `paths going entirely through vertical edges ${all.length - hasHorizontal}`);

const B = [-30, 8, 10, 11, 11];
console.log("\n2. per control: how many edges, how many paths close");
print(B, "control", "edges", "closed", "remaining", "closed %");
const ranked = controls.map((c) => ({ c, remaining: paths([c]).length }))
  .sort((a, b) => a.remaining - b.remaining);
for (const { c, remaining } of ranked) {
  print(B, c, EDGE.filter((k) => k.control === c).length, all.length - remaining, remaining,
    `${(100 * (all.length - remaining) / all.length).toFixed(0)}%`);
}

const C = [-30, 9, 11, 11];
console.log("\n3. adding controls from most effective (cumulative)");
print(C, "added", "controls", "remaining", "closed");
const chosen = [];
print(C, "none", 0, all.length, 0);
let remain = all.length;
while (remain > 0) {
  let best = null, least = remain;
  for (const c of controls.filter((x) => !chosen.includes(x))) {
    const k = paths([...chosen, c]).length;
    if (k < least) { least = k; best = c; }
  }
  if (best === null) break;
  chosen.push(best); remain = least;
  print(C, best, chosen.length, remain, all.length - remain);
}

const D = [-22, 10, 10, 16, 16];
console.log("\n4. per node: how many paths pass through it");
print(D, "node", "in paths", "percent", "incoming edges", "outgoing edges");
const passes = (d) => all.filter((y) =>
  y.some((k) => k.source === d || k.target === d)).length;
for (const d of node.sort((a, b) => passes(b) - passes(a))) {
  print(D, d, passes(d), `${(100 * passes(d) / all.length).toFixed(0)}%`,
    EDGE.filter((k) => k.target === d).length,
    EDGE.filter((k) => k.source === d).length);
}
```

```
9 nodes, 20 edges (12 vertical, 8 horizontal), 13 controls
there are 37 distinct paths from limited-user to domain-admin

1. distribution by path length
length       paths  vertical only  has horizontal  avg edges
2                1              1               0          2
3                8              2               6          3
4               16              2              14          4
5               10              0              10          5
paths passing through at least one horizontal edge 32/37, paths going entirely through vertical edges 5

2. per control: how many edges, how many paths close
control                          edges    closed  remaining   closed %
group-membership-enforcement         2        24         13        65%
per-machine-password                 2        16         21        43%
session-isolation                    2        16         21        43%
service-account-restriction          1        12         25        32%
policy-write-separation              2        12         25        32%
delegation-restriction               2        12         25        32%
policy-signing                       1        12         25        32%
backup-access-separation             2        11         26        30%
file-permission-narrowing            1        10         27        27%
unsigned-script-block                1         8         29        22%
least-privilege                      1         8         29        22%
group-membership-review              2         5         32        14%
reset-approval                       1         0         37         0%

3. adding controls from most effective (cumulative)
added                          controls  remaining     closed
none                                  0         37          0
group-membership-enforcement          1         13         24
unsigned-script-block                 2          5         32
group-membership-review               3          0         37

4. per node: how many paths pass through it
node                    in paths   percent  incoming edges  outgoing edges
limited-user                  37      100%               0               5
domain-admin                  37      100%               4               0
local-admin                   32       86%               3               4
other-user                    20       54%               3               2
server-admin                  16       43%               2               2
help-desk                     12       32%               2               3
service-account               12       32%               1               2
policy-admin                  12       32%               2               1
backup-operator               11       30%               3               1
```

The numbers are of the **measurement** class; their inputs are EI20–EI22 and the edge list.

#### Twenty Edges, Thirty-Seven Paths

The header line gives this lesson's first result: in a twenty-edge graph there are **37 distinct
paths** from a limited user to a domain administrator. Twenty items were written, thirty-seven
outcomes were born. Paths multiplying according to the **combination pattern** of edges, not
their count, is this measurement's core property; adding a new edge does not increase the path
count by one, it multiplies it.

The first table gives the distribution. The shortest path is two edges and has a single instance.
The three-edge path has eight instances, the four-edge path sixteen, the five-edge path ten. The
majority of paths cluster in the middle; the shortest and longest ends are sparse. This shows
that the only meaningful answer to the question "in how many steps can it be taken over" for an
inventory is a **distribution** — there is no single number.

The share of horizontal edges is decisive. **32** of the thirty-seven paths pass through at least
one horizontal edge; only five paths proceed with vertical edges from start to finish. There are
eight horizontal edges, that is, 40% of the edges; but they appear in 86% of the paths. Horizontal
transitions are not escalation itself — they do not change level — but they provide **access** to
vertical edges, and that is why they appear in most of the paths. An assessment that measures only
vertical escalation misses thirty-two of the thirty-seven paths.

#### How Many Paths Does One Control Close

The second table counts EI22. Group membership enforcement closes two edges and drops 24 paths —
65% of the total. Per-machine unique password and session isolation each close two edges as well,
but drop 16 paths apiece. The same number of edges, a different number of paths: an edge's value
lies not in its own count but in how many paths it appears in.

The table's last row is the extreme example of this. Reset approval closes one edge and drops
**zero paths**. The edge it closes — the move from help desk to another user — sits on no path at
all, because everywhere reachable after that edge is already reachable by shorter paths. The
control is not unnecessary; it may work against something else. But **its value by the
path-closing measure in this graph is zero**, and a security budget should not be allocated without
seeing this row.

The fourth table gives the same information from the node side. The local-admin node appears in
86% of the paths; it has three incoming and four outgoing edges. The next most loaded node is
other-user, appearing in 54% of the paths. A node's load behaves close to the product of its
incoming and outgoing edge counts: for local-admin, three times four gives the table's highest
row.

#### Where the Graph Itself Stands

Which item each of the twenty edges arises from is written in the list, and most of these items
are kept in one place: the central directory. Group memberships, delegated administrative
privileges, and the scope of service accounts are records of the repository measured in the first
lesson. **This lesson's trust root is the assumption that those records reflect the truth**; when
the repository's integrity breaks, the graph gains new edges that no one has seen, and the count
becomes stale.

Two edges show this directly. The edge arising from local administrator membership is the
counterpart of one of the twelve settings enforced by central policy in the previous lesson; on
the 30 machines that setting does not reach, the edge stays open. Likewise, on the 67 machines open
to local override, the edge closed by the unsigned-script block is reborn per machine. The missing
coverage in the propagation table is written directly into this graph's edge count.

This ties the two lessons' numbers together. The third table says three controls close
thirty-seven paths; the sixth lesson says those controls reach 90.8% of machines and never reach
9.2% of them. **The number of closed paths and the share of machines the closing reaches are two
separate measures**, and real coverage is their product. The path count that drops to zero in the
graph is not zero across the entire inventory; every unreachable machine keeps open, on itself, the
edges assumed to be closed, and those edges show as closed in the central report.

#### Three Controls Close Thirty-Seven Paths

The third table draws out the cumulative result by repeatedly selecting the most effective
control. Group membership enforcement reduces 37 paths to 13; unsigned-script block reduces 13 to
5; group membership review reduces 5 to zero. **Three controls close all thirty-seven paths.**

The shape of the result is surprising, and the reason lies in the graph's structure. The three
controls close three of the five edges **leaving** the limited-user node; the remaining two edges
go to the `other-user` node, and that node's own two outgoing edges are closed by the same three
controls as well. Every path leaving the start either gets cut or arrives at a dead end; no matter
how rich the rest of the graph is, it becomes unreachable. This is why the one-by-one ranking in
the second table and the cumulative selection in the third table highlight different controls:
policy signing alone closes 12 paths but is not selected, because the paths it closes are already
closed by others.

This is **path counting's most practical result**: a defense budget is allocated by cumulative
effect, not one-by-one effect. It may not be necessary to apply all thirteen of the thirteen
controls; but what says which three will suffice is neither experience nor intuition — it is
counting performed on the edge list.

The model's limit is written here too. The three controls close the paths **in this edge list**;
an edge not in the list — an unrecorded group membership, a forgotten service account — does not
enter the count and does not close. Path counting is only as accurate as the inventory's accuracy,
and a gap in the inventory shows up as a number that is too small.

#### Summary

- Privilege escalation is a set of edges: in a graph with 9 nodes and 20 edges, there are 37
  distinct paths from a limited user to a domain administrator.
- Path lengths range from 2 to 5 edges; the majority are 4-edge paths, and there is no single
  "how many steps" number.
- Horizontal edges are 40% of the edges but appear in 32 of the paths; an assessment that
  measures only vertical escalation misses 32 of the 37 paths.
- Two controls that close the same number of edges close different numbers of paths; one
  control closes 1 edge and drops 0 paths.
- Three controls close all 37 paths: they cut 3 of the 5 edges leaving the start node and turn
  the node the remaining 2 lead to into a dead end; cumulative effect produces a different
  ranking from one-by-one effect.

#### Next Step

This lesson counted escalation through configuration items: a membership, a permission, an
account. There is one more item that stays quiet behind the edge list. Moving from one account to
another often requires not knowing that account's password but capturing **the value used to
verify it**; in machines' memory, such a value sits for every signed-in user. The next lesson
measures this on the defense axis: which item stays in memory for how long, why pass-the-hash works
without knowing the password, and which control closes how many paths. That lesson also closes
out the course.

### Credential Theft

Source: https://academia.sh/en/courses/identity-and-cryptography/credential-theft

How long verification items stay in memory and the path controls close: 6 of 8 item types can be presented directly without knowing the password, 22 paths and 298,958 machine-hours of exposure, six controls closing 17 paths and dropping exposure to 1%, and the remaining 5 paths not closing under any control.

The previous lesson counted escalation through configuration items: a membership, a permission,
an account. There is one more item that stays silent behind the edge list. Moving from one account
to another usually does not require knowing that account's password, but rather getting hold of
**the value used to authenticate it**; such a value sits in the memory of machines for every
logged-in user.

This lesson measures those values on the defense axis: which item stays where for how long, how
many items can be presented directly without knowing the password, and which control closes how
many paths. No working step sequence, tool name, or command is written in this lesson; what is
measured is not how the item is obtained, but what it opens assuming it has been obtained. Running
such a test without written authorization and a defined scope is both illegal and unprofessional.

#### What Is Presented Is Not the Password

**EI23:** in most mechanisms the authentication input is not the password itself but a value
derived from or produced with it; the verifying side expects that value. **EI24:** because of
this, whoever gets hold of the value does not **need** to know the password — what is presented is
already not the password. **EI25:** an item's time in the machine is multiplied by its scope;
defense is measured by one of removing the item, shortening its lifetime, or narrowing its scope.

EI24 is this lesson's conceptual core and explains the entire class called **pass-the-hash**. The
password is never stored anywhere; what is stored and passes over the network is a value derived
from it. The verifying side compares the value presented to it with the value in its own record
and accepts if they match. This is not a flaw but a design — done so the password does not pass over
the network. But the same design also says this: **whoever can present the derived value gets the
same result as whoever knows the password**. This is why changing the password renews the item,
and it is also the only action to take for an account whose item was stolen.

#### Eight Item Types, Twenty-Two Paths

The inventory below is a **model**: the verification items sitting on machines after a session is
opened, in the fictional regional measurement network organization. There is no real organization,
real machine, or working step sequence.

```js
// theft/item.mjs — verification items sitting on a machine after a session is opened
// (model): there is no real organization, real machine, real item, or working step
// sequence. No randomness.

// EI23: in most mechanisms the authentication input is not the password itself but a value derived
// from or produced with it; the verifying side expects that value. EI24: because of this, whoever
// gets hold of the value does not need to know the password — what is presented is already not the password.
// EI25: an item's time in the machine is multiplied by its scope; defense is measured by one
// of removing the item, shortening its lifetime, or narrowing its scope.
// Columns: life = the item's time on the machine (hours), scope = number of systems the item
// can open, presentable = is it accepted when presented directly without knowing the password.
export const ITEM = `
  password-derived-verifier    720   13   yes
  ticket-granting-ticket        10   21   yes
  service-ticket                10    1   yes
  session-key                   10    1   no
  application-session-cookie    72    1   yes
  cached-verifier                8    1   no
  renewal-ticket               168   21   yes
  service-account-secret       720    1   yes`
  .trim().split("\n").map((r) => {
    const p = r.trim().split(/\s+/);
    return { name: p[0], life: +p[1], scope: +p[2], presentable: p[3] === "yes" };
  });

export const CLASS = [
  { name: "branch-workstation", count: 96 },
  { name: "server", count: 21 },
  { name: "field-laptop", count: 140 },
  { name: "meter-collector", count: 44 },
];

// Which item is found on which class: each pair is a path.
export const FOUND_ON = {
  "password-derived-verifier": ["branch-workstation", "server", "field-laptop"],
  "ticket-granting-ticket": ["branch-workstation", "server", "field-laptop"],
  "service-ticket": ["branch-workstation", "server", "field-laptop", "meter-collector"],
  "session-key": ["branch-workstation", "server", "field-laptop", "meter-collector"],
  "application-session-cookie": ["branch-workstation", "field-laptop"],
  "cached-verifier": ["branch-workstation", "field-laptop"],
  "renewal-ticket": ["branch-workstation", "field-laptop"],
  "service-account-secret": ["server", "meter-collector"],
};

// Each control states which (item, class) pair it removes, by its own criterion.
export const CONTROL = {
  "privileged-session-isolation": (k, s) => k.scope > 1 && s.name !== "server",
  "protected-account-group": (k) => k.name === "cached-verifier" ||
    k.name === "password-derived-verifier",
  "local-admin-restriction": (k, s) => s.name === "branch-workstation" || s.name === "meter-collector",
  "logoff-enforcement": (k) => k.life > 24,
  "service-account-isolation": (k) => k.name === "service-account-secret",
  "origin-bound-factor": (k) => k.name === "application-session-cookie" || k.name === "renewal-ticket",
};
```

```js
// theft/tally.mjs — which item stays where for how long, how many items can be presented
// without knowing the password, how many paths each control closes, and what exposure remains.
import { ITEM, CLASS, FOUND_ON, CONTROL } from "./item.mjs";

const print = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const classOf = (name) => CLASS.find((s) => s.name === name);
const PATH = ITEM.flatMap((k) => FOUND_ON[k.name].map((s) => ({ k, s: classOf(s) })));
const exposure = (y) => y.reduce((a, x) => a + x.s.count * x.k.life, 0);
const open = (closed = []) =>
  PATH.filter((y) => !closed.some((d) => CONTROL[d](y.k, y.s)));

console.log(`${ITEM.length} item types, ${CLASS.length} machine classes, ` +
  `${PATH.length} paths, ${CLASS.reduce((a, s) => a + s.count, 0)} machines`);
console.log(`items presentable directly without knowing the password ` +
  `${ITEM.filter((k) => k.presentable).length}/${ITEM.length}; ` +
  `total exposure ${exposure(PATH)} machine-hours`);

const A = [-28, 7, 8, 8, 8, 12];
console.log("\n1. per item: where, how long, how wide");
print(A, "item", "life", "scope", "class", "machine", "machine-hr");
for (const k of ITEM) {
  const y = PATH.filter((x) => x.k === k);
  print(A, k.name, k.life, k.scope, y.length, y.reduce((a, x) => a + x.s.count, 0), exposure(y));
}

const B = [-30, 8, 12, 12, 10];
console.log("\n2. paths closed and exposure per control");
print(B, "control", "closed", "open paths", "open expo", "open %");
for (const d of Object.keys(CONTROL)) {
  const rest = open([d]);
  print(B, d, PATH.length - rest.length, rest.length, exposure(rest),
    `${(100 * exposure(rest) / exposure(PATH)).toFixed(0)}%`);
}

const C = [-30, 8, 9, 12, 10];
console.log("\n3. controls added cumulatively");
print(C, "added", "paths", "closed", "exposure", "open %");
const chosen = [];
print(C, "none", PATH.length, 0, exposure(PATH), "100%");
for (const d of Object.keys(CONTROL)) {
  chosen.push(d);
  const rest = open(chosen);
  print(C, d, rest.length, PATH.length - rest.length, exposure(rest),
    `${(100 * exposure(rest) / exposure(PATH)).toFixed(0)}%`);
}

const D = [-26, -18, 8, 13];
console.log("\n4. paths remaining with all controls open");
const rest = open(Object.keys(CONTROL));
print(D, "item", "class", "machine", "machine-hr");
for (const y of rest) print(D, y.k.name, y.s.name, y.s.count, y.s.count * y.k.life);
console.log(`remaining paths ${rest.length}/${PATH.length}, ` +
  `remaining exposure ${exposure(rest)}/${exposure(PATH)} machine-hours`);
```

```
8 item types, 4 machine classes, 22 paths, 301 machines
items presentable directly without knowing the password 6/8; total exposure 298958 machine-hours

1. per item: where, how long, how wide
item                           life   scope   class machine  machine-hr
password-derived-verifier       720      13       3     257      185040
ticket-granting-ticket           10      21       3     257        2570
service-ticket                   10       1       4     301        3010
session-key                      10       1       4     301        3010
application-session-cookie       72       1       2     236       16992
cached-verifier                   8       1       2     236        1888
renewal-ticket                  168      21       2     236       39648
service-account-secret          720       1       2      65       46800

2. paths closed and exposure per control
control                         closed  open paths   open expo    open %
privileged-session-isolation         6          16       87030       29%
protected-account-group              5          17      112030       37%
local-admin-restriction             10          12      170590       57%
logoff-enforcement                   9          13       10478        4%
service-account-isolation            2          20      252158       84%
origin-bound-factor                  4          18      242318       81%

3. controls added cumulatively
added                            paths   closed    exposure    open %
none                                22        0      298958      100%
privileged-session-isolation        16        6       87030       29%
protected-account-group             13        9       70022       23%
local-admin-restriction              7       15       28630       10%
logoff-enforcement                   5       17        3430        1%
service-account-isolation            5       17        3430        1%
origin-bound-factor                  5       17        3430        1%

4. paths remaining with all controls open
item                      class              machine   machine-hr
ticket-granting-ticket    server                  21          210
service-ticket            server                  21          210
service-ticket            field-laptop           140         1400
session-key               server                  21          210
session-key               field-laptop           140         1400
remaining paths 5/22, remaining exposure 3430/298958 machine-hours
```

The numbers are of the **measurement** class; their inputs are EI23–EI25 and the item inventory.

#### The Product of Life and Scope

The first table lays the eight items side by side. The password-derived verifier stays 720 hours,
opens 13 systems, and is found on 257 machines across three classes: on its own it produces
185,040 machine-hours of exposure, 62% of the total. The ticket-granting ticket opens 21 systems
but stays only 10 hours; its exposure is 2,570. **Scope alone does not give a ranking; multiplied
by lifetime, it does.**

The renewal ticket ranks second: 168 hours, 21 systems, 39,648 machine-hours. The 72-hour session
window measured in the fourth lesson and the 168-hour renewal ticket seen here are two views of
the same setting — one written as revocation delay, the other as time-in-memory. The same number
produces cost in two different columns.

The service account secret is found on only 65 machines but stays 720 hours and yields 46,800
machine-hours. The second lesson measured that breaking one of 21 service-account keys drops 214
principals; the 46,800 here is the counterpart of how long that item stays exposed on machines.

#### Six Controls, Seventeen Paths

The second table runs the controls one at a time. The local-admin restriction closes the most
paths — 10 of 22 — but the remaining exposure is 57%, because the paths it closes are in classes
hosting short-lived items. The logoff enforcement closes only 9 paths but drops exposure to
**4%**; the items it removes are the long-lived ones.

These two rows repeat one of the course's rules: a number cannot be read without choosing a unit
of measure. Ranked by path count, local-admin restriction is first; ranked by exposure, logoff
enforcement is first. The two rankings give different defense priorities, and which one is correct
depends on what the organization is measuring.

The third table adds all six cumulatively. The path count drops from 22 to 5, exposure from
298,958 to 3,430; the remaining ratio is **1%**. The last two controls — service-account isolation
and origin-bound factor — close no new paths, because the items they close have already been
removed by the earlier controls. It is the same pattern seen in the seventh lesson: a control
that is effective on its own can contribute zero in the cumulative list.

#### The Five Paths That Do Not Close

The fourth table lists what remains with all six controls open. Five paths remain, and all five
are short-lived items on servers and field laptops: the ticket-granting ticket, the service
ticket, and the session key. Total exposure is 3,430 machine-hours.

These five do not close, because **removing them means removing the session itself**. If a user
has a session open on a server, that session's key has to stay there; without a ticket, the
service cannot be reached. This is where the course's measured root falls shortest most plainly:
the way to close the remaining paths is not a better control, it is shortening the session's
duration — and that also stops legitimate work.

#### Summary

- The authentication input is not the password itself but a value derived from it; 6 of 8 item
  types are accepted when presented directly without knowing the password.
- Exposure is the product of life and scope: the password-derived verifier alone produces 62% of
  the total 298,958 machine-hours with 720 hours and 257 machines.
- The control most effective by path count and the control most effective by exposure are not the
  same: one closes 10 paths and leaves 57%, the other closes 9 paths and leaves 4%.
- Six controls together close 17 paths and drop exposure to 1%; the last two controls contribute
  zero in the cumulative list.
- The remaining 5 paths do not close under any control, because removing them means removing the
  session itself.

#### Course Wrap-Up

Twenty-one lessons were measured with the same three columns. The table below lays each lesson's
trust root, the number of items that must be correct at the same time from root to use, and what
drops if the root breaks, side by side.

| Lesson | Trust root | Items that must hold | What drops if the root breaks |
|---|---|---|---|
| Hashing, Encryption, and Encoding | operation–domain match | 2 secret items | 24,800 records |
| Hash Functions | lookup cost | 25,242 items | 12,502 items |
| Salt and Password Storage | password unpredictability | 2 factors | 340 records |
| Symmetric Encryption | shared key | 4 items | 12,401 ends |
| Asymmetric Encryption | private key secrecy | 12,401 secret items | 1 participant |
| Key Exchange | secret exponent | 3 items | 52,560 sessions |
| Digital Signatures | signing key | 6 items | 2,190 signatures |
| Certificates | audited fields | 8 items | 13 attempts |
| Certificate Authorities | root certificate | 14 items | 8,802 items |
| Certificate Revocation | publication freshness | 29.5-hour window | 271,877 connections |
| TLS Handshake | the server's private key | 11 items | 5 presentations |
| Certificate Lifecycle | renewal automation | 4,397 items | 1,458 connections |
| End-to-End Encryption | the key at the end | 4 items | 172,200 records |
| Directory Services | the integrity of the central repository | 119 items | 646 items |
| Ticket-Based Authentication | the ticket-granting service's key | 471 key items | 214 principals |
| Authentication for Network Access | shared secret | 480 items | 80 devices |
| Single Sign-On | signing key | 90 items | 1,562 connections |
| Multi-Factor Authentication | the bond at the registration moment | 4 items | 10 paths |
| Group Policies | policy repository | 12 settings | 295 machines |
| Privilege Escalation | inventory accuracy | 20 edges | 37 paths |
| Credential Theft | the verifier in memory | 8 item types | 22 paths |

The table's common column is the second one. In no row does the root stand alone: the smallest
asks for two, the largest for more than twelve thousand items, and most of the items are not
cryptographic. An hour, a record, a distribution round, an inventory row — these items around the
root determine where the break actually comes from.

Three topics show three different root classes. In the `cryptography-fundamentals` lessons the
root is a **secret** or a cost assumption: a secret exponent, a private key, the unpredictability
of a password. In the `pki` lessons the root is a **decision**: which authority is trusted, which
field is checked, how often revocation is published. In the `enterprise-identity` lessons the
root is an **operational item**: a repository record, a distribution round, an inventory row, a
moment of registration. Moving from plain mathematics toward operations does not make protecting the
root harder — it makes **seeing** it harder.

The third column's magnitude follows this same order too. A secret exponent dropping opens 52,560
sessions and the event is obvious; a directory record getting corrupted drops 646 items and it
is not even known that it happened. Three lessons in the course showed that the root itself is not
enough: a signature does not prove who the signing key belongs to, a certificate does not guarantee
revocation is learned, multi-factor authentication cannot verify its own moment of registration. In
all three, what is missing is not inside the mechanism, it is around it.

The course's rule is this: **cryptography is not a secrecy tool, it is a method of reducing trust to
a single root; whoever does not know where the root stands cannot protect it.** The third column
writes the price of that — 646 items, 8,802 items, 214 principals, 1,562 connections. These
numbers do not come from the mechanism's weakness but from the reduction's success: because trust
is concentrated in one place, that is where it breaks.

In this course the root was named every time and its chain counted. But under every root an
unwritten assumption remained: the integrity of the machine the root **stands on**. A private key
sits in a file, a ticket in a region of memory, a policy repository on a disk. If that machine is
compromised, the whole chain — even if correctly set up, correctly renewed, correctly audited —
stays valid in the attacker's hands. The **Endpoint and System Hardening** course starts with this
question.

