---
title: 'Sender Verification Records'
source: 'https://academia.sh/en/courses/servers-and-hardening/sender-verification-records'
course: 'Servers, Networking and Hardening'
language: en
updated: '2026-09-13T05:08:30+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Distinguishes which separate question the sender policy record and the signature record each answer'
    - 'Runs both checks against a fictional set of messages to derive the counts of messages caught, passed, and mistakenly dropped'
    - 'Counts the extra address surface opened by authorizing a network block in the policy record'
---

# Sender Verification Records

Running the sender policy record and the signature record against a fictional set of messages: the counts of forged messages caught, forged messages that pass, and legitimate messages mistakenly dropped, and the surface opened by overly broad authorization.

The previous lesson measured where the name points. The same name does a second job: the
regional measurement network sends notification emails to field teams, and those messages
carry `measurenet.invalid`. The network is fictional; a collector that gathers meter
readings, a validator, and billing and work-order services all produce messages every night.

By default, nothing says those messages actually came from that name. The from field is just
text written inside the message; writing it does not require owning the name. When nothing is
done after setup, the open item is single and large: sending a message in the name's name is
open to anyone, and the more familiar the name, the more the forgery works. When a billing
notification is forged, the field team cannot tell the difference.

Two records close this gap, and they answer **separate questions**. The **sender policy
record** is the list of machines authorized to send messages in this name's name; its
question is "which machine did this message come from." The **signature record** publishes a
public key; the sending machine signs the message with the matching private key, and the
receiving side verifies the signature with the key in the record. Its question is "did this
content come from whoever holds the key, and did it change on the way." Neither substitutes
for the other, and as shown below, neither is sufficient alone.

## Two Checks, One Run

The program below actually runs both checks. Signatures are generated and verified with
`node:crypto`; keys are created at run time, and no key body is ever written into the text.

**DR7.** The message set is 16 fictional messages: 10 legit notifications, 6 forged. The
`kind` field exists only for counting; the checker never sees that field.
**DR8.** The signature is taken over the from–subject–body triple; a real mail message's
header set is not modeled.
**DR9.** The receiving side runs both checks and sees their results; what it does with the
results is not this lesson's subject.

```javascript
import { generateKeyPairSync, sign, verify } from 'node:crypto';

// Model: sender verification. Keys are generated at run time; no key body
// is ever written down or printed. The message set and records are fictional.
const KEYS = {
  k1: generateKeyPairSync('ed25519'),   // the measurement network's own key
  k2: generateKeyPairSync('ed25519'),   // the third-party sender's key
};
const FORGED_KEY = generateKeyPairSync('ed25519');   // the attacker's key

// The two records in the zone. The sender policy record is the list of
// authorized senders; the signature record carries one public key per tag.
const POLICY_RECORD = {
  singles: ['198.51.100.11', '198.51.100.12', '198.51.100.13', '198.51.100.14', '198.51.100.20'],
  networkBlocks: [{ prefix: '203.0.113.', size: 256 }],   // the third-party sender's whole egress network
};
const SIGNATURE_RECORD = { k1: KEYS.k1.publicKey, k2: KEYS.k2.publicKey };

const serialize = (m) => `${m.from}|${m.subject}|${m.body}`;

function signMessage(m, tag, privateKey) {
  m.tag = tag;
  m.signature = sign(null, Buffer.from(serialize(m)), privateKey);
  return m;
}

function policyCheck(source) {
  if (POLICY_RECORD.singles.includes(source)) return true;
  return POLICY_RECORD.networkBlocks.some((b) => source.startsWith(b.prefix));
}

function signatureCheck(m) {
  if (!m.signature || !SIGNATURE_RECORD[m.tag]) return false;
  return verify(null, Buffer.from(serialize(m)), SIGNATURE_RECORD[m.tag], m.signature);
}

// 16 fictional messages: 10 legit notifications, 6 forged.
const MESSAGES = [
  { id: 'm01', source: '198.51.100.11', from: 'collector@measurenet.invalid', subject: 'meter batch', body: 'nightly job done', kind: 'legit', key: 'k1' },
  { id: 'm02', source: '198.51.100.12', from: 'validator@measurenet.invalid', subject: 'deviation alert', body: 'zone 4 deviation', kind: 'legit', key: 'k1' },
  { id: 'm03', source: '198.51.100.13', from: 'billing@measurenet.invalid', subject: 'period close', body: 'period 7 closed', kind: 'legit', key: 'k1' },
  { id: 'm04', source: '198.51.100.14', from: 'workorder@measurenet.invalid', subject: 'field work order', body: '5 orders for team 2', kind: 'legit', key: 'k1' },
  { id: 'm05', source: '198.51.100.20', from: 'notify@measurenet.invalid', subject: 'weekly summary', body: 'summary attached', kind: 'legit', key: 'k1' },
  { id: 'm06', source: '203.0.113.40', from: 'notify@measurenet.invalid', subject: 'bulk announcement', body: 'maintenance window', kind: 'legit', key: 'k2' },
  { id: 'm07', source: '203.0.113.41', from: 'notify@measurenet.invalid', subject: 'bulk announcement', body: 'maintenance window', kind: 'legit', key: 'k2' },
  { id: 'm08', source: '198.51.100.13', from: 'billing@measurenet.invalid', subject: 'nightly report', body: 'batch job output', kind: 'legit', key: null },
  { id: 'm09', source: '192.0.2.77', from: 'alert@measurenet.invalid', subject: 'threshold exceeded', body: 'new alert service', kind: 'legit', key: 'k1' },
  { id: 'm10', source: '198.51.100.30', from: 'workorder@measurenet.invalid', subject: 'field work order', body: '2 orders for team 3', kind: 'legit', key: 'k1', tampered: ' -- forwarded' },
  { id: 'f01', source: '203.0.113.200', from: 'billing@measurenet.invalid', subject: 'payment request', body: 'account changed', kind: 'forged', key: null },
  { id: 'f02', source: '192.0.2.200', from: 'workorder@measurenet.invalid', subject: 'urgent work order', body: 'unplug the meter', kind: 'forged', key: null },
  { id: 'f03', source: '192.0.2.201', from: 'notify@measurenet.invalid', subject: 'password renewal', body: 'follow the link', kind: 'forged', key: 'forged' },
  { id: 'f04', source: '203.0.113.201', from: 'billing@measurenet.invalid', subject: 'period close', body: 'new account number', kind: 'forged', key: 'forged' },
  { id: 'f05', source: '198.51.100.13', from: 'billing@measurenet.invalid', subject: 'period close', body: 'compromised machine', kind: 'forged', key: 'k1' },
  { id: 'f06', source: '192.0.2.202', from: 'collector@measurenet.invalid', subject: 'meter batch', body: 'nightly job done', kind: 'forged', key: 'k1', tampered: ' extra line' },
];

for (const m of MESSAGES) {
  if (m.key === 'forged') signMessage(m, 'k1', FORGED_KEY.privateKey);
  else if (m.key) signMessage(m, m.key, KEYS[m.key].privateKey);
  if (m.tampered) m.body += m.tampered;   // body changes after signing
}

const evaluate = () => MESSAGES.map((m) => ({
  id: m.id, kind: m.kind, policy: policyCheck(m.source), signature: signatureCheck(m),
}));
const RULES = {
  'policy only': (s) => s.policy,
  'signature only': (s) => s.signature,
  either: (s) => s.policy || s.signature,
  both: (s) => s.policy && s.signature,
};
const legit = MESSAGES.filter((m) => m.kind === 'legit').length;
const forged = MESSAGES.length - legit;

function table(label) {
  const results = evaluate();
  console.log(`\n[${label}]  rule              forged caught    forged passed  legit dropped`);
  for (const [name, rule] of Object.entries(RULES)) {
    const passed = results.filter((s) => s.kind === 'forged' && rule(s)).length;
    const dropped = results.filter((s) => s.kind === 'legit' && !rule(s)).length;
    console.log('  '.padEnd(label.length + 5) + name.padEnd(18) + `${forged - passed}/${forged}`.padEnd(17) +
      String(passed).padEnd(15) + `${dropped}/${legit}`);
  }
}

console.log('message   kind     policy    signature');
for (const s of evaluate()) {
  console.log(`${s.id.padEnd(9)} ${s.kind.padEnd(8)} ${(s.policy ? 'passed' : 'dropped').padEnd(9)} ${s.signature ? 'passed' : 'dropped'}`);
}
console.log(`\nlegit messages: ${legit}  forged messages: ${forged}`);
table('wide');

const broadAuthorized = POLICY_RECORD.singles.length +
  POLICY_RECORD.networkBlocks.reduce((a, b) => a + b.size, 0);
const used = [...new Set(MESSAGES.filter((m) => m.kind === 'legit' && policyCheck(m.source))
  .map((m) => m.source))];
console.log('\nauthorized addresses:', broadAuthorized, '| actually sending:', used.length,
  '| authorized but unused:', broadAuthorized - used.length);

POLICY_RECORD.networkBlocks = [];       // network block removed
POLICY_RECORD.singles = used;           // only the used addresses remain
table('narrow');
```

```
message   kind     policy    signature
m01       legit    passed    passed
m02       legit    passed    passed
m03       legit    passed    passed
m04       legit    passed    passed
m05       legit    passed    passed
m06       legit    passed    passed
m07       legit    passed    passed
m08       legit    passed    dropped
m09       legit    dropped   passed
m10       legit    dropped   dropped
f01       forged   passed    dropped
f02       forged   dropped   dropped
f03       forged   dropped   dropped
f04       forged   passed    dropped
f05       forged   passed    passed
f06       forged   dropped   dropped

legit messages: 10  forged messages: 6

[wide]  rule              forged caught    forged passed  legit dropped
         policy only       3/6              3              2/10
         signature only    5/6              1              2/10
         either            3/6              3              1/10
         both              5/6              1              3/10

authorized addresses: 261 | actually sending: 7 | authorized but unused: 254

[narrow]  rule              forged caught    forged passed  legit dropped
           policy only       5/6              1              2/10
           signature only    5/6              1              2/10
           either            5/6              1              1/10
           both              5/6              1              3/10
```

## Reading the Numbers

No row catches all six of the six forgeries, and no row passes every legitimate message. The
decision is made between two columns.

**Policy only** misses three forgeries. The reason shows up in the column: `f01` and `f04`
came from inside the authorized network block, and `f05` came directly from the legitimate
machine itself. The policy record asks about the address, not the content — it passes
anything sent from an authorized address.

What this check asks about determines how much it closes. The policy record tests the
address of the connecting machine, not the from field written inside the message. There is no
mandatory link between the two, and because the check only sees the address, a message sent
from an authorized address passes no matter what name it carries inside. What the record
closes is sending a message in this name's name from anywhere on that network — not a small
thing, but it says nothing about the content.

**Signature only** catches five forgeries, and the only one it misses is `f05`. In exchange it
drops two legitimate messages: `m08` is not signed (it does not go through the nightly batch
job's signing step), and `m10`'s body changed after signing — an intermediate server appended
a line while forwarding the message. The signature asks about content, not source.

**Combining the two is a third decision.** The loose combination (accept if either passes)
brings the dropped-legitimate count down to 1 but keeps the passed-forged count at 3; the
strict combination (both must pass) brings the passed-forged count down to 1, at the cost of
dropping three legitimate messages. The same two records give a threefold difference in
outcome depending on the combination rule. Who declares this rule, and how, is the next
lesson's subject.

## The Record's Drift from Reality

The previous lesson counted the divergence between the zone file and the live inventory. The
same drift exists here too, and it moves faster, because the set of senders grows
independently of the set of records.

In the model, legitimate messages come from nine separate addresses, and the policy record
recognizes seven of them. The two it does not recognize speak for themselves: `m09` is a
newly set-up threshold-alert service that started sending messages but was never added to the
record; `m10` comes from a different machine as a result of a redirect. Adding to the record
is not part of any setup step — a service gets written, deployed, runs, sends messages, and no
one looks at the zone. The drift is one-directional and silent: a missing record does not
produce an error, it just makes a message lose one check.

Measuring this drift means comparing two lists: the addresses authorized in the record and
the addresses that actually sent messages in a recent period. Both directions of the
difference are items. An address that is in the record but does not send is unnecessary
surface; an address that sends but is not in the record is a legitimate message that is a
candidate for being dropped. Where these two numbers get read from is the reason the next
lesson's report flow exists; for now, both are visible only in this run, not in production.

## Overly Broad Authorization

Authorizing a network block instead of a single address in the policy record is a common
shortcut; it is done so the record does not break when the third-party sender changes its
egress address. The cost shows up in the numbers:

**DR10.** The network block in the policy record carries 256 addresses; the third-party
sender uses two of them.

The record authorizes **261 addresses**, and the number actually sending is **7**. The 254
addresses in between are ones no one uses but that are authorized to send messages in the
measurement network's name. These may be allocated to the third-party sender's other
customers; when one of those customers is compromised, the authority to send messages in
`measurenet.invalid`'s name changes hands along with it. That is exactly what `f01` and `f04`
are in the run.

When the block is narrowed, the numbers settle into place: policy only goes from 3/6 to
**5/6**, passed-forged drops from 3 to 1, and **dropped-legitimate does not increase at all**.
This is a step that closes an open item without stopping any legitimate work, and it is the
only example of that in this topic. In exchange, a maintenance burden is created: if the
record is not updated when the third-party sender changes address, legitimate announcements
get dropped, and that drop is silent.

The second layer of authorization must be counted separately too. The third-party sender is
authorized not only by the address block but also by the second tag in the signature record:
messages `m06` and `m07` are signed with its key and pass verification. That means this party
is a second party able to produce a valid signature in the measurement network's name. Even
if the address block is narrowed, this authority stands; it is only revoked by removing the
tag from the record. The two layers are two separate items, and unless both are counted
separately, the sentence "sender verification is set up" carries no number.

## The Item That Cannot Be Closed, and the New Surface

`f05` is caught by no rule.

**DR11.** A compromised legitimate machine also has access to the signing key sitting on that
machine.

This item cannot be closed, because both records ask whether the machine is authorized, not
whose hands it is currently in. The outcome even runs backward: a message that passes
verification looks **more** trustworthy on the receiving side than one that does not. `f05`
is not just uncaught — because it passes both checks, it is the most convincing of the forged
messages. Setting up sender verification raises the value of this one message; in exchange
for the five items it closes, the one item left behind gets heavier. Sender verification
prevents the name from being impersonated; it does not prevent the name from being abused
from its own machine. The measure of that is not in this topic but in hardening the machine.

`m10` is also among the items that cannot be closed: when a message is forwarded, the source
address changes and the body can change. Both checks treat this like an attack. No one can
manage forwarding from the zone file.

**DR12.** While the key is being rotated, the old tag stays in the record for a while longer;
during that time, signatures produced with either key still verify.

Closing this item has its own surface, and it sits where the key lives. The signature record
publishes a **public** key, but the matching private key sits on a machine and is a secret
there. Because the record carries a key per tag, more than one tag can be published at once —
a new tag is added while the key is rotated, and the old tag stays for a while. During that
time, **two keys are valid at once**, and whoever compromises the old key still produces a
valid signature. Key rotation is a window opened in exchange for an item closed; the length
of that window depends on the time to live measured in the previous lesson.

## Summary

- By default, sending a message in a name's name does not require owning that name; the open
  item after setup is single, and it grows with how recognizable the name is.
- The sender policy record answers "which machine," and the signature record answers "did it
  come from the key holder and did it change"; neither substitutes for the other.
- In the 16-message fictional set, policy only catches 3/6 forgeries and signature only
  catches 5/6; the signature drops two legitimate messages (an unsigned batch job and a
  notification whose body changed while being forwarded).
- The combination rule determines the outcome: the loose rule brings dropped-legitimate down
  to 1 but passes 3 forgeries, the strict rule passes 1 forgery but drops 3 legitimate
  messages.
- The network block in the policy record authorizes 261 addresses, and only 7 actually send;
  the 254 extra authorized addresses are a surface, and it closes without any
  legitimate-message loss when the block is narrowed.
- A message sent from a compromised legitimate machine is caught by no rule; key rotation,
  meanwhile, opens a window in which two keys are valid at once.

## Next Step

The two checks now produce one outcome: two bits per message. The real gap this lesson leaves
is that nothing says what to do with those two bits. Whether the receiving side delivers,
quarantines, or rejects a message that fails a check is currently the receiver's decision, not
the name owner's, and receivers behave differently. The next lesson covers declaring this
decision from the zone: the policy declaration is tightened in stages — report only,
quarantine, reject — and at each stage the count of dropped legitimate messages and blocked
forged messages is taken separately. The report flow itself is also a measurement tool: how
many unauthorized senders get found through it.
