Skip to content
academia.sh

Lesson 08 / 17

Authentication Implementation Errors

Measuring the implementation error in terms of leaked information: at eight authentication points, a distinct error message, work skipped when no record exists, and a comparison that exits on the first mismatch are counted as three channels, and five points leak; the response-time difference is shown through a band check rather than by writing the raw duration; a countermeasure that closes only one channel closes just one of the five; a central validator that closes all three channels automatically covers the ninth point added; and when a fourth channel is included in the count, the same pattern's number of uncovered points rises to two.

Published Updated

In this lesson

  • Counts the channels that leak user existence in a point inventory
  • Measures the response-time difference with a band check, without writing the raw duration
  • Compares the coverage of a single-channel countermeasure against a central validator
Contents

The previous topic drew the boundary of data coming from outside and wrote the same three columns for every point set: how many points there are, how many the pattern covers, and how many points are left uncovered. All of those measurements rested on a single assumption — that the identity of the person behind the request is already known. Data can conform to the rules and still have come from the wrong person.

This topic removes that assumption. Which flow authentication is done with is not addressed here; the types of flows and how credentials are stored are the subject of the Authentication and Authorization course. What is measured here is the implementation: does the response the server gives to someone who does not know the correct credential at all say something even without knowing the correct information?

Leaked Information Can Be Counted

An authentication point knows two things separately: whether the requested record exists, and whether the submitted credential is correct. The response should only state the second. The first being distinguishable from the outside is called user enumeration: an attacker learns which subscriber numbers are registered without knowing any password at all. This information does not grant access by itself; it narrows the target set for every subsequent attempt.

Attempting enumeration against a service is both illegal and unprofessional without written authorization and a defined scope. The inventory and measurement below run on the fictional regional measurement network’s own code; there is no real service, no real account, and no real password.

// identity/point.mjs — the subscriber portal's authentication points (model): no real
// service, no real account and no real password. No randomness, the amount of work is fixed.
import { createHash } from "node:crypto";

// IE1: a point can leak user existence through three separate channels — is the error
// message distinct, is the work done different when no record exists, does the comparison
// exit on the first mismatch. The fourth field `lock` is a channel not counted in the
// inventory; it is added to the count in the last section of the lesson.
export const POINT = [
  { name: "portal-login",          message: "single",   work: "fixed",        compare: "fixed",       lock: "distinct" },
  { name: "portal-reset",          message: "distinct", work: "early-return", compare: "fixed",       lock: "single" },
  { name: "portal-email-change",   message: "distinct", work: "fixed",        compare: "fixed",       lock: "single" },
  { name: "field-app-login",       message: "single",   work: "fixed",        compare: "fixed",       lock: "single" },
  { name: "meter-reading-key",     message: "single",   work: "early-return", compare: "early-exit",  lock: "single" },
  { name: "invoice-query",         message: "distinct", work: "early-return", compare: "early-exit",  lock: "single" },
  { name: "second-factor-code",    message: "single",   work: "fixed",        compare: "fixed",       lock: "distinct" },
  { name: "report-endpoint-key",   message: "single",   work: "fixed",        compare: "early-exit",  lock: "single" },
];

// IE2: the unit of work is a fixed-step hash loop; the raw duration is not written, the step
// count is.
export const SEARCH = 2000, VERIFY = 20000;
const loop = (seed, step) => {
  let s = seed;
  for (let i = 0; i < step; i++) s = createHash("sha256").update(s).digest("hex");
  return s;
};
const RECORD = new Set(["a-4471", "a-5120", "a-6033"]);

// Flawed implementation: when no record exists, the verification work is never done, and the
// message diverges too.
export function flawed(subscriber, submitted) {
  loop(subscriber, SEARCH);
  if (!RECORD.has(subscriber)) return { message: "subscriber not found", step: SEARCH };
  loop(submitted, VERIFY);
  return { message: "incorrect password", step: SEARCH + VERIFY };
}

// Patterned implementation: the same work is done even when no record exists, the message is
// single.
export function patterned(subscriber, submitted) {
  loop(subscriber, SEARCH);
  const found = RECORD.has(subscriber);
  loop(found ? submitted : "placeholder-comparison-seed", VERIFY);
  return { message: "incorrect subscriber number or password", step: SEARCH + VERIFY };
}
// identity/coverage.mjs — how many points there are, how many the pattern covers, how many
// points are left uncovered.
import { POINT, flawed, patterned } from "./point.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(""));

// IE3: a point is considered to leak user existence if at least one of the three channels is
// open.
const CHANNELS = ["message", "work", "compare"];
const channel = (n, closed = [], list = CHANNELS) => list.filter((k) =>
  !closed.includes(k) && n[k] !== "single" && n[k] !== "fixed");
const A = [-24, 9, 14, 15, 10, 12];
console.log(`1. ${POINT.length} authentication points and their open channels`);
write(A, "point", "message", "no record", "compare", "channels", "leaking");
for (const n of POINT)
  write(A, n.name, n.message, n.work, n.compare, channel(n).length, channel(n).length ? "YES" : "no");
const leaking = (set, closed, list) => set.filter((n) => channel(n, closed, list).length).length;
console.log(`leaking point: ${leaking(POINT, [])} / ${POINT.length}`);

// IE4: duration is not written raw. The expected ratio is derived from the step count; the
// measured value only passes through a band check and yes/no is written.
const median = (d) => d.slice().sort((a, b) => a - b)[d.length >> 1];
const measure = (f, subscriber) => {
  const d = [];
  for (let i = 0; i < 15; i++) {
    const t = process.hrtime.bigint();
    f(subscriber, "submitted-value");
    d.push(Number(process.hrtime.bigint() - t));
  }
  return median(d);
};
const B = [-16, 11, 12, 16, 13, 9, 17];
console.log("\n2. response-time difference (the raw duration is not written; the expected ratio comes from the step count)");
write(B, "implementation", "no record", "has record", "expected ratio", "band", "in band", "existence leaks");
for (const [name, f] of [["flawed", flawed], ["patterned", patterned]]) {
  const none = f("a-0000", "x").step, has = f("a-4471", "x").step;
  const expected = has / none, low = expected / 2, high = expected * 2;
  const measured = measure(f, "a-4471") / measure(f, "a-0000");
  write(B, name, none, has, expected.toFixed(2), `${low.toFixed(2)}..${high.toFixed(2)}`,
    measured >= low && measured <= high ? "yes" : "NO", expected > 1.5 ? "YES" : "no");
}

// 3. coverage: how many points a countermeasure closes on its own
const NEW_POINT = { name: "field-device-pairing", message: "distinct", work: "early-return", compare: "early-exit" };
const extended = [...POINT, NEW_POINT];
const C = [-44, 7, 11, 12];
console.log("\n3. how many points a countermeasure closes");
write(C, "countermeasure", "points", "leaking", "uncovered %");
for (const [name, k, closed] of [
  ["today's distributed state", POINT, []],
  ["constant-time comparison only", POINT, ["compare"]],
  ["single error message only", POINT, ["message"]],
  ["central validator (three channels)", POINT, ["message", "work", "compare"]],
  ["new point added, distributed", extended, []],
  ["new point added, central pattern", extended, ["message", "work", "compare"]],
]) write(C, name, k.length, leaking(k, closed), ((leaking(k, closed) / k.length) * 100).toFixed(1));

// 4. if the inventory itself is incomplete: the lock response is included in the count as a
// fourth channel
const FOUR = [...CHANNELS, "lock"];
console.log("\n4. once a fourth channel is included in the count");
write(C, "count", "points", "leaking", "uncovered %");
for (const [name, list, closed] of [
  ["three channels, central validator", CHANNELS, CHANNELS],
  ["four channels, central validator", FOUR, CHANNELS],
  ["four channels, lock response also uniform", FOUR, FOUR],
]) write(C, name, POINT.length, leaking(POINT, closed, list),
  ((leaking(POINT, closed, list) / POINT.length) * 100).toFixed(1));
1. 8 authentication points and their open channels
point                     message     no record        compare  channels     leaking
portal-login               single         fixed          fixed         0          no
portal-reset             distinct  early-return          fixed         2         YES
portal-email-change      distinct         fixed          fixed         1         YES
field-app-login            single         fixed          fixed         0          no
meter-reading-key          single  early-return     early-exit         2         YES
invoice-query            distinct  early-return     early-exit         3         YES
second-factor-code         single         fixed          fixed         0          no
report-endpoint-key        single         fixed     early-exit         1         YES
leaking point: 5 / 8

2. response-time difference (the raw duration is not written; the expected ratio comes from the step count)
implementation    no record  has record  expected ratio         band  in band  existence leaks
flawed                 2000       22000           11.00  5.50..22.00      yes              YES
patterned             22000       22000            1.00   0.50..2.00      yes               no

3. how many points a countermeasure closes
countermeasure                               points    leaking uncovered %
today's distributed state                         8          5        62.5
constant-time comparison only                     8          4        50.0
single error message only                         8          4        50.0
central validator (three channels)                8          0         0.0
new point added, distributed                      9          6        66.7
new point added, central pattern                  9          0         0.0

4. once a fourth channel is included in the count
count                                        points    leaking uncovered %
three channels, central validator                 8          0         0.0
four channels, central validator                  8          2        25.0
four channels, lock response also uniform         8          0         0.0

The numbers belong to the measurement class; their inputs are IE1–IE4.

Five Points and Three Channels

The first table opens the inventory. Five of the eight points leak user existence, and how many channels the leak comes from varies from point to point.

portal-email-change leaks only through the message: when no record exists it returns a different text, while the amount of work stays the same. report-endpoint-key is the opposite; its message is single and it does the same work when no record exists, but the key comparison exits as soon as it reaches the first mismatched byte. invoice-query keeps all three channels open at once.

This distribution shows why the inventory is derived point by point. The sentence “error messages are single” is a coverage claim, and it has a counterpart in the table: five points whose message column reads single. But two of those five points continue to leak through another channel. The channel count does not say how open a point is; it says how many separate places need a fix.

It is worth noting that not every point in the inventory is a login screen. meter-reading-key and report-endpoint-key are endpoints no human sees; the credential is a key, authentication still happens, and the same three channels are still open. If a point is counted as an authentication point only when a “user login” is found, the inventory comes out incomplete. The criterion is a single sentence: if a submitted value at a point is compared against a known record, that point belongs in the inventory.

Duration Is Not Written, the Band Is Checked

The second table is this lesson’s measurement pattern. The response-time difference is a real leak channel, but a raw millisecond is not a measurement: it depends on the machine, the load, and the workload at that moment, it gives two different numbers across two runs of the same code, and whether the two numbers are comparable at all is unknown.

The measurement is set up as follows. The expected value is derived from the input: the flawed implementation does 2,000 steps when no record exists and 22,000 steps when a record exists; the expected ratio is 11.00. The band lies between half and twice that value. The measured duration ratio is reported only by whether it falls within this band — the number itself is not written to the output, only yes is. In the patterned implementation, both paths take 22,000 steps, the expected ratio is 1.00, and the measured value falls in the 0.50–2.00 band.

The last column makes the call. If the expected ratio is close to 1, there is no information to be drawn from the time difference; if it is 11, there is. This distinction does not change from run to run, because it rests on the step count, not the raw duration. The measured value’s only job is to confirm that the model actually behaves this way.

Constant-time comparison closes only one part of this channel: the early exit inside the comparison itself. The verification work not being done at all when no record is found comes before that point, and constant-time comparison does not touch it. The 11.00 ratio in the second table stays at 11.00 with constant-time comparison.

What a Single-Channel Countermeasure Closes

The third table counts coverage. In today’s distributed state, five of the eight points leak; the uncovered rate is 62.5%.

If only constant-time comparison is applied, the number of leaking points drops from 5 to 4. The single point it closes is report-endpoint-key, because its only open channel was the comparison. Applying only a single error message gives the same result: 5 to 4. The two countermeasures close different points, but both have the same coverage. A control that measures a leak class through only one of the three channels leaves four-fifths of the job open.

The central validator, which closes all three channels together, brings the number of leaking points to zero. The pattern’s content is a single sentence: the same work is done whether or not the record is found, the same message is returned, and the comparison is constant-time. This pattern is not a list of rules but a single function; the points call it.

The difference is where the rule is written. In distributed defense, the rule “do not let error messages diverge” has to be applied in eight separate places, and whether it was applied correctly can only be known by reading all eight places one by one. In the central pattern, the same rule is applied in one place; there is one place to read and one place to test. This is also what the coverage count means: 8 out of 8 coverage is not eight correct implementations, it is one correct implementation and eight calls.

The last two rows answer this course’s third question. When a ninth point — field-device-pairing — is added to the inventory, the number of leaking points in the distributed state rises from 5 to 6 and the uncovered rate climbs to 66.7%. In the central pattern, the same point is covered with no extra work, and the number of leaking points stays at 0. The difference is not in the pattern’s strength but in whether the new point has to call the defense: in distributed defense every new point is a candidate for a hole, in the central pattern it is only a call.

The Inventory Itself Can Be Incomplete

The last row of the third table reads zero, and this number is misleading. Zero does not say “no point leaks”; it says no point leaks through the three channels being counted. The coverage measure depends on the set of channels being counted.

The fourth table adds a fourth channel to the inventory: the response reporting that the account has been locked after successive failed attempts. This response says that a lockable account exists; a subscriber number that is not registered is never locked. In the three-channel count, the central validator scored zero; in the four-channel count, the same validator leaks at 2 points — an uncovered rate of 25%. The lock decision is made not in the validator itself but in the counter layer that comes after it; because the pattern does not call that layer, it does not touch it.

The last row shows how the pattern is extended: once the lock response is also pulled into a uniform shape, the count resets to zero. The lesson here is not the pattern’s inadequacy but the limit of the measure. The coverage percentage does not state a defense’s strength; it states its position relative to the number of channels counted in the inventory. When a new channel is discovered, every past coverage count has to be recalculated; the pattern’s value is that this recalculation can be done in a single place.

Summary

  • An authentication implementation can leak information even without the correct credential ever being known; the leaked information is user existence, and it is measured by point count.
  • Three channels were counted in an eight-point inventory — a distinct error message, work skipped when no record exists, and a comparison that exits on the first mismatch — and five points leaked through at least one channel.
  • The response-time difference is written not as a raw duration but as a band check: the expected ratio is derived from the step count, and the measured value is reported only by whether it falls inside the band.
  • Constant-time comparison closes only one of the three channels; four of the five leaking points stay open. A single error message alone also leaves four open.
  • A central validator that closes all three channels at once gives zero leaking points and covers a ninth point added with no extra work; in the distributed state, the same point raises the uncovered count to 6.
  • The coverage count depends on the set of channels being counted: when the lock response is added as a fourth channel, the same central validator’s uncovered point count rises from 0 to 2.

Next Step

This lesson measured the moment identity is established. Once authentication succeeds, a new object appears: the session identifier generated so identity does not have to be asked for again on subsequent requests. When that identifier is generated, whether it changes when authentication succeeds, and whether it is actually invalidated server-side when the user logs out are separate decisions — and each of these decisions is made separately within a flow. The next lesson derives the flow set: how many flows generate a session identifier, how many of them fail to renew the old one when they do, and how many sessions continue to live on after logout when there is no server-side invalidation.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close