Signed manual pairing

User flow

  1. A opens /fips invite, enters a required wss:// signaling relay and optionally B's master public key in recipient (64-character hex).
  2. A reviews the summary and clicks Sign invitation with the connected master signer. The backend verifies the signature against the stored template.
  3. The plugin persists the relay, starts/restarts its incoming subscription, and requires advert publication acceptance on that specific relay before returning a fips2: code. Subscription health is not yet verified.
  4. A sends the code through an external messenger.
  5. B opens /fips add, pastes it, reviews A's verified master and FIPS keys, and clicks Accept & sign reply. A targeted invite must name B's master.
  6. After signature verification and relay publication, B's roster gains A and B receives a signed acceptance code using the same relay.
  7. B sends that code to A. A imports it with /fips add, reviews B's verified identity, and clicks Complete pairing. A's roster then gains B.

The signing preview uses separate block text rows. Successful signature callbacks return a rich result with the full code and a Copy code button; the generic signing client suppresses plain-text callback messages. Pairing command errors also return rich results so relay rejection or signature validation errors remain visible after signing. “Pairing signed” alone only confirms the client signer step, not backend completion or relay acceptance.

The UI uses existing command forms and the generic nostr.signEvent client action with the connected signer. No master secret is given to the plugin. Text-only execution can prepare an operation, but signing needs the signer-enabled Web UI or an externally signed template in the signedEvent callback argument. This is not unattended signing support.

Format and verification

fips2: contains base64url JSON of a complete signed Nostr event. Kind 21195 is a local pairing-envelope convention, not a standardized FIPS event. These envelopes are exchanged as codes, not published. Tags are empty; content contains protocol appweaver-fips-pairing, version 2, type invite or acceptance, a random UUID invitation ID, signing master key, authorized compressed FIPS key, normalized relay URL, optional recipient master key, and expiry. Acceptances reference the original signed invitation's event ID and are addressed to A. Invitations last 24 hours; acceptance inherits the same expiry.

Verification checks signatures, kind, strict payload schema, signer/key agreement, timestamps, size limit (32 KiB code), and stored operation. A also checks the original event ID, relay, expiry, and intended recipient. Signed callbacks must match the stored template and current local FIPS identity exactly.

Signatures establish key control, not a real-world name: recognize or independently check the master key. Codes are not encrypted. Recipient binding limits who can complete them, not who can read them. A master signature authorizes the endpoint association; the FIPS handshake must still prove endpoint-key control.

Persistence and duplicate behavior

fips_pairing stores templates, original incoming invitations, local signed events, and pending/issued/completed state. fips_pairing_receipts preserves the remote acceptance at A. Roster insertion and completion are transactional. fips_signal_relays stores the explicit runtime relay set.

A completed invitation cannot be accepted again. Reopening a pending incoming invite reuses its acceptance template. Outgoing invitations complete only once, including untargeted ones. Keep returned codes until both parties finish: code-recovery and cancellation UI are not implemented yet.

The public add command rejects unsigned fips1: and raw-key imports. Existing roster entries are retained, not retroactively verified. These new tables do not rebuild the peer table; legacy unique-master constraints still need migration.

Boundaries and remaining work

Pairing grants networking permission only, not app/project permission or shell/file access. Transport hardening remains in the implementation plan.

The runtime now uses persisted invitation relays instead of BOT_RELAYS or kind 10050 discovery defaults. It subscribes/publishes across that explicit set. Remote 37195 lookup remains pending: resolution uses the roster key. Adding a relay restarts the runtime and interrupts existing FIPS sessions.

Advert acceptance proves relay receipt, not peer availability or UDP reachability. Traversal offers/answers now require relay acknowledgement and emit correlated connection diagnostics. Relay acceptance still does not prove that the recipient received or accepted the signal.