feat(cli): --signing-key-file flag overrides signing identity per command#282
Merged
Conversation
…mand Adds a top-level `--signing-key-file <PATH>` flag (with `RIVER_SIGNING_KEY_FILE` env var fallback) that reads a raw 32-byte Ed25519 secret key and uses it in place of the room's stored `signing_key_bytes` for the current command. The override is in-memory only; `rooms.json` on disk is never modified. # Why `rooms.json` only stores ONE `signing_key_bytes` per room, but this machine often has multiple identities for the same room (room owner, invite bot, alt accounts). The UI's chat-delegate sync periodically overwrites `rooms.json[room].signing_key_bytes` with whatever the delegate has stored — silently leaving owner ops broken without a manual swap of the on-disk key. Pattern as documented in `river-official-room` skill: swap key → run command → optionally swap back. Fragile and recurring. The flag formalizes the "I have multiple identities, pick at command time" model: nominate the right identity per command, no rooms.json mutation, the existing identity stays loaded. Distinct from `message send --signing-key` which takes a base64- encoded inline key — the global file-based flag is preferred for non-test use because the key doesn't appear in shell history. # Tests - `signing_key_override_is_returned_and_not_persisted` in `cli/src/storage.rs` pins the contract: with override, `get_room` returns the override; without override (fresh Storage), the ORIGINAL stored bytes come back — proving the override is not written back to rooms.json. # Manual verification Sent the river#275/#276/#278 release announcement to the official Freenet River room as Room Owner via `--signing-key-file ~/.config/freenet-river-official/room_owner_signing_key.bin`, no rooms.json swap. Confirmed via `riverctl message list` that the message landed signed by Room Owner. # Follow-up #281 tracks the "perfect world" decentralized-version-pointer follow-up (a `freenet-updates` crate that uses a Freenet contract for tooling version checks). Separate PR. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Skeptical HIGH-1: identity export under `--signing-key-file` could produce a wire-format-corrupted token. When the override resolves to a different identity than the cached `self_authorized_member` in rooms.json, the bundled IdentityExport had a signing_key whose verifying_key() did NOT match authorized_member.member.member_vk. Importing such a token would silently fail every subsequent contract operation. Fix: validate `signing_key.verifying_key() == authorized_member.member.member_vk` just before constructing IdentityExport; error out cleanly with a message pointing at the override as the likely cause if they disagree. The owner-self-signed path (line 74-81) and the network-lookup path (line 82-113) already produce coherent pairs; only the cached-from-disk path could mismatch. Code-first/skeptical Important: doc-comment drift on `Storage::signing_key_override` field said `--signing-key` / `RIVER_SIGNING_KEY` but the actual flag/env are `--signing-key-file` / `RIVER_SIGNING_KEY_FILE`. Fixed both the field doc and the test docstring so a future grep for the right names actually hits this code. Code-first Important: the file-loader error message referenced `riverctl identity export` as if it produced raw 32-byte output. It doesn't — it produces an armored multi-field token. Clarified the error to point at the room-key .bin backup format (which IS raw 32 bytes) and explicitly contrast with `identity export`'s armored shape. Skeptical Important #5: added unit tests for the file loader's length validation. Extracted `parse_signing_key_bytes` as a pure helper so the tests don't need a tempfile. Tests cover: 32-byte accept, short reject (with byte-count in message), long reject (with base64 hint in message), empty reject. Other Important items (skeptical #2 info-log on override, #3 file-permissions warning, #5 env-vs-flag precedence test) are documented for follow-up but not blocking — the override-mismatch fix is the load-bearing correctness item from this review round. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
sanity
added a commit
that referenced
this pull request
May 18, 2026
- #282 (--signing-key-file flag): fixes recurring rooms.json drift for owner ops on the same machine. - #286 (UI polish bundle): #283 auto-scroll regression, #284 sync- window placeholder UX, #267 same-second DM revive, #279 processed- invite gate, #280 banned-card state. riverctl 0.1.61 published to crates.io alongside. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
`rooms.json` only stores ONE `signing_key_bytes` per room, but this machine often has multiple identities for the same room (room owner, invite bot, alt accounts). The UI's chat-delegate sync periodically overwrites `rooms.json[room].signing_key_bytes` with whatever the delegate has stored — silently leaving owner ops (announcements, bans, room config edits) broken without a manual swap of the on-disk key.
The current mitigation in the `river-official-room` skill is: stash the existing `rooms.json`, copy the room-owner backup key into it, run the command, optionally swap back. Fragile, recurring, and easy to forget.
Approach
Add a top-level `--signing-key-file ` flag (with `RIVER_SIGNING_KEY_FILE` env var fallback) that reads a raw 32-byte Ed25519 secret key and uses it in place of the room's stored `signing_key_bytes` for the current command. The override is in-memory only; `rooms.json` on disk is never modified.
Implementation:
Distinct from `message send --signing-key` which takes a base64-encoded inline key — the global file-based flag is preferred for non-test use because the key doesn't appear in shell history.
Testing
Unit test `signing_key_override_is_returned_and_not_persisted` in `cli/src/storage.rs` pins the contract: with override, `get_room` returns the override; without override (fresh Storage from same data dir), the original stored bytes come back — proving the override is not written back to `rooms.json`.
Manual end-to-end: sent the river#275/#276/#278 release announcement to the official Freenet River room as Room Owner via `--signing-key-file ~/.config/freenet-river-official/room_owner_signing_key.bin`, no `rooms.json` swap. Confirmed via `riverctl message list` that the message landed signed by Room Owner. This is the use case that motivated the PR.
Notes
[AI-assisted - Claude]