matrix-e2ee-bin: Binary auf mautrix-go und PHP-Seite (#845 in site-cpd) #1

Merged
andi merged 13 commits from initial-build into main 2026-10-23 10:32:42 +00:00
Collaborator

Bezug: CrowdPower/site-cpd#845 Ende-zu-Ende-Verschlüsselung in CPD

Was

Das Composer-Paket crowdpower/matrix-e2ee-bin: ein statisches Go-Binary auf mautrix-go (goolm, ohne cgo), das ein Matrix-Konto zu einem Ende-zu-Ende-verschlüsselnden Gerät macht, von PHP je Ereignis gestartet, dazu eine Drupal-freie PHP-Seite (Runner, Exceptions je Exit-Code, Fake-Binary für Tests der Nutzer). Die README ist die Spezifikation (Befehle, Flags, Umgebung, Exit-Codes, stdout, Bedrohungsmodell, Build).

Wie

  • Aus dem Spike vom 2026-10-22 (Session 2026-10-22-vorbereiten-assistent-verschluesselung.md in site-cpd, Abschnitt C „A′“) nach einem vorab geschriebenen Vertrag gebaut; der Vertrag ist in die README eingeflossen.
  • Reproduzierbarer Build in Docker (build.sh, VERSION mit SHA-256 und Versionen, build.sh --check baut neu und vergleicht Byte für Byte).
  • Tests: Go 41 Tests (89 mit Subtests, eigener Test-Homeserver mit echten Olm-Konten und Cross-Signing), PHP 59 Tests; scripts/integration.sh gegen Continuwuity v26.9.1 in Docker, 38 von 38 Schritten, Ergebnis in der README.
  • Ein unabhängiges Review (Bedrohungsmodell: kompromittierter Homeserver, untergeschobene Geräte, geteilter Host, Abbruch und Gleichzeitigkeit, verlorener Store) fand drei Lücken, alle behoben und getestet: Replay eigener Nachrichten mit fremdem Absender galt als verified; send teilte mit jedem Mitglied, das der Server nennt (jetzt optionale Empfängerliste, Exit 4 unexpected_members); pin brauchte eine Schlüsselabfrage. Dazu Exit 5 enger gefasst (ein E/A-Fehler auf dem Netzdateisystem verwirft keinen gesunden Store mehr), Rotation bei Mitgliederwechsel in beide Richtungen, keine Beantwortung von Schlüsselanfragen, Gesamt-Timeout je Befehl, fcntl-Sperre im Binary als zweite Sicherung, Secrets nicht mehr in argv und nicht über json_encode lesbar.

Review-Fragen

  • Lizenz EUPL-1.2 und Composer-Vendor crowdpower sind vom Bau gesetzt, nicht entschieden. Annahme: wie hook-dev-alter/typst-bin.
  • goolm (pure-Go-Olm) statt libolm oder vodozemac, weil ohne cgo; ein unabhängiges Audit von goolm ist nicht bekannt. Annahme: für Demo-Daten tragbar, Frage an das Review vor echten Daten (A7 in #845).
  • Relationen (m.relates_to) sind nach Matrix-Spezifikation im Klartext der Hülle und damit vom Server fälschbar; dokumentiert, nicht behebbar.
  • Nicht gegen Synapse getestet.

Offen, nicht in diesem PR

  • login --password für ein gewöhnliches Konto (chat2note, #844).
  • build.sh --check in einer CI des Pakets; Build-Image per Digest pinnen; Idempotenz von send.

🤖 Generated with Claude Code

Bezug: CrowdPower/site-cpd#845 Ende-zu-Ende-Verschlüsselung in CPD ## Was Das Composer-Paket `crowdpower/matrix-e2ee-bin`: ein statisches Go-Binary auf mautrix-go (`goolm`, ohne cgo), das ein Matrix-Konto zu einem Ende-zu-Ende-verschlüsselnden Gerät macht, von PHP je Ereignis gestartet, dazu eine Drupal-freie PHP-Seite (Runner, Exceptions je Exit-Code, Fake-Binary für Tests der Nutzer). Die README ist die Spezifikation (Befehle, Flags, Umgebung, Exit-Codes, stdout, Bedrohungsmodell, Build). ## Wie - Aus dem Spike vom 2026-10-22 (Session `2026-10-22-vorbereiten-assistent-verschluesselung.md` in site-cpd, Abschnitt C „A′“) nach einem vorab geschriebenen Vertrag gebaut; der Vertrag ist in die README eingeflossen. - Reproduzierbarer Build in Docker (`build.sh`, `VERSION` mit SHA-256 und Versionen, `build.sh --check` baut neu und vergleicht Byte für Byte). - Tests: Go 41 Tests (89 mit Subtests, eigener Test-Homeserver mit echten Olm-Konten und Cross-Signing), PHP 59 Tests; `scripts/integration.sh` gegen Continuwuity v26.9.1 in Docker, 38 von 38 Schritten, Ergebnis in der README. - Ein unabhängiges Review (Bedrohungsmodell: kompromittierter Homeserver, untergeschobene Geräte, geteilter Host, Abbruch und Gleichzeitigkeit, verlorener Store) fand drei Lücken, alle behoben und getestet: Replay eigener Nachrichten mit fremdem Absender galt als `verified`; `send` teilte mit jedem Mitglied, das der Server nennt (jetzt optionale Empfängerliste, Exit 4 `unexpected_members`); `pin` brauchte eine Schlüsselabfrage. Dazu Exit 5 enger gefasst (ein E/A-Fehler auf dem Netzdateisystem verwirft keinen gesunden Store mehr), Rotation bei Mitgliederwechsel in beide Richtungen, keine Beantwortung von Schlüsselanfragen, Gesamt-Timeout je Befehl, `fcntl`-Sperre im Binary als zweite Sicherung, Secrets nicht mehr in argv und nicht über `json_encode` lesbar. ## Review-Fragen - Lizenz EUPL-1.2 und Composer-Vendor `crowdpower` sind vom Bau gesetzt, nicht entschieden. Annahme: wie `hook-dev-alter/typst-bin`. - `goolm` (pure-Go-Olm) statt libolm oder vodozemac, weil ohne cgo; ein unabhängiges Audit von goolm ist nicht bekannt. Annahme: für Demo-Daten tragbar, Frage an das Review vor echten Daten (A7 in #845). - Relationen (`m.relates_to`) sind nach Matrix-Spezifikation im Klartext der Hülle und damit vom Server fälschbar; dokumentiert, nicht behebbar. - Nicht gegen Synapse getestet. ## Offen, nicht in diesem PR - `login --password` für ein gewöhnliches Konto (chat2note, #844). - `build.sh --check` in einer CI des Pakets; Build-Image per Digest pinnen; Idempotenz von `send`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Runner with one method per command and typed results built offensively
from stdout, one exception per exit code, Seeds and SendRequest value
objects, the caller's lock around every process start, connection and
pickle key only through the environment, seeds and events on stdin.
The fake binary plays the binary from a JSON script and logs every
call; FakeBinary and Stdout let consumers' tests script it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
identity, login, sync, decrypt, send, trust, pin and tofu-reset, flags
and MATRIX_E2EE_* environment as in the contract, one JSON object on
stdout and exit codes 0 to 5. sync saves since only after processing
the response; decrypt re-encodes stdin with Go so escaped slashes do
not corrupt the Megolm ciphertext; send fetches members and device
lists before encrypting. The store opens SQLite with modernc.org/sqlite
in delete or wal journal mode. Tests need no homeserver.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Measured against Continuwuity: a deleted device answers 403 M_FORBIDDEN
"Device does not exist ...", a wrong as_token 401 M_UNKNOWN_TOKEN.
In appservice mode the token is the as_token, so M_UNKNOWN_TOKEN is a
configuration error and becomes exit 1; moving the store aside would
not help. M_FORBIDDEN naming the device counts with 403 and with
Synapse's 400.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
build.sh runs the contract's go build in golang:<toolchain>-alpine with
the toolchain pinned in go.mod, writes the binary's SHA-256 and the
versions of Go, mautrix-go, go.mau.fi/util and modernc.org/sqlite to
VERSION, and with --check rebuilds and compares byte for byte.
BinaryHashTest checks the committed binary against VERSION;
RealBinaryTest drives the packaged binary through the Runner where no
homeserver is needed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
scripts/integration.sh drives the binary against Continuwuity and the
partner dave (Element X with strict key sharing) from a fresh store:
first message, reply and edit, identity reset with exit 4 and withheld
session, tofu-reset, re-login from the seeds, the exit 5 cases and a
leak check of stderr at debug level. Not CI.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Documents the final state of binary and PHP side, including where it
goes beyond the contract (exit 1 for a wrong as_token, nullable sender
fields, pin writing first-seen itself, rotation on identity change),
and the result table of the integration run of 2026-10-23. The
integration script now prints paths relative to the package.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The README states everything the contract agreed, as built; keeping
both would leave two descriptions of the same interface to drift.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
send takes {"text", "html"} as one JSON object on stdin instead of
--text and --html: argv is readable in /proc by every user of the host
while the process runs, and freistilbox is a shared host. --room,
--msgtype, --reply-to and --edit-of stay flags. The Runner's API is
unchanged; SendRequest::stdin() carries the body.

A store that holds a device but no since is exit 5 instead of 2: it is
what a login leaves that died before saving since, and the caller's
recovery (move aside, login from the seeds, pin) repairs it.

Rebuilt binary; integration run 35 of 35 steps ok.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
States the measured homeserver answers under exit 5: M_FORBIDDEN
naming the device (403 on Continuwuity) is exit 5, a wrong as_token is
exit 1 and leaves the store intact, Synapse is untested. Adds the store
without since to exit 5 and the new integration table.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
M1 decrypt and sync refuse an event decrypted with the bot's own
session under another sender (own_session_replay, exit 4) and report
a trust state from cross-signed-tofu up only for the sender's own
device. M2 send takes an optional recipient list on stdin and stops
with exit 4 unexpected_members before encrypting. M3 pin upserts the
first-seen key without a key query.

S1 sync keeps since when a store write failed while processing.
S2 exit 5 only for a missing store, no device, no since, a wrong
pickle key, the device gone on the server and own keys disagreeing.
S3 login refuses an existing path. S4/S5 decrypt and send fetch keys
themselves and fail on a missed user. S6 sync --min-trust,
timeline_withheld, limited_rooms, content re-encoded. S7 rotation on
joins and leaves against recorded session members; tofu-reset drops
sessions in every shared room. S8 no key request is answered.
S9 store read errors propagate. S12 one deadline per command.
S13 fcntl lock on <db>.lock. S14 no --as-token flag. S15
devices_shared counts devices that received the key. S16 every store
command needs since. Pickle key at least 32 bytes; trailing seeds
rejected; unparsable log lines with req_body dropped; sender_device_id
only from the store.

Tests against a test homeserver with really cross-signed partners.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SendRequest takes recipients; exit 4 of send becomes
UnexpectedMembersException; TrustException carries reason. sync()
takes minTrust; SyncResult has withheld, limitedRooms and
timelineInvalid, parsing timeline entries one by one; decrypt content
is any JSON object or list; stdout is decoded with invalid UTF-8
substituted. A process killed by a signal becomes
BinaryFailedException; an explicit binary that is not executable
throws BinaryNotFoundException. Connection and Seeds keep their
secrets outside their properties, refuse json_encode and serialize,
and Connection needs a pickle key of 32 bytes. The Runner's methods
are unchanged except sync()'s new optional parameter.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
New rows: a bot message replayed as dave (exit 4 own_session_replay),
send with dave outside the recipients (exit 4, nothing sent),
sync --min-trust withholding dave's event after his reset. A store
path that is a directory is exit 1 now. 38 of 38 steps ok.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Adds the threat model, the narrowed exit 5 with the note that a
homeserver can trigger it at will, recipients and withheld events,
pin without a key query, the binary's own store lock, unauthenticated
relations, open points (send idempotency, image digest, CI for
build.sh --check) and the new integration table.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
andi merged commit e00826ca23 into main 2026-10-23 10:32:42 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Packages/matrix-e2ee-bin!1
No description provided.