The host-key verifier was misclassifying connections as `changed` in three
situations that had nothing to do with a real key rotation:
1. Records imported from the system `~/.ssh/known_hosts` (or older builds)
landed in localStorage without a `fingerprint` field. The verifier then
re-derived the fingerprint from the stored `publicKey` blob on every
connect — a brittle path that produced a different value than ssh2 if
anything about the serialization differed by even one byte.
2. `classifyHostKey` had a loose "single candidate with unknown / empty
keyType → changed" heuristic. Any imported record whose keyType failed
to parse would be promoted to a rotation warning the first time the
server presented a real algorithm, even though the user had never
actually trusted any fingerprint for that algorithm.
3. A host that genuinely had multiple algorithms (e.g. one stored ssh-rsa
record plus a live ssh-ed25519 handshake) was being reported as
`changed` instead of `unknown`, even though we had no comparable
record for the algorithm the server presented.
Tabby (`tabby-ssh/src/session/ssh.ts`) and OpenSSH both treat case (3) as a
first-time prompt rather than a mismatch; this change brings Netcatty in
line with that model.
Changes:
- `domain/knownHosts.ts` ports `fingerprintFromPublicKey` to TS and adds
`normalizeKnownHost` / `normalizeKnownHosts` so the renderer can backfill
legacy records on hydration. Pure-JS SHA-256 keeps the migration
synchronous so it can run inline in `useVaultState` without async
plumbing.
- `application/state/useVaultState.ts` runs the migration on hydration
and on cross-window storage events. When anything changes on hydration
the migrated list is written back to localStorage so the next launch
starts clean.
- `components/KnownHostsManager.tsx` populates `fingerprint` at import
time instead of leaving it for the verifier to re-derive.
- `electron/bridges/hostKeyVerifier.cjs` simplifies `classifyHostKey` to
fingerprint-first, then strict (host, port, keyType) match for the
changed branch, then fall through to `unknown`. Two existing tests
that locked in the loose heuristic are updated to assert the new
(safer) behavior, and a new test covers the multi-algorithm
first-encounter case.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>