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" + ); + } }