All articles

Relay

Algorithm: Who Gets to Listen

8 min read

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


1. A count that decides whether your phone rings

The relay is publish/subscribe with no durable mailbox, and 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:

  • The sender treats a non-zero count as delivery, and stops trying.
  • A zero count is what triggers a push wake, so the recipient's phone gets a nudge only when nothing was already listening.
  • The recipient's own device treats what arrives on its subscriptions as addressed to it.

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.


2. Two kinds of address, and only one of them is public

Whether that attack is even reachable depends entirely on which kind of subject is involved, 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. 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 directory-derivable — 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.


3. The difficulty: a subject cannot authenticate itself

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


4. The claim

The relay pins each discovery subject to the first key that proves possession over it, and requires that same key afterwards. Trust on first use, applied to an address rather than a host.

What the subscriber signs is a flat, domain-tagged preimage:

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 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, not a gate that can be talked past.

And the domain tag is not decoration. The push registry runs the same shape 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.


5. The same shape at the push registry, plus one field

The subscribe path was only half the exposure, and the other half was worse.

Push registration was admitted by an anonymous rate-limit token, which proves "some valid user" and says nothing whatever about the subject being registered. The handler then upserted 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 deliberately jittered refreshes, 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 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.


6. Whose key signs, and why it is not the device's

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 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 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 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 the squatting attack the pin exists to stop is untouched.

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.


7. What the pin does not cover

The residual, 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 that ships everyone's SSH known-hosts file, with the same shape of residual risk.

The binding is state the relay holds. A subject-to-key mapping lives in Redis 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.


References

  • RFC 8446 §1 and the SSH host-key model — trust on first use as a deliberate engineering bargain rather than a failure to authenticate.
  • RFC 9421, "HTTP Message Signatures" — the signed-request shape this borrows: a domain tag, the resource being claimed, and a timestamp inside one preimage.
  • Wikipedia: Trust on first use, Replay attack.

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.