Skip to content

Remote Signal keys for zero-knowledge linked devices - #48

Open
purpshell wants to merge 4 commits into
purpshell/shadow-clientfrom
polymorfa/remote-cipher
Open

purpshell wants to merge 4 commits into
purpshell/shadow-clientfrom
polymorfa/remote-cipher

Conversation

@purpshell

@purpshell purpshell commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

Summary

Lets HyperMeow run a linked device whose Signal private keys, sessions and message plaintext live in a customer-run Signal service, so Polymorfa's runner never sees message content (the A2 boundary in polymorfa/polymorfa internal/docs/superpowers/specs/2026-10-04-customer-owned-data-and-zero-knowledge-sessions.md).

NewRemoteSignalClient keeps the socket, receipts, retry receipts, prekey uploads and QR pairing. Each received <enc> goes to RemoteSignal.Receive and only delivery metadata returns (events.RemoteMessage), plus an own-device app-state key share. Outgoing messages are stanzas the remote store built, sent with SendPrebuiltStanza. The Rust side is polymorfa/whatsapp-rust#5.

Stacked on #43 because both change the same Signal call sites.

Impact

Impact table

Area Updated / blocked Change or named blocker Paths / companion PRs / evidence
Libraries (HyperMeow) Updated Remote-keyed client, remote prekey store, receive/pairing/retry/send diversions, RemoteKeys guard, events.RemoteMessage remote_signal.go, message.go, pair.go, retry.go, send.go, store/
Libraries (whatsapp-rust) Updated Signal service side polymorfa/whatsapp-rust#5
Runners, Temporal, customer API, Admin API Blocked The runner must implement RemoteSignal over the runner–service contract; not started backend PR in polymorfa/polymorfa
Pair-code pairing on remote keys Blocked Needs a remote operation for the whole pair-code step (X25519 with the identity key) returns ErrRemoteSignalUnsupported meanwhile
Bot messages (msmsg) on remote keys Blocked Message secrets live in the remote store; acked with NackMissingMessageSecret decryptMessagesRemote
SDKs, CLI, docs, Console, Admin, Inspector Blocked Depend on the backend contract spec ladder

No impact table

Area Why there is no impact Supporting paths or evidence
Existing clients (NewClient, NewShadowClient) Every new branch is gated on isRemoteSignal(); the only shared refactor moves the incoming-retry limit check into acceptIncomingRetry unchanged go test ./... passes
Billing/PAYG, Usage, customer Analytics, Logs, audit logs, onboarding, product tracking Library change with no deployment or metering; assessed in the backend PR spec cross-cutting checks

Verification

Tested revision a4e906e on polymorfa/remote-cipher (base 49a8071).

  • go test -count=1 ./...: all packages pass.
  • 17 new tests in remote_signal_test.go: public-only device and the RemoteKeys panic guard; receive delivers metadata and a receipt; already-delivered only acks; decrypt failure sends a retry receipt and UndecryptableMessage; a store outage neither acks nor sends a retry receipt; undecodable is nacked with 491; unuploaded prekeys are reused and only the uploaded batch is marked, and retry-receipt prekeys are marked published; pairing rejects a signature over different details, one that does not verify, and one from a different identity; SendPrebuiltStanza waits for the ack and rejects non-message stanzas; retry receipts go to Resend and honour PreRetryCallback; identity changes reach ForgetIdentity, with hosted senders sent as their base user; a phone-number sender's sessions move to its LID through MigratePNToLID once per user after success; local SendMessage, SendFBMessage and PairPhone are refused.
  • Review: all 7 Codex findings were valid and are fixed in 86ec616; each thread has a reply naming the change and its test. A re-check found finding 7 missed hosted devices and that PN-to-LID migration still ran against the unused local store; both are fixed in a4e906e with Customer-held Signal state: fenced sealed store and Signal operations whatsapp-rust#5.
  • go vet ./... and gofmt -l .: clean.

Not verified: after a store outage the stanza is left unacknowledged; recovery relies on the server redelivering it, which is expected on the next reconnect but has not been observed. Unuploaded prekeys are tracked in memory, so a restart leaves them unused in the remote store. no remote store or real WhatsApp session has been connected; the runner implementation and the runner–service contract do not exist yet.

Session provenance and UI evidence

  • Agent registration PR: polymorfa/polymorfa#518 (purpshell T3 Claude).
  • Sanitized transcript checkpoint and evidence commit: not yet published for this thread; it will be committed under internal/agent-sessions/ with the monorepo backend PR.
  • Capture coverage/gaps and tested revision: d4c58f2; no published checkpoint yet.
  • UI screenshots: none; no UI changes.

Rollout and rollback

Merge after #43 (this PR targets its branch). Nothing uses NewRemoteSignalClient until the backend PR pins a HyperMeow revision containing it, so merging changes no runtime behaviour. Rollback: revert; no consumer exists. A remote-keyed device persists zero private-key fields; the runner must set RemoteKeys again on load, which the backend PR covers.

NewRemoteSignalClient runs a linked device whose Signal private keys,
sessions and message plaintext live outside the client, behind the
RemoteSignal interface. The client keeps its socket, receipts, retry
receipts, prekey uploads and QR pairing, but never decrypts or encrypts:

- receive hands each <enc> payload to the remote store and gets back only
  delivery metadata (events.RemoteMessage), plus an app-state key share
  when an own device sent one; errors map to the same retry receipts and
  nacks as the local path;
- prekeys come from a remote-backed PreKeyStore that marks whole upload
  batches;
- pairing keeps the local HMAC and account-signature checks and takes the
  device signature from the store, refusing one over different details;
  the store learns its own JIDs before pairing is confirmed;
- incoming retry receipts ask the store to resend from its own record;
- SendPrebuiltStanza transmits stanzas the store built and waits for the
  ack. Local SendMessage and pair-code pairing are refused.

The device store holds only public material and is marked RemoteKeys; the
local Signal store panics instead of using its zero private keys.
@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • dev

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Team
  • Run ID: caa0ef56-c533-463a-adfb-8cec88efc248

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-10T02:22:23.579906Z d4c58f2 PR opened
🔒 Security Review ✅ Completed 2026-10-10T02:18:56.462406Z d4c58f2 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

golang.org/x/tools v0.50.0 and later require Go 1.26, so installing
goimports@latest fails the Go 1.25 build job on every new run. Pin
v0.49.0, the newest release on Go 1.25.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4c58f25a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread retry.go
Comment thread remote_signal.go
Comment thread remote_signal.go
Comment thread remote_signal.go
Comment thread remote_signal.go Outdated
Comment thread send.go
Comment thread remote_signal.go
- Remote retry path calls PreRetryCallback (message unavailable) before resending.
- Retry-receipt prekeys from GenOnePreKey are marked published.
- A store transport error is neither acked nor retried; the server redelivers.
- Verify the remote device signature against the remote identity key.
- Reuse unuploaded prekeys and mark only the batch that was uploaded.
- SendFBMessage refuses local encryption on a remote client.
- Identity changes forget the sender's sessions in the remote store.
- RemoteSignal gains MigratePNToLID. On a remote client, the PN-to-LID
  migration in decryptMessages goes to the remote store (once per user
  after success) instead of the unused local session store.
- forgetRemoteIdentity sends hosted JIDs as their base user; the store
  clears hosted devices with it, matching the local cleanup.
- Document that PreRetryCallback gets a nil message on remote clients.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant