Skip to content

M19.2: hashed-emit + revoke writers (FR-84 write, FR-86)#23

Merged
UnbreakableMJ merged 1 commit into
mainfrom
feature/m19-2-hostkey-writes
May 4, 2026
Merged

M19.2: hashed-emit + revoke writers (FR-84 write, FR-86)#23
UnbreakableMJ merged 1 commit into
mainfrom
feature/m19-2-hostkey-writes

Conversation

@UnbreakableMJ

Copy link
Copy Markdown
Contributor

Summary

Second slice of M19 (PRD §5.8.8). Adds the write-side counterparts to M19.1's read-side HashedHost, plus the @revoked prepend helper FR-86 needs. Also promotes two previously-private helpers to pub so the upcoming gitway hosts verbs (M19.4) drive them without re-export shims.

New public surface (hostkey)

  • pub fn append_known_host(path, host, fp) — promoted from pub(crate). Plaintext append.
  • pub fn append_known_host_hashed(path, host, fp) — emits |1|<base64-salt>|<base64-hmac-sha1> <fp> with a fresh 20-byte OS-RNG salt per call. Round-trippable through parse_known_hosts + HashedHost::matches(host).
  • pub fn prepend_revoked(path, host_pattern, fp) — atomic prepend via tempfile + std::fs::rename. 1 MiB input cap; refused with a clear hint pointing at --known-hosts for splitting.
  • pub fn all_embedded() -> Vec<(String, String, &'static str)> — embedded fingerprint catalogue tagged by algorithm (ed25519/ecdsa/rsa) for gitway hosts list.
  • pub enum HashMode { Empty, Plaintext, Hashed } + pub fn detect_hash_mode(path) — inspect-existing-file decision for the gitway hosts add default.
  • pub fn default_known_hosts_path() — promoted from private.

Tests

tests/test_hostkey_writes.rs (new, 13 tests): plaintext + hashed append round-trips, distinct-salt-per-call (privacy of HashKnownHosts yes), revoke prepend position + oversize refusal, hash-mode detection across empty / comments-only / plaintext / hashed files, embedded-catalogue count + algorithm completeness, default-path platform check.

Stacked on #22 (M19.1, merged). Rebased onto fresh main.

Test plan

  • cargo fmt --all -- --check
  • cargo clippy --all-targets --all-features --locked -- -D warnings
  • cargo test --lib --tests --locked — all green (13 new tests pass)
  • CI matrix on this PR (Linux/macOS/Windows + MSRV 1.88)

🤖 Generated with Claude Code

Adds the write-side counterparts to M19.1's read-side `HashedHost`,
plus the `@revoked` prepend helper FR-86 needs.  Also promotes two
previously-private helpers to `pub` so the upcoming `gitway hosts`
verb family (M19.4) can drive them without re-export shims.

src/hostkey.rs:

- `pub fn append_known_host(path, host, fp)` — promoted from
  `pub(crate)`.  Plaintext append; creates parent dir; no locking
  (M19 explicit non-goal).

- `pub fn append_known_host_hashed(path, host, fp)` (new) — emits
  `|1|<base64-salt>|<base64-hmac-sha1> <fp>` with a fresh 20-byte
  OS-RNG salt per call.  Round-trippable through
  `cert_authority::parse_known_hosts` + `HashedHost::matches(host)`,
  pinned by `tests/test_hostkey_writes.rs::append_known_host_hashed_writes_round_trippable_entry`.

- `pub fn prepend_revoked(path, host_pattern, fp)` (new) — atomic
  prepend via tempfile + `std::fs::rename`.  1 MiB cap on input
  file (refused with a clear `tips-thinking`-style message
  pointing at `--known-hosts` for splitting).  Trust-merger
  treats `@revoked` as a hard reject regardless of position; the
  prepend is purely a readability convention.

- `pub fn all_embedded()` (new) — returns `(host, fingerprint, alg)`
  triples for the `gitway hosts list` embedded section.  3 hosts
  × 3 algorithms = 9 entries, alg in {"ed25519", "ecdsa", "rsa"}.

- `pub enum HashMode { Empty, Plaintext, Hashed }` and
  `pub fn detect_hash_mode(path) -> Result<HashMode, AnvilError>`
  (new) — inspect-existing-file decision for `gitway hosts add`'s
  default hashed/plaintext choice.  Short-circuits on the first
  hashed token seen.

- `pub fn default_known_hosts_path()` — promoted from private.
  `~/.config/gitway/known_hosts` via `dirs::config_dir()`.

- New private `ensure_parent_exists` helper extracted from the
  existing `mkdir -p` block so all four writers share one error
  message shape.

Cargo.toml:

- Adds `rand_core = { version = "0.6", features = ["std", "getrandom"] }`
  is already a runtime dep; M19.2 just uses it directly via
  `OsRng.fill_bytes`.

tests/test_hostkey_writes.rs (new, ~13 tests):

- Plaintext append: file creation, append-to-existing.
- Hashed append: round-trip parse + match, distinct-salt-per-call
  (privacy property of `HashKnownHosts yes`).
- Revoke prepend: file creation, prepend-position assertion,
  oversize refusal.
- detect_hash_mode: missing file, comments-only, plaintext-only,
  short-circuit on first hashed token.
- all_embedded: count + algorithm-tag completeness.
- default_known_hosts_path: ends-with assertion.

Stacks on M19.1 (#22, merged).

Plan: M19.2 of anvil-gitway-milestone-plan.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@UnbreakableMJ
UnbreakableMJ merged commit b3f961e into main May 4, 2026
5 checks passed
@UnbreakableMJ
UnbreakableMJ deleted the feature/m19-2-hostkey-writes branch May 4, 2026 17:36
UnbreakableMJ added a commit that referenced this pull request May 4, 2026
Final Anvil-side slice of M19.  Bumps anvil-ssh from 0.6.0 to 0.7.0
to publish the M19.1 + M19.2 work as a single crates.io release.
The Gitway-side `gitway hosts` verb family (M19.4 + M19.5) lands
against this 0.7.0; the M19.X PRD doc PR closes the milestone with
a Gitway v1.0.0-rc.7 tag.

Cargo.toml:
- version "0.6.0" -> "0.7.0"

Cargo.lock:
- regenerated locally; reflects the 0.7.0 version.

CHANGELOG.md:
- 0.7.0 entry covering the new HashedHost type, the
  append_known_host_hashed / prepend_revoked / all_embedded /
  HashMode + detect_hash_mode public surface, the
  default_known_hosts_path + append_known_host visibility
  promotions, and the new direct deps (hmac/sha1/base64).
- HMAC-SHA1 privacy-vs-security caveat documented inline.

Stacked after PRs #22 (M19.1, merged) and #23 (M19.2, merged).

Plan: M19.3 of anvil-gitway-milestone-plan.md.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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