M14.1: cert_authority parser (FR-60, FR-64)#15
Merged
Conversation
…0, FR-64) Adds the parsing surface for OpenSSH @cert-authority lines and the Anvil @Revoked shorthand in known_hosts-style files. The next slice (M14.2) consumes these in check_server_key for revocation enforcement; the live cert-during-handshake verification path (FR-61, FR-62, FR-63) is deferred to a follow-up that lands once russh exposes the server's certificate to the check_server_key callback (see M14 plan for the upstream blocker). src/cert_authority.rs (new, public module): - struct CertAuthority { host_pattern, algorithm, fingerprint, openssh } - struct RevokedEntry { host_pattern, fingerprint } - struct DirectHostKey { host_pattern, fingerprint } - struct KnownHostsFile { direct, cert_authorities, revoked } - pub fn parse_known_hosts(content) -> Result<KnownHostsFile, AnvilError> The parser: - recognizes @cert-authority and @Revoked markers case-insensitively per OpenSSH (markers must be followed by whitespace); - splits comma-separated host patterns into multiple entries; - accepts the Anvil-format `host SHA256:fp` direct line as before; - accepts the OpenSSH-format `algorithm AAAA... comment` pubkey on @cert-authority lines (parsed via ssh_key::PublicKey::from_openssh for fingerprint computation + algorithm extraction); - skips hashed entries (|1|...|...) with a debug log; full support documented as a follow-up; - soft-skips malformed @Revoked lines with a warn so an operator typo doesn't brick the connection. src/lib.rs: add `pub mod cert_authority;`. Tests: 15 new unit tests in cert_authority::tests covering empty input, comments + blanks, direct lines, comma-separated hosts, @cert-authority parse + case-insensitive marker + invalid pubkey error, @Revoked parse + case-insensitive + comma hosts + missing fingerprint, hashed-entry skip, marker-without-space negative case, mixed three-class file, whitespace tolerance. Total now 200 lib tests, 0 failures, 5 ignored. Plan: M14.1 of let-us-plan-on-bright-cosmos.md.
This was referenced May 4, 2026
UnbreakableMJ
added a commit
that referenced
this pull request
May 4, 2026
Wires the M14.1 cert_authority parser into the connect-time host-key check. @Revoked entries become a hard, policy-overriding blocklist: even StrictHostKeyChecking::No cannot accept a key whose fingerprint appears in a matching @Revoked line. src/hostkey.rs: - New public type HostKeyTrust { fingerprints, cert_authorities, revoked } combining the embedded set, parsed direct pins, matching cert authorities, and matching revoked entries from one known_hosts pass. - New public fn host_key_trust(host, custom_path) -> HostKeyTrust: 1. Seeds with embedded fingerprints (GitHub / GitLab / Codeberg). 2. Reads the user-supplied or default known_hosts file. 3. Filters direct, cert-authority, and revoked entries by the host pattern using ssh_config::lexer::wildcard_match. 4. Returns an empty trust set without erroring (unlike fingerprints_for_host) — the caller's policy decides. - Existing fingerprints_for_host preserved verbatim for back-compat. - New embedded_fingerprints helper extracted from fingerprints_for_host so host_key_trust can share the well-known-host seeding logic. src/session.rs: - GitwayHandler gains revoked: Vec<String> field. - check_server_key consults `revoked` FIRST, before the StrictHostKeyChecking::No bypass and the fingerprint match path: any presented key whose SHA-256 fingerprint hits a revoked entry is rejected with a host_key_mismatch error and a hint mentioning the @Revoked entry. - build_handler_pieces switched from fingerprints_for_host to host_key_trust so direct pins, revocations, and cert authorities are pulled in one pass. The empty-fingerprint branch reproduces the long-form actionable hint that fingerprints_for_host emitted. Tests: +7 new hostkey unit tests: - host_key_trust embeds well-known fingerprints. - host_key_trust pattern matches @cert-authority by host glob. - host_key_trust excludes non-matching @cert-authority. - host_key_trust pattern matches @Revoked. - host_key_trust combines direct pins with embedded set. - host_key_trust tolerates a missing custom_path file. - host_key_trust returns empty for an unknown host with no file (caller decides whether the absence is fatal — the AcceptNew path relies on this). Total: 207 lib tests, 0 failures, 5 ignored (pre-existing). Plan: M14.2 of let-us-plan-on-bright-cosmos.md. Stacked on M14.1 (PR #15).
UnbreakableMJ
added a commit
that referenced
this pull request
May 4, 2026
…64) (#16) Wires the M14.1 cert_authority parser into the connect-time host-key check. @Revoked entries become a hard, policy-overriding blocklist: even StrictHostKeyChecking::No cannot accept a key whose fingerprint appears in a matching @Revoked line. src/hostkey.rs: - New public type HostKeyTrust { fingerprints, cert_authorities, revoked } combining the embedded set, parsed direct pins, matching cert authorities, and matching revoked entries from one known_hosts pass. - New public fn host_key_trust(host, custom_path) -> HostKeyTrust: 1. Seeds with embedded fingerprints (GitHub / GitLab / Codeberg). 2. Reads the user-supplied or default known_hosts file. 3. Filters direct, cert-authority, and revoked entries by the host pattern using ssh_config::lexer::wildcard_match. 4. Returns an empty trust set without erroring (unlike fingerprints_for_host) — the caller's policy decides. - Existing fingerprints_for_host preserved verbatim for back-compat. - New embedded_fingerprints helper extracted from fingerprints_for_host so host_key_trust can share the well-known-host seeding logic. src/session.rs: - GitwayHandler gains revoked: Vec<String> field. - check_server_key consults `revoked` FIRST, before the StrictHostKeyChecking::No bypass and the fingerprint match path: any presented key whose SHA-256 fingerprint hits a revoked entry is rejected with a host_key_mismatch error and a hint mentioning the @Revoked entry. - build_handler_pieces switched from fingerprints_for_host to host_key_trust so direct pins, revocations, and cert authorities are pulled in one pass. The empty-fingerprint branch reproduces the long-form actionable hint that fingerprints_for_host emitted. Tests: +7 new hostkey unit tests: - host_key_trust embeds well-known fingerprints. - host_key_trust pattern matches @cert-authority by host glob. - host_key_trust excludes non-matching @cert-authority. - host_key_trust pattern matches @Revoked. - host_key_trust combines direct pins with embedded set. - host_key_trust tolerates a missing custom_path file. - host_key_trust returns empty for an unknown host with no file (caller decides whether the absence is fatal — the AcceptNew path relies on this). Total: 207 lib tests, 0 failures, 5 ignored (pre-existing). Plan: M14.2 of let-us-plan-on-bright-cosmos.md. Stacked on M14.1 (PR #15).
UnbreakableMJ
added a commit
that referenced
this pull request
May 4, 2026
…ity + host_key_trust Adds tests/test_known_hosts_cert.rs — four hermetic integration tests that exercise the M14.1 parser and the M14.2 trust-aggregation API through the published crate boundary (no internal helpers, no network, no russh server). Coverage: - parses_three_classes_in_one_file Single known_hosts blob containing a direct pin, an @cert-authority *.example.com line, and an @Revoked entry. Asserts each class lands in the right vector with the right shape (host pattern, fingerprint prefix, algorithm). - host_key_trust_filters_by_host_pattern Two cert-authorities + two revoked entries with mixed host globs. Calls host_key_trust(foo.example.com, path) and asserts only the *.example.com matches come back; calls again with third-party.io and asserts both vectors are empty. Locks in the wildcard_match plumbing from M12. - host_key_trust_reports_well_known_embedded_set Calls host_key_trust(github.com, &None) — no custom file — and asserts the three embedded SHA256 fingerprints from the M11.5 pin set are still surfaced. Confirms the M14.2 refactor preserved the embedded path. - malformed_cert_authority_pubkey_errors_with_clear_message Feeds parse_known_hosts a syntactically @cert-authority line with a non-base64 key blob and asserts the error message references @cert-authority so an operator can find the failing line. The tempfile dependency was already a dev-dep from M12. No production code changes. Plan: M14.5 of let-us-plan-on-bright-cosmos.md. Stacked on M14.2 (PR #16), which is stacked on M14.1 (PR #15). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UnbreakableMJ
added a commit
that referenced
this pull request
May 4, 2026
…ity + host_key_trust (#17) Adds tests/test_known_hosts_cert.rs — four hermetic integration tests that exercise the M14.1 parser and the M14.2 trust-aggregation API through the published crate boundary (no internal helpers, no network, no russh server). Coverage: - parses_three_classes_in_one_file Single known_hosts blob containing a direct pin, an @cert-authority *.example.com line, and an @Revoked entry. Asserts each class lands in the right vector with the right shape (host pattern, fingerprint prefix, algorithm). - host_key_trust_filters_by_host_pattern Two cert-authorities + two revoked entries with mixed host globs. Calls host_key_trust(foo.example.com, path) and asserts only the *.example.com matches come back; calls again with third-party.io and asserts both vectors are empty. Locks in the wildcard_match plumbing from M12. - host_key_trust_reports_well_known_embedded_set Calls host_key_trust(github.com, &None) — no custom file — and asserts the three embedded SHA256 fingerprints from the M11.5 pin set are still surfaced. Confirms the M14.2 refactor preserved the embedded path. - malformed_cert_authority_pubkey_errors_with_clear_message Feeds parse_known_hosts a syntactically @cert-authority line with a non-base64 key blob and asserts the error message references @cert-authority so an operator can find the failing line. The tempfile dependency was already a dev-dep from M12. No production code changes. Plan: M14.5 of let-us-plan-on-bright-cosmos.md. Stacked on M14.2 (PR #16), which is stacked on M14.1 (PR #15). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
UnbreakableMJ
added a commit
that referenced
this pull request
May 4, 2026
) (#18) Bumps anvil-ssh from 0.4.0 to 0.5.0 to publish the M14.1 + M14.2 + M14.5 work as a single crates.io release. Highlights (full text in CHANGELOG.md): - new public module `anvil_ssh::cert_authority` with `parse_known_hosts(&str) -> Result<KnownHostsFile, AnvilError>` and the `CertAuthority` / `RevokedEntry` / `DirectHostKey` / `KnownHostsFile` types (FR-60, FR-64); - new public fn `anvil_ssh::hostkey::host_key_trust(host, custom_path)` returning `HostKeyTrust { fingerprints, cert_authorities, revoked }`, combining embedded fingerprints + direct pins + matching CAs + matching revoked entries from one known_hosts pass; - @Revoked enforcement in `check_server_key`: revoked fingerprints are checked first, before the StrictHostKeyChecking::No bypass — no policy can override. FR-61, FR-62, FR-63 (live `@cert-authority` validation during the SSH handshake) are deferred to a follow-up that lands once russh upstream exposes the server's certificate to `check_server_key`. Russh 0.59's `Preferred::DEFAULT.key` does not list the *-cert-v01@openssh.com host-key algorithms, so KEX never asks for a cert-bearing host key. The CHANGELOG records the exact upstream patch sketch. Public-API additions only. Existing `fingerprints_for_host` is preserved verbatim; the empty-fingerprint branch in `build_handler_pieces` reproduces the long-form actionable hint inline. Lib tests: 207 passed, 5 ignored (pre-existing). Integration tests: 4 in tests/test_known_hosts_cert.rs. Plan: M14.3 of let-us-plan-on-bright-cosmos.md. Stacked after M14.1 (#15), M14.2 (#16), M14.5 (#17), all merged to main. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the parsing surface for OpenSSH
@cert-authoritylines and the Anvil@revokedshorthand inknown_hosts-style files. Crate-public, consumed by M14.2's revocation enforcement and (eventually) the deferred FR-61..63 cert-validation path.Files added
src/cert_authority.rs— public types + parser:struct CertAuthority { host_pattern, algorithm, fingerprint, openssh }struct RevokedEntry { host_pattern, fingerprint }struct DirectHostKey { host_pattern, fingerprint }struct KnownHostsFile { direct, cert_authorities, revoked }pub fn parse_known_hosts(content) -> Result<KnownHostsFile, AnvilError>Parser behavior
@cert-authority/@revokedmarkers case-insensitively (require trailing whitespace per OpenSSH).host SHA256:fpform: existing Anvil convention, preserved.@cert-authority host algorithm AAAA... comment— full OpenSSH-format pubkey, parsed viassh_key::PublicKey::from_opensshfor algorithm + SHA-256 fingerprint extraction.@revoked host SHA256:fp— Anvil shorthand fingerprint form.|1|...|...) skipped with debug log (full support: follow-up).@revokedsoft-skipped with warn (operator typo does not brick the connection).Tests (15 new)
Empty input, comments + blanks, direct line, comma-separated hosts,
@cert-authorityparse + case-insensitive marker + invalid-pubkey error,@revokedparse + case-insensitive + comma hosts + missing fingerprint soft-skip, hashed-entry skip, marker-without-space negative case, mixed three-class file, whitespace tolerance. Total: 200 lib tests, 0 failures, 5 ignored (pre-existing).Out of scope for this PR
check_server_key. Russh 0.59's KEX negotiation does not include*-cert-v01@openssh.cominPreferred::DEFAULT.key.@revoked— Anvil takes the SHA256-fingerprint shorthand; full pubkey form is a follow-up if requested.|1|...|...) — skipped with debug log.!example.com) in@cert-authority— rare; documented as follow-up.Test plan
cargo fmt/cargo clippy --all-targets -- -D warningsclean.cargo test --lib --tests --lockedgreen.Plan: M14.1 of let-us-plan-on-bright-cosmos.md.
🤖 Generated with Claude Code