Peering card: show the outbound key as its own row with a fingerprint (symmetric with inbound) #241

Closed
opened 2026-08-15 14:43:07 +00:00 by Cordy · 1 comment
Owner

Found dogfooding v0.6.24 (the #236 "Replace key" affordance).

The problem

The relationship card renders the two key sides asymmetrically:

  • Inbound key (the one we issued the peer) has its own labelled row: INBOUND KEY 3fd2ac2451e1… [Revoke]. The fingerprint gives the operator a stable, recognizable identity for the credential.
  • Outbound key (the one the peer issued us, Peer.Key) has no display at all#236 landed only a bare [Replace key] button floating in the actions row next to [Remove].

The functional gap #236 closed is real (you can replace the key), but from a UX standpoint it's a dead end: pressing it, pasting, and saving produces no visible change. The operator has no "before" to compare against and no "after" to confirm the swap took. A destructive-ish action with no state feedback reads as "did that do anything?".

The fix

Give the outbound key a row that mirrors the inbound one, directly below it:

INBOUND KEY   3fd2ac2451e1…   [Revoke]
OUTBOUND KEY  da2ed8d3fb77…    [Replace key]

Replacing the key then visibly changes the fingerprint — the confirmation the action currently lacks. The [Replace key] button moves out of the actions row into this row (superseding the bare button from #236).

Why this is clean (grounded in the code)

The outbound key is a live credential and listPeers deliberately never echoes it ("The peer's key is never echoed back"). But we don't need the key — we need a non-secret fingerprint, and one already exists:

  • peering.KeyIDOf(secret string) (internal/peering/auth.go) parses the stored CAIRN-PEER1. secret, derives the Ed25519 public half, and returns sha256(pub)[:8] hex — documented as leaking nothing about the private key.
  • This is the same derivation (keyIDFromPublic) used for the inbound key.id already exposed by listKeys (#136). So exposing the outbound fingerprint is exactly as safe as the inbound id we already show.

Bonus property: because both sides derive the id from the same public key, the outbound fingerprint shown here equals the inbound key id the peer shows for us. Two admins can read the two fingerprints to each other out of band and confirm they hold the matching pair — a real verification nicety, not just cosmetics.

Shape

  • Backend: listPeers adds "keyFingerprint": KeyIDOf(pr.Key) per peer (non-secret; raw key still never returned).
  • Frontend: new OUTBOUND KEY row in peerCardEl below the inbound row, showing keyFingerprint.slice(0,12)+"…" + the relocated [Replace key]. Remove the bare button from the actions row.
  • i18n: peeringOutbound.

Refs: #150, #236, #136.

Found dogfooding v0.6.24 (the #236 "Replace key" affordance). ## The problem The relationship card renders the two key sides asymmetrically: - **Inbound key** (the one we issued the peer) has its own labelled row: `INBOUND KEY 3fd2ac2451e1… [Revoke]`. The fingerprint gives the operator a stable, recognizable identity for the credential. - **Outbound key** (the one the peer issued us, `Peer.Key`) has **no display at all** — #236 landed only a bare `[Replace key]` button floating in the actions row next to `[Remove]`. The functional gap #236 closed is real (you *can* replace the key), but from a UX standpoint it's a dead end: pressing it, pasting, and saving produces **no visible change**. The operator has no "before" to compare against and no "after" to confirm the swap took. A destructive-ish action with no state feedback reads as "did that do anything?". ## The fix Give the outbound key a row that mirrors the inbound one, directly below it: ``` INBOUND KEY 3fd2ac2451e1… [Revoke] OUTBOUND KEY da2ed8d3fb77… [Replace key] ``` Replacing the key then visibly changes the fingerprint — the confirmation the action currently lacks. The `[Replace key]` button moves out of the actions row into this row (superseding the bare button from #236). ## Why this is clean (grounded in the code) The outbound key is a live credential and `listPeers` deliberately never echoes it (`"The peer's key is never echoed back"`). But we don't need the key — we need a **non-secret fingerprint**, and one already exists: - `peering.KeyIDOf(secret string)` (internal/peering/auth.go) parses the stored `CAIRN-PEER1.` secret, derives the Ed25519 **public** half, and returns `sha256(pub)[:8]` hex — documented as leaking nothing about the private key. - This is the **same derivation** (`keyIDFromPublic`) used for the inbound `key.id` already exposed by `listKeys` (#136). So exposing the outbound fingerprint is exactly as safe as the inbound id we already show. Bonus property: because both sides derive the id from the same public key, the outbound fingerprint shown here **equals the inbound key id the peer shows for us**. Two admins can read the two fingerprints to each other out of band and confirm they hold the matching pair — a real verification nicety, not just cosmetics. ## Shape - Backend: `listPeers` adds `"keyFingerprint": KeyIDOf(pr.Key)` per peer (non-secret; raw key still never returned). - Frontend: new `OUTBOUND KEY` row in `peerCardEl` below the inbound row, showing `keyFingerprint.slice(0,12)+"…"` + the relocated `[Replace key]`. Remove the bare button from the actions row. - i18n: `peeringOutbound`. Refs: #150, #236, #136.
Cordy closed this issue 2026-08-15 14:56:13 +00:00
Author
Owner

Shipped in v0.6.25 (PR #242, merged). The card now renders:

INBOUND KEY   3fd2ac2451e1…   [Revoke]
OUTBOUND KEY  <fingerprint>…   [Replace key]

listPeers returns keyFingerprint = peering.KeyIDOf(pr.Key) (sha256 of the public half — the same value the peer shows as its inbound key id; raw key still never echoed). Replace moved from the actions row into this row, so a key swap now visibly changes the fingerprint.

Live on both dogfood instances on 192.168.10.245/cordy/cairn:v0.6.25@sha256:7d8ec426…, both pods 1/1. Closing.

Shipped in v0.6.25 (PR #242, merged). The card now renders: ``` INBOUND KEY 3fd2ac2451e1… [Revoke] OUTBOUND KEY <fingerprint>… [Replace key] ``` `listPeers` returns `keyFingerprint = peering.KeyIDOf(pr.Key)` (sha256 of the public half — the same value the peer shows as its inbound key id; raw key still never echoed). Replace moved from the actions row into this row, so a key swap now visibly changes the fingerprint. Live on both dogfood instances on `192.168.10.245/cordy/cairn:v0.6.25@sha256:7d8ec426…`, both pods 1/1. Closing.
Sign in to join this conversation.
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: Cordy/Cairn#241
No description provided.