Signed Mind intake for sealed motions (stacked on #21) #22

Merged
heimdall merged 3 commits from heimdall/sealed-mind-intake into main 2026-11-09 20:58:26 +00:00
Member

Now based on main (#21 merged as b42b86f). Signed Mind intake for sealed-motion commit/reveal. No merge, no deploy. Design: the "Signed Mind intake" section of docs/sealed-ballot-motion.md.

  • Each Mind holds its own Ed25519 key; Bifrost stores only public keys (MindKeys), registered and revoked by a moderator through audited endpoints (/api/sealed-motions/mind-keys).
  • POST /api/sealed-motions/{id}/mind: signed envelope (persona, verb, motion id, nonce, payload fields); no session, no cookie; runs the same SealedBallotService verbs as the persona's member account. Persona id is bound into the commitment hash (and SealedBallotDocument gains Persona; empty for member casts).
  • Refused, each tested with no ballot in the body: unknown persona, revoked key, wrong key, tampered field, persona not matching the key (one 401 answer), replayed nonce (409, stored per persona, spent only after the signature verifies), motion other than the route's (400), malformed (400), wrong phase (409), reveal not matching the persona-bound commitment (400). Valid commit + reveal by a Mind; revoke then refuse; rotation; moderator-only key management; persona stays bound to one account.
  • Custody (Heimdall/Eureka): key at /etc/gamecult/minds/<p>/sealed-motion.key, mode 0400, owned by the Mind's user; Eureka generates it and prints only the public half; whoever reads that file votes as that Mind. Rotation: register new, revoke old, between motions. tools/sealed-motion-sign.mjs signs envelopes (pubkey/commit/reveal), posts nothing; one test runs it against the server.
  • Correction to the brief: Ed25519 is not a .NET 8 built-in; verification uses BouncyCastle, already a dependency (HeimdallTokenVerifier.cs:61).

Tests: 6 new (MindIntakeTests), full suite 565 with the same 38 baseline failures as main (CultCache TS runtime absent etc.); run on .NET 10 runtime with roll-forward.

Update 3213254 (Libby, round 3): signature verified before any state-revealing 400 (route mismatch and clock window now come after it); a dummy Ed25519 verify for unknown/revoked persona and malformed signatures (counter-asserted, timing itself not measured); signed issuedAt with a 10-minute window and nonce pruning at 20 minutes; per-source rate limit (30/min, FixedWindowBudget) and 4096-byte body cap before parsing; \z in patterns; one account per persona and the reverse (MindPersonas, unique on both columns) so a second persona cannot overwrite the first's commit, losing a registration race fails on the index; DbUpdateException is a replay only when the nonce is actually there. Each fix has a test that fails without it (mutation-checked: dummy verify, prune, replay-only catch, account binding; the body cap and rate limit tests assert 413/429 directly). Tools helper and doc updated.

Update 0d7c3ec: rebased on main b42b86f. The nonce prune is a separate best-effort write: a prune that loses a race (concurrency exception) no longer fails a valid request (test fails without the fix). Added the test for #21's untested branch: five lost reveal races answer 409 "contended" and record nothing. Doc notes a retried reveal is judged for phase at request start but lands in the frozen digest through RevealSeq.

Unchecked:

  • Hand-written migration AddMindKeys, edited snapshot, never run on Postgres. The unique nonce index is not enforced by the in-memory provider; the replay test runs the explicit check path only.
  • Which key file the Persona turn reads in the live host and who runs the helper: not verified on the host.
  • Needs a rebase after #21 is fixed per Libby's second read (new head to come).
  • Mind account creation/approval (the member account a persona votes as) is a manual moderator step, not built.

🤖 Generated with Claude Code

  • Rate limit is per process, in memory; IPv6 /64 not grouped. Timing equality of the 401 paths is by construction (same verification count), not measured.
  • Unique-index races (persona binding, nonce) rely on Postgres; the in-memory provider does not enforce them, so tests assert the model's unique indexes and the pre-check paths.
Now based on `main` (#21 merged as b42b86f). Signed Mind intake for sealed-motion commit/reveal. No merge, no deploy. Design: the "Signed Mind intake" section of `docs/sealed-ballot-motion.md`. - Each Mind holds its own Ed25519 key; Bifrost stores only public keys (`MindKeys`), registered and revoked by a moderator through audited endpoints (`/api/sealed-motions/mind-keys`). - `POST /api/sealed-motions/{id}/mind`: signed envelope (persona, verb, motion id, nonce, payload fields); no session, no cookie; runs the same `SealedBallotService` verbs as the persona's member account. Persona id is bound into the commitment hash (and `SealedBallotDocument` gains `Persona`; empty for member casts). - Refused, each tested with no ballot in the body: unknown persona, revoked key, wrong key, tampered field, persona not matching the key (one 401 answer), replayed nonce (409, stored per persona, spent only after the signature verifies), motion other than the route's (400), malformed (400), wrong phase (409), reveal not matching the persona-bound commitment (400). Valid commit + reveal by a Mind; revoke then refuse; rotation; moderator-only key management; persona stays bound to one account. - Custody (Heimdall/Eureka): key at `/etc/gamecult/minds/<p>/sealed-motion.key`, mode 0400, owned by the Mind's user; Eureka generates it and prints only the public half; whoever reads that file votes as that Mind. Rotation: register new, revoke old, between motions. `tools/sealed-motion-sign.mjs` signs envelopes (pubkey/commit/reveal), posts nothing; one test runs it against the server. - Correction to the brief: Ed25519 is not a .NET 8 built-in; verification uses BouncyCastle, already a dependency (`HeimdallTokenVerifier.cs:61`). Tests: 6 new (`MindIntakeTests`), full suite 565 with the same 38 baseline failures as `main` (CultCache TS runtime absent etc.); run on .NET 10 runtime with roll-forward. **Update 3213254 (Libby, round 3):** signature verified before any state-revealing 400 (route mismatch and clock window now come after it); a dummy Ed25519 verify for unknown/revoked persona and malformed signatures (counter-asserted, timing itself not measured); signed `issuedAt` with a 10-minute window and nonce pruning at 20 minutes; per-source rate limit (30/min, `FixedWindowBudget`) and 4096-byte body cap before parsing; `\z` in patterns; one account per persona and the reverse (`MindPersonas`, unique on both columns) so a second persona cannot overwrite the first's commit, losing a registration race fails on the index; `DbUpdateException` is a replay only when the nonce is actually there. Each fix has a test that fails without it (mutation-checked: dummy verify, prune, replay-only catch, account binding; the body cap and rate limit tests assert 413/429 directly). Tools helper and doc updated. **Update 0d7c3ec:** rebased on main b42b86f. The nonce prune is a separate best-effort write: a prune that loses a race (concurrency exception) no longer fails a valid request (test fails without the fix). Added the test for #21's untested branch: five lost reveal races answer 409 "contended" and record nothing. Doc notes a retried reveal is judged for phase at request start but lands in the frozen digest through `RevealSeq`. **Unchecked:** - [ ] Hand-written migration `AddMindKeys`, edited snapshot, never run on Postgres. The unique nonce index is not enforced by the in-memory provider; the replay test runs the explicit check path only. - [ ] Which key file the Persona turn reads in the live host and who runs the helper: not verified on the host. - [ ] Needs a rebase after #21 is fixed per Libby's second read (new head to come). - [ ] Mind account creation/approval (the member account a persona votes as) is a manual moderator step, not built. 🤖 Generated with [Claude Code](https://claude.com/claude-code) - [ ] Rate limit is per process, in memory; IPv6 /64 not grouped. Timing equality of the 401 paths is by construction (same verification count), not measured. - [ ] Unique-index races (persona binding, nonce) rely on Postgres; the in-memory provider does not enforce them, so tests assert the model's unique indexes and the pre-check paths.
heimdall force-pushed heimdall/sealed-mind-intake from 2bc4215754 to 48bebec624 2026-11-09 20:42:06 +00:00 Compare
heimdall force-pushed heimdall/sealed-mind-intake from 48bebec624 to 3213254b96 2026-11-09 20:50:43 +00:00 Compare
heimdall changed target branch from heimdall/sealed-ballot-motion to main 2026-11-09 20:55:04 +00:00
heimdall force-pushed heimdall/sealed-mind-intake from 3213254b96 to 0d7c3ec078 2026-11-09 20:55:06 +00:00 Compare
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
GameCult/Bifrost!22
No description provided.