Lesson 07 / 20
On-Path Attacks
Measuring an on-path attack as a missing chain of evidence: 3 of 8 flows having no chain at all across 36 hops and 16 of those hops being able to read the content, the number of parties that could produce the identity claim being 272, endpoint verification raising that number to 509, root pinning lowering it to 11, and the single flow carrying 89,400 requests staying chainless because it cannot verify.
Published Updated
In this lesson
- Defines an on-path attack in terms of the chain of evidence's missing link and measures it per flow
- Calculates the number of parties that could produce an identity claim based on chain type
- Compares the cost in root items, extra round trips, and cut flows between endpoint verification and root pinning
Contents
The previous lesson measured the quantity of an unproven claim: how many requests, how many slots, how many seconds. When load drops or the defense is enough, the service stays up and the measurement ends. But the same unproven claim can also be used without consuming any capacity and without tripping any counter.
If the two ends of a connection do not recognize each other by proof, every hop between them can present itself as the other side, and the connection continues at its ordinary rate and ordinary volume. What gets measured here is not a load or a counter; what is missing is a proof. This lesson counts that gap.
An On-Path Attack Is a Gap
PS19: an on-path attack is not a capability but a gap — the chain of evidence that ties the two ends to each other’s identity claim is missing. PS20: a chain of evidence’s strength is not its link count but the number of parties that could produce that claim; if that number is one the chain is single-source, and it weakens as the number grows. PS21: the chain’s links can be counted; a link with no anchor is a link accepted no matter who writes it.
PS20 is the item that frees this lesson’s measurement from counting links. A three-link chain is not stronger than a one-link chain; what decides it is how many separate parties could have written the last of those three links. The algorithmic side of encryption in transit and certificate verification is not this course’s subject and will be measured in M24/K04; the only thing counted here is how many parties a claim could come from.
The flow set below is a model: the flows, hop counts, and chain types of a fictional measurement network are written as data; there is no real network, real name, or real root list. The code captures or alters no traffic; the only thing that runs is chain counting. Testing a path’s hops outside your own network is both illegal and unprofessional without written authorization and a defined scope.
// networks/chain.mjs — the chain of evidence for flows in a fictional regional measurement network // (model): no real network, real name, or real root list. No randomness. // PS19: an on-path attack is not a capability but a gap: the chain of evidence that ties // the two ends to each other's identity claim is missing. // PS20: a chain of evidence's strength is not its link count but the number of parties // that could produce that claim; the smaller that number, the stronger the chain. export const ROOT = 84; // items in the root list endpoints accept unconditionally // Columns: flow, hop count on the path, chain type, daily requests, can the endpoint // verify (y/n). export const FLOW = ` field-terminal>collector 6 none 12400 y meter-endpoint>intake-endpoint 4 none 89400 n branch-gateway>central-gateway 5 shared-root 43200 y management-endpoint>branch-gateway 3 first-use 268 y billing>verifier 2 shared-root 22800 y verifier>measurement-store 2 pinned-root 61300 y field-terminal>name-server 6 none 34600 y external-client>shared-interface 8 shared-root 1150 y` .trim().split(/\s+/).reduce((l, _, i, a) => (i % 5 ? l : [...l, { name: a[i], hops: +a[i + 1], chain: a[i + 2], requests: +a[i + 3], verifiable: a[i + 4] === "y", }]), []); // PS21: the chain's links can be counted; a link with no anchor is accepted no matter // who writes it. // links = a verification step in the chain, unanchored = a link the endpoint cannot test. export const CHAIN = { "none": { links: 0, unanchored: 0, reads: true }, "first-use": { links: 1, unanchored: 1, reads: false }, "shared-root": { links: 3, unanchored: 1, reads: false }, "pinned-root": { links: 3, unanchored: 0, reads: false }, }; // The number of separate parties that could produce a flow's identity claim. export function producers(a) { if (a.chain === "none") return a.hops; if (a.chain === "first-use") return a.hops; // only on the first connection if (a.chain === "shared-root") return ROOT; return 1; } // Defense: endpoint verification moves chainless flows to a shared root, pinning moves // shared-root flows to a pinned root. A flow whose endpoint cannot verify stays as is. export function apply(defense = []) { return FLOW.map((a) => { let z = a.chain; if (defense.includes("endpoint-verification") && a.verifiable && z === "none") z = "shared-root"; if (defense.includes("endpoint-verification") && a.verifiable && z === "first-use") z = "shared-root"; if (defense.includes("pinning") && a.verifiable && z === "shared-root") z = "pinned-root"; return { ...a, chain: z }; }); }
// networks/chain-audit.mjs — how many flows have a broken chain of evidence, how many parties // could produce the identity claim, what endpoint verification and pinning change, and the cost. import { FLOW, ROOT, CHAIN, producers, apply } from "./chain.mjs"; const write = (g, ...s) => console.log(s.map((v, i) => (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join("")); const readingHops = (l) => l.reduce((t, a) => t + (CHAIN[a.chain].reads ? a.hops : 0), 0); const totalProducers = (l) => l.reduce((t, a) => t + producers(a), 0); const SETUPS = [[], ["endpoint-verification"], ["pinning"], ["endpoint-verification", "pinning"]]; console.log(`${FLOW.length} flows, ${FLOW.reduce((t, a) => t + a.hops, 0)} hops, ` + `root list ${ROOT} items; flows with no chain ` + `${FLOW.filter((a) => a.chain === "none").length}, readable hops ${readingHops(FLOW)}, ` + `30-day total requests ${FLOW.reduce((t, a) => t + a.requests, 0)}`); const A = [-35, 7, 14, 8, 12, 12, 14]; console.log("\n1. chain of evidence per flow"); write(A, "flow", "hops", "chain", "links", "unanchored", "producers", "30d requests"); for (const a of FLOW) write(A, a.name, a.hops, a.chain, CHAIN[a.chain].links, CHAIN[a.chain].unanchored, producers(a), a.requests); const B = [-33, 10, 12, 12, 15, 14, 21]; console.log("\n2. defenses: the state of the chain"); write(B, "defense", "no chain", "unanchored", "producers", "sole producer", "reading hops", "no-chain requests %"); for (const s of SETUPS) { const l = apply(s); write(B, s.join(" + ") || "none", l.filter((a) => a.chain === "none").length, l.reduce((t, a) => t + CHAIN[a.chain].unanchored, 0), totalProducers(l), l.filter((a) => producers(a) === 1).length, readingHops(l), ((l.filter((a) => a.chain === "none").reduce((t, a) => t + a.requests, 0) / FLOW.reduce((t, a) => t + a.requests, 0)) * 100).toFixed(1)); } const C = [-33, 11, 13, 8, 14, 14]; console.log("\n3. the cost of the same defenses"); write(C, "defense", "root items", "extra trips", "manual", "flows cut", "out of scope"); for (const s of SETUPS) { const l = apply(s), scope = FLOW.filter((a) => a.verifiable); const pinned = l.filter((a) => a.chain === "pinned-root" && a.verifiable).length; write(C, s.join(" + ") || "none", s.includes("endpoint-verification") ? ROOT : 0, s.includes("endpoint-verification") ? scope.length * 2 : 0, s.includes("pinning") ? pinned : 0, s.includes("pinning") ? pinned : 0, FLOW.length - scope.length); } const D = [-35, 14, 12, 14]; console.log("\n4. remaining open when both defenses are on"); write(D, "flow", "chain", "producers", "30d requests"); for (const a of apply(["endpoint-verification", "pinning"]).filter((x) => producers(x) > 1)) write(D, a.name, a.chain, producers(a), a.requests);
8 flows, 36 hops, root list 84 items; flows with no chain 3, readable hops 16, 30-day total requests 265118 1. chain of evidence per flow flow hops chain links unanchored producers 30d requests field-terminal>collector 6 none 0 0 6 12400 meter-endpoint>intake-endpoint 4 none 0 0 4 89400 branch-gateway>central-gateway 5 shared-root 3 1 84 43200 management-endpoint>branch-gateway 3 first-use 1 1 3 268 billing>verifier 2 shared-root 3 1 84 22800 verifier>measurement-store 2 pinned-root 3 0 1 61300 field-terminal>name-server 6 none 0 0 6 34600 external-client>shared-interface 8 shared-root 3 1 84 1150 2. defenses: the state of the chain defense no chain unanchored producers sole producer reading hops no-chain requests % none 3 4 272 1 16 51.4 endpoint-verification 1 6 509 1 4 33.7 pinning 3 1 23 4 16 51.4 endpoint-verification + pinning 1 0 11 7 4 33.7 3. the cost of the same defenses defense root items extra trips manual flows cut out of scope none 0 0 0 0 1 endpoint-verification 84 14 0 0 1 pinning 0 0 4 4 1 endpoint-verification + pinning 84 14 7 7 1 4. remaining open when both defenses are on flow chain producers 30d requests meter-endpoint>intake-endpoint none 4 89400
The numbers are in the measurement class; their inputs are PS19–PS21.
Thirty-Six Hops, Three Chainless Flows
Eight flows pass through thirty-six hops in total. Three flows have no chain of evidence at all, and sixteen hops across those three flows can read the content. The remaining five flows have a chain, but the chain’s mere presence is not by itself a conclusion.
The first table shows this in the sixth column. The verifier>measurement-store flow’s chain
has three links and no unanchored link: the number of parties that could produce the identity
claim is one. The billing>verifier flow’s chain also has three links, but one of them is
unanchored, and the number of producing parties is 84. The two flows have the same link
count, and an eighty-four-fold difference sits between them.
The source of the difference is the unanchored link. Under the shared-root arrangement, the endpoint accepts a statement written by any item in the root list; the claim can be produced by as many parties as there are items in that list. Under the pinned-root arrangement, the endpoint accepts only a single item’s statement. The chain’s length does not measure trust; the list’s length does.
The number is smaller in chainless flows, and at first glance this looks backward. The
field-terminal>collector flow’s producer count is six — the six hops on its path. That is
small next to a shared-root flow’s eighty-four. The difference is qualitative: in a chainless
flow, those six hops sit on the path and can also read the traffic; the eighty-four items
in a shared-root arrangement do not sit on the path and cannot read the content. The numbers
cannot be compared on the same axis; both are written down anyway.
The picture sharpens further when request volume is read instead of flow count. Eight flows
carry 265,118 requests over thirty days; the three flows with no chain at all carry
136,400 of that, or 51.4%. The single flow whose chain reduces to one link —
verifier>measurement-store — carries 23.1% of the total with 61,300 requests. Chainlessness,
which is three in eight by flow count, is more than half by volume. Chain-of-evidence
coverage is reported by the request volume those flows carry, not by counting flows.
The fourth row’s first-use trust arrangement is a separate item. In this flow, the number of parties that could produce the claim is three only at the first connection; once the first connection completes, the endpoint stores the value it saw and the number drops to one. The entire chain hangs on a single moment, and that moment does not come again. This arrangement’s measure is not a party count but a window count: how many times a first connection gets established. The window reopens on a device change, a reinstall, or a key rotation.
Where Endpoint Verification Changes Direction
The second table produces this lesson’s most striking row. When endpoint verification is turned on, the chainless-flow count drops from three to one and the reading-hops count falls from sixteen to four — the expected improvement. In that same row, though, the total producer count rises from 272 to 509. The unanchored-link count also climbs from four to six.
Why it rises is readable from the first table. Two chainless flows carried a claim producible by the six hops on their path; once endpoint verification is turned on, these flows move to the shared-root arrangement and the number of parties that could produce the claim jumps from six to eighty-four. Reading closes off entirely, but the set of parties that could produce the identity claim grows.
Endpoint verification takes the claim away from the hops on the path and hands it to a root list; the gain is on the reading side, and the loss is in the growth of the trust set. An assessment that folds these two outcomes into a single “it is secure now” sentence never sees the second column’s direction at all.
The third row measures pinning on its own: the total producer count drops from 272 to 23 and the single-producer flow count rises from one to four. Reading hops stay at sixteen, because pinning does not touch chainless flows. The fourth row combines both and gives the best result on both axes: unanchored links at zero, total producers at 11, single-producer flows at seven, reading hops at four.
The Remaining Flow and Its Cost
The fourth table names what remains. One of the eight flows — the flow from the meter endpoints to the intake endpoint — cannot verify and stays chainless. This flow carries 89,400 requests over thirty days, the largest share of the eight flows’ total volume. The same reason that left seven unverifiable acceptance points in the second lesson applies here too: the endpoints cannot produce proof. The defense is not enough, and the reason it is not enough is not a configuration issue.
The third table writes the cost. Endpoint verification asks for an eighty-four-item root list to be continuously managed and two extra round trips per flow — fourteen round trips in total for the seven flows in scope. Pinning alone brings four manual items, and each item means that flow gets cut when the pinned root changes: four cut flows; the number rises to seven when both defenses are on together. The out-of-scope flow count is one in every row, and no defense changes it. In volume terms, this single flow means 89,400 of the 265,118 requests that stay chainless when all three defenses are on — 33.7% of the total; down from the initial 51.4%, but a third of it is still unproven.
The root-list item and the manual pinning item work in opposite directions. The eighty-four-item list is managed centrally and is independent of flow count; the seven pinning entries are per flow, and every new flow adds an entry. Pinning is cheap on a small network and brings the trust set down to one; as the flow count grows, the entry count grows linearly and every root change produces a cutoff. The second table’s last column sums the payoff of these two costs into a single number: whichever defense is turned on, the chainless-request share either stays at 51.4% or drops to 33.7%, because the entire difference comes from the flows endpoint verification covers, and pinning never changes that share at all.
Summary
- An on-path attack is a missing chain of evidence: 3 of 8 flows have no chain at all across 36 hops, and 16 hops across those three flows can read the content.
- The chain’s strength is not its link count but the number of parties that could produce the claim: of two flows with the same 3 links, one can be produced by 1 party and the other by 84; a single unanchored link creates the difference.
- Endpoint verification lowers reading hops from 16 to 4, but raises the total producer count from 272 to 509; it takes the claim off the hops on the path and hands it to an 84-item root list.
- Root pinning lowers the total producer count to 23, and both together to 11, and zeroes out the unanchored link; the cost is an 84-item list, 14 extra round trips, and 7 cut flows at root rotation.
- The single flow that cannot verify carries 89,400 requests over 30 days, and no defense covers it.
Next Step
This lesson asked who a claim came from and tied the answer to a chain. Under the pinned-root arrangement, the number of parties that could produce the claim dropped to one; the proof of identity is complete. Yet one property of a message produced with a complete chain of evidence, from the correct source, with no unanchored link at all, still went unasked: when it was produced. When the same message is presented once more, the proof of identity is still valid, the chain is still complete, and the recipient accepts it exactly as it did the first time. The next lesson measures this in terms of a freshness proof: how many of a message set get accepted when replayed, how many of them a counter, a timestamp, and a one-time value catch, how many still go uncaught, and what that costs in state, clock, and storage.
To keep your progress and take notes, Log in
My notes
Log in to take notes.