Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 41 additions & 30 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ tokio = { version = "1.54.1", features = ["full"] }
# empty, so it never negotiates one; the handler in
# src/ssh/tokio_client/connection.rs refuses certificates because bssh
# verifies no CA signatures.
russh = "0.65.0"
russh = "0.65.1"
# Upstream russh-sftp. bssh's pipelined client transfers live in
# src/ssh/tokio_client/sftp_client.rs and its read-ahead / write-coalescing
# server loop in src/server/sftp_loop.rs, both on russh-sftp's public API.
Expand Down
1 change: 1 addition & 0 deletions docs/architecture/ssh-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
- `StrictHostKeyChecking::AcceptNew` - TOFU mode: a key matching any of a host's recorded known_hosts entries is accepted (not just the first one, matching OpenSSH rather than short-circuiting on the first same-algorithm mismatch), unknown hosts are recorded and accepted, and a key is rejected only when the host has recorded keys and none of them match. If no default known_hosts path can be determined, bssh pins the first key for each host:port in memory for the lifetime of the process instead of disabling verification.
- CLI flag `--strict-host-key-checking` with default "accept-new"
- Uses system known_hosts file (~/.ssh/known_hosts); accept-new creates the directory (0700) and file (0600) with their final permissions up front rather than tightening the mode after creation, validates that an existing known_hosts path is a readable regular file before treating it as trust state, and serializes concurrent first-time recordings behind both a process-wide mutex and a sibling advisory lock file so parallel connects to the same new host produce a single entry across cooperating bssh processes
- With russh 0.65.1, `check_server_key` preflights host trust (including certificate authorities and revocation) without recording TOFU keys or publishing verified connection state. `kex_done` then compares the retained key type against the negotiated algorithm before committing those changes or exposing the connected handle to authentication. File/process pins are rechecked under their locks at commit, while `KnownHostsCommand` runs only during preflight. The raw KEX shared secret is unused. Rekey host-key re-verification remains limited by the upstream API (tracked in #336); a later epoch's algorithm is never compared against a stale initial key.
- Changed keys surface as a dedicated `HostKeyChanged` error (host, port, offending line) instead of a generic check failure, in strict and accept-new modes alike; a key matching a known_hosts `@revoked` marker line is rejected separately as `HostKeyRevoked`, since russh's parser otherwise reads the marker itself as the literal host field and never matches these lines against the real host, silently letting a revoked key through TOFU
- Host certificates are validated in `src/ssh/tokio_client/host_certificate.rs` and applied from `check_server_key` in `connection.rs`. A certificate is accepted only when a `@cert-authority` line names the host (or `HostKeyAlias`, with `!pattern` negation and hashed host fields honored; on a non-default port a bare-host line also matches), the certificate is a host certificate with a principal matching the host (case-sensitive glob), the CA signature verifies before the principals are read, the validity window holds, there are no critical options, the CA signature algorithm is allowed by `CASignatureAlgorithms`, and neither key is revoked by `@revoked` lines or `RevokedHostKeys`. With a matching CA, any other failure refuses the connection instead of downgrading to TOFU. Without a matching CA the certified key is checked as a plain host key if `HostKeyAlgorithms` permits its plain type, so a certificate-only list never falls back to known_hosts. Pinned-key check methods ignore CA lines. `KnownHostsCommand` can supply the CAs: its output is parsed for `@cert-authority` and `@revoked` lines exactly like known_hosts lines, and for a certificate the command is run with the certificate as `%K` and its type as `%t`. `RevokedHostKeys` takes a list of files, and a key or certificate listed in any of them is refused. A `RevokedHostKeys` path that cannot be expanded, or a KRL, refuses every host key (fail closed)
- The hostname is lowercased once before both the lookup and the recorded entry, matching OpenSSH's case-insensitive known_hosts comparison, and rejected outright if it cannot round-trip through a known_hosts line (contains whitespace, a control character, or a known_hosts/glob metacharacter), since an unrecordable hostname can never be verified on a later connection either
Expand Down
2 changes: 1 addition & 1 deletion docs/openssh-compatibility-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The implemented control socket is bssh's private framed-JSON protocol. Its passe

Unix-domain forwarding (`-L`/`-R` with a socket path on either side, including `-O forward`/`-O cancel`) never unlinks an existing path before binding and refuses to serve a bound path that is a symlink or not a socket; the listener socket is owner-only (mode 0600) without any mode change through the path name: on Linux the unbound socket is narrowed before `bind(2)` so the node is created 0600; elsewhere it is bound in a private `mkdtemp` directory beside the path, narrowed there with `fchmodat(AT_SYMLINK_NOFOLLOW)`, and moved into place without replacing anything (`renamex_np(RENAME_EXCL)` on macOS, `link(2)` then `unlink(2)` on other BSDs), so the node never appears at the path with a wider mode. Only a socket node is removed on shutdown. `StreamLocalBindUnlink` and `StreamLocalBindMask` are still parsed without a runtime consumer; the OpenSSH defaults (`no`, `0177`) are what the implementation applies.

Host certificates are accepted only after full validation: a `@cert-authority` line (in a known_hosts file or in the `KnownHostsCommand` output, which receives the certificate as `%K`/`%t` and runs once per check) naming the host (or `HostKeyAlias`) with the signing CA key, host certificate type, a principal matching the host name (principals may be case-sensitive wildcard patterns, as in OpenSSH, and are read only after the CA signature verifies), no critical options, a CA signature algorithm permitted by `CASignatureAlgorithms`, the validity window, a verified signature, and no revocation by `@revoked` lines in either source or by any `RevokedHostKeys` file (plain key lists; a KRL or unreadable file anywhere in the list fails closed). On a non-default port a CA line for the bare host name also matches, as OpenSSH's portless retry does. Without a matching CA the certified key is checked as a plain host key, but only if `HostKeyAlgorithms` permits its plain type, so a certificate-only list never falls back to known_hosts or first-use recording; a pinned key (`ServerCheckMethod::PublicKey`/`PublicKeyFile`) never consults CA lines; with a matching CA any other validation failure refuses the connection instead of downgrading, which is stricter than OpenSSH. `HostKeyAlgorithms` accepts certificate names and advertises the certificate form of every default algorithm; russh always sends certificate names ahead of plain names, so a mixed list cannot interleave them, and a certificate-only list yields no plain algorithm at all. The host key the server presents must have the type the key exchange negotiated, as `kex_verify_host_key` requires (`SSH_ERR_KEY_TYPE_MISMATCH`): `rsa-sha2-256` and `rsa-sha2-512` select an RSA key, a `*-cert-v01@openssh.com` name selects a certificate for that key type, and ECDSA curves must match, also for certificates. A mismatch fails the connection with `host key type ... does not match the negotiated host key algorithm ...` before any known_hosts or `StrictHostKeyChecking` decision; russh 0.65.0 verifies the exchange signature with whatever key arrives, so without this check a `HostKeyAlgorithms` restriction would not limit which trusted key type a server could use. russh does not hand the host key of a re-key to the client, so this check, like the known_hosts check, covers the initial key exchange only. `@cert-authority` host fields follow OpenSSH matching, including hashed (`|1|`) entries and `!pattern` negations that exclude a host from the CA's scope. A `RevokedHostKeys` path that cannot be expanded refuses every host key instead of disabling revocation, and `RevokedHostKeys` applies to certificates under `StrictHostKeyChecking=no` as it does to plain keys.
Host certificates are accepted only after full validation: a `@cert-authority` line (in a known_hosts file or in the `KnownHostsCommand` output, which receives the certificate as `%K`/`%t` and runs once per check) naming the host (or `HostKeyAlias`) with the signing CA key, host certificate type, a principal matching the host name (principals may be case-sensitive wildcard patterns, as in OpenSSH, and are read only after the CA signature verifies), no critical options, a CA signature algorithm permitted by `CASignatureAlgorithms`, the validity window, a verified signature, and no revocation by `@revoked` lines in either source or by any `RevokedHostKeys` file (plain key lists; a KRL or unreadable file anywhere in the list fails closed). On a non-default port a CA line for the bare host name also matches, as OpenSSH's portless retry does. Without a matching CA the certified key is checked as a plain host key, but only if `HostKeyAlgorithms` permits its plain type, so a certificate-only list never falls back to known_hosts or first-use recording; a pinned key (`ServerCheckMethod::PublicKey`/`PublicKeyFile`) never consults CA lines; with a matching CA any other validation failure refuses the connection instead of downgrading, which is stricter than OpenSSH. `HostKeyAlgorithms` accepts certificate names and advertises the certificate form of every default algorithm; russh always sends certificate names ahead of plain names, so a mixed list cannot interleave them, and a certificate-only list yields no plain algorithm at all. The host key the server presents must have the type the key exchange negotiated, as `kex_verify_host_key` requires (`SSH_ERR_KEY_TYPE_MISMATCH`): `rsa-sha2-256` and `rsa-sha2-512` select an RSA key, a `*-cert-v01@openssh.com` name selects a certificate for that key type, and ECDSA curves must match, also for certificates. A mismatch fails the connection with `host key type ... does not match the negotiated host key algorithm ...` after trust preflight but before recording TOFU keys, publishing verified state, or allowing authentication; russh 0.65.1 verifies the exchange signature with whatever key arrives, so without this check a `HostKeyAlgorithms` restriction would not limit which trusted key type a server could use. russh does not hand the host key of a re-key to the client, so this check, like the known_hosts check, covers the initial key exchange only. `@cert-authority` host fields follow OpenSSH matching, including hashed (`|1|`) entries and `!pattern` negations that exclude a host from the CA's scope. A `RevokedHostKeys` path that cannot be expanded refuses every host key instead of disabling revocation, and `RevokedHostKeys` applies to certificates under `StrictHostKeyChecking=no` as it does to plain keys.

## Security keys

Expand Down
Loading
Loading