From 2185478294912d9e043b75eadf7636da774c60a5 Mon Sep 17 00:00:00 2001 From: Jeongkyu Shin Date: Mon, 24 Aug 2026 13:43:00 +0900 Subject: [PATCH] update: upgrade russh to 0.63.1 and refuse host certificates russh 0.63.0 widened `client::Handler::check_server_key` from `&PublicKey` to `&PublicKeyOrCertificate` so a server can present an OpenSSH host certificate, and 0.63.1 adds two security fixes on top: client-side `Handler` callbacks were reachable with never-opened channel IDs (GHSA-47hw-gvq5-r2gm), and a MAC-requiring block cipher could negotiate `mac=none` and panic the session task (GHSA-p8qx-h547-fjw9). `ClientHandler::check_server_key` now fails closed on a certificate. bssh never advertises certificate host key algorithms: both `Preferred` overrides change only `compression` and inherit `host_key_certificates` from `Preferred::DEFAULT`, which is empty, so a server cannot negotiate one. If a peer sends one regardless, bssh has no CA signature verification (the `@cert-authority` scan only warns and falls back to TOFU), so the key inside the certificate has never been vouched for by anything bssh trusts and matching it against known_hosts would answer a different question. `ServerCheckMethod::NoCheck` still accepts, because there the operator turned verification off outright. `server::Config` gained a `certificates` field that the existing `..Default::default()` leaves empty. Reviewed the full 0.62.7 to 0.63.1 source diff for behavior changes that compile silently. The MAC fix cannot reach bssh: `SAFE_HMAC_ORDER` contains no `none` and bssh does not override `mac`. The channel-ID gating is invisible to a correct server, which confirms a channel open before sending data. The `Handler` trait method set is unchanged on both sides (27 client, 34 server), and russh's own dependency requirements are identical between the two versions, so no new transitive crates or duplicates appear. Cargo.lock also carries a routine `cargo update` refresh of 70 other packages that was already pending in the working tree. Validation: `cargo check --all-targets`, `clippy -D warnings`, and `cargo fmt --check` all clean; `cargo +1.96 check --workspace --locked` passes on the declared MSRV. Full test suite green except the three `integration_test` cases that CI already skips, which fail on a stale localhost entry in the developer's `~/.ssh/known_hosts`. End-to-end handshakes verified against bssh-server (exec, ping, SFTP round trip with matching SHA-256) and against OpenSSH 10.3p1 (exec plus a 300 KB SFTP download with matching SHA-256). Three regression tests pin the certificate behavior: every verifying mode refuses one, `NoCheck` accepts, and `host_key_certificates` is asserted empty. --- ARCHITECTURE.md | 24 ++ Cargo.lock | 337 +++++++++++----------- Cargo.toml | 9 +- src/ssh/tokio_client/connection.rs | 28 +- src/ssh/tokio_client/host_verification.rs | 138 ++++++++- 5 files changed, 357 insertions(+), 179 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9398e3bd..770e2762 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -860,6 +860,30 @@ but not with themselves; omit the key (plain `#[serial]`) when in doubt. - Per-host configuration support - Host key fingerprint display +#### OpenSSH host certificates + +russh 0.63 widened `client::Handler::check_server_key` from `&PublicKey` to +`&PublicKeyOrCertificate`, so the callback can now also receive an OpenSSH host +certificate. bssh does not participate in that scheme: + +- It never advertises certificate host key algorithms. Both `Preferred` + overrides (`src/ssh/tokio_client/connection.rs` for the client, + `src/server/mod.rs` for the server) change only `compression` and inherit + `host_key_certificates` from `Preferred::DEFAULT`, which is empty. A server + therefore cannot negotiate a certificate with bssh. +- If a peer sends one regardless, `ClientHandler::check_server_key` refuses it + and returns `ServerCheckFailed`. bssh verifies no CA signatures (the + `@cert-authority` scan in `src/ssh/tokio_client/host_verification.rs` only + warns and falls back to TOFU), so the key inside a certificate has never been + vouched for by anything bssh trusts. Matching it against known_hosts would + answer a different question than the one the certificate poses. +- `ServerCheckMethod::NoCheck` still accepts, because there the operator has + turned host verification off outright. + +`server::Config` likewise gained a `certificates` field; bssh's construction +ends in `..Default::default()`, so it stays empty and the server keeps +presenting a plain host key. + ### Data Protection - No credential logging diff --git a/Cargo.lock b/Cargo.lock index b72e29c6..da49d369 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,16 +32,16 @@ dependencies = [ [[package]] name = "aes-gcm" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" +checksum = "7f2b8006a0c83f52b62ba44a97b58bf76fe2f70a329e588f67f89691d93d498f" dependencies = [ "aead", "aes", "cipher", "ctr", + "ctutils", "ghash", - "subtle", "zeroize", ] @@ -71,9 +71,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -197,13 +197,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -229,9 +229,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.3" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -240,9 +240,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.43.0" +version = "0.44.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" dependencies = [ "cc", "cmake", @@ -265,9 +265,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b25655df2c3cdd83c5e5b293b88acd880332b2ddadd7c30ac43144fdc0033da9" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" [[package]] name = "base64ct" @@ -281,7 +281,7 @@ version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0cd0bd35a28836d528d2b58ad499bc3c5641d59379421b1be9eeb0c2f2b912a" dependencies = [ - "base64 0.23.0", + "base64 0.23.1", "blowfish", "getrandom 0.4.3", "subtle", @@ -435,7 +435,7 @@ dependencies = [ "ssh-key", "tempfile", "terminal_size", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "tokio-rustls", "tokio-test", @@ -463,7 +463,7 @@ dependencies = [ "log", "serde", "serde_bytes", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "tokio-util", "wasm-bindgen-futures", @@ -525,9 +525,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.0" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", "jobserver", @@ -615,9 +615,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.5" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301b56658598e48f3648647ac6fc887be7e7108eddfa4e9b63fcf3ec58c0cadf" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" dependencies = [ "clap_builder", "clap_derive", @@ -625,9 +625,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.5" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a65403d1a1bd28f7dc68eb8506e8874808ee5eecb59298de588e2e1407a078" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" dependencies = [ "anstream", "anstyle", @@ -644,7 +644,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -792,9 +792,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" dependencies = [ "cfg-if", ] @@ -1005,9 +1005,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88490bf1b990d87eaaa7ac8aa887f629a08e7359765b4911faf63c3763347d23" +checksum = "ed17f5901b6630b993ca003def43f2f8ef4014fc13b047b57aad617ff32bc2ec" dependencies = [ "darling_core", "darling_macro", @@ -1015,26 +1015,26 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084e274f91c482280130e1e34e0b8d6e66776a060d7b6de7b84289ca778868c4" +checksum = "6837e2cf7485aaae18f86181d2f0e9a7ed297a025e220aeabf63fdebd3a2ddff" dependencies = [ "ident_case", "proc-macro2", "quote", "strsim", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] name = "darling_macro" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5792fa0d41cd2325ce0ffa64f0a340eaebd4971a3a0c5e1ffd2cc488a355e" +checksum = "2ac7135c3ef02b2f7833bbeb1be5ba7f966dcde8a87c6b87f65a778d71a02785" dependencies = [ "darling_core", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -1174,7 +1174,7 @@ checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -1241,9 +1241,9 @@ dependencies = [ [[package]] name = "either" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "elliptic-curve" @@ -1309,9 +1309,9 @@ dependencies = [ [[package]] name = "error-code" -version = "3.3.2" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" [[package]] name = "euclid" @@ -1367,9 +1367,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "finl_unicode" @@ -1431,9 +1431,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -1446,9 +1446,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -1456,15 +1456,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -1473,38 +1473,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] name = "futures-sink" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -1529,9 +1529,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.4.4" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab4e5aa225bc56696909483320f0ff9b600f1a971b52e07a17d70f3d9b43254b" +checksum = "337d46834ee672ab3e48caca2cb0c78cc174fb12b3a68d0d88f99a0519a5e36e" dependencies = [ "generic-array 0.14.7", "rustversion", @@ -1582,6 +1582,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" dependencies = [ "polyval", + "zeroize", ] [[package]] @@ -1615,9 +1616,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "839c0e8a181239723652be9062bb56ca5bf5f64011f73b623f6f4fc59086a228" dependencies = [ "atomic-waker", "bytes", @@ -1738,9 +1739,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -1857,9 +1858,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -1871,9 +1872,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -1884,9 +1885,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1898,16 +1899,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -1918,15 +1920,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -2028,7 +2030,7 @@ dependencies = [ "indoc", "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -2097,9 +2099,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ "cfg-if", "futures-util", @@ -2114,14 +2116,14 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" dependencies = [ "hashbrown 0.16.1", "portable-atomic", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] name = "keccak" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" +checksum = "d8f198d1db720e4940b5a493201d199d9f24f568f8f746bd13706243a2f71598" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -2163,9 +2165,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" +checksum = "28d0a00925a9f930d679b6789b721e3a7f9ed110f41b86d2497caa780c3a070a" dependencies = [ "libc", ] @@ -2187,9 +2189,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "litrs" @@ -2208,15 +2210,15 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru" -version = "0.18.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" +checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a" dependencies = [ "hashbrown 0.17.1", ] @@ -2453,9 +2455,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -2530,7 +2532,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.19", + "thiserror 2.0.20", "tracing", ] @@ -2560,7 +2562,7 @@ dependencies = [ "opentelemetry_sdk", "prost", "reqwest", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "tonic", "tonic-types", @@ -2592,7 +2594,7 @@ dependencies = [ "percent-encoding", "portable-atomic", "rand 0.9.5", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "tokio-stream", ] @@ -2671,9 +2673,9 @@ dependencies = [ [[package]] name = "pageant" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3a5ae18f65a85c67a77d18d42d3606c07948e3c17c1e5f74852b26589e88a5" +checksum = "3adadc44070da6f464b0918655a12f5792c156e088d8c4082d13e27d94c3e791" dependencies = [ "base16ct", "byteorder", @@ -2683,7 +2685,7 @@ dependencies = [ "log", "rand 0.10.2", "sha2 0.11.0", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "windows", "windows-strings", @@ -2792,9 +2794,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.8" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df728be843c7070fab6ab7c328c4e9e9d78e23bf749c0669c86ee7ebfa050a2" +checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf" dependencies = [ "memchr", "ucd-trie", @@ -2802,9 +2804,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.8" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e2dd6fc3b26b3462ee188aac870f5a41d398f1cd5e2408d16531bd71c9591fd" +checksum = "b3a83744a5c8455b8b3e0dc5031362780a347c878bdd11584d1a8984228cc88d" dependencies = [ "pest", "pest_generator", @@ -2812,9 +2814,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.8" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a7a9205cfb6f596a9e8b689c0a15f9ceb7a1aafae7aaf788150ac65b29975b6" +checksum = "e0cd3451aa3de60d4b9a1e736885e4dea6b31617598026f12256ad566d63304a" dependencies = [ "pest", "pest_meta", @@ -2825,9 +2827,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.8" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85abd351c0de1e8384fc791a0737111a350394937e92b956b743dac12429f57c" +checksum = "e04d3a0849e241d7dfce834c83b1c5edc8622009e8dd51a12ba1927c32f05496" dependencies = [ "pest", ] @@ -2961,9 +2963,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "plotters" @@ -3013,19 +3015,20 @@ dependencies = [ "cpubits", "cpufeatures 0.3.0", "universal-hash", + "zeroize", ] [[package]] name = "portable-atomic" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -3266,7 +3269,7 @@ dependencies = [ "palette", "serde", "strum", - "thiserror 2.0.19", + "thiserror 2.0.20", "unicode-segmentation", "unicode-truncate", "unicode-width", @@ -3372,7 +3375,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -3389,9 +3392,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -3501,9 +3504,9 @@ dependencies = [ [[package]] name = "russh" -version = "0.62.5" +version = "0.63.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da7c230e0ed9cbeb92fbad6c8848985d6df2a1464c0dc247a021abd666e9005e" +checksum = "35bab1b87d915817d5d9cc352637cd40d5f0b298a48c6309af9156a4addc3031" dependencies = [ "aes", "aws-lc-rs", @@ -3526,7 +3529,7 @@ dependencies = [ "enum_dispatch", "flate2", "futures", - "generic-array 1.4.4", + "generic-array 1.4.5", "getrandom 0.4.3", "ghash", "hex-literal", @@ -3564,7 +3567,7 @@ dependencies = [ "ssh-encoding", "ssh-key", "subtle", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "typenum", "universal-hash", @@ -3655,9 +3658,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "aws-lc-rs", "ring", @@ -3834,7 +3837,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -4220,9 +4223,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -4365,11 +4368,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.19", + "thiserror-impl 2.0.20", ] [[package]] @@ -4385,13 +4388,13 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -4426,9 +4429,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -4469,7 +4472,7 @@ checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 3.0.3", + "syn 3.0.4", ] [[package]] @@ -4784,9 +4787,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +checksum = "f053576934f05a761a402421fbbe3d425d9366f75f978806a037b3ca481abecc" dependencies = [ "atomic", "getrandom 0.4.3", @@ -4869,9 +4872,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -4882,9 +4885,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.76" +version = "0.4.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" dependencies = [ "js-sys", "wasm-bindgen", @@ -4892,9 +4895,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4902,9 +4905,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", @@ -4915,18 +4918,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -5016,9 +5019,9 @@ dependencies = [ [[package]] name = "whoami" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" +checksum = "626c4bac6755d76ffc12cb01b2eac751db1996b9e0041de9aa02c8c211ddc82c" dependencies = [ "libc", "libredox", @@ -5278,9 +5281,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "yoke" @@ -5307,18 +5310,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.55" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", @@ -5368,9 +5371,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -5379,9 +5382,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -5390,13 +5393,13 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a59d5750..541f7f3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,12 @@ tokio = { version = "1.52.3", features = ["full"] } # Upstream russh: both fork patches are now upstreamed # - handle-data high-frequency PTY output fix: upstream #731 (v0.62.0) # - SHA-1 MAC exclusion from Preferred::DEFAULT: upstream #690 (v0.60.2) -russh = "0.62.1" +# 0.63 widened `client::Handler::check_server_key` to `PublicKeyOrCertificate` +# and added opt-in host certificates. bssh leaves +# `Preferred::host_key_certificates` 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.63.1" # Use our internal russh-sftp fork tracking upstream 2.3.0 # (adds pipelined File I/O; serde_bytes perf fix is now upstreamed) russh-sftp = { package = "bssh-russh-sftp", version = "2.3.0", path = "crates/bssh-russh-sftp" } @@ -69,7 +74,7 @@ ipnetwork = "0.21" bcrypt = "0.19" argon2 = { version = "0.5", features = ["std"] } rand = "0.10" -# Pinned to match russh's ssh-key (0.62.x uses =0.7.0-rc.11) so the workspace +# Pinned to match russh's ssh-key (0.63.x uses =0.7.0-rc.11) so the workspace # resolves a single ssh-key version instead of 0.6 + 0.7-rc side by side. ssh-key = { version = "=0.7.0-rc.11", features = ["std"] } async-compression = { version = "0.4", features = ["tokio", "gzip"] } diff --git a/src/ssh/tokio_client/connection.rs b/src/ssh/tokio_client/connection.rs index a368b791..fed7ca34 100644 --- a/src/ssh/tokio_client/connection.rs +++ b/src/ssh/tokio_client/connection.rs @@ -706,8 +706,34 @@ impl Handler for ClientHandler { async fn check_server_key( &mut self, - server_public_key: &russh::keys::PublicKey, + server_key: &russh::keys::PublicKeyOrCertificate, ) -> Result { + // russh 0.63 widened this callback to also deliver OpenSSH *host + // certificates*. bssh never advertises certificate host key algorithms + // (both `Preferred` overrides only touch compression, and + // `Preferred::DEFAULT.host_key_certificates` is empty), so a server + // cannot legitimately negotiate one. Should a peer send one anyway, + // fail closed: bssh has no CA signature verification, so the key inside + // the certificate has never been vouched for by anything we trust, and + // matching it against known_hosts would answer the wrong question. + // `NoCheck` still wins, because there the user disabled verification. + let server_public_key = match server_key { + russh::keys::PublicKeyOrCertificate::PublicKey { key, .. } => key, + russh::keys::PublicKeyOrCertificate::Certificate(cert) => { + if matches!(self.server_check, ServerCheckMethod::NoCheck) { + return Ok(true); + } + tracing::error!( + "Host '{}' presented an OpenSSH host certificate (key ID '{}'); \ + bssh cannot verify certificate signatures, so the host is rejected. \ + Configure the server to offer a plain host key.", + self.hostname, + cert.key_id() + ); + return Err(super::Error::ServerCheckFailed); + } + }; + match &self.server_check { ServerCheckMethod::NoCheck => Ok(true), ServerCheckMethod::PublicKey(key) => { diff --git a/src/ssh/tokio_client/host_verification.rs b/src/ssh/tokio_client/host_verification.rs index 0df258bf..9516b371 100644 --- a/src/ssh/tokio_client/host_verification.rs +++ b/src/ssh/tokio_client/host_verification.rs @@ -1149,6 +1149,39 @@ mod tests { (dir, path, path_str) } + /// Wrap a plain host key the way russh 0.63 hands it to + /// `Handler::check_server_key`, which now takes a `PublicKeyOrCertificate`. + fn host_key(key: &PublicKey) -> russh::keys::PublicKeyOrCertificate { + russh::keys::PublicKeyOrCertificate::from(key.clone()) + } + + /// Build the OpenSSH *host certificate* form of `check_server_key`'s + /// argument: `subject`'s public key signed by `ca`. + /// + /// bssh never advertises certificate host key algorithms, so russh cannot + /// negotiate one in practice; these tests pin the fail-closed behavior for + /// the case where a peer sends one anyway. + fn host_certificate( + subject: &PrivateKey, + ca: &PrivateKey, + ) -> russh::keys::PublicKeyOrCertificate { + let mut builder = russh::keys::ssh_key::certificate::Builder::new_with_random_nonce( + &mut rand::rng(), + subject.public_key(), + 0, + u64::MAX, + ) + .expect("certificate builder should accept a valid validity window"); + builder.key_id("bssh-test").unwrap(); + builder + .cert_type(russh::keys::ssh_key::certificate::CertType::Host) + .unwrap(); + builder.valid_principal("node1.example.com").unwrap(); + russh::keys::PublicKeyOrCertificate::Certificate( + builder.sign(ca).expect("signing with an ED25519 CA works"), + ) + } + #[tokio::test] async fn test_accept_new_records_unknown_host_and_accepts() { let (_dir, path, path_str) = temp_known_hosts(); @@ -1632,7 +1665,9 @@ mod tests { .unwrap(); let mut handler = handler_for(ServerCheckMethod::KnownHostsFile(path_str)); - let result = handler.check_server_key(revoked.public_key()).await; + let result = handler + .check_server_key(&host_key(revoked.public_key())) + .await; assert!( matches!(result, Err(Error::HostKeyRevoked { .. })), "strict mode must also honor @revoked, got {result:?}" @@ -1696,7 +1731,7 @@ mod tests { "127.0.0.1:22".parse().unwrap(), ServerCheckMethod::KnownHostsFile(path_str), ); - let result = handler.check_server_key(key.public_key()).await; + let result = handler.check_server_key(&host_key(key.public_key())).await; assert!( matches!(result, Ok(true)), "strict mode must match an existing pin regardless of hostname casing, got {result:?}" @@ -1984,7 +2019,7 @@ mod tests { let key = generate_key(); let mut handler = handler_for(ServerCheckMethod::KnownHostsFile(path_str)); - let result = handler.check_server_key(key.public_key()).await; + let result = handler.check_server_key(&host_key(key.public_key())).await; assert!( matches!(result, Ok(false)), "unknown host must be rejected in strict mode, got {result:?}" @@ -2006,7 +2041,9 @@ mod tests { .unwrap(); let mut handler = handler_for(ServerCheckMethod::KnownHostsFile(path_str.clone())); - let result = handler.check_server_key(imposter.public_key()).await; + let result = handler + .check_server_key(&host_key(imposter.public_key())) + .await; assert!( matches!(result, Err(Error::HostKeyChanged { .. })), "strict mode must report a changed key specifically, got {result:?}" @@ -2014,7 +2051,9 @@ mod tests { // The original key still verifies. let mut handler = handler_for(ServerCheckMethod::KnownHostsFile(path_str)); - let result = handler.check_server_key(original.public_key()).await; + let result = handler + .check_server_key(&host_key(original.public_key())) + .await; assert!(matches!(result, Ok(true))); } @@ -2040,7 +2079,9 @@ mod tests { .unwrap(); let mut handler = handler_for(ServerCheckMethod::KnownHostsFile(path_str)); - let result = handler.check_server_key(current.public_key()).await; + let result = handler + .check_server_key(&host_key(current.public_key())) + .await; assert!( matches!(result, Ok(true)), "strict mode must accept a key matching any recorded entry, got {result:?}" @@ -2053,7 +2094,7 @@ mod tests { let key = generate_key(); let mut handler = handler_for(ServerCheckMethod::NoCheck); - let result = handler.check_server_key(key.public_key()).await; + let result = handler.check_server_key(&host_key(key.public_key())).await; assert!(matches!(result, Ok(true))); assert!(!path.exists(), "NoCheck must not create a known_hosts file"); } @@ -2065,7 +2106,7 @@ mod tests { let key = generate_key(); let mut handler = handler_for(ServerCheckMethod::AcceptNewKnownHostsFile(path_str)); - let result = handler.check_server_key(key.public_key()).await; + let result = handler.check_server_key(&host_key(key.public_key())).await; assert!(matches!(result, Ok(true))); assert_eq!(entry_lines(&path).len(), 1); } @@ -2170,10 +2211,89 @@ mod tests { "127.0.0.1:22".parse().unwrap(), ServerCheckMethod::KnownHostsFile(path_str), ); - let result = handler.check_server_key(key.public_key()).await; + let result = handler.check_server_key(&host_key(key.public_key())).await; assert!( matches!(result, Err(Error::ServerCheckFailed)), "strict mode must refuse an unrecordable hostname, got {result:?}" ); } + + // Host certificates (russh 0.63 widened `check_server_key`). + + /// A host certificate carries a key signed by a CA. bssh verifies no CA + /// signatures, so every verifying mode must refuse it rather than fall + /// back to matching the key inside it against known_hosts. + #[tokio::test] + async fn test_host_certificate_is_rejected_by_verifying_modes() { + let ca = generate_key(); + let subject = generate_key(); + let cert = host_certificate(&subject, &ca); + + let (_dir, path, path_str) = temp_known_hosts(); + // Pin the key *inside* the certificate, so a naive implementation that + // unwrapped the certificate would wrongly accept. + std::fs::write( + &path, + format!( + "node1.example.com {}\n", + subject.public_key().to_openssh().unwrap() + ), + ) + .unwrap(); + + for check in [ + ServerCheckMethod::KnownHostsFile(path_str.clone()), + ServerCheckMethod::AcceptNewKnownHostsFile(path_str), + ServerCheckMethod::AcceptNewInMemory, + ServerCheckMethod::PublicKey( + subject.public_key().to_openssh().unwrap()[..] + .split_whitespace() + .nth(1) + .unwrap() + .to_string(), + ), + ] { + let mut handler = handler_for(check.clone()); + let result = handler.check_server_key(&cert).await; + assert!( + matches!(result, Err(Error::ServerCheckFailed)), + "{check:?} must refuse an unverifiable host certificate, got {result:?}" + ); + } + } + + /// `NoCheck` means the user turned host verification off entirely, so it + /// stays permissive for certificates too. + #[tokio::test] + async fn test_host_certificate_is_accepted_under_no_check() { + let ca = generate_key(); + let subject = generate_key(); + let cert = host_certificate(&subject, &ca); + + let mut handler = handler_for(ServerCheckMethod::NoCheck); + let result = handler.check_server_key(&cert).await; + assert!( + matches!(result, Ok(true)), + "NoCheck disables verification for certificates too, got {result:?}" + ); + } + + /// The certificate rejection must not have been reachable by accident: + /// bssh never advertises certificate host key algorithms, so russh cannot + /// negotiate one. + #[test] + fn test_bssh_never_advertises_host_key_certificates() { + assert!( + russh::Preferred::DEFAULT.host_key_certificates.is_empty(), + "bssh's Preferred overrides inherit this field from DEFAULT; a \ + non-empty default would silently opt bssh into host certificates" + ); + assert!( + crate::ssh::tokio_client::Config::default() + .preferred + .host_key_certificates + .is_empty(), + "the client config must not advertise certificate host key algorithms" + ); + } }