This series has spent most of its length making the sender invisible, and one article making the sender pay. It has never asked the mirror question: who gets to receive? The relay hands bytes to whoever is subscribed, and for the one class of address that is published so strangers can find you, nothing made that subscriber be you. Because there is no mailbox, an eavesdropper on your inbox is not merely reading. They are the reason your phone never rings.
Prerequisites: Ephemeral Delivery, which establishes that a live subscriber count is the only delivery signal there is, and Anonymous Admission, whose question (who is allowed to send) this one mirrors.
The relay is publish/subscribe with no durable mailbox: it hands each message to whoever is listening at that moment and stores nothing for later. That design choice has a consequence that reaches much further than storage. When a sender publishes, the only thing that comes back is how many live subscribers received it. That number is not a diagnostic. It is load-bearing in three separate places:
Now suppose someone else can subscribe to your address. The count goes to one without you being there. The sender is told the message landed. The push wake (the mechanism that exists precisely for the case where you are offline) is suppressed, because from the relay's side you appeared to be online. You are not notified, the sender believes you were, and neither of you finds out.
That is not eavesdropping with an extra step. It is a denial of service that reports success, and it costs the attacker one WebSocket: a single open network connection.
Whether that attack is even reachable depends entirely on which kind of subject is involved (a subject being the relay's word for an address, the byte string a device subscribes to), and the two kinds have opposite exposure.
A pair subject is derived from a channel key that only two devices hold, mixed with the recipient device's own public key and an epoch, a rotation counter both sides track. Guessing one is guessing a secret. An attacker who could subscribe to your pair subjects has already broken something upstream, and the subscription would be the least of the damage.
A discovery inbox is the opposite by construction. Its entire purpose is that a stranger who knows your phone number can find it, resolve it, and publish a first hello to it. It is derivable from the public contact directory (that is the feature), and it is therefore the one subject an attacker can obtain by ordinary means, using the same lookup any prospective contact uses.
So the surface is narrow and specific: the address that must be public is the address that cannot defend itself by being secret.
The obvious fix is to make the subscriber prove it owns the address. The obvious fix does not work, and the reason is worth stating precisely because it shapes everything after it.
A subject is thirty-two bytes of HKDF output (HKDF being a standard key-derivation function, a one-way recipe that stretches a secret into fixed-length bytes). A subject is not a public key. There is no private half, no signature that verifies "under" it, nothing to challenge. Asking the subscriber to prove ownership of a subject is asking it to prove knowledge of a value that is, by design, published.
Nor can the relay fall back on the socket's identity: /ws, the connection endpoint, takes no authentication at all, and
that is deliberate rather than an omission. The relay must not learn who is behind a subject:
that is the whole sealed-sender posture, the design rule that even our own server never learns who
is talking to whom. Adding a login to the subscribe path would buy
authorization by giving up the property the rest of the series is built on.
So the relay cannot verify a subject, and must not identify a subscriber. What is left is memory.
The relay pins each discovery subject to the first key that proves possession over it, and requires that same key afterwards. This is trust on first use (remember whoever shows up first, and hold everyone after to that memory), applied to an address rather than a host.
What the subscriber signs is a flat, domain-tagged preimage, the exact byte string fed into the signature (the ‖ below simply means the pieces are laid end to end), prefixed with a fixed tag that names the one purpose this signature may serve:
tag ‖ subject(32) ‖ ts_be(8) tag = "pixie:relay-subscribe-claim:v1"
and the verifier does three things, in an order chosen with some care:
if now.abs_diff(ts) > PUSH_SUBJECT_MAX_SKEW_SECS { return Err(StaleTimestamp); }
if let Some(owner) = current_owner {
if owner != owner_pubkey { return Err(WrongOwner); }
}
verifying.verify(&subscribe_preimage(subject, ts), &Signature::from_bytes(&sig))
The timestamp bounds replay (an attacker capturing a valid proof and re-sending it later) to a five-minute window. The ownership comparison happens before any cryptography, so a wrong-owner attempt is never distinguishable by timing from a bad signature: an attacker probing a subject learns "refused" and not "refused because someone else owns this." Only then is the signature checked.
Two behaviours around the edges matter as much as the check itself.
A socket that presents no claim at all is still admitted to an unclaimed subject. That keeps an older client working. What it can never do is displace an owner: once a subject is pinned, an unproven subscribe is refused outright. The pin is a ratchet (it only ever tightens), not a gate that can be talked past.
And the domain tag is not decoration. The push registry runs the same shape of check with its own tag, so a proof captured from one surface cannot be replayed on the other, a property the test suite pins directly, since it is exactly the kind of thing that silently stops being true.
The subscribe path was only half the exposure, and the other half was worse.
Push registration was admitted by an anonymous admission token, which proves "some valid user" and says nothing whatever about the subject being registered. The handler then upserted (inserted a new row, or overwrote the existing one) keyed on the caller-supplied subject. So any account could resolve a victim's discovery inbox (an ordinary, budgeted lookup) and re-point that subject's push token at its own device.
Every offline wake for the victim went to the attacker: inbound contact requests, and the alert-class "approve on another device" wake that the sign-in flow depends on. The victim was silently never woken. And because a device only re-registers on launch, with refreshes deliberately spread out at random intervals, re-hijacking was cheap.
The registry now pins the subject to an owner the same way, with the same first-claim-wins rule and the same adoption of pre-existing unbound rows. The one difference is what the signature covers:
tag ‖ subject(32) ‖ sealed_token ‖ ts_be(8) tag = "pixie:push-subject-claim:v1"
The sealed token (the encrypted push token being registered) is inside the preimage. Without it, a captured proof could be replayed to point the same subject at a different token; the claim would still verify, since it only ever spoke about the subject. Binding the token means a proof authorises exactly one registration and cannot be recycled into another.
The pin does not make the hijack impossible. It converts an endlessly renewable attack into a one-shot race that the legitimate device wins by registering at launch. And, just as importantly, it makes a lost race visible: the real device's re-registration starts failing loudly instead of silently losing its wakes.
There is one detail here that looks like an implementation choice and is actually a requirement.
The discovery inbox belongs to an account, not a handset: every device on the account holds the same discovery secret, which is what makes any-device sign-in approval work. If the subject claim were signed with a per-device key, whichever sibling device connected first would pin the account's inbox to itself, and every other device on the account would be refused from its own address.
So the claim key is derived from the epoch seed (the shared discovery secret for the current epoch, which every device on the account holds) rather than from the device:
HKDF<SHA256>.deriveKey(
inputKeyMaterial: SymmetricKey(data: epoch.seed),
info: Data("pixie:discovery-subject-claim:v1".utf8),
outputByteCount: 32)
Every device holding the epoch seed derives the same keypair, presents the same claim, and is admitted. And it states the right being claimed more honestly than a device key would: the entitlement to receive on a discovery inbox belongs to whoever holds the discovery secret, which is the account. An outsider who merely learned the subject still cannot produce the signature, so sharing the key across the account gives up nothing against the squatting attack the pin exists to stop.
This is also why revoking a device has to move the address and not just the key. A removed device still holds the epoch seed, so it still derives a valid claim; the only way to stop it receiving is to stop publishing there. Rotating the Key Strangers Seal To is that argument in full.
The residual: what this mechanism still leaves exposed. And it is a long one, which is the point of writing it down.
Only the discovery path is gated. The subscribe claim is checked on the discovery branch. Pair subjects are not gated, and deliberately so: they are secrets rather than published addresses, and an attacker who can name one has already won something larger. But it does mean the mechanism in this article protects exactly one class of address, not the subscribe path in general.
The binding is memory, not proof. The relay remembers who claimed a subject; it cannot know who should have. A device that registers first owns the address, and the honest description of what that buys is a narrowed race, not an eliminated one. It is the same trust-on-first-use bargain behind SSH's known-hosts file (the list where your terminal records a server's key the first time you connect), with the same shape of residual risk.
The binding is state the relay holds. A subject-to-key mapping lives in Redis (an in-memory data store) with a thirty-day refresh window. It is bounded and it names no user, but it is a record that outlives the publish it was created for, and an honest inventory of what the relay retains has to include it.
A lost race is recoverable only by rotating. If an attacker does win the initial claim, the legitimate account cannot displace them at that address: that is what first-claim-wins means. What it can do is mint a new address, which is precisely the identity rotation that device removal already performs.
Back to Ephemeral Delivery, whose delivered count is what a squatter was forging, or across to Rotating the Key Strangers Seal To, which explains why revocation has to move an address that a claim alone cannot take back.