Skip to content

feat(cli): --signing-key-file flag overrides signing identity per command#282

Open
sanity wants to merge 1 commit into
mainfrom
feat/cli-signing-key-override
Open

feat(cli): --signing-key-file flag overrides signing identity per command#282
sanity wants to merge 1 commit into
mainfrom
feat/cli-signing-key-override

Conversation

@sanity
Copy link
Copy Markdown
Contributor

@sanity sanity commented May 18, 2026

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:

  • `Storage::new_with_override` accepts an optional `SigningKey`. Stored on the `Storage` struct.
  • `Storage::resolve_signing_key(stored_bytes)` returns the override if present, else `SigningKey::from_bytes(stored_bytes)`.
  • `Storage::get_room` uses the resolver, so every command that pulls a signing key from storage gets the override transparently.
  • `ApiClient::ensure_room_migrated` (which has its own `load_rooms` snapshot) uses the same resolver.
  • `ApiClient::new_with_signing_key_override` threads the override from CLI to Storage.
  • `main.rs` parses the flag, reads the file (rejects non-32-byte payloads with a clear error message), constructs the `SigningKey`, and passes it through.

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]

…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 <[email protected]>
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