All articles

Relay

Waking a Sleeping Phone: Push Without a Social Graph

7 min read

Everything so far assumed we could reach Bob. But Bob's phone is asleep, the app is suspended, and the only way to wake it is to ask Apple to ring a doorbell — using a token that, in the wrong hands, ties Bob's phone number to every account he talks to. This is the hardest privacy problem in the relay, and the one where we have to be most honest about the floor.

The ephemeral relay drops a message when no device is live. For that to be safe rather than lossy, we need to make a device live — to wake Bob's phone. This article is about doing that without rebuilding the social graph in the one place we can't fully control: the push provider.


1. Why waking a phone is a privacy problem

Modern phones aggressively suspend backgrounded apps. Our relay cannot keep a socket open to a sleeping iPhone; iOS won't allow it. The single sanctioned way to wake an app is a push notification through Apple Push Notification service (APNs). To send one, you present Apple a device token — an opaque handle Apple issued to that specific phone — and Apple rings it.

Here is the trap. That device token is minted by Apple and tied to a physical device, which is tied (through the App Store account, the phone number on the SIM, the whole Apple identity) to a person. So whoever holds (the device token) + (the list of which conversations should wake it) can join Bob's phone-linked identity to his account's traffic — exactly the phone↔account link the rest of the system is built to prevent. A naive push service is a table of (device token, which subjects wake me), which is a social graph wearing a hard hat.

And there is a second, subtler leak: Apple itself sees the pattern of wakes. Every push we send is a timing event Apple observes — this device, woken now, again in ten minutes. Even if Apple never learns the content, a dense enough timing series is a signal.

So there are two adversaries to design against: our own push infrastructure (which we can engineer away) and Apple (which we can only minimize and then disclose).


2. Split the job so no one component can do the harm

The damage requires one party to hold both the seal-opening key and the account-linked registry. So we split the push path into two services that each hold only half, plus a client rule that keeps them honest.

push-svc — the blind registry. This service stores the mapping the relay needs: "to wake subject S, here is a wake token." But every stored token is sealed — encrypted so push-svc itself cannot read it — with a fresh key-encapsulation per row, so two stored tokens for the same device are mutually unlinkable at rest (they look like independent random blobs). push-svc holds no seal-opening key, no APNs credentials, and no account identity. Rows carry a short time-to-live and are admitted by an anonymous token, so even the registry is not a durable identity map — it is a pile of per-row-encrypted, expiring blobs. A dump of push-svc yields ciphertext with no way to group it by device or tie it to an account.

push-sender — the decrypt-and-forget worker. This is the only component that can open a sealed wake token, and it is deliberately minimal: it holds the seal-opening key, receives one sealed blob, decrypts it in memory, extracts the device token, fires the one push at APNs, and forgets. It keeps no registry, does no joins, writes nothing down. It is a pure function from one sealed wake to one API call. We call it "the TEE workload" because its intended home is a trusted execution environment (more on that, honestly, below).

The client rule — seal only to attested keys. None of the above helps if a malicious operator can just publish their own seal key and collect readable wake tokens. So the client will only seal a wake token to a seal key that comes with a valid attestation — a signed statement that the key lives inside the expected, measured software, pinned to a trusted root. If the attestation is missing or wrong, the client refuses to seal, fail-closed, and the wake simply doesn't get set up. The exact attestation construction — the sealed box and the signed measurement a client checks — is its own article: The Attested Push Seal.

The shape is a sealed pneumatic tube through a doorman. The registry is a wall of locked tubes it cannot open; the sender is a doorman who opens exactly one tube, rings exactly one buzzer, and remembers nothing; and you only drop your tube in if the doorman can prove he is the real, bonded doorman and not an impostor with a photocopier.


3. Waking carefully: the WakeClass, again

Recall the coarse WakeClass the relay reads. It is precisely the signal that decides how to wake, and it is tuned to spend Apple's channel — and Apple's observation of us — as sparingly as possible:

  • voip (a call) wakes hard and is never suppressed — a ringing phone that sometimes doesn't ring is a broken phone, so a call always rings.
  • alert / sync / silent use a quieter wake that is deduplicated until reconnect (next section).
  • call_tail / ack never wake — call teardown and tiny receipts are not worth a push, so they wait for the next time the app is up.

Matching wake aggressiveness to the coarse class is what keeps the push volume — and thus Apple's timing view of us — as low as the feature set allows.


4. Dedup-until-reconnect: rationing the doorbell

If Alice sends Bob ten background messages while he's asleep, we do not want to fire ten pushes. That would drain the silent-push budget iOS grants us, and it would hand Apple a ten-event timing series about Bob.

So background wakes are deduplicated until reconnect: the first miss for a sleeping device fires one wake; every subsequent miss is suppressed until that device reconnects, at which point the mark resets. It is implemented as an atomic mark-and-test so a burst of simultaneous sends still yields a single doorbell. The woken app reconnects, pulls everything waiting obliviously, and the counter arms again. One wake per sleep, not one per message — the doorbell rings once, and you collect all the mail at once. (Calls, again, are exempt: each ring is real-time and must fire.)


5. The honest floor

This is where we refuse to overclaim, because the temptation to say "sealed in a hardware enclave, nobody can touch it" is strong and, today, would be a lie.

What is shipped and tested: the registry/sender split; the per-row sealing that makes stored tokens unlinkable; the client's fail-closed refusal to seal to a non-attested key; and a software attestation path (a signed measurement over an Ed25519 key) with end-to-end tests. The compelled-operator join — the phone↔account link — is closed as long as push-sender is actually running the attested image, because that image is the only thing that decrypts-and-forgets, and the client won't seal to anything else.

What is a marked seam, not yet shipped: the hardware enclave itself. The design's destination is a real trusted execution environment — an AWS Nitro Enclave or SEV-SNP — that makes the seal key non-exportable even by the operator running the box, and whose attestation is rooted in the CPU vendor rather than in our own software. That provider is stubbed and fails loud: ask push-sender to run in Nitro mode today and it refuses to start; ask the client to trust a hardware-rooted attestation and it throws. So the precise, honest statement of today's guarantee is "gated on running the attested software image," not "the key is sealed in hardware." The enclave is the remaining hardware-gated work, and the code has the seam cut for it.

The irreducible floor: even a perfect enclave does not remove Apple. To ring Bob's phone, someone must hand Apple Bob's device token, and Apple sees the wake happen. We minimize what reaches Apple (content-free pushes — the notification carries no message, just "wake up and go pull"), we ration the timing (dedup, WakeClass), and we seal everything at rest so no one else can join the graph. But Apple remains a join oracle for the wake event, and we disclose that rather than pretend a design trick removes it. It is the same posture as the Apple-gated post-quantum items: name the vendor floor precisely instead of hiding it.


6. The residual, stated plainly

  • push-svc holds only per-row-sealed, expiring, unlinkable blobs — no account map.
  • push-sender opens one wake in memory and forgets — no registry, no joins — when it is the attested image, which today is a software (not hardware-enclave) guarantee.
  • Apple sees content-free wake events, rationed and timing-blurred, and is disclosed as the one join oracle no design here removes.

That is the most private way we know to wake a sleeping phone through a channel we do not own — and an explicit, honest account of the one place we cannot.


References & further reading