mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
feat(e2ee): F3 identity/TOFU + W2-4/W3-3 hardening — checkpoint before F3 UI
WIP save point. Server + W2-4/W3-3 complete and gate-green; F3 voice E2EE identity keys + TOFU implemented and MITM-verified-closed; the F3 voice-panel UI (safety-number display, verified/mismatch badge, re-pin modal) is still TODO. - W2-4 attachment link (coverage confirmed); W3-3a XFF CIDR pre-parse; W3-3b update-binary TOCTOU (single-handle verify + O_EXCL staging) - F3 server: migration 017 identity_public_key, PATCH /users/me persist, ready/member_join/user_update carry key, signed voice_e2ee_announce - F3 client: ECDSA identity keypair (keyring + pin store), publish wired into ready, verifyPeerAnnounce pin-before-legacy, rePinPeerIdentity recovery - Gates: server full CI mirror green (-race/-deadlock/lint/4 build tags); client typecheck/lint/format + 3337 vitest green. Rust CI-verify only. Next: build F3 voice-panel UI, then adversarial review, then finalize commit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,22 @@
|
||||
# Plan: Remediate security-hardening review regressions
|
||||
|
||||
**Status:** mostly landed — verified 2026-07-23 (deletion audit): every item
|
||||
except **W2-4** and **W3-3** has been implemented or superseded. This doc is
|
||||
the tracker of record for those two; close it when they land.
|
||||
**Status:** COMPLETE — verified 2026-07-23 (branch `feat/e2ee-identity-tofu`): every item
|
||||
has been implemented or superseded. W2-4 and both halves of W3-3 are the last to land.
|
||||
**W2-4:** DONE 2026-07-23 — `Server/db/attachment_queries.go:111`
|
||||
`LinkAttachmentsToMessage` links atomically and skips (not fails) already-linked,
|
||||
non-owned, and missing ids; legacy `uploader_id IS NULL` rows claimable. Locked by
|
||||
`TestLinkAttachmentsToMessage_SkipsAlreadyLinked` and
|
||||
`TestLinkAttachmentsToMessage_OwnershipGuard` (`Server/db/attachment_queries_test.go`).
|
||||
**W3-3:** DONE 2026-07-23 — two halves. (a) **XFF CIDR pre-parse:** `trustedCIDRs` parsed
|
||||
once at middleware construction into `[]*net.IPNet`; `clientIPWithProxies` takes the parsed
|
||||
form, `isTrustedProxy` deleted (callers use `ipInNets`), invalid entries warn at startup not
|
||||
per request. Locked by `TestRateLimitMiddleware_InvalidCIDRWarnsAtConstructionNotPerRequest`;
|
||||
leftmost-valid XFF fallback + `AdminIPRestrict` fail-closed unchanged. (b) **Update TOCTOU:**
|
||||
`DownloadAndVerify` returns the trusted hash; new `updater.OpenVerifiedBinary`/`Commit`
|
||||
verify through one open handle and confirm via `os.SameFile` that the renamed file is the one
|
||||
verified; O_EXCL 0600 staging refuses pre-planted paths. Locked by
|
||||
`TestOpenVerifiedBinary_SwapAfterVerifyDetectedAtCommit` + `TestDownloadFile_RefusesPreExistingDest`
|
||||
(Linux fd/tarball path is CI-verified). Server `-race` + `-tags deadlock` green.
|
||||
**Owner:** TBD
|
||||
**Tracks:** code review of branch `fix/security-hardening-review` (2026-07-17)
|
||||
**Estimated effort:** 2–4 focused days
|
||||
|
||||
@@ -12,7 +12,7 @@ This is a continuation/handoff doc: what is done, what remains, and how to resum
|
||||
|---|-----|---------|--------|
|
||||
| F1 | MED | Login lockout keyed on un-canonicalized username (vs `COLLATE NOCASE`) | ✅ done, committed `7145f76` |
|
||||
| F2 | MED | Unsynchronized concurrent wazero module invocation (data race) | ✅ done, committed `71b5f13` |
|
||||
| F3 | MED | Voice E2EE trusts server-relayed ECDH keys (server MITM) | ⏳ **TODO — designed, not started** |
|
||||
| F3 | MED | Voice E2EE trusts server-relayed ECDH keys (server MITM) | ✅ **implemented (branch `feat/e2ee-identity-tofu`)** — MITM closed for published+pinned peers; UI surfacing is follow-up (see below) |
|
||||
| F4 | MED | HTTP TOFU proxy accepts any cert on first use (credential exposure) | ✅ done, committed `f22985a` |
|
||||
| F5 | LOW | Voice perms use stale connect-time role snapshot | ✅ done, committed `260d038` |
|
||||
| F6 | LOW | Lost cache invalidation in `PermissionService.getOrPopulate` | ✅ done, committed `e6a0d87` |
|
||||
@@ -26,7 +26,8 @@ This is a continuation/handoff doc: what is done, what remains, and how to resum
|
||||
`cd Client/tauri-client/src-tauri && cargo clippy -- -D warnings` (or push and
|
||||
let CI do it). Pure `tofu` logic has `#[cfg(test)]` unit tests; the frontend is
|
||||
covered by the 3311-green unit suite.
|
||||
2. **Then F3** — the only remaining finding (below). (F6 landed 2026-07-23 as
|
||||
2. ~~**Then F3**~~ — **DONE 2026-07-23** on branch `feat/e2ee-identity-tofu` (see the
|
||||
"F3 status 2026-07-23" block directly below). (F6 landed 2026-07-23 as
|
||||
`e6a0d87`, split out from the D13 permission-consolidation commits that
|
||||
followed it on this branch.)
|
||||
|
||||
@@ -39,6 +40,46 @@ perms served up to `permCacheTTL`). Fix: a `gen uint64` counter bumped by every
|
||||
cache if it changed. Test `TestGetOrPopulate_InvalidationDuringPopulateNotLost`
|
||||
locks it. Verified `-race` + `-tags deadlock` green.
|
||||
|
||||
## F3 status 2026-07-23 (branch `feat/e2ee-identity-tofu`)
|
||||
|
||||
**Implemented, test-first, MITM path verified closed by a 3-lens adversarial panel + a
|
||||
dedicated re-verification pass.** The original implementation shipped the crypto but had a
|
||||
dead publish path (the feature was inert); that and three related defects were caught by
|
||||
review and fixed. What is done:
|
||||
|
||||
- **Server:** migration `017_user_identity_key.sql` (`users.identity_public_key`);
|
||||
`UpdateUserIdentityKey` + column in user/`ListMembers` SELECTs; `PATCH /users/me`
|
||||
accepts+persists the key (via `UserService.UpdateIdentityKey`, audited); key carried in
|
||||
`ready`/`member_join`/`user_update`; `voice_e2ee_announce` gains an optional `signature`
|
||||
validated + stored + relayed (incl. the late-joiner replay). Legacy unsigned announces
|
||||
still accepted (client enforces fail-closed). `make sqlc-verify`/`protocol-verify` green;
|
||||
server `-race` + `-tags deadlock` green.
|
||||
- **Client:** ECDSA P-256 identity keypair (OS keyring via new Rust
|
||||
`save/load/delete_identity_key` + pin store `identity_pins.json`); ephemeral announces
|
||||
signed at all sites; **publish wired into the `ready` flow** (dispatcher publishes the key
|
||||
once, with username, when the server copy is absent/stale); `verifyPeerAnnounce` resolves
|
||||
the **pin before** the legacy shortcut (a stripping server can't downgrade a pinned peer);
|
||||
`rePinPeerIdentity` for key-rotation recovery. Full client suite 3337 green;
|
||||
typecheck/lint/format clean. (Rust halves compile-checked only — no local MSVC; **CI
|
||||
must verify `cargo`**.)
|
||||
|
||||
**Verified closed:** a malicious/stripping server can no longer silently MITM a peer whose
|
||||
identity key is published and locally pinned — an ephemeral-key swap fails ECDSA
|
||||
verification and the room key is never wrapped for the attacker.
|
||||
|
||||
**Follow-up (not MITM holes — deferred, none block the crypto):**
|
||||
1. **Surface the safety number in the voice panel.** `safetyNumber`/`peerVerifications` are
|
||||
computed and stored but **no component renders them**, so the out-of-band check that
|
||||
detects the inherent TOFU *first-contact* window is not user-reachable yet.
|
||||
2. **Wire the verified/unverified/mismatch badge + a re-pin affordance.** `rePinPeerIdentity`
|
||||
exists but no UI calls it — a legitimately rotated peer key currently blocks voice with no
|
||||
in-app recovery (mirror `main.ts`'s `createCertMismatchModal onAccept` flow).
|
||||
3. `getIdentityPin` **fail-opens** on a transient local keyring/store read error (one announce
|
||||
falls through to legacy). Not server-controllable; consider fail-closed when a pin *may*
|
||||
exist.
|
||||
4. Fast-join timing: a peer joining voice before peers process its `user_update` is seen as
|
||||
legacy for that announce — degrades to *unverified*, never wrongly-*verified*.
|
||||
|
||||
## F3 — Voice E2EE identity keys + TOFU (the remaining work)
|
||||
|
||||
**Problem.** `voice_e2ee_announce` carries only `{public_key}`; the server
|
||||
|
||||
+42
-9
@@ -246,7 +246,7 @@ Sent once after `auth_ok` (fresh connection or replay fallback).
|
||||
|
||||
**dm_channels[]:** `channel_id`, `recipient` (user object with `id`, `username`, `avatar`, `status`), `last_message_id`, `last_message`, `last_message_at`, `unread_count`
|
||||
|
||||
**members[]:** All registered users with `id`, `username`, `avatar`, `role` (lowercase name), `status`
|
||||
**members[]:** All registered users with `id`, `username`, `avatar`, `role` (lowercase name), `status`, `identity_public_key` (base64 long-term E2EE identity key, omitted when the user has not published one — see voice E2EE TOFU)
|
||||
|
||||
**voice_states[]:** All users currently in any voice channel: `channel_id`, `user_id`, `muted`, `deafened`
|
||||
|
||||
@@ -531,12 +531,16 @@ Sent when a user first connects (fresh connection, not reconnect replay).
|
||||
"id": 5,
|
||||
"username": "newuser",
|
||||
"avatar": null,
|
||||
"role": "member"
|
||||
"role": "member",
|
||||
"identity_public_key": "base64-identity-pubkey"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`identity_public_key` is the user's long-term E2EE identity public key (see
|
||||
voice E2EE TOFU); omitted when the user has not published one.
|
||||
|
||||
### member_update (Server -> Client, broadcast)
|
||||
|
||||
Triggered when an admin changes a user's role.
|
||||
@@ -565,7 +569,7 @@ Triggered when an admin changes a user's role.
|
||||
### user_update (Server -> Client, broadcast)
|
||||
|
||||
Broadcast when a user changes their own profile via `PATCH /api/v1/users/me`
|
||||
(username and/or avatar).
|
||||
(username, avatar and/or identity key).
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -574,12 +578,15 @@ Broadcast when a user changes their own profile via `PATCH /api/v1/users/me`
|
||||
"payload": {
|
||||
"user_id": 5,
|
||||
"username": "newname",
|
||||
"avatar": "uuid.png"
|
||||
"avatar": "uuid.png",
|
||||
"identity_public_key": "base64-identity-pubkey"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`avatar` may be `null` when unset.
|
||||
`avatar` may be `null` when unset. `identity_public_key` carries the user's
|
||||
current long-term E2EE identity key and is omitted when none is published;
|
||||
peers that pinned a different key must surface a TOFU mismatch.
|
||||
|
||||
### member_leave (reserved)
|
||||
|
||||
@@ -739,24 +746,50 @@ the room key so departed members cannot decrypt future media.
|
||||
Both E2EE message types are rate limited at 5 per second per user. Key
|
||||
material must be standard-alphabet base64 (padded or unpadded).
|
||||
|
||||
**Identity keys + TOFU:** each client holds a long-term ECDSA P-256 identity
|
||||
keypair, published via `PATCH /api/v1/users/me` (`identity_public_key`) and
|
||||
distributed in the `ready` / `member_join` / `user_update` member payloads.
|
||||
Peers pin the key on first sight (trust-on-first-use) and verify each
|
||||
announce's `signature` against the pin, so a malicious server cannot swap
|
||||
`user_id ↔ ephemeral pubkey` after first contact. A later key change is
|
||||
surfaced to the user as a TOFU mismatch.
|
||||
|
||||
### voice_e2ee_announce (Client -> Server)
|
||||
|
||||
Announce this participant's ECDH public key to the channel.
|
||||
Announce this participant's ephemeral ECDH public key to the channel.
|
||||
`signature` is the ECDSA P-256 signature by the sender's long-term identity
|
||||
key over `"owncord-voice-e2ee-announce-v1" ‖ userId ‖ ephemeral-pubkey-raw`
|
||||
(TOFU — see above). It is optional at the protocol level: legacy clients omit
|
||||
it, and receiving clients enforce the fail-closed posture (peer has a
|
||||
published identity key but the signature is missing/invalid → reject).
|
||||
|
||||
```json
|
||||
{ "type": "voice_e2ee_announce", "payload": { "public_key": "base64-ecdh-pubkey" } }
|
||||
{
|
||||
"type": "voice_e2ee_announce",
|
||||
"payload": {
|
||||
"public_key": "base64-ecdh-pubkey",
|
||||
"signature": "base64-ecdsa-signature"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The server validates `signature` like `public_key` (standard-alphabet base64,
|
||||
max 128 chars) and stores it alongside the key, but never verifies it — only
|
||||
clients hold the pinned identity keys.
|
||||
|
||||
### voice_e2ee_announce (Server -> Client, broadcast to voice channel)
|
||||
|
||||
Relayed to the other participants with the sender's user ID attached:
|
||||
Relayed to the other participants with the sender's user ID attached. Also
|
||||
replayed to late joiners from the stored key+signature. `signature` is
|
||||
omitted when the announcing client did not send one:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "voice_e2ee_announce",
|
||||
"payload": {
|
||||
"user_id": 1,
|
||||
"public_key": "base64-ecdh-pubkey"
|
||||
"public_key": "base64-ecdh-pubkey",
|
||||
"signature": "base64-ecdsa-signature"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
+7
-1
@@ -59,6 +59,7 @@ CREATE TABLE IF NOT EXISTS schema_versions (
|
||||
| `014_events_table.sql` | Adds `events` — persistent broadcast log for reconnect cold-tier replay |
|
||||
| `015_plugins.sql` | Adds `plugins` and `plugin_kv` for the WASM plugin runtime |
|
||||
| `016_announcement_channel_type.sql` | Recreates the channel-type triggers to allow `announcement` |
|
||||
| `017_user_identity_key.sql` | Adds `users.identity_public_key` (long-term E2EE identity key for voice TOFU) |
|
||||
|
||||
---
|
||||
|
||||
@@ -105,12 +106,17 @@ CREATE TABLE users (
|
||||
last_seen TEXT,
|
||||
banned INTEGER NOT NULL DEFAULT 0,
|
||||
ban_reason TEXT,
|
||||
ban_expires TEXT
|
||||
ban_expires TEXT,
|
||||
identity_public_key TEXT
|
||||
);
|
||||
```
|
||||
|
||||
Valid status values: `online`, `idle`, `dnd`, `offline`. All statuses are reset to `offline` on server startup.
|
||||
|
||||
`identity_public_key` (added in migration 017) is the user's long-term E2EE
|
||||
identity public key (base64 ECDSA P-256) used for TOFU pinning of voice E2EE
|
||||
announces; `NULL` = not published (legacy client).
|
||||
|
||||
---
|
||||
|
||||
### sessions
|
||||
|
||||
Reference in New Issue
Block a user