Skip to content

Latest commit

 

History

History
134 lines (83 loc) · 14.7 KB

File metadata and controls

134 lines (83 loc) · 14.7 KB

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[0.5.1] - 2026-08-22

Fixed

  • Fingerprint output used the URL-safe base64 alphabet (-/_) instead of the standard one (+//) that OpenSSH's own SHA256 fingerprint format actually uses — the format ssh-keygen -l prints and that hosts publish in their docs. Purely cosmetic for fingerprint/verify's own output and invisible to verify's internal known_hosts matching (which compares raw key bytes, never the fingerprint string), but a real correctness bug for check-fingerprint: any externally-sourced fingerprint containing a byte that encodes to + or / could never match, however correct the actual key was. Found via the GitHub Action's first live smoke-test run against github.com. If you've been comparing khm's fingerprint output against ssh-keygen/a host's published fingerprint by eye, re-check now that the alphabet matches.

[0.5.0] - 2026-08-22

Added

  • khm check-fingerprint <host[:port]> --expect <fingerprint> — compares a host's live SSH key against an explicit, caller-supplied fingerprint instead of known_hosts. Unlike fingerprint, which just shows you the live key, this checks it against a value you already trust (a secret, a provisioning system's output, something published out-of-band) — built for ephemeral environments like CI runners, where there's no persistent known_hosts to compare against and no first connection worth trusting on faith before a deploy. --expect accepts the fingerprint with or without the SHA256: prefix. --json supported, with its own result shape (khm_fp_check_result_t) rather than reusing verify's, since there's no known_hosts record involved.
  • New exit-code contract for check-fingerprint, deliberately narrower than verify's: 0 match, 1 mismatch (security failure — host identity changed), 2 error (timeout/refused/unreachable/non-SSH peer — an infrastructure failure, not necessarily an identity one). Kept as two separate codes rather than one "anything went wrong" code so a CI gate (or a future GitHub Action wrapping this) can branch on which happened.

[0.4.0] - 2026-08-12

Added

  • doctor: new file_permissions check — warns if known_hosts is group/world-writable (anyone else on the system could plant or alter trusted keys), notes (info) if it's world-readable or a symlink.
  • doctor: @cert-authority marker findings now carry an explicit caveat that khm tracks and preserves the marker but performs no certificate/CA chain verification — full validation would need a much bigger chunk of the SSH protocol than khm's partial handshake implements.
  • scan: CIDR ranges reaching outside private address space (RFC 1918, loopback, link-local, RFC 6598 CGNAT) now require --force. Not a vulnerability in khm — a good-citizenship guard so khm scan 0.0.0.0/0 or a typo'd prefix doesn't start knocking on strangers' infrastructure by accident.
  • known_hosts files over 100 MiB, or with more than 200,000 lines, are now refused before parsing rather than read in full — a sanity ceiling well above any real file, there purely to bound memory use against a hostile or malformed input (khm_entry_t is large enough, at ~6KB, that unbounded parsing was a real memory-amplification concern, not a theoretical one).

Fixed

  • normalize --write's temp file (<path>.khm-tmp) is now opened with O_CREAT|O_EXCL|O_NOFOLLOW, closing a narrow TOCTOU window where something pre-creating that exact path as a symlink could redirect the write. (known_hosts itself being a symlink was already safe against the final rename() step — POSIX rename() replaces the destination link rather than following it — this hardens the one step that wasn't already covered by that.)

0.3.0 - 2026-08-10

Added

  • khm lookup <host>[:port] — finds a host's known_hosts record, including HashKnownHosts (|1|salt|hash|) entries whose hostname isn't stored in the clear. Recomputes HMAC-SHA1(salt, candidate-hostname) using the entry's own salt and compares against the stored hash — the same check sshd itself does on every connection, not a weakness in the scheme. Matches plain-text entries too (case-insensitively, like verify). --json supported.
  • verify/verify --all: new NEW_ALGORITHM status, distinct from NEW. Previously a host trusted under one algorithm (e.g. ssh-rsa on file) but now offering a different one (e.g. ssh-ed25519) was reported identically to a host never seen before — materially different situations. The --json result gained a known_algorithms field (every algorithm already on file for the host, independent of which one matched); human-readable output now shows known/fetched fingerprints on CHANGED and known algorithms on NEW_ALGORITHM/OK instead of a bare verdict. New exit code 4 for NEW_ALGORITHM (previously folded into 3/NEW).
  • normalize --write now backs up the original file to <path>.khm-backup (preserving permissions) before the atomic replace, unless --no-backup is passed. If the backup itself can't be created, the write is aborted rather than proceeding without a safety net.
  • scan: --concurrency N replaces the hardcoded 64-thread batch size (clamped to [1, 64]); --verbose lists every individual miss instead of folding non-timeout misses into the summary count.
  • doctor: two new info-severity checks (never fail the run) — wildcard_pattern flags a */? hostname pattern (trusts a whole class of hosts under one key, not a single host, even though it's often intentional), and marker_line reports each @cert-authority/@revoked line present.

Fixed

  • @cert-authority/@revoked marker lines were silently dropped by the parser — treated exactly like a blank line or comment, never stored anywhere. In practice this meant khm normalize --write — already the most trust-sensitive command in this tool — deleted every marker line in a known_hosts file on a routine rewrite, with no warning, error, or trace. A CA trust anchor or an explicit revocation could vanish without a sign anything had happened. Markers are now parsed into their own record and preserved verbatim (original order) by normalize, and surfaced by doctor's new marker_line check.
  • scan: every non-SSH address used to collapse into one N no-ssh count, whether the port was closed, the host was down, DNS failed, or something answered but wasn't a usable SSH peer. Now classified into refused / unreachable / timeout / ssh-error, each with a different next action. khm_fetch_hostkey()'s error codes were extended (KHM_FETCH_ERR_REFUSED, KHM_FETCH_ERR_UNREACHABLE, KHM_FETCH_ERR_SSH_ERROR, alongside the existing _GENERIC/_TIMEOUT) rather than replaced — every existing r < 0 / r == -2 call site kept working unmodified.

Changed

  • README: documented lookup, the normalize backup guarantee and --no-backup, scan's new failure classes and flags, and doctor's two new checks. Source tree listing updated to match.

0.2.3 - 2026-07-19

Security

  • Write buffer overflow in host key base64 encoding. keydata_b64 in khm_hostkey_t was sized for the raw key bytes (KHM_KEYDATA_MAX), but base64 expands 3 bytes into 4 characters — a host key blob approaching the existing size cap overflowed the buffer by roughly 700 bytes. Triggerable by a malicious or compromised SSH server responding to any command that fetches a live host key (verify, fingerprint, scan, doctor --check-reachable). More serious than the read-only underflow fixed in 0.2.2. Added a correctly-sized KHM_KEYDATA_B64_MAX and a dst_cap bound directly in the base64 encoders as a backstop against this class of bug recurring.

Added

  • ls as a full alias for list (same options, same output).
  • -v/--version — prints the build via git describe, so the binary is self-identifying instead of relying on a manually-bumped string.
  • tests/fuzz/: libFuzzer harnesses — fuzz_kex_reply.c targets the SSH wire-format parsers (the only attacker-reachable input path in khm) with a seed corpus entry reproducing the keydata_b64 overflow above in under a second against unpatched code; fuzz_parser.c targets the known_hosts line parser. make fuzz-build / make fuzz-smoke, wired into a separate, non-blocking weekly + manual-dispatch CI workflow.
  • tests/cli_smoke.sh — CLI-level smoke tests, covering the ls alias and --version/-v.

Fixed

  • normalize --write now preserves the original file's permission bits (stat + chmod on the temp file before rename) instead of silently loosening known_hosts permissions to whatever fopen() + umask produced.

0.2.2 - 2026-07-18

Security

  • Integer underflow in SSH packet length parsing. khm_ssh_packet_lengths() didn't validate pad_len against pkt_len before computing the payload length — a malicious or compromised server could send pkt_len=2, pad_len=255 to trigger a uint32_t underflow, producing a payload length far larger than the actual buffer and causing a heap out-of-bounds read the next time KEX_ECDH_REPLY was parsed. The degenerate zero-payload case is also now rejected.

Added

  • Dependency-free test framework (tests/test.h) and the first unit tests, covering parser.c, json.c, and the packet-length validation above (76 tests total). Wired into CI via make test.
  • Global options documented in README (custom port, --file, --json, --no-color).

Changed

  • Bumped the SSH client banner from khm_0.1 to khm_0.2 (was stale since the initial release).
  • README: sharpened the project's framing (known_hosts as a trust database people treat as a disposable cache, not just "SSH gives you no tooling for X"), added an OpenSSH-vs-khm comparison table and a "real-world use cases" section.
  • License changed (see LICENSE).

0.2.1 - 2026-07-06

Security

  • KEX_ECDH_INIT used an all-zero ephemeral key instead of a random one. Root cause of reports of "connection refused or protocol failure" against modern servers (GitHub's included): an all-zero curve25519 point is one of a handful of known low-order points, and RFC 8731 requires compliant servers to detect and reject exactly these (to prevent small-subgroup confinement issues) — the connection was silently dropped by the server the instant KEX_ECDH_INIT was sent, with no SSH_MSG_DISCONNECT. Older/more permissive servers had tolerated the placeholder value; a compliant modern one wouldn't. Fixed with a new khm_rand_bytes() (seeded from time+pid — cryptographic quality isn't needed here, since the exchange is discarded immediately after the host key is read; it only needs to avoid the small set of known-bad points). The same helper now also generates the KEXINIT cookie, previously drawn from an unseeded rand().

Added

  • Opt-in KHM_DEBUG=1 tracing through the SSH handshake — no behavior change unless set. Also surfaces SSH_MSG_DISCONNECT reason + description whenever a server sends one, rather than collapsing it into a generic protocol-failure message.

Fixed

  • verify --all and doctor --check-reachable no longer read as a silent all-clear when every entry in the file is hashed. verify --all now reports "(N hashed entries skipped — hostname unknown, can't verify without it)"; doctor gained a separate reachability_note info finding for the same case (kept distinct from unreachable_host so it doesn't flip that row red for something that isn't a failure).

0.2.0 - 2026-07-06

Added

  • Global --json flag, recognized anywhere on the command line (not just before the subcommand). New json.c module with two output shapes: static known_hosts records vs. live-check results.
  • verify --all — checks every non-hashed host in a known_hosts file in one pass, deduping (host, port) pairs, with a human-readable summary or a --json array. Exit code 0 only if nothing changed or was unreachable (CI-friendly).
  • khm fingerprint <host[:port]> — shows a host's live key fingerprint without touching known_hosts at all. The pre-TOFU "what would I be trusting right now" check.
  • khm export [--file <path>] [--format csv|md|html|json] — CSV (default), Markdown, HTML, or JSON export for audits. Multiple hostnames on one line are joined with ; in CSV so the comma stays the delimiter.
  • khm normalize [--file <path>] [--write] — dedupes exact-duplicate lines and merges plain-text entries that share the same algorithm+key across different hostnames. Single-file only, by design: no merging across known_hosts/.old/.bak, since a cross-file merge can silently hide a real key change — that's what doctor/verify are for, not normalize. Default is a stdout preview; --write does an atomic replace (temp file + rename()).
  • khm doctor [--check-reachable] — health check for a known_hosts file, offline by default. Six checks: malformed_line (error), duplicate_entries / hashed_duplicate / obsolete_algorithm (warnings), mixed_algorithms (info only), unreachable_host (warning, opt-in via --check-reachable, the only check touching the network). --json emits an array of {check, severity, message}.
  • khm_entry_t gained line_number; khm_db_t gained an errors list capturing lines that looked like they should be a record but didn't parse, instead of silently discarding them the way comments/blank lines are.

Fixed

  • release.yml had drifted from the Makefile's source list and was missing several files added this release — would have failed the release build outright. Switched to make release CC=musl-gcc ... so the Makefile's SRCS is the only place this needs to be maintained.

[0.1.0] - 2026-06-26

Initial release.

Added

  • Core: known_hosts parser, self-contained SHA-256, and a hand-rolled partial SSH handshake (banner exchange → KEXINITKEX_ECDH_INIT/_REPLY) to fetch a live host key without a full SSH session — no libssh, no OpenSSL.
  • khm list — pretty-print known_hosts entries.
  • khm verify <host[:port]> — compare a host's live key against what's on file.
  • khm diff <file1> <file2> — compare two known_hosts files.
  • khm scan <cidr|file|host> — parallel scan (pthread-based) checking every host's live key against known_hosts.
  • CI + release workflow.