---
title: 'On-Path Attacks'
source: 'https://academia.sh/en/courses/network-security/on-path-attacks'
course: 'Network Security'
language: en
updated: '2026-10-21T05:02:57+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "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'
---

# 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.

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**.

```js
// 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 };
  });
}
```

```js
// 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.
